P1600 [NOIP2016 提高组] 天天爱跑步

题目来源

[NOIP2016 提高组] 天天爱跑步 - 洛谷

题目考点
线段树   最近公共祖先,LCA树链剖分,树剖     Link-Cut Tree,LCT差分

题目描述

小c 同学认为跑步非常有趣,于是决定制作一款叫做《天天爱跑步》的游戏。《天天爱跑步》是一个养成类游戏,需要玩家每天按时上线,完成打卡任务。

这个游戏的地图可以看作一一棵包含 n 个结点和 n−1 条边的树,每条边连接两个结点,且任意两个结点存在一条路径互相可达。树上结点编号为从 1 到 n 的连续正整数。

现在有 m 个玩家,第 i 个玩家的起点为 si​,终点为 ti​。每天打卡任务开始时,所有玩家在第 0 秒同时从自己的起点出发,以每秒跑一条边的速度,不间断地沿着最短路径向着自己的终点跑去,跑到终点后该玩家就算完成了打卡任务。 (由于地图是一棵树,所以每个人的路径是唯一的)

小c 想知道游戏的活跃度,所以在每个结点上都放置了一个观察员。在结点 j 的观察员会选择在第 wj​ 秒观察玩家,一个玩家能被这个观察员观察到当且仅当该玩家在第 wj​ 秒也正好到达了结点 jj。小c 想知道每个观察员会观察到多少人?

注意:我们认为一个玩家到达自己的终点后该玩家就会结束游戏,他不能等待一 段时间后再被观察员观察到。 即对于把结点 j 作为终点的玩家:若他在第 wj​ 秒前到达终点,则在结点 j 的观察员不能观察到该玩家;若他正好在第 wj​ 秒到达终点,则在结点 j 的观察员可以观察到这个玩家。

输入格式

第一行有两个整数 n 和 m。其中 n 代表树的结点数量, 同时也是观察员的数量, m 代表玩家的数量。

接下来 n−1 行每行两个整数 u 和 v,表示结点 u 到结点 v 有一条边。

接下来一行 n 个整数,其中第 j 个整数为 wj​ , 表示结点 j 出现观察员的时间。

接下来 m 行,每行两个整数 si​,和 ti​,表示一个玩家的起点和终点。

对于所有的数据,保证 1≤si​,ti​≤n,0≤wj​≤n。

输出格式

输出 1 行 n 个整数,第 j 个整数表示结点 j 的观察员可以观察到多少人。

输入输出样例

输入 #1

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

输出 #1

2 0 0 1 1 1 

输入 #2

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

输出 #2

1 2 1 0 1 

说明/提示

【样例1说明】

对于 11 号点,w_i=0wi​=0,故只有起点为 11 号点的玩家才会被观察到,所以玩家 11 和玩家 22 被观察到,共有 22 人被观察到。

对于 22 号点,没有玩家在第 22 秒时在此结点,共 00 人被观察到。

对于 33 号点,没有玩家在第 55 秒时在此结点,共 00 人被观察到。

对于 44 号点,玩家 11 被观察到,共 11 人被观察到。

对于 55 号点,玩家 11 被观察到,共 11 人被观察到。

对于 66 号点,玩家 33 被观察到,共 11 人被观察到。

【子任务】

每个测试点的数据规模及特点如下表所示。
提示: 数据范围的个位上的数字可以帮助判断是哪一种数据类型。

【提示】

如果你的程序需要用到较大的栈空间 (这通常意味着需要较深层数的递归), 请务必仔细阅读选手目录下的文本文档 running/stack.txt, 以了解在最终评测时栈空间的限制与在当前工作环境下调整栈空间限制的方法。

在最终评测时,调用栈占用的空间大小不会有单独的限制,但在我们的工作环境中默认会有 1 \text{MiB}1MiB 的限制。 这可能会引起函数调用层数较多时, 程序发生栈溢出崩溃。

我们可以使用一些方法修改调用栈的大小限制。 例如, 在终端中输入下列命令 ulimit -s 1048576

此命令的意义是,将调用栈的大小限制修改为 1 \text{GiB}1GiB。

例如,在选手目录建立如下 sample.cpp 或 sample.pas

