【2011集训队出题】聪聪可可 (点分治模板练习题及点分治分析)

2 篇文章 0 订阅
2 篇文章 0 订阅

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

大概就是这样了












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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值