【国家集训队】聪聪可可 ——树形DP

时间:2024-01-05 21:22:02

感觉是一道很妙的树形DP题,充分利用到了树的性质(虽然说点分治也可以做,,,,但是本蒟蒻不会啊)

然而某Twilight_Sx大佬表示这道题真的非常水,,,本蒟蒻也只能瑟瑟发抖了

本蒟蒻表示还是要经过一些思考的吧

虽然说是要获取概率,但是要输出分数形式,显然直接算可能获胜的次数再除所有可能,并且用gcd约分会更好,

对于每个点而言,要么做中转(LCA),要么是其中一个点,由于是树,统计这些点对可以保证不重不漏,

所以枚举到每一个点时,f[i][j],表示在i的子树中,到i这个节点距离为j的个数,

如果这个点是其中一个点,那么任意一个子树对它的贡献是能够和它到对应儿子的距离互补的节点个数,

也就是说设点i到它的某个儿子k的距离为t(mod 3),那么这个子树对ans的贡献是f[k][3-t].

同时因为f[k][j]不包括k,所以要特判加入i,k这个点对

如果这个点是中转(LCA),那么每次枚举到一个子树时,这个子树对它的贡献是:

设当前节点到这个儿子k的距离为t(mod 3),

那么由于每次枚举后都会把子树对f[i]的贡献统计进来,所以当前的f[i][j]就是之前枚举到的子树里,

到当前节点i的距离为j的点的个数,因此对于现在枚举到的这个子树,依次枚举0~2(j的大小)

那么f[k][j]的点对应到之前的子树就应该要对应到3-(j+t)%3上,因为这样加起来才是3的倍数,

(因为这个中转到f[k][j]中的点的距离是(j+t)%3 (mod 3)),

并且跟普通枚举防止重复同理,每棵子树只统计它和之前就枚举到的子树里的点对,就可以防止一个点对重复被枚举到,

但是这样统计的话,由于每次是统计新增子树对之前子树的贡献,而f[i][j]代表的是子树中的,而不包括自己,

因此对于i而言,它做中转,枚举到子树k时,它之前的子树里面可选的都已经包括到f[i]当中了,

但是f[k]本来就是不包括k的,所以就会遗漏点k到i之前的子树中的点对的贡献,因此就要特判加入。

判断加入距离为t(mod 3)时,f[i][3-t]即可,但是对于t == 0,那么对面肯定不能选j == 3,

所以这个要特判,如果t == 0,那么加入的是f[i][0],同理,对于之前统计子树的情况,

(f[k][j]中(j+t)%3 也可能 == 0,所以这个时候要加入f[i][0])

 #include<bits/stdc++.h>
using namespace std;
#define R register int
#define LL long long
#define AC 40400
int n;
int date[AC],Next[AC],Head[AC],tot,value[AC];
int f[AC][],ans;
bool z[AC];
inline int read()
{
int x=;char c=getchar();
while(c>'' || c<'') c=getchar();
while(c>='' && c<='') x=x*+c-'',c=getchar();
return x;
} inline void add(int f,int w,int S)
{//因为不知道哪个是根,所以要加双向边
date[++tot]=w,Next[tot]=Head[f],value[tot]=S,Head[f]=tot;
date[++tot]=f,Next[tot]=Head[w],value[tot]=S,Head[w]=tot;
} inline int gcd(int x,int y)
{
int t;
while(y)
{
t=x%y;
x=y;
y=t;
}
return x;
} void pre()
{
R a,b,c;
n=read();
for(R i=;i<n;i++)
{
a=read(),b=read(),c=read();
add(a,b,c);
}
} void DFS(int x)
{
R k;
z[x]=true;
for(R i=Head[x]; i ;i=Next[i])
{
k=date[i];
if(z[k]) continue;//跳过父亲
DFS(k);
int t=value[i]%;
if(!t)
{
ans++;//如果直接就是一个点对,那就加上
ans+=f[k][];//并且加上距离为0的点
}
else ans+=f[k][-t];//不然取互补点
//以上是x为点对中的一个点的情况,以下为中转
if(!t)//因为x做中转时,k还没统计进来,所以要特判k与之前子树所形成的点对
ans+=f[x][];//如果t是0的话,就直接加距离为0的点就可以了
else ans+=f[x][-t];//不然加互补的
for(R j=;j<;j++)
{
int go= - (t + j) % ;//到儿子的距离为j,那么到x的距离就为(t+j)%3,所以互补就是3 - (t + j) % 3;
if(go == ) go=;//如果go为3,那么实际距离应该是0,不过貌似可以通过对go取mod的方式避免特判?
ans+=f[k][j] * f[x][go];
}
for(R j=;j<;j++)//error!!!统计入f这种事应该在统计完ans之后才可以做,不然就无法保证当前f[x]里面一定是之前的子树了
f[x][(t+j)%]+=f[k][j];//到k的距离为j,那么实际应该贡献给f[x][(t+j)%3](算上儿子的子树)
f[x][t]++;//算上儿子
}
} void work()
{
DFS();
//printf("%d\n",ans);
ans=ans*+n;//两个点不重合的方案可以互换--->*2,可以两个人选一个点--->+n
int k=n*n,g=gcd(ans,k);//全部方案
printf("%d/%d\n",ans/g,k/g); /*for(R i=1;i<=n;i++)
{
for(R j=0;j<3;j++)
printf("%d ",f[i][j]);
printf("\n");
}*/
}
int main()
{
// freopen("in.in","r",stdin);
pre();
work();
// fclose(stdin);
return ;
}