将上述源代码编译为可执行文件 sample 后,可以在终端中运行如下命令运行该程序

./sample

如果在没有使用命令“ ulimit -s 1048576”的情况下运行该程序, sample 会因为栈溢出而崩溃; 如果使用了上述命令后运行该程序,该程序则不会崩溃。

特别地, 当你打开多个终端时, 它们并不会共享该命令, 你需要分别对它们运行该命令。

请注意, 调用栈占用的空间会计入总空间占用中, 和程序其他部分占用的内存共同受到内存限制。

题解

首先声明这不是一篇算法独特的题解,仍然是“LCA+桶+树上差分”,但这篇题解是为了让很多很多看了很多题解仍然看不懂的朋友们看懂的,其中就包括我,我也在努力地把解题的“思维过程”呈现出来,希望能帮助到别人。实在是佩服那些考场AC的大牛,再次向你们献上敬意!

原文链接(https://www.cnblogs.com/lfyzoi/p/10221884.html)

1. 第一步

  • 首先可以初步判断这个题肯定要计算LCA,方法有倍增/Tarjan-DFS,我们就写个简单的倍增吧,使用链式前向星存储边。
  • 选择1号结点开始dfs,别的结点也可以
  • dfs过程中计算fa[][]数组(fa[x][i]表示 xx 结点的 2^i2i 代祖先是谁)和deep[]数组(deep[x]表示结点 xx 在树中的深度)
​
#include<bits/stdc++.h>
using namespace std;
const int SIZE=300000;
int n, m, tot, h[SIZE], deep[SIZE], fa[SIZE][20], w[SIZE];      //w[i]表示i结点出现观察员的时间
struct edge
{
	int to, next;
}E[SIZE*2], e1[SIZE*2], e2[SIZE*2];                             //边集数组e1,e2留待备用

void add(int x, int y)                                          //加边函数
{
	E[++tot].to=y;
	E[tot].next=h[x];
	h[x]=tot;
}

void dfs1(int x)                                                //dfs的过程中完成“建树”,预处理fa[][]数组, 计算deep[]数组
{
	for(int i=1; (1<<i)<=deep[x]; i++)
		fa[x][i]=fa[fa[x][i-1]][i-1];                           //x的2^i代祖宗就是x的2^{i-1}代祖宗的2^{i-1}代祖宗
	for(int i=h[x]; i; i=E[i].next)
	{
		int y=E[i].to;
		if(y==fa[x][0])	continue;                               //如果y是父结点,跳过
		fa[y][0]=x;
		deep[y]=deep[x]+1;
		dfs1(y);
	}
}

int get_lca(int x, int y)                                      //计算x和y的最近公共祖先
{
	if(x==y) return x;                                         //没有这一行,遇到 lca(x, x) 这样的询问时会挂掉
	if(deep[x]<deep[y]) swap(x, y);                            //保持x的深度大于y的深度
	int t=log(deep[x]-deep[y])/log(2);
	for(int i=t; i>=0; i--)                                    //x向上跳到和y同样的深度
	{
		if(deep[fa[x][i]]>=deep[y])
			x=fa[x][i];
		if(x==y)
			return x;
	}
	t=log(deep[x])/log(2);
	for(int i=t; i>=0; i--)                                    //x和y一起向上跳
	{
		if(fa[x][i]!=fa[y][i])
			x=fa[x][i], y=fa[y][i];
	}
	return fa[x][0];
}

int main()                                                     //先把主函数写上一部分
{
	scanf("%d%d", &n, &m);
	for(int i=1; i<n; i++)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		add(u, v);
		add(v, u);
	}
	deep[1]=1;
	fa[1][0]=1;
	dfs1(1);
	for(int i=1; i<=n; i++) scanf("%d", &w[i]);
	
    /
    未完待续///
    /
    
	return 0;
}

​

2. 第二步

大概分析一下,m个玩家对应m条路径,有了起点和终点的 lca 后,如果我们模拟这个过程:

直觉

  • 从起点 S_iSi​ 跑到 LCALCA 在树长得很匀称的情况下为 O(lgn)O(lgn)
  • 从起点 LCALCA 跑到 T_iTi​ 在树长得很匀称的情况下为 O(lgn)O(lgn)
  • 因此,模拟一个玩家的跑步过程为 O(lgn)O(lgn),m个玩家为 O(mlgn)O(mlgn)
  • 理想情况下是可行的,但现实就是不理想
  • 题目清楚告诉你,树会退化成一条链,因此模拟一个过程变成 O(n)O(n),总的就是。。。O(mn)O(mn),必挂无疑
  • 此法不是正解!

尝试

  • 我们能不能改变模拟跑步的过程,从 O(n)O(n) 优化到 O(lgn)O(lgn) 呢?思前想后不可能,有 nn 个观察员矗在那里,你可以对哪个视而不见?
  • 路已走到尽头

转换

  • 这时候需要放大招,转换思想!或许解决问题的思路压根就不是一个玩家一个玩家模拟,而是整体处理呢?
  • 也就是说,我们不枚举每个运动员而是枚举每个观察员i,看看哪些结点会为这个观察员i做贡献(刚好在w_iwi​秒跑到他这儿)。
  • 枚举观察员的过程就是DFS整颗树的过程,我们可以在 O(n)O(n) 内搞定!
  • 对于观察员i,哪些人会为他做贡献呢?

深入分析

  • 对于结点 PP, 如果他位于一条起点、终点分别为 s_isi​, t_iti​ 的跑步路径上,如何判断这名选手会不会为 PP 作贡献呢?
  • 分情况考虑
  • 如果 PP 是在从 s_isi​ 到 LCALCA 的路上,如下图:

  • 我们可以得出结论:当起点 s_isi​ 满足 deep[s_i]=w[P]+deep[P]deep[si​]=w[P]+deep[P]时,起点 s_isi​会为 PP 观察员做一个贡献(运动员从s_isi​出发,可以被PP处的观察员在w[P]w[P]秒看到)

  • 如果 PP 是在从 LCALCA 到 t_iti​ 的路上,如下图:

  • 定义 dist[s_i, t_i]dist[si​,ti​]为从 s_isi​出发到t_iti​的路径长度,如果运动员从s_isi​出发,可以被PP处的观察员在w[P]w[P]秒观察到,可以由上图得出以下式子:
  • dist[s_i, t_i]-w[P]=deep[t_i]-deep[P]dist[si​,ti​]−w[P]=deep[ti​]−deep[P],移项后得到:
  • dist[s_i, t_i]-deep[t_i]=w[P]-deep[P]dist[si​,ti​]−deep[ti​]=w[P]−deep[P]
  • 我们可以得出结论:当终点 t_iti​ 满足 dist[s_i, t_i]-deep[t_i]=w[P]-deep[P]dist[si​,ti​]−deep[ti​]=w[P]−deep[P]时,终点 t_iti​会为 PP 观察员做一个贡献
  • 做一个重要的总结:上行过程中,满足条件的起点可以做贡献,下行过程中,满足条件的终点可以做贡献,但无论是哪一种情形,能对 PP 做贡献的起点或终点一定都在以PP为根的子树上,这使得可以在DFS回溯的过程中处理以任意节点为根的子树。

3. 第三步

如何统计子树贡献

  • 递归以PP为根的子树时,可以统计出其子树中所有的起点和终点对它的贡献
  • 这里又需要转换
  • 子树中有的起点和终点对PP产生了贡献,有些不对其产生贡献但对PP以外的结点产生了贡献
  • 所以我们不能枚举每个点(子树根),找子树中哪些点对其产生贡献,这样复杂度就上去了
  • 而是对于树上的任何一个起点和终点,把其产生的贡献放在桶里面,回溯到子树根的时候再到桶里面查询结果
  • 有人产生疑问了,也是很多人看不懂这里桶用法的地方,疑问如图:

  • cc点产生贡献放在桶的deep[c]deep[c]位置,计算bb点获得的贡献时当然是从bucket1[deep[b]+w[b]]bucket1[deep[b]+w[b]]位置获取,于是得到1个贡献,你发现aa结点也是用的同一个桶,这个还好,因为cc确实给他做了贡献,可是ee点呢?他是不应该获得贡献的!既然我会给和我无关的结点做贡献,那么其它无关的结点难免也会给我做贡献!
  • 问题总结一下,对于一个点PP来说,究竟哪些点在桶里面产生的贡献才是有效的。
  • 答案是:以PP为根递归整颗子树过程中在桶内产生的差值才是有效的

