[Usaco2010 Hol]cowpol 奶牛政坛(入门oj Problem 5274)

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

第三次发题解哈。有什么表述错误的请见谅(可以提出)。

题目源地址:https://www.lydsy.com/JudgeOnline/problem.php?id=1776

题目描述

农夫约翰的奶牛住在N (2 <= N <= 200,000)片不同的草地上,标号为1到N。恰好有N-1条单位长度的双向道路,用各种各样的方法连接这些草地。而且从每片草地出发都可以抵达其他所有草地。也就是说,这些草地和道路构成了一种叫做树的图。输入包含一个详细的草地的集合,详细说明了每个草地的父节点P_i (0 <= P_i <= N)。根节点的P_i == 0, 表示它没有父节点。因为奶牛建立了1到K一共K (1 <= K <= N/2)个政党。每只奶牛都要加入某一个政党,其中, 第i只奶牛属于第A_i (1 <= A_i <= K)个政党。而且每个政党至少有两只奶牛。 这些政党互相吵闹争。每个政党都想知道自己的“范围”有多大。其中,定义一个政党的范围是这个政党离得最远的两只奶牛(沿着双向道路行走)的距离。 比如说,记为政党1包含奶牛1,3和6,政党2包含奶牛2,4和5。这些草地的连接方式如下图所 示(政党1由-n-表示): 政党1最大的两只奶牛的距离是3(也就是奶牛3和奶牛6的距离)。政党2最大的两只奶牛的距离是2(也就是奶牛2和4,4和5,还有5和2之间的距离)。 帮助奶牛们求出每个政党的范围。

输入

  • 第一行: 两个由空格隔开的整数: N 和 K * 第2到第N+1行: 第i+1行包含两个由空格隔开的整数: A_i和P_i

输出

  • 第1到第K行: 第i行包含一个单独的整数,表示第i个政党的范围。

样例输入

6 2
1 3
2 1
1 0
2 1
2 1
1 5

样例输出

3
2

附图

在这里插入图片描述
身为蒟蒻,对这一题的意思还是斟酌了很久的。给大家讲一下题面:
输入
1:读入n和k,表示这棵树由几个点组成和有多少个政党。
2~n+1:读入A_i和P_i,表示点i属于第A_i个政党的势力范围内(暂且就这么叫吧)并给出父节点P_i。
输出
1~k:输出每个政党的势力范围。

势力范围事实上是解这题的关键突破口,题面中给这个词语的解释换句话来说就是要在树上找同政党中相隔距离最远的两点。而这个距离怎么去确定?我给大家打个比方,一条Y字形道路,我们要确定“Y”字任意两端点的距离,就应该计算这两个端点与中间一点的距离和,而这中间一点就是我们今天要讨论的LCA(最近公共祖先)

  1. 目的
    顾名思义,LCA要求的就是树上两点的最近公共祖先,有两层含义:
    ①:公共祖先:这个意思不难理解,就是类似于两条街的分岔口,例如好比一棵树的根节点是所有节点的祖先(PS:LCA可以是两个点中的任意一点,类似于求最小公倍数)
    ②:最近:既是公共祖先,又必须离两个点最近。

  2. 实现
    一般来讲我们应该讲树上的两个结点想象成两个动点,首先得让他们“移动”到同一深度处,然后每个点都同时向上移动一次,最后重合的点即为他们的公共祖先,但是很显然,题目中的数据过大(上述算法时间复杂度为O(n2)),之后还要进行枚举,若依然按照原始方法——O(n3)那无疑是一个可怕的时间量,偏偏这一题的时间限制只有1000ms。

优化!LCA!

我们又见到了老朋友(如果你总是看我的博客的话就会记忆深刻),也就是RMQ思想,在这里就不细讲了,有兴趣的神犇们可以去这个网址https://blog.csdn.net/weixin_44613722/article/details/86615493也是本人的一篇蒟蒻题解,讲的比较详细(jianlou)。当然这个rmq思想也可以用i的n次方而不仅仅是2的n次方(n为非负整数)来分割,但是考虑到2n较容易理解,且任何数都能用不重复的单项式(2n)组成的多项式来表达(这样要快一些,可以少几次循环)。在这里我们给出一些例子:

其实这个道理很简单解释,通俗点讲就是一分为二,初中应该就学过这个公式:20+21+22+23+…+2n=2n+1
好的不扯了我们具体讲讲如何优化:

  1. 原来我们是每一个点移动一次,但其实我们可以每一个点都移动2n次,在这里我们要定义一个二维数组
    f[i][j]表示在点i向上跳2j次所到达的地方,同样我们可以得出:
