初见安~这里是传送门:洛谷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的值的路径的条数即可。
对于一棵子树,其答案就是:。后面一个*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——