这题初赛让我白给了6分,于是我决定回来解决一下它。
说实话,看原题题面和看CCF代码真是两种完全不同的感受……
------------
思路分析:
把$s$串删去一部分之后,会把$s$串分成两部分,当然其中一部分有可能为空。$t$串作为$s$串的字串,在删去一部分之后也会被分为两部分。因此我们可以枚举$t$串被分开的位置,然后进行计算。
设$t$串在$t[p]$和$t[p+1]$之间被分开,$s$串在$s[i]$和$s[i+1]$之间被分开。因为要使答案最大,因此我们要让$s$串的左半部分包含且仅包含一个$t$串的左半部分,右半部分也是一样。
因此,按照CCF的讲法,设$s$串和$t$串的长度分别为$slen$和$tlen$,我们可以设$head[i]=p$表示$s[0...i]$包含且仅包含一个子串为$t[0...p]$,设$back[i]=p$表示$s[i...slen-1]$包含且仅包含一个字串为$t[p...tlen-1]$。
如何递推$head$和$back$数组?很显然了,用两个指针分别指向$s$串和$t$串,表示当前位置,相同即匹配成功。
因为指针初值需要设置为0,因此以下代码将$s$串和$t$串都整体后移了一位。
p=1; for(int i=1;i<s.size();i++) { head[i]=head[i-1]; if(s[i]==t[p]) head[i]=p++; } p=t.size()-1;back[s.size()]=t.size(); for(int i=s.size()-1;i;i--) { back[i]=back[i+1]; if(s[i]==t[p]) back[i]=p--; }
递推出$head$和$back$数组后,枚举断点计算答案就行了。用两个指针$i,j$分别指向删除的部分两端。为了让答案最大,应该找到满足$head[i]=p$的最小的$i$,以及满足$back[j]=p+1$的最大的$j$,这两个步骤用两个while循环就可以轻松搞定了。有一个需要注意的点,当被分开的两部分其中一部分包含整个$t$串时,另一部分取空串是最优的,这个需要特判一下。答案即为$j-i-1$。计算答案时也要特判不合法的情况。各个指针的初值也需要注意。
#include<iostream> #include<cstdio> #include<string> using namespace std; const int N=1e6; int p=1,ans; int head[N],back[N]; string s,t; int main() { cin>>s>>t;s='.'+s,t='.'+t;//后移一位 for(int i=1;i<s.size();i++) { head[i]=head[i-1]; if(s[i]==t[p]) head[i]=p++; } p=t.size()-1;back[s.size()]=t.size(); for(int i=s.size()-1;i;i--) { back[i]=back[i+1]; if(s[i]==t[p]) back[i]=p--; } p=0; for(int i=0,j=1;i<s.size(),j<s.size(),p<t.size();p++) { while(head[i]<p && i<s.size()) i++; while(back[j+1]<=p+1 && j<s.size()) j++;//找到最优的i和j if(p==t.size()-1) j=s.size();//贪心地让另一部分为空串 if(i<s.size() && j<=s.size())//合法才更新答案 ans=max(ans,j-i-1); } printf("%d",ans); return 0; }
因为指针$i,j$一定是递增的,因此时间复杂度$O(n)$。
------------
既然是初赛原题,那么再来看看CCF的代码:(实测比我的代码快15ms...不过我的代码有的地方常数的确比较大)
#include<iostream> #include<string> using namespace std; const int maxl=1e6;//改了一下数组大小,可以过困难版 string s,t; int pre[maxl],suf[maxl];//分别相当于head和back数组 int main(){ cin>>s>>t; int slen=s.length(),tlen=t.length(); for(int i=0,j=0;i<slen;++i){ if(j<tlen && s[i]==t[j]) ++j; pre[i]=j; } for(int i=slen-1,j=tlen-1;i>=0;--i){ if(j>=0 && s[i]==t[j]) --j; suf[i]=j; } suf[slen]=tlen-1;//递推基本相同,表示略有区别 int ans=0; for(int i=0,j=0,tmp=0;i<=slen;++i){ while(j<=slen && tmp>=suf[j]+1) ++j; ans=max(ans,j-i-1); tmp=pre[i]; } cout<<ans<<endl; return 0; }
可以发现思路实际上大体相同,但是计算答案时略有差别。
分析一下这个计算答案的过程,枚举$s$的断点。可以发现,对于当前的循环,$tmp=pre[i-1]$,即$t[0...tmp-1]$是$s[0...i-1]$的字串,那么接下来为了使答案最大,应该找到满足$t[tmp...tlen-1]$是$s[j...slen-1]$的$j$。而这份代码中的while循环找到的$j$应该会比我们要找的$j$大1,因此要删除的部分就是$s[i...j-2]$,长度$j-i-1$。
然后分析一下题目:(理解了题意之后感觉想错都难qwq)
1.
Q:程序输出时,suf数组满足:对于任意$0leq ileq slen$,$suf[i]leq suf[i+1]$。
A:T,显然$suf$数组是递增的。
2.
Q:当$t$是$s$的子序列时,输出一定不为0。
A:F,反例样例3。
3.
Q:程序运行到第23行时,“j-i-1”一定不小于0。
A:F,在本题中的确不会出现这种情况,但初赛题目中可没保证$t$是$s$的子串,$t$不是$s$的字串时就会出现这种情况。
4.
Q:当$t$是$s$的子序列时,$pre$数组和$suf$数组满足:对于任意$0leq i<slen$,$pre[i]>suf[i+1]+1$。
A:F,反例样例1。
5.
Q:若$tlen=10$,输出为0,则$slen$最小为( )。
A:输出为0说明$t$不是$s$的子序列或者$s=t$,显然前者可以使答案更小,即$t$越短越好。由于cin不能输入空串,因此最短只能是1。
6.
Q:若$tlen=10$,输出为2,则$slen$最小为( )。
A:$s$最多删去两个字符使$t$仍是$s$的字串,显然$t$串最短长度为12。
最后祝大家CSP-J/S 2019rp++。