f[i][j]=f[f[i][j-1]][j-1]或者是f[i][j]=f[i+1<<(j-1)][j-1]
  1. 我们可以做一个逆循环枚举一下j,因为如果做正循环从小到大做你无法知道在这个时候符合要求(保证两点仍在一棵子树的左右两侧,,因为我们没办法确定两点所跳到的某个祖先是最近的)的情况下所能跳的最多步数。
  2. 实际解决问题当中可能一开始两点都在一棵子树的两侧,那么这样的话LCA当然就是深度小的那个点,我们可以加一个判断条件,如果当两点跳至同一深度时,检测两点的标号是否一致,若一致则返回任意一点的标号。
  3. 我们还要注意的一点是:当两点已经不能再跳时(就是一步也跳不上去的时候,因为跳一步就到了同一个点,也就是祖先点),这个时候就说明这两点的父亲点就是原先要求的两点的最近公共祖先。
  4. 代码如下O(nlogn):
int lca(int x,int y)
{
	if(dep[x]<dep[y])swap(x,y);//如果x的深度小于y,就交换一下,便于后面的操作
	for(int i=20;i>=0;i--)//2的20次方大概有一百万了,这一题的数据只有二十万
	if(dep[f[x][i]]>=dep[y])x=f[x][i];//将两点处理到同一深度
	if(x==y)return x;//点3所阐述的情况
	for(int i=20;i>=0;i--)
	{
		if  (f[x][i]!=f[y][i])
		x=f[x][i],y=f[y][i];
	}
	return f[x][0];//x或y的父亲点就是原先两点的最近公共祖先
}

优化!枚举!

其实原先就有过暗示这一步肯定是有优化的,暴力做法是把同一个政党的点全部统计出来,两两做一次LCA,但实际上来说我们可以确定其中一个点(这个点是深度最深的那个点),但不能确定两个点,因为距离是相对来说的,并不是深度越大的两点距离越远,例如一颗满二叉树最深的两点且它们的父亲点就是他们的最近公共祖先,深度是相对于根节点而言的,所以只能确定一个点而剩下的那个点只能靠枚举。确定最深的点应该就不需要本蒟蒻赘言了吧…时间复杂度为O(n2)

优化!计算!

照引言上的说法,我们要计算LCA与两点的距离和,一般人可能会从LCA开始做一个dfs去找那两点并计算(各位神犇大佬请不要喷我),但其实我们没必要这样我们可以拿两点的深度和减去两倍的LCA深度,用画图来表示大概就是这样:
在这里插入图片描述
比方说我们要求8和10的距离(其LCA为2),LCA的深度已被我们用红色部分表示,其余两点的深度被黑色部分表示,所以(dis数组是距离的意思,dep数组是深度的意思)dis[1…8]-dis[1…2]=dis[2…8],dis[1…10]-dis[1…2]=dis[2…10].由于我们要求的是dis[2…8]+dis[2…10],所以可以替换为dis[1…8]+dis[1…10]-2×dis[1…2],经过推算我们可以发现dis[1…n]=dep[n]-1
所以计算出来就是(dep[8]-1)+(dep[10]-1)-2×(dep[2]-1),化简得dep[8]+dep[10]-2×dep[2].
所以对于任意两点x,y则它们的距离为dep[x]+dep[y]-2×dep[lca(x,y)].

代码

代码有些丑,别嫌弃…

#include <bits/stdc++.h>
using namespace std;
int n,k,ai,pi,tot,rt;
int pre[400001],son[400001],now[400001],zd[400001],dep[400001],dest[200001],desti[200001],f[400001][21],Ans[200001];

void put(int x,int y)//建立边表
{
	pre[++tot]=now[x];
	now[x]=tot;
	son[tot]=y;
}
void dfs(int u,int deep)
{
	dep[u]=deep;
	for(int i=now[u];i;i=pre[i])
	{
		f[son[i]][0]=u;
		dfs(son[i],deep+1);
	}
}
int lca(int x,int y)
{
	if(dep[x]<dep[y])swap(x,y);
	for(int i=20;i>=0;i--)
	if(dep[f[x][i]]>=dep[y])x=f[x][i];
	if(x==y)return x;
	for(int i=20;i>=0;i--)
	{
		if  (f[x][i]!=f[y][i])
		x=f[x][i],y=f[y][i];
	}
	return f[x][0];
}
int main()
{
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&ai,&pi);
		put(pi,i);
		if(pi==0)rt=i;
		zd[i]=ai;
	}
	dfs(rt,1);
	for(int i=1;i<=20;i++)
	{
		for(int j=1;j<=n;j++)
		f[j][i]=f[f[j][i-1]][i-1];
	}
	for(int i=1;i<=n;i++)//检测每个政党深度最大的一点
	{
		if  (dep[i]>dest[zd[i]])
		{
			dest[zd[i]]=dep[i];//dest记录深度
			desti[zd[i]]=i;//desti记录标号
		}
	}
	for(int i=1;i<=n;i++)
	Ans[zd[i]]=max(Ans[zd[i]],dest[zd[i]]+dep[i]-2*dep[lca(desti[zd[i]],i)]);
	for(int i=1;i<=k;i++)printf("%d\n",Ans[i]);
	return 0;
}

如有什么不足之处,还请各位神犇指导。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值