競技プログラミング日記

主に AtCoder の記事です

AtCoder Beginner Contest 333F問題

\(\def \set #1#2{\{ #1 \ \vert \ #2 \}}\)

ABC333F

巡回しているため,無限に同じ局面になる可能性がある. しかし,確率を計算すれば収束するため,値は有限になる. 巡回部分をどの様に回避するかが鍵になる.

解法0

等比級数の公式を用いる方法. \(abs(r) < 1\) のとき, \[\begin{matrix} S &:=& a + ar + ar^{2} + \cdots \\ &=& a + rS. \end{matrix}\] よって \(S = \frac{a}{1-r}\). 上の式で,\(S\) を表すために \(S\) 自身を用いている. そこが巡回にあたる部分で,それを上手くまとめることができるのが 等比級数の和の公式.
本問ではどこで巡回しているかというと, 人数が変わらない限りループしてしまう. よって,等比級数の公式を用いて,人数が減る確率を求めておけば巡回を回避できる. そうすれば DAG になるので dp で解ける.

\(dp_{i,j} := \) 今 \(i\) 人残っていて,この \(i\)人のうち最後に \(j\) 番目の人が残る確率

とおく. \(i\) 人残っているときに,\(j\) 番目の人が消える確率を求めてけば良い.

解法1

\(dp\) の定義は解法0 と同じ. あとは,各 \(i\) に対して \(x := dp_{i,0}\) とおいて, \(dp_{i,j}\) を求める.
普通に求めようとするとサイクルになってしまう. そこで,状態(つまり頂点) \(\ (i,0)\ \) の部分でグラフを分裂させる. 頂点 \(\ (i,0)\ \) をコピーして \(\ (i,0)'\ \) とおく. \(\ (i,0)\ \)に入ってくる辺の行き先を \(\ (i,0)'\ \) に置き換える. \(\ (i,0)\ \)から出ていく辺はそのままとする. このグラフで考えれば DAG となって dp が出来る.
DP を行って,\(\ (i,0)'\ \) における値を \(x\) で表す. 実際は \(x\) の 1次式となり,\(ax + b\) の形で書ける. \(\ (i,0)'\ \) と \(\ (i,0)\ \) は,元々は同じ頂点だったので, \(dp_{(i,0)} = x\) と \(dp_{(i,0)'} = ax + b\) は一致するはずである. よって \(x = ax + b\) を得るので,これより \(x = \frac{b}{1-a}\). なお,遷移する確率が \(\frac{1}{2}\) であるから,\(a \neq 1\) を得る. これは mod \(p\) で考えても,今回は起こらない. \(p \neq 2\) のとき, \(\frac{1}{2^{m}} \equiv 1 \) mod \(p\) であることは, \(m \equiv 0\) mod \(p-1\) であることと必要十分.

注意: 解法1

実装するときは,\(dp_{(i,0)}\) の値を上書きする. += の様に書いてしまうと, \(x := dp_{(i,0)}\) に対して \(dp_{(i,0)} \leftarrow dp_{(i,j)} \) の様になってしまい, \(dp_{(i,0)'}\) に余分な \(x\) が追加されてしまう.

使っている記号,マクロ等 "https://ecsmtlir.hatenablog.com/entry/2022/12/23/131925"

//---------------------------------------------------------------------------------------------------
// periodic  
struct D{
  mint a,b;
  D(){ // 初期化を書けば,resize したときに問題が起きない.
    a = 0;
    b = 0;
  }
  D(mint a, mint b): a(a), b(b){}

  D operator += (D x){
    a += x.a;
    b += x.b;
    return *this;
  }

  D operator / (mint m) const {
    return D(a/m, b/m);
  }
 
};

int main() {
  ll n;
  cin >> n;
  vector<vector<D>> dp(n+1);
  rep1(i,n) dp[i].resize(i);

  dp[1][0] = D(0,1);
  srep(i,2,n+1){
    // x := dp[i][0]
    dp[i][0] = D(1,0);
    rep1(j,i){
      D now;
      // rem: / mint(2)
      if(j != i) now += dp[i-1][j-1]/mint(2);
      now += dp[i][j-1]/mint(2);

      // rem: periodic で [i][0] に戻ってくるので,
      // そのときに上書きが正しい.間違いなのは += .
      dp[i][j%i] = now;
    }

    mint x;
    {
      mint a = dp[i][0].a;
      mint b = dp[i][0].b;
      x = b / (mint(1)-a);
      dp[i][0] = D(0,x);
    }
    srep(j,1,i){
      mint a = dp[i][j].a;
      mint b = dp[i][j].b;
      dp[i][j] = D(0, a*x + b);
    }
  }

  rep(j,n)
    cout << dp[n][j].b.val() << endl;


  return 0;
}

//---------------------------------------------------------------------------------------------------
 

//---------------------------------------------------------------------------------------------------
// 等比級数の和の公式
int main() {
  ll n;
  cin >> n;

  mint q = mint(1)/mint(2);
  vector<vector<mint>> dp(n+1, vector<mint>(n+1));

  dp[1][0] = 1;
  srep(i,2,n+1){
    vector<mint> r(n+1);
    r[0] = q/(mint(1)-q.pow(i));
    // mint ri = r[0] / q;
    rep(i,n) r[i+1] = r[i] * q;

    rep1(k,i-1) dp[i][0] += r[k] * dp[i-1][(i-1-k)%i]; // rem: (i-1-k)%i : 一人消えた後なので -1
    rep(j,i-1){
      // dp[i][j+1] = q * (dp[i][j] + (- r[i-1] + ri )*dp[i-1][j]);
      dp[i][j+1] = q*(dp[i][j] - r[i-1] *dp[i-1][j]) + r[0]*dp[i-1][j];
    }

  }

  rep(j,n){
    cout << dp[n][j].val() << endl;
  }

  return 0;
}

//---------------------------------------------------------------------------------------------------