【CF932G】Palindrome Partition(回文树,动态规划)

15 篇文章 0 订阅
8 篇文章 0 订阅

题面

CF
翻译:
给定一个串,把串分为偶数段
假设分为了 s1,s2,s3....sk
求,满足 s1=sk,s2=sk1...... 的方案数

题解

反正我是不会做
基本就是照着 laofu 的打了一遍(laofu太强啦)

这题分成了两个步骤
如果直接分 k 段我们是没法直接判断的
假设两段si,ski+1
因为 si=ski+1=x1x2.....xj
假设 si 的开始位置为 p
假设原串S的长度为 n
si=S[p]S[p+1]....S[p+j1]
ski+1=S[njp+1]S[njp+2]...S[np+1]
对应相等的关系是
S[p]=S[npj+1]
如果 p=1 考虑一下特殊情况
那么就是 S[1]=S[nj]
那么,如果我们有一个串 S
S=S[1]S[n]S[2]S[n2].....
那么,对应到上面的相等关系里面,
就是 S[1..j] 是一个回文串
其他的每一组对应关系也是如此
所以题目转换成了:
告诉你了 S 回答把 S 分为若干个长度为偶数的回文串的方案数
(如果没有搞清楚可以手玩一下)

这个就是一个裸的 dp
f[i] 表示 S[1..i] 划分为若干长度为偶数的回文串的方案数
得到转移方程:

f[i]=jf[j1]

其中, S[j,i] 是回文串
那么,每一个 j 对应的位置相当于是S[1..i]的回文后缀
构建出回文树之后沿着 last fail 就可以得到所有的 j 的位置
然后我们就写出来了一份代码:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<set>
#include<map>
#include<vector>
#include<queue>
using namespace std;
#define ll long long
#define RG register
#define MAX 1000100
#define MOD 1000000007
inline int read()
{
    RG int x=0,t=1;RG char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
    if(ch=='-')t=-1,ch=getchar();
    while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
    return x*t;
}
char ch[MAX],s[MAX];
int n,f[MAX];
struct Palindromic_Tree
{
    struct Node
    {
        int son[26];
        int ff,len;
    }t[MAX];
    int last,tot;
    void init()
    {
        t[tot=1].len=-1;
        t[0].ff=t[1].ff=1;
    }
    void extend(int c,int n,char *s)
    {
        int p=last;
        while(s[n-t[p].len-1]!=s[n])p=t[p].ff;
        if(!t[p].son[c])
        {
            int v=++tot,k=t[p].ff;
            t[v].len=t[p].len+2;
            while(s[n-t[k].len-1]!=s[n])k=t[k].ff;
            t[v].ff=t[k].son[c];
            t[p].son[c]=v;
        }
        last=t[p].son[c];
    }
}PT;
int main()
{
    PT.init();
    scanf("%s",ch+1);
    n=strlen(ch+1);
    if(n&1){puts("0");return 0;}
    for(int i=1;i<=n;i+=2)s[i]=ch[(i+1)/2];
    reverse(&ch[1],&ch[n+1]);
    for(int i=2;i<=n;i+=2)s[i]=ch[(i+1)/2];
    f[0]=1;
    for(int i=1;i<=n;++i)
    {
        PT.extend(s[i]-97,i,s);
        int p=PT.last;
        while(p!=1)
        {
            if(PT.t[p].len%2==0&&PT.t[p].len>0)f[i]=(f[i]+f[i-PT.t[p].len])%MOD;
            p=PT.t[p].ff;
        }
    }
    printf("%d\n",f[n]);
    return 0;
}

然后在CF上交一发:
这里写图片描述

看一看数据是啥呢?
这里写图片描述

如果我们像这份代码一样,沿着 fail 一路上跳
因为所有字符都相同
所以要跳 O(n)
复杂度变为了 O(n2)
完美 TLE

所以肯定就不能这么做。。。
然后我就不会了


假设对于某个位置,它对应的若干回文后缀

这里写图片描述

如果他们的长度 >len/2
可以证明他们的长度等差
这里写图片描述
证明?根据回文串的性质,上面的那些圈圈都是相等的串
证毕

