公式

G - Wipeout 解説 by physics0523 印刷ページ


原案: physics0523

同じカードを \(3\) 度以上表に向ける意味はありません(書かれた整数を知るための最初、食べるための最後以外に表を向けても、ただ操作回数が増大します)。
この時点で、 \(M > 2N\) である場合の解が \(0\) であることが分かります。
更に、あなたが取る行動を以下の \(2\) 通りまで絞り込んで構いません。

  • 既に知っている \(x\) が書かれたカードを選び、表に向ける。その結果、そのカードを食べることになる。
  • 表に書かれた整数が未知であるカードをどれでもいいので \(1\) 枚選び、表に向ける。

このことから、あなたが取る戦略は以下であることが分かります。

  • \(x\) が書かれたカードが既知なら、そのカードを選ぶ。
    • \(x\) が書かれたカードが既知であるのに未知のカードを選んだ場合と比べて、既知の \(x\) を選択して食べるとして損しません。
  • そうでないなら未知のカードをどれでもいいので \(1\) 枚選ぶ。

要した操作の総数は、 \(N\) に「未知のカードを選択し、それが直ちに食べられるものではなかった回数」を足したものであることも分かります。
では、「未知のカードを選択する」のはどのような状況でしょうか?
未知のカードの中に直ちに食べられるカードが \(1\) 枚だけ含まれる時、未知のカードを選択することになります。そうでなければ、既知の直ちに食べられるカードを選択して食べるべきだからです。
更に言うと、未知のカードが \(k\) 枚あるとき、現在何が書かれたカードが既知であるかに依らず \(1/k\) の確率でそのカードを直ちに食べることができます。
さらに、上記の戦略に沿っている限りどのような経過を辿っても全ての \(i\) (\(1 \le i \le N\)) について未知のカードが \(i\) 枚存在する状態が丁度 \(1\) 回訪れます。
よって、解は以下の式の \(x^{M-N}\) に付く係数です。

  • \(\displaystyle \prod_{k=1}^{N} \left ( \frac{k-1}{k}x + \frac{1}{k} \right )\)

なお、上記の式の第 \(k\) 項に \(k\) を掛け最後に \(N!\) で割ると、最後に割る部分以外は全て整数係数となり更に筋が良くなります。

あとは、 \(N\) 個の \(1\) 次式の総積を求めることができればよいです。
これは、 \(O(\log N)\) 段かけて畳み込みを二分木状に取っていくことで、各段 \(O(N \log N)\) 、全体 \(O(N \log^2 N)\) で求めることができます (実装上は多項式を queue に突っ込み、前から \(2\) 要素取り出して畳み込んで後ろに入れることを繰り返せば良いです) 。

本解法の時間計算量は \(O(N \log^2 N)\) です。

なお、解に \(N!\) を掛けた際の値の配列が 第一種スターリング数 を成すため、実験的にこの事実を導き出し求値することも可能だと思われます。

実装例 (C++):

#include<bits/stdc++.h>
#include<atcoder/all>

using namespace std;
using namespace atcoder;

using mint=modint998244353;

int main(){
  ios::sync_with_stdio(false);
  cin.tie(nullptr);
  int n,m;
  cin >> n >> m;
  if(m>2*n){cout << "0\n"; return 0;}
  queue<vector<mint>> q;
  for(int k=1;k<=n;k++){
    q.push({1,(mint)(k-1)});
  }
  while(q.size()>=2){
    auto l=q.front(); q.pop();
    auto r=q.front(); q.pop();
    q.push(convolution(l,r));
  }
  mint div=1;
  for(int i=2;i<=n;i++){
    div*=((mint)i);
  }
  auto f=q.front(); q.pop();
  cout << (f[m-n]/div).val() << "\n";
  return 0;
}

投稿日時:
最終更新: