Official

E - 圧縮番号列の復元 / Restoration of Compressed Number Sequence Editorial by physics0523


任意の箱の列に対する圧縮番号の割り当て方をそのまま DP に起こすことで、この問題を解くことができます。実際にやってみましょう。

圧縮番号は以下のように割り当てられます。

  • 既に来たことのある番号が来た場合
    • その番号に対応した圧縮番号が既に決まっており、それが使われます。
  • まだ来たことのない番号が来た場合
    • 新たな圧縮番号が今来た番号に対して払い出されます。

これを DP に起こします。
\(dp[\) 直前に払い出された圧縮番号 \(]=\{\) 場合の数 \(\}\) を実行しましょう。

\(A_i\) を考慮する前の DP テーブルを \(dp\) 、考慮した後の DP テーブルを \(ndp\) とします。

\(i\) 項目の \(dp[j]\) からの遷移は次の通りです。

  • \(A_i = 0\) の場合 (圧縮番号が判読不可である場合)
    • 既存の圧縮番号が使われるケースとして、 \(ndp[j]\)\(dp[j] \times j\) を加算する。(使う圧縮番号だけ自由度があります)
    • 新たな圧縮番号が払い出されるケースとして、 \(ndp[j+1]\)\(dp[j] \times (M-j)\) を加算する。(割り当てる元の番号だけ自由度があります)
  • \(A_i \neq 0\) の場合 (圧縮番号が決まっている場合)
    • \(A_i \le j\) の場合、既存の圧縮番号を使うことになるので \(ndp[j]\)\(dp[j]\) を加算する。
    • \(A_i = j+1\) の場合、新たな圧縮番号を払い出す必要がある ( = 今までに来たことのない番号が来る) ので \(ndp[j+1]\)\(dp[j] \times (M-j)\) を加算する。(割り当てる元の番号だけ自由度があります)
    • どちらでもない場合、圧縮番号 \(A_i\) を与えることが不可能なのでそこからは遷移しない。

DP テーブルの大きさは \(O(\min(N,M))\) でよいので、時間計算量が \(O(N \times \min(N,M))\) の解法を得ることができました。

実装例 (C++):

#include<bits/stdc++.h>

using namespace std;

using ll=long long;
const ll mod=998244353;

int main(){
  ll N,M;
  cin >> N >> M;
  ll dmx=min(N,M);
  vector<ll> dp(dmx+1,0);
  dp[0]=1;
  for(ll i=0;i<N;i++){
    ll a;
    cin >> a;
    vector<ll> ndp(dmx+1,0);
    for(ll j=0;j<=dmx;j++){
      if(a==0){
        ndp[j]+=dp[j]*j;
        ndp[j]%=mod;
        if(j!=dmx){
          ndp[j+1]+=dp[j]*(M-j);
          ndp[j+1]%=mod;
        }
      }
      else if(a<=j){
        ndp[j]+=dp[j];
        ndp[j]%=mod;
      }
      else if(a==j+1){
        if(j!=dmx){
          ndp[j+1]+=dp[j]*(M-j);
          ndp[j+1]%=mod;
        }
      }
      else{}
    }
    dp=ndp;    
  }
  ll res=0;
  for(auto &nx : dp){res+=nx;}
  cout << res%mod << "\n";
  return 0;
}

posted:
last update: