CF1203D2 Remove the Substring (hard version) 题解

这题初赛让我白给了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数组满足:对于任意$0\leq i\leq 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$数组满足:对于任意$0\leq 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++。

posted on 2019-10-31 21:24  TEoS  阅读(206)  评论(0编辑  收藏  举报