CF1200E Compress Words ~ 洛谷
给定\(n\)个字符串,请按下面的规则,从左往右依次合并\(n\)个字符串,成为\(1\)个字符串:
- 将\(A,B\)合并,就是找到最大的\(i\),使得\(A\)的长为\(i\)的后缀和\(B\)的长为\(i\)的前缀相等,删除\(A\)的这个后缀,并将\(B\)连接到它的后面。
注意每次应该将第\(i\)个字符串与\(1\sim (i-1)\)合并后的结果进行新的一轮合并,而非输入字符串之间合并。
\(n\le 10^5\),字符串总长\(\le 10^6\)。
我们考虑\(A,B\)合并,其实就是找到\(A\)的后缀 和 \(B\)的前缀的最长共同部分,然后删掉其中一个,再把\(A,B\)连接起来。比如ABCDCD
和CDCDEF
的共同部分就是那个CDCD
。
怎么找这个共同部分呢?
我们设T=B+'~'+A
,那么\(T\)的最长公共前后缀就是我们要求的最长公共部分,而这个最长公共前后缀可以直接套用KMP的\(nxt\)数组。
为什么要隔一个~
呢?因为不隔的话,我们找的公共部分会重叠,导致错误。
还需要注意的是,如果每次都把当前的答案\(S\)和新字符串\(A\)连接在一起,时间复杂度就是\(O(n^2)\)。而我们知道公共部分的长度就是\(\min(\)S.size()
\(,\)A.size()
\()\),所以应该先截取一下,让T.size()
\(<2*\)A.size()
。这样时间复杂度就是\(O(n)\)了。
点击查看代码
#include<bits/stdc++.h>
#define N 1000010
using namespace std;
int t,n,m,nxt[N];
string s,a;
int main(){cin>>t;while(t--){cin>>a;n=s.size(),m=a.size();//为了拼接更方便,下标从0开始int minlen=min(n,m);string ts=a.substr(0,minlen)+'~'+s.substr(n-minlen,minlen);int tn=ts.size();for(int i=1,j=0;i<tn;i++){while(ts[i]!=ts[j]&&j) j=nxt[j-1];nxt[i]=(ts[i]==ts[j]?++j:0);//别忘了置0}//nxt[tn-1]就是重合部分的长度s+=a.substr(nxt[tn-1],m);//小技巧,超出边界自动调整}cout<<s<<"\n";return 0;
}
其中代码第\(18\)行用到一个小技巧,string.substr(pos,size)
,意为从\(pos\)开始截取\(size\)个字符,如果\(pos+size-1\)超过右边界,会自动调整到右边界,不要误以为代码中的\(m\)就是右边界哦。
还有,参数\(size\)是允许超界的,但如果参数\(pos>\)string.size()
会RE!
另外,这道题也可以用字符串哈希做,不过KMP比哈希快一倍(可能是因为字符串哈希要双哈希,所以有\(2\)倍常数)。
P4824 [USACO15FEB] Censoring S
给定字符串\(A,B\),请重复下面的操作,直到\(A\)中不存在\(B\):
- 删除\(A\)中最靠前的\(B\)。
请输出最后的\(A\)。
(注意删除\(B\)之后,两端的字符串可能拼接成一个新的\(B\))
\(1\le |B|\le |A|\le 10^6\)
我们可以想到,建立一个栈,依次加入\(i\)指针。如果中途凑成一个\(B\)则把这些元素删掉。而判断凑成\(B\)就可以用KMP。
在KMP的过程中进行比对,如果凑出一个\(B\),就把匹配的下标全部出栈,所谓删掉,其实就是把\(j\)指针回溯成之前的状态,即变成出栈后栈顶\(i\)指针所对应的\(j\)。所以我们用\(t[i]\)来表示\(i\)指针所对应的\(j\)指针,出栈后\(j\)设为\(t[st[top]]\)即可。
点击查看代码
#include<bits/stdc++.h>
#define N 1000010
using namespace std;
string a,b;
int n,m,nxt[N],t[N],st[N],top;
int main(){cin>>a>>b;n=a.size(),m=b.size();for(int i=1,j=0;i<m;i++){while(b[i]!=b[j]&&j) j=nxt[j-1];nxt[i]=(b[i]==b[j]?++j:0);}for(int i=0,j=0;i<n;i++){while(a[i]!=b[j]&&j) j=nxt[j-1];t[i]=j,st[++top]=i;if(a[i]==b[j]){if(j==m-1) top-=m,j=t[st[top]];j++;}}for(int i=1;i<=top;i++) cout<<a[st[i]];return 0;
}
P3435 [POI2006] OKR-Periods of Words
给定字符串\(S\),请求出\(S\)每个前缀的答案之和。
定义字符串\(T\)的答案为:
- 如果存在字符串\(P\)是\(T\)的真前缀,且\(T\)是\(P+P\)的前缀,则答案就是\(P\)可能的最长长度。
- 如果不存在字符串\(P\),则答案为\(0\)。
题面看上去有点绕,但其实找一找规律:
如上图,红色部分是整个字符串的答案。
可以发现黑色部分其实就是这个字符串的“最短非空公共前后缀”。
所以先跑一遍KMP的前半部分,把\(nxt\)数组求出来。
然后遍历每个前缀,不断沿着\(nxt\)数组往前跳,直到找到该前缀的“最短非空公共前后缀”长度,记为\(j\)。
对于长度为\(i\)的前缀,如果\(j=0\),则答案不累加;否则答案累加\(i-j\)即可。
但是这样做会超时,不妨利用记忆化的思想,\(j\)每跳一下,就把\(nxt[i]\)更新为最新的\(j\),这样其他\(j\)再跳到\(i\)这个位置就能直接得到答案了。
点击查看代码
#include<bits/stdc++.h>
#define N 1000010
using namespace std;
int n,nxt[N];
long long ans;
string s;
int main(){cin>>n>>s;s=' '+s;//下标从1开始for(int i=2,j=1;i<=n;i++){while(s[i]!=s[j]&&j>1) j=nxt[j-1]+1;if(s[i]==s[j]) nxt[i]=j++;}for(int i=1;i<=n;i++){int j=nxt[i];while(nxt[j]) j=nxt[i]=nxt[j];if(j) ans+=i-j;}cout<<ans<<"\n";return 0;
}