聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃、两个人都想玩儿电脑(可是他们家只有一台电脑)……遇到这种问题,一般情况下石头剪刀布就好了,可是他们已经玩儿腻了这种低智商的游戏。他们的爸爸快被他们的争吵烦死了,所以他发明了一个新游戏:由爸爸在纸上画n个“点”,并用n-1条“边”把这n个“点”恰好连通(其实这就是一棵树)。并且每条“边”上都有一个数。接下来由聪聪和可可分别随即选一个点(当然他们选点时是看不到这棵树的),如果两个点之间所有边上数的和加起来恰好是3的倍数,则判聪聪赢,否则可可赢。聪聪非常爱思考问题,在每次游戏后都会仔细研究这棵树,希望知道对于这张图自己的获胜概率是多少。现请你帮忙求出这个值以验证聪聪的答案是否正确。
Input
输入的第1行包含1个正整数n。后面n-1行,每行3个整数x、y、w,表示x号点和y号点之间有一条边,上面的数是w。
Output
以即约分数形式输出这个概率(即“a/b”的形式,其中a和b必须互质。如果概率为1,输出“1/1”)。
Sample Input
5
1 2 1
1 3 2
1 4 1
2 5 3
Sample Output
13/25
【样例说明】
13组点对分别是(1,1) (2,2) (2,3) (2,5) (3,2) (3,3) (3,4) (3,5) (4,3) (4,4) (5,2) (5,3) (5,5)。
【数据规模】
对于100%的数据,n<=20000。
线性结构在分治时一般选择二分,因为这样可以使得分出来子结构中最大的那个最小,划分的次数少,递归的深度小
同样的,对树进行分治时,也要尽可能让分出来的最大的子树小,这样递归的深度才小
树的重心: 以一个点为根,若这个点的最大子树最小,则这个点是树的重心
树重心的求法:
先假设一个重心,让它的最大子树大小为inf
dfs整棵树,对每个点,记录它最大子树的大小,如果它的最大子树大小比重心的最大子树要小,更新重心
因为树本身是无向的,但dfs是随便找一个点 当根,有向地搜的,所以父亲结点那边也是一棵子树。
int root,f[maxn],vis[maxn],siz[maxn],sum;//f[u]表示以u为根的最大子树的大小
void getroot(int u,int fa)
{
siz[u] = 1, f[u] = 0;
for(int i=head[u];~i;i=e[i].next)
{
int v = e[i].to;
if(v==fa || vis[v]) continue;
getroot(v,u);
siz[u] += siz[v];
f[u] = max(f[u],siz[v]);//找子树大小的最大值
}
f[u] = max(f[u],sum - siz[u]);//把父亲看作是连在上方的子树,getroot前把sum初始化为整棵树的大小
if(f[u] < f[root])
root = u;
}
找完重心就以重心为根dfs处理子树信息
处理树中路径信息时,有两种情况: 经过根节点(跨子树) 和 不经过根节点(在一棵子树中)
处理的时候是无论经过与否一并统计的。可以发现,不经过根节点的路径在某个子树中一定是经过那个子树的根的
也就是说这部分的信息计了两次。把子树的信息算一遍减掉得到的就是正确答案了。
int t[3];
void getdis(int u,int dis,int fa)//把u为根的子树的信息全处理出来
{
++t[dis];
for(int i=head[u];~i;i=e[i].next)
{
int v = e[i].to;
if(v!=fa&&!vis[v])
getdis(v,(dis + e[i].w)%3,u);
}
}
int calc(int u,int dis)
{
t[0] = t[1] = t[2] = 0;
getdis(u,dis%3,0);
return t[0]*t[0] + 2*t[1]*t[2];
//t[0]记录的是子树中到u的距离为3的倍数的点,这些点两两间的路径显然距离也是3
//t[1]的点到t[2]的点的路径距离是3的倍数,正反两个方向*2
}
int ans;
void solve(int u)//先加上u的贡献,再减去u的子树v的贡献
{
ans += calc(u,0);
vis[u] = 1;//相当于删除u这个点
for(int i=head[u];~i;i=e[i].next)
{
int v = e[i].to;
if(vis[v]) continue;
ans -= calc(v,e[i].w);
root = 0, sum = siz[v];
f[root] = 0x7fffffff;
getroot(v,0);
solve(root);
}
}
为什么减去的部分一开始是e[i].w呢, 因为getdis算出来的是子树中每个点到根的距离,而不经过根的路径每次计算必然都算多了根和根的儿子这一路径
完整代码
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 2e4 + 5;
int N;
struct Edge
{
int to, next, w;
}e[maxn<<1];
int edgenum,head[maxn];
void add(int u,int v,int w)
{
e[edgenum].to = v;
e[edgenum].next = head[u];
e[edgenum].w = w;
head[u] = edgenum++;
}
int root,f[maxn],vis[maxn],siz[maxn],sum;//f[u]表示以u为根的最大子树的大小
void getroot(int u,int fa)
{
siz[u] = 1, f[u] = 0;
for(int i=head[u];~i;i=e[i].next)
{
int v = e[i].to;
if(v==fa || vis[v]) continue;
getroot(v,u);
siz[u] += siz[v];
f[u] = max(f[u],siz[v]);//找子树大小的最大值
}
f[u] = max(f[u],sum - siz[u]);//把父亲看作是连在上方的子树
if(f[u] < f[root])
root = u;
}
int t[3];
void getdis(int u,int dis,int fa)//把u为根的子树的信息全处理出来
{
++t[dis];
for(int i=head[u];~i;i=e[i].next)
{
int v = e[i].to;
if(v!=fa&&!vis[v])
getdis(v,(dis + e[i].w)%3,u);
}
}
int calc(int u,int dis)
{
t[0] = t[1] = t[2] = 0;
getdis(u,dis%3,0);
return t[0]*t[0] + 2*t[1]*t[2];
//t[0]记录的是子树中到u的距离为3的倍数的点,这些点两两间的路径显然距离也是3
//t[1]的点到t[2]的点的路径距离是3的倍数,正反两个方向*2
}
int ans;
void solve(int u)//先加上u的贡献,再减去u的子树v的贡献
{
ans += calc(u,0);
vis[u] = 1;//相当于删除u这个点
for(int i=head[u];~i;i=e[i].next)
{
int v = e[i].to;
if(vis[v]) continue;
ans -= calc(v,e[i].w);
root = 0, sum = siz[v];
f[root] = 0x7fffffff;
getroot(v,0);
solve(root);
}
}
void init(int N)
{
ans = root = 0;
sum = N;
f[0] = 0x7fffffff;
++N;
memset(head,-1,4*N);
memset(vis,0,4*N);
}
int gcd(int a,int b)
{
return b==0 ? a : gcd(b,a%b);
}
int main()
{
int u,v,w,a;
scanf("%d",&N);
init(N);
for(int i=0;i<N-1;++i)
{
scanf("%d%d%d",&u,&v,&w);
add(u,v,w%3);
add(v,u,w%3);
}
getroot(1,0);//随便找一个点把无根树拉成有根树,第一个参数填什么都行(只要<=N,填1最稳)
//跑完getroot之后root就是树的重心了
solve(root);
a = gcd(N*N,ans);
printf("%d/%d\n",ans/a,N*N/a);
return 0;
}