天天看点

CF 494B 【Obsessive String】

很有趣的一道题

这道题提议很难懂,其实就是让你求合法的集合数目。合法的集合定义为:

1、集合中的所有串都是s的子串,且互不重叠 2、集合中的所有串都含有子串t。

看到网上很多题解说要用kmp,但我就不用...

因为仅需进行一个字符串匹配,而hash是很好写的匹配啊

而且kmp的next指针在dp中并没有起到作用。

说一下主体思路吧:

设两个字符串为s,t,长度分别为l1,l2

首先我们在原串中查找所有的位置i,使s中以i为结尾的子串与t匹配

对于所有的位置i,标记flag[i]=1;

然后我们进行dp

设dp[i]表示以选取的所有集合中集合的最后一个元素的结尾均为i,开头为j(j不体现在状态中,1<=j<=i-l2+1)的所有方案数

那么答案就是∑(i=1~l1)dp[i]

接下来我们考虑转移

首先,对于某一位置,如果flag[i]=0,我们有:

dp[i]=dp[i-1]

原因:如果到这一位置没有匹配上,那么说明这个位置只能被包含在前一个状态中。

那么,如果flag[i]=1,怎么办?

我们考虑集合中元素的个数:

如果只有一个元素,那么由于flag[i]=1,说明i-l2+1~i与t是可以完全匹配的,所以从1到i-l2+1都可以作为这个元素的起点,所以方案数为i-l2+1

如果有两个以上元素,那么最后一个元素的起点就可以是2~i-l2+1

那么我们设这个起点是k

于是上一个元素的终点就可以是1~k-1

所以如果起点是k,总方案数就是∑(i=1~k-1)dp[i]

那这个东西就可以用一个前缀和s来维护

而由于终点可以是2~i-l2+1,所以一共的总方案数就是:

∑(i=2-i-l2+1)s[i-1]

也就是:

∑(i=1-i-l2)s[i]

发现这也是一个前缀和的形式,于是我们把s维护成一个前缀和ss

结合以上两个分析,得到转移为:

dp[i]=i-l2+1+ss[i-l2]

边递推边维护即可。

#include <cstdio>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <queue>
#include <stack>
#define seed 13131
#define ull unsigned long long
#define mode 1000000007
#define ll long long
using namespace std;
ll dp[100005];
ll s1[100005];
ll s2[100005];
char s[100005];
char t[100005];
ull has,has1[100005];
ull v;
bool flag[100005];
int main()
{
    scanf("%s%s",s+1,t+1);
    v=1;
    int l1=strlen(s+1),l2=strlen(t+1);
    for(int i=1;i<=l1;i++)
    {
        has1[i]=has1[i-1]*seed+s[i]-'a'+1;
    }
    v=1;
    for(int i=1;i<=l2;i++)
    {
        has=has*seed+t[i]-'a'+1;
        v*=seed;
    }
    for(int i=l2;i<=l1;i++)
    {
        int st=i-l2;
        ull hast=has1[i]-has1[st]*v;
        if(hast==has)
        {
            flag[i]=1;
        }
    }
    for(int i=1;i<=l1;i++)
    {
        if(!flag[i])
        {
            dp[i]=dp[i-1];
        }else
        {
            dp[i]=((i-l2+1)+s2[i-l2])%mode;
        }
        s1[i]=(s1[i-1]+dp[i])%mode;
        s2[i]=(s2[i-1]+s1[i])%mode;
    }
    printf("%lld\n",s1[l1]);
    return 0;
}