【LOJ6172】Samjia 和大树(树形DP+猜结论)
大致题意: 给定一棵树,你可以给每个点赋一个\(1\sim m\)之间的整数权值,要求所有相邻点权值之差的绝对值\(\ge k\)。求方案数。
暴力\(DP\)
首先我们考虑暴力\(DP\),显然就是设\(f_{i,j}\)表示第\(i\)个点权值为\(j\)时,填完\(i\)子树的方案数。
前缀和/后缀和优化一下转移,就可以做到\(O(Tnm)\)。
但由于\(m\)过大,这个方法无法接受。
猜结论
根据数据范围,盲猜可以把值域通过一些奇奇怪怪的方式压到\(O(nk)\)级别。
然后思索了半个小时,最后猜出一个结论:\(DP\)数组除了左右两侧的\(nk\)个元素之外,中间那一大串的值都是相等的。
会猜出这个结论自然是有一定依据的,不过更多是感性的臆想。。。
但结论的验证是非常简单的,只要用暴力\(DP\)随便造个数据打个表即可。
有了这个结论,剩下的应该就非常简单了吧。
\(Upt\)
- 这个猜想实际上可以通过归纳法来证明。
- 其实还容易发现,\(DP\)数组还是对称的,因此完全可以写得更简单,是我写复杂了。
代码
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100
#define S 10000
#define X 1000000007
#define add(x,y) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y)
#define Inc(x,y) ((x+=(y))>=X&&(x-=X))
using namespace std;
int n,m,k,ee,lnk[N+5];struct edge {int to,nxt;}e[2*N+5];
namespace BF//小数据跑暴力
{
int f[N+5][2*S+5],g1[2*S+5],g2[2*S+5];
I void DP(CI x,CI lst=0)
{
RI i,j;for(i=1;i<=m;++i) f[x][i]=1;for(i=lnk[x];i;i=e[i].nxt) if(e[i].to^lst)
{
for(DP(e[i].to,x),j=1;j<=m;++j) g1[j]=(g1[j-1]+f[e[i].to][j])%X;
for(j=m;j;--j) g2[j]=((j==m?0:g2[j+1])+f[e[i].to][j])%X;
for(j=1;j<=m;++j) f[x][j]=1LL*f[x][j]*
(0LL+(j>k?g1[j-k]:0)+(j+k<=m?g2[j+k]:0)+(k?0:X-f[e[i].to][j]))%X;
}
}
I void Solve()
{
RI i,t=0;for(DP(1),i=1;i<=m;++i) Inc(t,f[1][i]);printf("%d\n",t);
}
}
namespace TreeDP//大数据利用猜得的结论
{
int w,s,f[N+5][2*S+5],g1[2*S+5],g2[2*S+5];
I int Get1(CI x)//求前缀和,分三类讨论(其实我写复杂了)
{
if(x<=S) return g1[x];if(x<=m-S) return (1LL*(x-S)*s+g1[S])%X;
return (1LL*s*(m-w)+g1[x-(m-S)+S+1])%X;
}
I int Get2(CI x)//求后缀和,同样分三类讨论
{
if(x>=m-S) return g2[x-(m-S)+S+1];if(x>S) return (1LL*(m-S-x)*s+g2[S+1])%X;
return (1LL*s*(m-w)+g2[x])%X;
}
I void DP(CI x,CI lst=0)//动态规划
{
#define T(x) (((x)>k?Get1((x)-k):0)+((x)+k<=m?Get2((x)+k):0))
RI i,j;for(i=1;i<=w;++i) f[x][i]=1;for(i=lnk[x];i;i=e[i].nxt) if(e[i].to^lst)//枚举子节点
{
DP(e[i].to,x),s=f[e[i].to][S+1];//先DP子节点
for(j=1;j<=w;++j) g1[j]=(g1[j-1]+f[e[i].to][j])%X;//前缀和
for(j=w;j;--j) g2[j]=((j==w?0:g2[j+1])+f[e[i].to][j])%X;//后缀和
for(j=1;j<=S+1;++j) f[x][j]=1LL*f[x][j]*T(j)%X;//DP
for(j=1;j<=S;++j) f[x][S+1+j]=1LL*f[x][S+1+j]*T(m-S+j)%X;//DP
}
}
I void Solve()
{
RI i,t=0;for(w=2*S+1,DP(1),i=1;i<=w;++i) Inc(t,f[1][i]);
printf("%d\n",(1LL*f[1][S+1]*(m-w)+t)%X);//注意答案的输出
}
}
I int QP(RI x,RI y) {RI t=1;W(y) y&1&&(t=1LL*t*x%X),x=1LL*x*x%X,y>>=1;return t;}//快速幂
int main()
{
RI Tt,i,x,y;scanf("%d",&Tt);W(Tt--)
{
for(scanf("%d%d%d",&n,&m,&k),ee=0,i=1;i<=n;++i) lnk[i]=0;//清空
for(i=1;i^n;++i) scanf("%d%d",&x,&y),add(x,y),add(y,x);//建树
if(!k) printf("%d\n",QP(m,n));else if(m<=2*S) BF::Solve();else TreeDP::Solve();//分情况采用不同的解法
}return 0;
}
待到再迷茫时回头望,所有脚印会发出光芒