洛谷·[国家集训队]聪聪可可

初见安~这里是传送门:洛谷P2634

题目描述

聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃、两个人都想玩儿电脑(可是他们家只有一台电脑)……遇到这种问题,一般情况下石头剪刀布就好了,可是他们已经玩儿腻了这种低智商的游戏。

他们的爸爸快被他们的争吵烦死了,所以他发明了一个新游戏:由爸爸在纸上画n个“点”,并用n-1条“边”把这n个“点”恰好连通(其实这就是一棵树)。并且每条“边”上都有一个数。接下来由聪聪和可可分别随即选一个点(当然他们选点时是看不到这棵树的),如果两个点之间所有边上数的和加起来恰好是3的倍数,则判聪聪赢,否则可可赢。

聪聪非常爱思考问题,在每次游戏后都会仔细研究这棵树,希望知道对于这张图自己的获胜概率是多少。现请你帮忙求出这个值以验证聪聪的答案是否正确。

输入格式

输入的第1行包含1个正整数n。后面n-1行,每行3个整数x、y、w,表示x号点和y号点之间有一条边,上面的数是w。

输出格式

以即约分数形式输出这个概率(即“a/b”的形式,其中a和b必须互质。如果概率为1,输出“1/1”)。

输入 

5
1 2 1
1 3 2
1 4 1
2 5 3

输出 

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。

题解

本题是点分治算法的一个十分基础的板子题之一。

题意很明显:我们需要求的是长度为3的倍数的路径的数量。

基于点分治的概念,我们开一个数组记录长度%3的值的路径的条数即可。

对于一棵子树,其答案就是:cnt[0]*cnt[0]+2 *cnt[1]*cnt[2]。后面一个*2是因为u->v和v->u是不同的路径。

但是我们很难注意到的一点是——这样算还是会有重复,因为有可能某两条路径的长度可以匹配但是属于同一个u的子树,这样的话就相当于是过了一个u子树里面的点而并非u。所以我们还要去重——去掉u的各个子树对答案的贡献

然后这个题就切掉了呢~上代码:

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<queue>
#define maxn 20005
using namespace std;
typedef long long ll;
int read() {
	int x = 0, f = 1, ch = getchar();
	while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
	while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
	return x * f;
}

struct edge {
	int to, w, nxt;
	edge() {}
	edge(int tt, int ww, int nn) {to = tt, w = ww, nxt = nn;}
}e[maxn << 1];

int head[maxn], k = 0;
void add(int u, int v, int w) {e[k] = edge(v, w, head[u]); head[u] = k++;}

int n, root = 0, max_num, part[maxn], size[maxn], ans = 0;
bool vis[maxn];
void get_root(int u, int fa) {
	size[u] = 1; part[u] = 0;
	for(int i = head[u]; ~i; i = e[i].nxt) {
		register int v = e[i].to; if(v == fa || vis[v]) continue;
		get_root(v, u); size[u] += size[v];
		part[u] = max(part[u], size[v]);
	}
	part[u] = max(part[u], max_num - size[u]);
	if(part[u] < part[root]) root = u;
}

int dis[maxn], cnt[5];
void get_dis(int u, int fa) {
	cnt[dis[u] % 3]++;
	for(int i = head[u]; ~i; i = e[i].nxt) {
		register int v = e[i].to; if(v == fa || vis[v]) continue;
		dis[v] = dis[u] + e[i].w; get_dis(v, u);
	}
}

int cal(int x) {//都还是挺好理解的吧……
	cnt[0] = cnt[1] = cnt[2] = 0;	
	get_dis(x, 0);
	return cnt[0] * cnt[0] + ((cnt[1] * cnt[2]) << 1);
}

void solve(int u) {
	vis[u] = true; dis[u] = 0;
	ans += cal(u);//求答案
	
	for(int i = head[u]; ~i; i = e[i].nxt) {
		register int v = e[i].to; if(vis[v]) continue;
		dis[v] = e[i].w; ans -= cal(v);//去重
		max_num = size[v];
		root = 0; get_root(v, u);
		solve(root);
	}
}

int gcd(int a, int b) {return b? gcd(b, a % b) : a;}
signed main() {
	memset(head, -1, sizeof head);
	n = read();
	for(int i = 1, u, v, w; i < n; i++) u = read(), v = read(), w = read(), add(u, v, w), add(v, u, w);
	
	max_num = part[0] = n; root = 0;
	get_root(1, 0);
	
	solve(root);
	
	int sum = n * n;
	printf("%d/%d\n", ans / gcd(sum, ans), sum / gcd(sum, ans));//记得约分
	return 0;
}

那么考虑到点分治的模板题的做题思路——我们其实也可以如法炮制一波。下面是解题思路2:

思路1去重是因为直接整体匹配数量导致,那么我们能不能统计出来某一颗子树的信息过后和前面的子树匹配,然后再把答案累加进去和后面的子树匹配继续呢?当然可以!!!这样的话是肯定不会出现不合法情况的,记得最后答案*2就好。

那么还有一个很小的问题,相信你这样写出来过后就会发现样例输出来并不对了。为什么?因为这样匹配的时候,单点的dis并不作数。单点的时候没有前面的子树跟你匹配,而在上面一个算法中dis进去就是cnt++了的,所以匹配的上。所以最后答案还要记得+n。

上有改动的核心代码~

int cal(int x) {
	sum[0] = sum[1] = sum[2] = 0;//sum针对某一棵子树,cnt用于以u为根节点的树
	get_dis(x, 0);
	return sum[0] + cnt[0] * sum[0] + cnt[1] * sum[2] + cnt[2] * sum[1];
}

void solve(int u) {
	vis[u] = true; dis[u] = 0;
	cnt[0] = cnt[1] = cnt[2] = 0;
	for(int i = head[u]; ~i; i = e[i].nxt) {
		register int v = e[i].to; if(vis[v]) continue;
		dis[v] = e[i].w; ans += cal(v);
		cnt[0] += sum[0], cnt[1] += sum[1], cnt[2] += sum[2];//累加答案
	}
	
	for(int i = head[u]; ~i; i = e[i].nxt) {
		register int v = e[i].to; if(vis[v]) continue;
		max_num = size[v];
		root = 0; get_root(v, u);
		solve(root);
	}
}

//最后ans = (ans << 1) + n

迎评:)
——End——

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值