题目
虽然探索金字塔是极其老套的剧情,但是有一队探险家还是到了某金字塔脚下。经过多年的研究,科学家对这座金字塔的内部结构已经有所了解。首先,金字塔由若干房间组成,房间之间连有通道。如果把房间看作节点,通道看作边的话,整个金字塔呈现一个有根树结构,节点的子树之间有序,金字塔有唯一的一个入口通向树根。并且,每个房间的墙壁都涂有若干种颜色的一种。
探险队员打算进一步了解金字塔的结构,为此,他们使用了一种特殊设计的机器人。这种机器人会从入口进入金字塔,之后对金字塔进行深度优先遍历。机器人每进入一个房间(无论是第一次进入还是返回),都会记录这个房间的颜色。最后,机器人会从入口退出金字塔。
显然,机器人会访问每个房间至少一次,并且穿越每条通道恰好两次(两个方向各一次), 然后,机器人会得到一个颜色序列。但是,探险队员发现这个颜色序列并不能唯一确定金字塔的结构。现在他们想请你帮助他们计算,对于一个给定的颜色序列,有多少种可能的结构会得到这个序列。因为结果可能会非常大,你只需要输出答案对10^9 取模之后的值。
题解
区间DP
容易设计状态表示f[l][r]表示s[l~r]可能对应多少中金字塔方案数。
转移相当棘手!
以区间[l,r]为例,s[l]一定为子树的根节点,s[l+1](有的话)一定为s[l]一个子节点,接下来的子树就是乱七八槽的了。
我们不妨先考虑一个简化版问题,把颜色序列改成编号,即没有两个节点有相同的颜色。
根据DFS序的思想,我们可以得到,当s[l]重复出现时,机器人一定重新回到了根节点。
我们可以把一棵子树拆成 以s[l+1]为根节点的子树 + (以s[l]为根节点的子树 - s[l+1]为根节点的子树)。
写一个solve函数来解决以s[l]为根节点,区间[l,r]的金字塔方案数。
对于每个区间可以枚举一个划分点k,这个k必须使得[k,r](包含k及其右边)是一棵完整的树,即s[k]==s[r];自然的[l+1,k-1]也会是一棵除去根节点s[l]的完整的树。两个子问题均可以用solve函数解决。
对于所有的k(l+2<=k<=r)的方案数要累加,对于两个子问题的答案要相乘。
如果允许颜色相同,最大的影响就是可能会重复计数。
对于样例ABABABA,产生
ABABABA
B B B
\ | /
A
有可能有两种情况:
B ABABA ABABABA
B B B B B B
\ + | / = \ | /
A A
或
BAB AB ABABABA
B B B B B B
\ | + / = \ | /
A A A
但是由于我们出色的DP方式,这个问题并不存在。在我们的转移方法中,我们会通过上面的方法得到这种情况。因为对于下面的情况,加号左边不是一棵不包含根的子树。
更抽象地说,我们的不含根子树的颜色序列长度为(k-1)-(l+1)+1,k在不断的增大,意味着这个子树的大小在不断增加。又因为“我们认为子树之间是有序的”,即对于以s[l]为根的所有子树大小(1,2)和(2,1)计入两次,而(1,1,1)计入一次。所以只要第一棵子树的大小在改变,其方案数一定是新的贡献。
下面给出转移方程数学表达式:
若k=r-1,
若k=r,
综上,我们可以简化代码成:
初始化:
代码
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn=310;
const ll mod=1e9;
char s[maxn];int n;
int f[maxn][maxn];
int solve(int l,int r)
{
if(f[l][r]!=-1) return f[l][r];
if(l>r) return f[l][r]=0;
if(l==r) return f[l][r]=1;//初态
f[l][r]=0;
for(int k=l+2;k<=r;k++)
if(s[l]==s[k])
f[l][r]=(f[l][r]+1ll*solve(l+1,k-1)*solve(k,r)%mod)%mod;
for(int k=l+2;k<=r;k++)
return f[l][r];
}
int main()
{
scanf("%s",s+1);
n=strlen(s+1);
memset(f,-1,sizeof(f));
printf("%d\n",solve(1,n));
return 0;
}