还要考虑一种情况

  • 先看图:

  • 看懂了吗?对于以PP为根的内部路径(不经过PP),这条路径的起点和终点产生的贡献是不应该属于PP的
  • 所以dfs过程中,在统计当前结点作为起点和终点所产生的贡献后,继而计算出当前结点作为“根”上的差值后,在回溯过程中,一定要减去以当前结点为LCALCA的起点、终点在桶里产生的贡献,这部分贡献在离开这个子树后就没有意义了。

代码说明

  • e1,tot1,h1,add1是使用链式前向星的方法存储每个结点作为终点对应的路径集合
  • e2,tot2,h2,add2是使用链式前向星的方法存储每个结点作为LCA对应的路径集合
  • b1,b2是两组桶,分别用于上行阶段下行阶段的贡献统计
  • js[SIZE]用于统计以每个结点作为起点的路径条数
  • dist[SIZE], s[SIZE], t[SIZE]用于统计m条路径对应的长度,起点和终点信息
  • ans[SIZE]存储最后输出的答案,是每个结点观察员看到的人数
​
int tot1, tot2, h1[SIZE], h2[SIZE];
void add1(int x, int y)
{
	e1[++tot1].to=y;
	e1[tot1].next=h1[x];
	h1[x]=tot1;
}

void add2(int x, int y)
{
	e2[++tot2].to=y;
	e2[tot2].next=h2[x];
	h2[x]=tot2;
}

int b1[SIZE*2], b2[SIZE*2], js[SIZE], dist[SIZE], s[SIZE], t[SIZE], ans[SIZE];

void dfs2(int x)
{
	int t1=b1[w[x]+deep[x]], t2=b2[w[x]-deep[x]+SIZE];      //递归前先读桶里的数值,t1是上行桶里的值,t2是下行桶的值
	for(int i=h[x]; i; i=E[i].next)                         //递归子树
	{
		int y=E[i].to;
		if(y==fa[x][0]) continue;
		dfs2(y);
	}
	b1[deep[x]]+=js[x];                                     //上行过程中,当前点作为路径起点产生贡献,入桶
	for(int i=h1[x]; i; i=e1[i].next)                       //下行过程中,当前点作为路径终点产生贡献,入桶
	{
		int y=e1[i].to;
		b2[dist[y]-deep[t[y]]+SIZE]++;
	}
	ans[x]+=b1[w[x]+deep[x]]-t1+b2[w[x]-deep[x]+SIZE]-t2;   //计算上、下行桶内差值,累加到ans[x]里面
	for(int i=h2[x]; i; i=e2[i].next)                       //回溯前清除以此结点为LCA的起点和终点在桶内产生的贡献,它们已经无效了
	{
		int y=e2[i].to;
		b1[deep[s[y]]]--;                                   //清除起点产生的贡献
		b2[dist[y]-deep[t[y]]+SIZE]--;                      //清除终点产生的贡献
	}
}

int main()
{
重复部分跳过
文末提供完整代码
	for(int i=1; i<=m; i++)                                 //读入m条询问
	{
		scanf("%d%d", &s[i], &t[i]);
		int lca=get_lca(s[i], t[i]);                        //求LCA
		dist[i]=deep[s[i]]+deep[t[i]]-2*deep[lca]];         //计算路径长度
		js[s[i]]++;                                         //统计以s[i]为起点路径的条数,便于统计上行过程中该结点产生的贡献
		add1(t[i], i);                                      //第i条路径加入到以t[i]为终点的路径集合中
		add2(lca, i);                                       //把每条路径归到对应的LCA集合中
		if(deep[lca]+w[lca]==deep[s[i]]) ans[lca]--;        //见下面的解释
	}
	dfs2(1);                                                //dfs吧!
	for(int i=1; i<=n; i++) printf("%d ", ans[i]); 
	return 0;
}

​

