大致题意:给你一个字符串,让你把它分为k个部分,k为偶数。设这k个部分分别表示为s1,s2,...,sk,问使得对于每一个i都满足的划分方法有多少种,也即使得这k个部分构成回文的方法有多少种。
由于是回文,我们当然要想办法往回文上面去靠。根据题意有
我们假设第一段长度为j,令
有
如果我们把排列方式换一下,令
我们可以发现,S'恰好是一个回文串。
那么我们推广一下,我们不仅仅是把S1和Sk这么表示,我们把整个串这么表示。
也即,令,最后发现问题变成了,求把S'分成偶数个回文串的方案数。
我们考虑这个问题,显然是可以建立一个回文自动机/回文树,然后做一个简单的dp。我们令dp[i]表示到S'的第i个字符时候的方案数,显然当i为奇数时dp[i]为0。状态转移方程也很容易得到,dp[i]=Σdp[j],要求子串是回文串。具体实现,在回文自动机上也是很容易实现。每次插入一个字符,对于对应的last节点,遍历它所有的fail指针指向的节点,这些节点的长度对应转移方程中的j。
然而,这个dp看似非常的简单,但是实际上并不能通过这道题目。注意到,每次我们需要遍历一个节点所有的fail指针指向的点。如果我们输入的字符串全部是一个字符,那么每次添加一个字符就会遍历之前加入的所有字符,也即算法退化成了平方的算法,对于百万的数据显然是会TLE的。
下面开始讨论一个关于回文串的一个很巧妙的性质。
之所以会出现退化,是因为关联的fail指针节点太多了,例如下面一组数据:
ab
abab
ababab
abababab
ababababab
多观察几组数据,我们可以得出一个很显然的结论。
即如果一个字符串,设它的长度为len,那么它的所有长度大于len/2的fail指针节点构成一个等差数列。
这个结论反过来,如果该串有n个fail指针的节点,那么后n/2个串一定构成等差数列。
如果我们利用这个性质,把每一组等差数列和为一组一起求和计算,那么每次最多只有log个组,计算的代价也就是log。
具体来说,对于每一个串对应的位置,根据它与fail的长度差确定当前是否是等差数列的首项,如果不是则从fail那继承记录下来的首项。然后在dp的过程中,每次从当前位置直接往前跳到首项,每次往前跨越一个等差数列,跨越的同时统计贡献的前缀和。然后如果当前位置是偶数,那么更新dp数组。具体看代码吧,还是没有说的太清楚:
#include
#define INF 0x3f3f3f3f
#define fi first
#define se second
#define LL long long
#define sc(x) scanf("%d",&x)
#define scc(x,y) scanf("%d%lld",&x,&y)
#define sccc(x,y,z) scanf("%d%d%d",&x,&y,&z)
using namespace std;
const int mod = 1e9 + 7;
const int N = 1000100;
int dp[N],f[N],nxt[N],d[N],len;
char s[N],ss[N];
struct Palindromic_Tree
{
int tot,last;
struct node{int ch[26],fa,len;} T[N];
inline void init()
{
memset(T,0,sizeof(T));nxt[0]=1;
T[tot=1].len=-1,T[0].fa=T[1].fa=1;
}
inline void ins(int c,int n,char *s)
{
int p=last;
while(s[n-T[p].len-1]!=s[n])p=T[p].fa;
if(!T[p].ch[c])
{
int v=++tot,k=T[p].fa;
T[v].len=T[p].len+2;
while(s[n-T[k].len-1]!=s[n])k=T[k].fa;
T[v].fa=T[k].ch[c];T[p].ch[c]=v;
d[v]=T[v].len-T[T[v].fa].len;
nxt[v]=d[v]==d[T[v].fa]?nxt[T[v].fa]:T[v].fa;
}
last=T[p].ch[c];
}
}PT;
int main()
{
scanf("%s",s);
len=strlen(s);
if (len&1)
{
puts("0");
return 0;
}
dp[0]=1;
PT.init();
for(int i=0;i
你可能感兴趣的:(回文自动机,---------Online,Judge--------,CodeForces,自动机dp)