洛谷P2634_[国家集训队]聪聪可可:(树形dp_点分治)

聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃、两个人都想玩儿电脑(可是他们家只有一台电脑)……遇到这种问题,一般情况下石头剪刀布就好了,可是他们已经玩儿腻了这种低智商的游戏。
他们的爸爸快被他们的争吵烦死了,所以他发明了一个新游戏:由爸爸在纸上画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),不过这道题是点分治的板子题,当然要码一下点分治算法了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值