一些重要补充

  • 上述代码中有一行未加解释if(deep[lca]+w[lca]==deep[s[i]]) ans[lca]--;
  • 考虑路径是这样的,如图:

  • 这个图可能不太好懂,意思是:

  • 如果路径起点或终点刚好为LCA且LCA处是可观察到运动员的,那么我们在上行统计过程中和下行统计过程中都会对该LCA产生贡献,这样就重复计数一次!

  • 好在这种情况很容易发现,我们提前预测到,对相应的结点进行ans[x]--即可。

  • 此外,在使用第二个桶时,下标是w[x]-deep[x]会成为负数,所以使用第二个桶时,下标统一+SIZE,向右平移一段区间,防止下溢。

4. 结束

我不知道自己说清楚没有,但愿大家不要拍砖头!下面是完整代码

​
#include<bits/stdc++.h>
using namespace std;
const int SIZE=300000;
int n, m, tot, h[SIZE], deep[SIZE], fa[SIZE][20], w[SIZE];
struct edge
{
	int to, next;
}E[SIZE*2], e1[SIZE*2], e2[SIZE*2];

void add(int x, int y)
{
	E[++tot].to=y;
	E[tot].next=h[x];
	h[x]=tot;
}

int tot1, tot2, h1[SIZE], h2[SIZE];
void add1(int x, int y)
{
	e1[++tot1].to=y;
	e1[tot1].next=h1[x];
	h1[x]=tot1;
}
void add2(int x, int y)
{
	e2[++tot2].to=y;
	e2[tot2].next=h2[x];
	h2[x]=tot2;
}

void dfs1(int x)
{
	for(int i=1; (1<<i)<=deep[x]; i++)
		fa[x][i]=fa[fa[x][i-1]][i-1];
	for(int i=h[x]; i; i=E[i].next)
	{
		int y=E[i].to;
		if(y==fa[x][0])	continue;
		fa[y][0]=x;
		deep[y]=deep[x]+1;
		dfs1(y);
	}
}

int get_lca(int x, int y)
{
	if(x==y) return x;
	if(deep[x]<deep[y]) swap(x, y);
	int t=log(deep[x]-deep[y])/log(2);
	for(int i=t; i>=0; i--)
	{
		if(deep[fa[x][i]]>=deep[y])
			x=fa[x][i];
		if(x==y)
			return x;
	}
	t=log(deep[x])/log(2);
	for(int i=t; i>=0; i--)
	{
		if(fa[x][i]!=fa[y][i])
			x=fa[x][i], y=fa[y][i];
	}
	return fa[x][0];
}

int b1[SIZE*2], b2[SIZE*2], js[SIZE], dist[SIZE], s[SIZE], t[SIZE], l[SIZE], ans[SIZE];
void dfs2(int x)
{
	int t1=b1[w[x]+deep[x]], t2=b2[w[x]-deep[x]+SIZE];
	for(int i=h[x]; i; i=E[i].next)
	{
		int y=E[i].to;
		if(y==fa[x][0]) continue;
		dfs2(y);
	}
	b1[deep[x]]+=js[x];
	for(int i=h1[x]; i; i=e1[i].next)
	{
		int y=e1[i].to;
		b2[dist[y]-deep[t[y]]+SIZE]++;
	}
	ans[x]+=b1[w[x]+deep[x]]-t1+b2[w[x]-deep[x]+SIZE]-t2;
	for(int i=h2[x]; i; i=e2[i].next)
	{
		int y=e2[i].to;
		b1[deep[s[y]]]--;
		b2[dist[y]-deep[t[y]]+SIZE]--;
	}
}

int main()
{
	scanf("%d%d", &n, &m);
	for(int i=1; i<n; i++)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		add(u, v);
		add(v, u);
	}
	deep[1]=1;
	fa[1][0]=1;
	dfs1(1);
	for(int i=1; i<=n; i++) scanf("%d", &w[i]);
	for(int i=1; i<=m; i++)
	{
		scanf("%d%d", &s[i], &t[i]);
		int lca=get_lca(s[i], t[i]);
		dist[i]=deep[s[i]]+deep[t[i]]-2*deep[lca];
		js[s[i]]++;
		add1(t[i], i);
		add2(lca, i);
		if(deep[lca]+w[lca]==deep[s[i]]) ans[lca]--;
	}
	dfs2(1);
	for(int i=1; i<=n; i++) printf("%d ", ans[i]);
	return 0;
}

​

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值