这个题的重点是转移方程的优化和匹配字符串的技巧,所以我们分开讲。
Solution——字符串匹配
1.KMP算法
因为是首尾拼接,所以拿KMP算法将nxt数组算出来。可以更简便的运算第 (j) 种字符串接在第 (i) 种结尾的最小增加量。
for(int k=1;k<=n;k++){
int j=0;
for(int i=2;i<=len[k];i++){
while(j&&s[k][j+1]!=s[k][i]) j=nxt[k][j];
if(s[k][j+1]==s[k][i]) j++;
nxt[k][i]=j;
}
}
2.AC自动机
和KMP其实差不多(好像官方正解是这个),AC自动机就是算出fail数组而已,功能一样。
inline void insert(char *s,int id){
int len=strlen(s+1),now=0;
for(int i=1;i<=len;i++){
int tmp=s[i]-'a';
if(!ch[now][tmp]) ch[now][tmp]=++tot;
now=ch[now][tmp];
}
ed[now]=id;pos[id]=now;
}
inline void get_fail()
{
queue<int>q;q.push(0);
while(q.size())
{
int x=q.front();q.pop();
for(int i=0;i<26;i++){
int y=ch[x][i];
if(!y) continue;
int k=fail[x];
while(k&&(!ch[k][i])) k=fail[k];
fail[y]=x?ch[k][i]:0;
q.push(y);
}
}
}
3.Hash
先拿的单Hash试了试 ,发现如果模数不太行的话,是会WA好几个点的。
也可能是我脸黑,一发入魂
inline bool check(int x,int y,int l){
//第三个(ll) 不写会锅4个点 亲测
return (ll)(Hash[x][len[x]-1]-Hash[x][len[x]-l-1]+mod)%mod
==(ll)((ll)Hash[y][l-1]*pow_26[len[x]-l]%mod);
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
int l=Min(len[i],len[j]);
for(int k(i==j?l-1:l);k;--k)
if(check(i,j,k)){
f[0][i][j]=len[j]-k;
break;
}
if(f[0][i][j]>10000000) f[0][i][j]=len[j];
}
//配合下面的倍增Floyd观看
我最后用的 mod=19280817
4.双Hash
和单Hash不一样,对于双Hash来讲,只要你不zz,脸也不算太黑,出题人没有卡你双Hash,应该是没问题的。
但是,很不幸
这个题的内存是 (125MB) ,双Hash是会MLE的 〒▽〒
inline int get_Hash(const int x,const int s,const int e){
if(s>e) return 0;
int t=(Hash[x][e+1]-1ll*Hash[x][s]*p26_1[e-s+1]%MOD)+MOD;
return t>=MOD?t-MOD:t;
}
inline int get_HASH(const int x,const int s,const int e){
if(s>e) return 0;
int t=(HASH[x][e+1]-1ll*HASH[x][s]*p26_2[e-s+1]%mod)+mod;
return t>=mod?t-mod:t;
}
inline int work(const int x,const int y){
int m=min(len[x],len[y]);
if(x==y) --m;
for(int i=m;~i;--i){
if((get_Hash(x,len[x]-i,len[x]-1)==get_Hash(y,0,i-1))&&(get_HASH(x,len[x]-i,len[x]-1)==get_HASH(y,0,i-1)))
return len[y]-i;
}
return 0;
}
//借用大佬的部分代码
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
dis[i][j]=work(i,j);
//这个dis[i][j]对应的是矩阵优化中的dis
Solution——转移方程优化
1.倍增Floyd
我们首先转化题意:
有 (n) 个点,两个点 (i,j) 之间的权值为将第 (j) 种字符串接在第 (i) 种结尾的最小增加量(注意此处 (i) 可以和 (j) 相等)。求一条经过 (m) 个点的最短路径,起点和终点自己选择。
暴力求法很好想,因为 (nleq 200) ,所以考虑Floyd。设 (f_{k,i,j}) 为 (i) 到 (j) 经过 (k) 个点的最短路径,然后一层一层枚举,直到 (m) ,就求出来了。
时间复杂度为 (O(n^3 m))
这不直接满分?
所以考虑怎么优化——这时倍增Floyd出场了。
现在需要干啥呢?在 (f_{k,i,j}) 的基础上将意义改为从 (i) 到 (j) 经过 (2^k) 个点的最短路,这样转移的复杂度就从 (O(m)) 变成了 (O(log m)) 。
for(int t=1;t<=30;t++)
for(int k=1;k<=n;k++)
for(int j=1;j<=n;j++)
for(int i=1;i<=n;i++)
f[t][i][j]=Min(f[t-1][i][k]+f[t-1][k][j],f[t][i][j]);
for(int i=0;i<=30;i++)
if(m&(1<<i)){
for(int j=1;j<=n;j++){
tmp[j]=0x3f3f3f3f3f3f3f3fll;
for(int k=1;k<=n;k++)
tmp[j]=tmp[j]<dis[k]+f[i][k][j]?tmp[j]:dis[k]+f[i][k][j];
}
for(int j=1;j<=n;j++)
dis[j]=tmp[j];
}
现在时间复杂度为 (O(n^3log m)) 。
2.矩阵优化
先考虑不加优化时的朴素做法,即暴力。
设 (dp_{i,j}) 表示现在串里有 (i) 个字符串,并以第 (j) 种字符串结尾的最短长度, (dis_{i,j}) 表示将第 (j) 种字符串接在第 (i) 种结尾的最小增加量。
那么转移方程也很好得到: (dp_{i,j}=min{dp_{i-1,k}+dis_{k,j}}) ,其中初始状态应为 (dp_{1,i}=len_i)
我们看一下这部分的代码:
for(int i=2;i=m;i++)
for(int j=1;j<=n;j++)
for(int k=1;k<=n;k++)
dp[i][j]=min(dp[i][j],dp[i-1][k]+dis[k][j]);
等等,这一坨东西是不是似曾相识。
是不是在矩阵乘法中见过一个类似的,这启发我们用矩阵去优化这个转移。
for(int x=1;x<=n;x++){
I.a[0][x]=len[x];I.a[x][0]=INF;
for(int y=1;y<=n;y++){
int j=0;
for(inti i=2;i<=len[x];i++){
whle(j&&s[y][j+1]!=s[x][i]) j=nxt[y][j];
if(s[y][j+1]==s[x][i]) j++;
if(i==len[x]) I.a[x][y]=len[y]-j;
}
}
}
for(m--;m;m>>=1){
if(m&1) Ans=Ans*I;
I=I*I;
}
//搭配上面的KMP观看
时间复杂度也是 (O(n^3log m)) 。
注意:我将上面的几乎每个组合都试了一遍,除了双Hash在不管无论开不开O2的情况下都是第一个点MLE,别的方法在不开O2的情况是第一个点TLE。(咱也不知道为什么时间复杂度对了还会T,这也卡常?)
代码
刚刚每个都放了部分代码,这里就咕咕咕
还是放一个倍增Floyd的完整代码吧
( ̄▽ ̄)*
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=210,L=100010,mod=19260817;
char s[N][L];
ll f[35][N][N],dis[N],tmp[N],n,m,ans;
int len[N],Hash[N][L],pow_26[L];
ll Min(ll x,ll y){
return x<y?x:y;
}
inline bool check(int x,int y,int l){
//第三个(ll) 不写会锅4个点 亲测
return (ll)(Hash[x][len[x]-1]-Hash[x][len[x]-l-1]+mod)%mod==(ll)((ll)Hash[y][l-1]*pow_26[len[x]-l]%mod);
}
inline void init(){
pow_26[0]=1;
for(int i=1;i<=100000;i++)
pow_26[i]=pow_26[i-1]*26%mod;
memset(f,0x3f,sizeof(f));
}
int main(){
init();
scanf("%lld%lld",&n,&m); m--;
for(int i=1;i<=n;i++){
scanf("%s",s[i]);
len[i]=strlen(s[i]);
dis[i]=len[i];
for(int j=0;j<len[i];j++)
if(j) Hash[i][j]=(Hash[i][j-1]+pow_26[j]*(s[i][j]-'a'+1))%mod;
else Hash[i][j]=s[i][j]-'a'+1;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
int l=Min(len[i],len[j]);
for(int k(i==j?l-1:l);k;--k)
if(check(i,j,k)){
f[0][i][j]=len[j]-k;
break;
}
if(f[0][i][j]>10000000) f[0][i][j]=len[j];
}
for(int t=1;t<=30;t++)
for(int k=1;k<=n;k++)
for(int j=1;j<=n;j++)
for(int i=1;i<=n;i++)
f[t][i][j]=Min(f[t-1][i][k]+f[t-1][k][j],f[t][i][j]);
for(int i=0;i<=30;i++)
if(m&(1<<i)){
for(int j=1;j<=n;j++){
tmp[j]=0x3f3f3f3f3f3f3f3fll;
for(int k=1;k<=n;k++)
tmp[j]=tmp[j]<dis[k]+f[i][k][j]?tmp[j]:dis[k]+f[i][k][j];
}
for(int j=1;j<=n;j++)
dis[j]=tmp[j];
}
ans=0x3f3f3f3f3f3f3f3fll;
for(int i=1;i<=n;i++)
ans=Min(ans,dis[i]);
printf("%lld
",ans);
return 0;
}