ak树(点分治模板)

前言:重拾信竞,模板忘得很干净。所以写一篇博客,以供复习。
【题目】
描述
XXX正在和别人玩一个游戏:在一棵树上随机取两个点,如果这两个点的距离是4的倍数,那么算XXX赢,否则对方赢。现在XXX想知道他能获胜的概率。

输入
本题多组数据。对于每组数据:
第一行一个数n,表示树上的节点个数
接下来n-1条边a,b,c描述a到b有一条长度为c的路径
当n=0时表示读入结束
数据组数不超过10。无部分分

输出
最终输出的概率要求分数的分子和分母的和尽量小且非负数

【思路】
点分治是一种适合解决树上路径统计的问题。
我们考虑这样处理一棵树,找出重心,处理经过这个重心路径的问题,然后删除重心并递归地处理以重心为根子树。
那么我们可以明确以下几点:

1.所有节点都会在某次处理时成为重心。

2.如果当前重心子树的重心作为当前重心的儿子,那么这样将会形成一颗新的树,并且这个树的深度不会超过 log ⁡ n \log n logn(可根据重心性质证明),我们称这颗树为点分树。

3.每条路径会且仅会在处理路径上所有点中最先成为重心的点(即在点分树上深度最小)时被统计,所以可以保证答案不重不漏。

那么点分治的思想简单介绍到这里。那么这道题就重点在于如何快捷地统计答案了。这道题统计路径就是统计点对。我们通过枚举子树处理子树的方式来处理重心,我们定义两个数组,pre[4]和num[4]。pre[i]表示已经处理过所有子树中到当前重心的距离模4为i的节点数,num[i]表示正在处理这个子树中到当前重心的距离模4为i的节点数。对于num数组,我们可以搜索正在处理的子树的方式进行更新。对于pre数组,我们可以在处理完当前子树后把num累加到pre数组中。根据乘法原理,距离为4的倍数的点对数目显然可以通过每枚举一颗子树的时候就累加一下 ∑ i = 0 3 p r e [ i ] ∗ n u m [ ( 4 − i ) m o d    4 ] \sum_{i=0}^{3}pre[i]*num[(4-i)\mod 4] i=03pre[i]num[(4i)mod4]。这样每个点对会且只会在处理第二个点所在子树时被统计到。由于统计的点对位于重心的不同子树,保证了统计的点对路径一定通过了重心。根据样例,点对有序,所以统计的数目需要乘2;两个相同的点构成的点对也需要统计到,所以需要再累加一个n。这样从点分树的角度来看每一层(深度相同)节点成为重心的复杂度都是均摊O(n)的,最多不超过 log ⁡ n \log n logn层,时间复杂度为 O ( n log ⁡ n ) O(n\log n) Onlogn

以下为部分数组、变量含义和代码:
vis[i]:表示第i个点是否已经成为过重心(被删除)。
siz[i]:表示以当前节点为根的子树大小。
son[i]:表示第i个点最大的一个子树的大小。
root:表示当前的重心。
size:表示当前处理的尚未找到重心的子树的大小。
(多组数据,仔细考虑每个数组是否需要清零)

代码:

#include<bits/stdc++.h>
#define re register
using namespace std;
int n,a,b,c;
const int N=2e4+5;
struct node{int u,v,w;}e[N<<1|1];
int nxp[N<<1|1],f[N],cnt=0,size=0,num[4],pre[4],ans=0,siz[N],root=0,son[N];
inline void add(int u,int v,int w){
	e[++cnt]=(node){u,v,w};
	nxp[cnt]=f[u];f[u]=cnt;
}
bool vis[N];
void findroot(int u,int fa){
	siz[u]=1;son[u]=0;
	for(int re i=f[u];i;i=nxp[i]){
		int v=e[i].v;
		if(vis[v]||v==fa)continue;
		findroot(v,u);siz[u]+=siz[v];
		son[u]=max(son[u],siz[v]);
	}son[u]=max(son[u],size-siz[u]);
	if(son[u]<son[root])root=u;
}
void dfs(int u,int d,int fa){
	num[d]++;
	for(int re i=f[u];i;i=nxp[i]){
		int v=e[i].v;
		if(vis[v]||v==fa)continue;
		dfs(v,(d+e[i].w)%4,u);
	}
}
void solve(int u){
	vis[u]=1;
	memset(num,0,sizeof num);
	memset(pre,0,sizeof pre);pre[0]=1;
	for(int re i=f[u];i;i=nxp[i]){
		int v=e[i].v;
		if(vis[v])continue;
		dfs(v,e[i].w,u);
		for(int re j=0;j<4;j++)ans+=pre[j]*num[(4-j)%4];
		for(int re j=0;j<4;j++)pre[j]+=num[j],num[j]=0;
	}for(int re i=f[u];i;i=nxp[i]){
		int v=e[i].v;
		if(vis[v])continue;
		son[0]=size=siz[v];root=0;
		findroot(v,0);solve(root);
	}
}
int total=0;
inline int gcd(int a,int b){return b?gcd(b,a%b):a;}
int main()
{
	while(scanf("%d",&n)&&n){
		ans=0;cnt=0;
		memset(f,0,sizeof f);
		for(int re i=1;i<n;i++)
			scanf("%d%d%d",&a,&b,&c),add(a,b,c%4),add(b,a,c%4);
		memset(vis,0,sizeof vis);
		son[0]=size=n;root=0;
		findroot(1,0);solve(root);
		total=n*n;ans=ans*2+n;
		int gd=gcd(ans,total);
		printf("%d/%d\n",ans/gd,total/gd);
	}	
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值