Preface
退隐了一年多,再一次碰触到OI,已经有点生疏了,说来惭愧,还是没有之前没有把这坚持到底。
步入高中,压力也随之而来,我们始终还是要面对残酷的现实,面对一直回避的东西,面对不敢面对的自己。
不忘初心,卸下浮华,踏实面对,咬牙坚持,重新开始。
Problem
聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃、两个人都想玩儿电脑(可是他们家只有一台电脑)……遇到这种问题,一般情况下石头剪刀布就好了,可是他们已经玩儿腻了这种低智商的游戏。他们的爸爸快被他们的争吵烦死了,所以他发明了一个新游戏:由爸爸在纸上画n个“点”,并用n-1条“边”把这n个“点”恰好连通(其实这就是一棵树)。并且每条“边”上都有一个数。接下来由聪聪和可可分别随即选一个点(当然他们选点时是看不到这棵树的),如果两个点之间所有边上数的和加起来恰好是3的倍数,则判聪聪赢,否则可可赢。聪聪非常爱思考问题,在每次游戏后都会仔细研究这棵树,希望知道对于这张图自己的获胜概率是多少。现请你帮忙求出这个值以验证聪聪的答案是否正确。
Sample Input
5
1 2 1
1 3 2
1 4 1
2 5 3
Sample Output
13/25
Analysis
就是从这道题开始,我接触到了点分治
首先我们先要懂得
点分治是什么?
点分治主要用于树上路径点权统计问题。
明白了这个后,那么这道题就很显然是有关储存树上路径的信息的类型的题目,这就很合点分治胃口了。
那么,这道题就可以直接套点分治模板了
要求找3的倍数,所以点分治统计%3=0,1,2的个数,要注意点对可以两个数字相同,颠倒前后顺序算两个,所以统计结果为
余0*余0+余1*余2*2.
至于具体实现,我们可以对于当前的树,直接找到重心X,然后从X出发,搜索与X相邻的点,计算边长的余数分别是0,1,2的情况数,用t[0],t[1],t[2]表示
CODE参考如下
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <iostream>
#define fo(i,a,b) for (int i=a;i<=b;i++)
#define fd(i,a,b) for (int i=a;i>=b;i--)
#define N 20005
using namespace std;
struct node
{
int to ,next,val;
node(void){}
node(int a,int b,int c) : to(a),next(b),val(c){}
}e[N * 2];
int Final[N];
int ans,tot,root,t[4],d[N],son[N],F[N],sum;
bool Flag[N];
int read(int &n)
{
char ch = ' ';
int q = 0, w = 1;
for (;(ch != '-') && ((ch < '0') || (ch> '9'));ch = getchar());
if (ch == '-') w = -1,ch = getchar();
for (; ch >= '0' && ch <= '9';ch = getchar()) q = q * 10 + ch - 48;
n = q * w;
return n;
}
void Link(int x,int y,int z)
{
e[++ tot] = node(y,Final[x],z),Final[x] = tot;
e[++ tot] = node(x,Final[y],z),Final[y] = tot;
}
void Getroot(int x,int fa)
{
son[x] = 1; F[x] = 0;
for (int i = Final[x];i;i = e[i].next)
if (! Flag[e[i].to] && e[i].to != fa)
{
Getroot(e[i].to,x);
son[x] += son[e[i].to];
F[x] = max(F[x],son[e[i].to]);
}
F[x] = max(F[x],sum - son[x]);
if (F[x] < F[root]) root = x;
}
void Getdeep(int x,int fa)
{
t[d[x]] ++;
for (int i = Final[x];i;i = e[i].next)
if (! Flag[e[i].to] && e[i].to != fa)
{
d[e[i].to] = (d[x] + e[i].val) % 3;
Getdeep(e[i].to,x);
}
}
int Calc(int x,int w)
{
t[0] = t[1] = t[2] = 0;
d[x] = w;
Getdeep(x,0);
return t[1] * t[2] * 2 + t[0] * t[0];
}
void Solve(int x)
{
ans += Calc(x,0);
Flag[x] = true;
for (int i = Final[x];i;i = e[i].next)
if (! Flag[e[i].to])
{
ans -= Calc(e[i].to,e[i].val);
root = 0;
sum = son[e[i].to];
Getroot(e[i].to,0);
Solve(root);
}
}
int Gcd(int a,int b){return b == 0 ? a : Gcd(b,a%b);}
int main()
{
int n,u,v,w;
read(n);
fo(i,1,n - 1)
{
read(u);read(v);read(w);
w %= 3;
Link(u,v,w);
}
sum = n;
F[0] = n;
Getroot(1,0);
Solve(root);
int x = Gcd(ans,n*n);
printf("%d/%d\n",ans/x,n*n/x);
}
切入正题点分治分析
一、【具体流程】
1,选取个点,将无根树变成有根树
为了使每次的处理最优,我们通常要选取树的重心。
何为“重心”,就是要保证与此点连接的子树的节点数最大值最小,可以防止被卡。
重心求法:
1。dfs一次,算出以每个点为根的子树大小。
2。记录以每个节点为根的最大子树大小
3。判断:如果以当前节点为根更优,就更新当前根。
求重心
void Getroot(int x,int fa)//x 表示当前节点,fa表示其父亲节点 { son[x] = 1; F[x] = 0;//F数组记录以x为根的最大子树大小 for (int i = Final[x];i;i = e[i].next) if (! Flag[e[i].to] && e[i].to != fa) { Getroot(e[i].to,x);//得到子结点信息,递归更新 son[x] += son[e[i].to];//计算x结点大小 F[x] = max(F[x],son[e[i].to]);//比较每个子树,更新F数组 } F[x] = max(F[x],sum - son[x]);//sum表示当前树的大小,因为以x为根的情况还要考虑以x的父亲为根的子树大小 if (F[x] < F[root]) root = x;//更新当前根 }
2、处理联通块中通过根结点的路径。
3、标记根结点(相当于处理过后,将根结点从子树中删除)。
4、递归处理以当前点的儿子为根的每棵子树。
算法框架
int solve(int x) 2 { 3 vis[x]=1;//将当前点标记 4 for(int i=Link[x];i;i=e[i].next) 5 if(!vis[e[i].y]) 6 { 7 root=0;//初始化根 8 sum=e[i].y;//初始化sum 9 getroot(x,0);//找连通块的根 10 solve(e[i].y);//递归处理下一个连通块 11 } 12 } 13 int main() 14 { 15 build();//建树 16 sum=f[0]=n;//初始化sum和f[0] 17 root=0;//初始化root 18 getroot(1,0);//找根 19 solve(root);//点分治 20 }至于为什么solve的风格不同,是因为博客贴的代码太丑了QAQ
大概就是这样了