聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃、两个人都想玩儿电脑(可是他们家只有一台电脑)……遇到这种问题,一般情况下石头剪刀布就好了,可是他们已经玩儿腻了这种低智商的游戏。
他们的爸爸快被他们的争吵烦死了,所以他发明了一个新游戏:由爸爸在纸上画n个“点”,并用n-1条“边”把这n个“点”恰好连通(其实这就是一棵树)。并且每条“边”上都有一个数。接下来由聪聪和可可分别随即选一个点(当然他们选点时是看不到这棵树的),如果两个点之间所有边上数的和加起来恰好是3的倍数,则判聪聪赢,否则可可赢。
聪聪非常爱思考问题,在每次游戏后都会仔细研究这棵树,希望知道对于这张图自己的获胜概率是多少。现请你帮忙求出这个值以验证聪聪的答案是否正确。
解法1_点分治:一看到概率以为是道概率题,还好输出不是求逆元(蒟蒻到现在还不会求逆元),显然所有的可能点对是n*n对,那么概率就是所有路径长度为3的倍数的点对除以n*n,约分的话求一下gcd即可。点分治 的思路是将路径分解为n类经过i点的路径进行处理,经过i点的路径实际上是解决以i点为根的子树的问题,因此可用树分治解决。整除3等价于余数相加%3为0,求出i点为根结点的子树内所有点到i点的距离%3的余数,并以余数分类计数:令dp[k]表示当前根结点为i时子结点的距离模3余数为k的点的个数,答案就是2*dp[1]*dp[2]+dp[0]*dp[0],当然这样会重复计算子树那些非简单路径,用容斥原理扣掉子树的答案即可。
#include<bits/stdc++.h>
using namespace std;
#define pii pair<int,int>
#define pb push_back
const int maxn = 2e4+10;
int n;
vector<pii> g[maxn];
int dp[4],num[maxn],f[maxn],sz,root;
bool done[maxn];
long long res = 0;
int gcd(int a,int b){
return !b?a:gcd(b,a%b);
}
void getroot(int s,int fa){
num[s]=1;f[s]=0;
for(int i = 0; i < g[s].size(); i++){
int v = g[s][i].first,w = g[s][i].second;
if(v==fa||done[v]) continue;
getroot(v,s);
f[s]=max(f[s],num[v]);
num[s]+=num[v];
}
f[s]=max(f[s],sz-num[s]);
if(f[root]>f[s]||root==0) root = s;
}
void dfs(int s,int fa,int val){
dp[val]++;
for(int i = 0; i < g[s].size(); i++){
int v = g[s][i].first,w = g[s][i].second;
if(v==fa||done[v]) continue;
dfs(v,s,(val+w)%3);
}
}
int cal(int s,int fa,int init){
int ans = 0;
memset(dp,0,sizeof(dp));
dfs(s,fa,init);
for(int i = 0; i < 3; i++)
ans+=dp[i]*dp[(3-i)%3];
return ans;
}
void solve(int s,int fa){
done[s]=true;res+=cal(s,fa,0);
for(int i = 0; i < g[s].size(); i++){
int v = g[s][i].first,w = g[s][i].second;
if(v==fa||done[v]) continue;
res-=cal(v,s,w%3);
}
}
void divide(int s){
solve(s,-1);
for(int i = 0; i < g[s].size(); i++){
int v = g[s][i].first,w = g[s][i].second;
if(done[v]) continue;
sz = num[v];root=0;
getroot(v,s);divide(root);
}
}
int main(){
scanf("%d",&n);
for(int i = 1 ;i < n; i++){
int x,y,w;
scanf("%d%d%d",&x,&y,&w);
g[x].pb(pii(y,w));
g[y].pb(pii(x,w));
}
sz = n;root = 0;getroot(1,-1);
divide(root);
int p = gcd(res,n*n);
printf("%d/%d\n",res/p,n*n/p);
return 0;
}
解法2_树形dp:树形dp和树分治思路相同,不同的地方在于处理,令dp[i][k]表示i结点为根节点,子节点到i结点的距离模3等于k的点的个数,树形dp不会重复计数,但因为点对有序(i,j 和 j,i 是不重复的答案),且(i,i)也为合法答案,因此最后答案要乘2加n。
#include<bits/stdc++.h>
using namespace std;
#define pii pair<int,int>
#define pb push_back
const int maxn = 2e4+10;
int n;
vector<pii> g[maxn];
int dp[maxn][4];
long long res = 0;
int gcd(int a,int b){
return b==0?a:gcd(b,a%b);
}
void dfs(int s,int fa){
dp[s][0] = 1;
dp[s][1]=dp[s][2]=0;
for(int i = 0; i < g[s].size(); i++){
int v = g[s][i].first,w = g[s][i].second;
if(v==fa) continue;
dfs(v,s);
for(int i = 0; i < 3; i++){
int p = (i+w)%3;
res+=dp[v][i]*dp[s][(3-p)%3];
}
for(int i = 0; i < 3; i++)
dp[s][(i+w)%3]+=dp[v][i];
}
}
int main(){
scanf("%d",&n);
for(int i = 1 ;i < n; i++){
int x,y,w;
scanf("%d%d%d",&x,&y,&w);
g[x].pb(pii(y,w));
g[y].pb(pii(x,w));
}
dfs(1,-1);
res=res*2+n;
int p = gcd(res,n*n);
printf("%d/%d\n",res/p,n*n/p);
return 0;
}
树形dp代码异常简短,且复杂度是o(n),不过这道题是点分治的板子题,当然要码一下点分治算法了。