如果把这一些看成一组
每一组都至少要 ÷2
所以至多只会有 log
这样能够保证复杂度,所以我们考虑能否一组一组转移
g[x] 表示这一组东西的和
因为一组不能直接在串中表示
所以用回文树上的节点来表示
所以, g[x] 表示从节点 x 开始,一直到缩短的值不再是当前这个等差的位置产生的贡献

这里写图片描述

比如对于这一组,它产生的贡献就是
g[x]=f[j1]+f[j2]+f[j3]
(最底下那个是原串)

好的,假设我们当前位置是 i
也就是最底下的原串是S[1..i]

最长的回文后缀,也就是当前回文树上的 last
p=last
因为是最长的一个,所以之前肯定不会计算过这个位置的值
所以 g[p]=f[j1]

然后往上面的那些回文后缀看
这里写图片描述
这些红色的代表着关于下面的父亲对称的回文前缀
我们惊奇的发现:
它们的开始位置怎么就这么巧呢?
正好就是我要算的东西
也就是说 g[p]+=g[p.fail]
当然,前提是 p.fail 还在这一组等差的串里面
然后 p 就跳到第一个不是它所在的等差的回文后缀的位置
所以f[i]=pg[p],前提是 i%2=0

最后,总结一下算法
对于给定的串 S
把它变成S=S[1]S[n]S[2]S[n1].....
然后依次构建回文树
每个节点要记录一下它所在的这个等差的回文后缀在哪个节点结束
然后对于每次的 last 节点向上跳
跳到第一个不是自己这一段回文的位置
因为每次至少要 ÷2 ,所以至多跳 log
综上,时间复杂度 O(nlog)
空间复杂度 O(n) 代表字符集大小

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<set>
#include<map>
#include<vector>
#include<queue>
using namespace std;
#define ll long long
#define RG register
#define MAX 1000100
#define MOD 1000000007
inline int read()
{
    RG int x=0,t=1;RG char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
    if(ch=='-')t=-1,ch=getchar();
    while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
    return x*t;
}
char ch[MAX],s[MAX];
int n,anc[MAX],diff[MAX];
int ans[MAX],f[MAX];
struct Palindromic_Tree
{
    struct Node
    {
        int son[26];
        int ff,len;
    }t[MAX];
    int last,tot;
    void init()
    {
        t[tot=1].len=-1;
        t[0].ff=t[1].ff=1;
        anc[0]=1;
    }
    void extend(int c,int n,char *s)
    {
        int p=last;
        while(s[n-t[p].len-1]!=s[n])p=t[p].ff;
        if(!t[p].son[c])
        {
            int v=++tot,k=t[p].ff;
            t[v].len=t[p].len+2;
            while(s[n-t[k].len-1]!=s[n])k=t[k].ff;
            t[v].ff=t[k].son[c];
            t[p].son[c]=v;
            diff[v]=t[v].len-t[t[v].ff].len;
            anc[v]=(diff[v]==diff[t[v].ff])?anc[t[v].ff]:t[v].ff;
        }
        last=t[p].son[c];
    }
}PT;
int main()
{
    PT.init();
    scanf("%s",ch+1);
    n=strlen(ch+1);
    if(n&1){puts("0");return 0;}
    for(int i=1;i<=n;i+=2)s[i]=ch[(i+1)/2];
    reverse(&ch[1],&ch[n+1]);
    for(int i=2;i<=n;i+=2)s[i]=ch[(i+1)/2];
    ans[0]=1;
    for(int i=1;i<=n;++i)
    {
        PT.extend(s[i]-97,i,s);
        for(int k=PT.last;k;k=anc[k])
        {
            f[k]=ans[i-PT.t[anc[k]].len-diff[k]];
            if(anc[k]!=PT.t[k].ff)
                f[k]=(f[k]+f[PT.t[k].ff])%MOD;
            if(!(i&1))ans[i]=(ans[i]+f[k])%MOD;
        }
    }
    printf("%d\n",ans[n]);
    return 0;
}
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值