Official

F - Critical Misread Editorial by physics0523


まず最初に思いつくのは、 \(dp[\) 文字列の末尾 \(k\) 文字 \(] = \{\) その末尾を持つ文字列の個数 \(\}\) のような DP です。
残念ながら、この DP は時間計算量 \(O(c^k N)\) ( \(c=26\) は文字の種類数) であり、 \(k \le 10\) という面でも \(N \le 10^9\) という面でもとても間に合いません。
しかし、文字列の末尾の情報を一定程度持ちながら DP する必要はありそうです。どうすればよいでしょうか?

\(S\) の要素の接頭辞の集合を \(T\) とします。
例えば、 \(S=\{\) abc, abde, bcd \(\}\) であるとき、 \(T=\{\) (空文字列), a, ab, abc, abd, abde, b, bc, bcd \(\}\) です。
これを用いて先程の DP を改善することを考えます。現状の文字列の接尾辞として、 \(T\) の要素のうち最も長いものを維持しておけば情報は足りています。
例えば、 xyzab という文字列に対しては 「 b が接尾辞である」という情報だけでは次に c を加えて abc が部分文字列となる情報が欠落しますが、 \(T\) 中で最も長い「 ab が接尾辞である」という情報を持っておけば \(S\) の要素を部分文字列として検出するには十分です。

なお、ここまでの議論は次のように言い換えることもできます。

  • 文字列集合 \(S\) に対して Aho–Corasick法 でオートマトンを構成し、現在いる頂点の番号を DP の添え字に持つ。

これで、 DP の添え字の種類数は \(O(|T|)=O(\sum|S_i|)\) まで落ちました。しかしながら、時間計算量 \(O(N c \sum |S_i|)\) では依然間に合いません。

この DP を更に高速化することを考えます。
現在の接尾辞 \(T_i\) と次に来る文字 \(c\) から、次の接尾辞 \(T_j\) が定まります。
このことから、行列 \(M_{i,j} = \{\) 接尾辞 \(T_i\) から接尾辞 \(T_j\) に行く文字 \(c\) の種類数 \(\}\) という行列が構成できます。
行列累乗を活用すると、 \(k\) 文字追加の遷移が \(M^k\) による遷移として実現できます。

その結果、遷移の予計算に \(O(c (\sum |S_i|)^3)\) 、行列累乗に \(O(\log N (\sum |S_i|)^3)\) といった時間計算量でこの問題に正解できます。

余談: この問題は NDPC-C を誤読したことで生まれた問題です。 NDPC-C も「オートマトンDP」と説明されていますが、本問題はより直接的にオートマトン上で DP をすることになります。

実装例 (C++):

#include<bits/stdc++.h>

using namespace std;
using ll=long long;

const ll mod=998244353;

vector<vector<ll>> mmul(vector<vector<ll>> &l,vector<vector<ll>> &r){
  ll n=l.size();
  vector<vector<ll>> res(n,vector<ll>(n,0));
  for(ll i=0;i<n;i++){
    for(ll j=0;j<n;j++){
      for(ll k=0;k<n;k++){
        res[i][k]+=l[i][j]*r[j][k];
        res[i][k]%=mod;
      }
    }
  }
  return res;
}

bool issuffix(string &a,string &b){
  ll al=a.size(),bl=b.size();
  if(al<bl){return false;}
  for(ll i=0;i<bl;i++){
    if(a[al-1-i]!=b[bl-1-i]){return false;}
  }
  return true;
}

int main(){
  ios::sync_with_stdio(false);
  cin.tie(nullptr);
  ll n,k;
  cin >> n >> k;
  vector<string> s(k);
  map<string,ll> mp;
  mp[""]=0;
  for(auto &nx : s){
    cin >> nx;
    string pref;
    for(auto &ny : nx){
      pref.push_back(ny);
      mp[pref]=0;
    }
  }
  
  ll vn=0;
  vector<string> vs;
  for(auto &nx : mp){
    vs.push_back(nx.first);
    nx.second=vn; vn++;
  }

  vector<ll> ban(vn,0);
  vector<vector<ll>> mat(vn,vector<ll>(vn,0));
  for(ll i=0;i<vn;i++){
    for(ll j=0;j<k;j++){
      auto pos=vs[i].find(s[j]);
      if(pos!=string::npos){
        ban[i]=1; break;
      }
    }
  }
  for(ll i=0;i<vn;i++){
    if(ban[i]){continue;}
    for(char c='a';c<='z';c++){
      string ss=vs[i];
      ss.push_back(c);
      ll tg=0;
      for(ll j=0;j<vn;j++){
        if(issuffix(ss,vs[j])){
          if(vs[tg].size()<vs[j].size()){tg=j;}
        }
      }
      if(ban[tg]==0){
        mat[i][tg]++;
      }
    }
  }
  vector<ll> vec(vn,0);
  vec[0]=1;
  while(n>0){
    if(n&1ll){
      vector<ll> nvec(vn,0);
      for(ll i=0;i<vn;i++){
        for(ll j=0;j<vn;j++){
          nvec[j]+=vec[i]*mat[i][j];
          nvec[j]%=mod;
        }
      }
      vec=nvec;
    }
    mat=mmul(mat,mat);
    n>>=1;
  }
  ll res=0;
  for(auto &nx : vec){res+=nx; res%=mod;}
  cout << res << "\n";
  return 0;
}

posted:
last update: