NOIP2016·洛谷·天天爱跑步

初见安~这里是传送门:洛谷P1600

题目描述

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

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

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

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

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

输入格式:

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

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

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

接下来 mm行,每行两个整数S_iSi​,和T_iTi​,表示一个玩家的起点和终点。

对于所有的数据,保证1\leq S_i,T_i\leq n, 0\leq W_j\leq n1≤Si​,Ti​≤n,0≤Wj​≤n 。

输出格式:

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

输入样例#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,故只有起点为1号点的玩家才会被观察到,所以玩家11和玩家22被观察到,共有22人被观察到。

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

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

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

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

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

【子任务】

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

题解:

首先声明,这个题本蒟蒻不会做,看题解+写出代码共消耗了我一整天除去整个下午的时间。所以这个思路并不是我的~

题意很简单——每个点有一个时间,问刚好在这一时刻出现在这一节点的人数是多少。

看到路径是肯定要求LCA的。我们继续看——如果具体看每一条路径,那么复杂度就可以很高。所以我们可以考虑把路拆分成两条链。假设这条路是从u->v,那么我们可以将其拆分为u->lca和lca->v两条链。

那么怎么处理呢?在u->lca这条路上,易得:如果有dep[i] + w[i] == dep[u],那么这条路径上的人一定可以在lca处被看到,对答案有贡献。而对于在lca->v这条路上,同理易得\dpi{120} dep[i] - w[i] == dep[v] - dis(u, v)。所以我们要求的就是在路径上所有满足上面两式【lca替换为i】的点并打上标记。

如果公式没有看明白的话,我们可以画图再来理解一下【确定理解了的自行跳过】

如图,我们把从u->v的路径断开为u->lca和lca->v。在u->lca这条路上的点,只要是dep[i]-dep[u]==w[i],也就是距离刚好是i点观测的时间,那么在点i出发的点都可以被看到;同理,在lca->v路径上,dis(i, v) = dep[v] - dep[i],如果有dis(u, v) - dis(i, v)=dis(u, i) = w[i],那么同样可以被看到。为了计算方便,我们带入化简可得:dis(u, v)-dep[v]+dep[i]==w[i],再移向可得上方式子:dep[v] - dis(u, v) = dep[i] - w[i]

然后我们就可以考虑具体实现操作了。

每走到一个点u,我们就可以考虑一下以u为lca的路径。枚举路径还是会爆掉,所以我们考虑到定量——只要是在u下方w[u]处出发的点,对答案都有贡献。也就是说我们走过每一个观察员,分别考虑这个点作为lca时到起点的路径和作为终点时从lca过来的路径。但是们得出这个点的位置只有其深度,所以我们可以开一个cnt1[maxn]处理前一条链,cnt2[maxn]处理第二条链

正如前文所解释的,我们处理第一条路径的时候是以当前节点为lca计算出在路径上满足第一个公式的点并看到在那个点出发的人,所以我们能处理的就只是深度。所以cnt1[ i ]可以表示在第i层出发可以被看到的人数。具体更新很简单,记录下当前的cnt值,计算一波后差分得出答案。但是有一个小细节——由于这条路径(u->v)在当前点就拐弯了,所以对于其他的非这个点子树的点都是没有任何贡献的,即使是满足了公式也都不应算作答案。所以我们要记录一下以这个节点为lca的起点,在差分的时候减去

第二条路径相对麻烦一些,因为我们的处理方式是作为终点。在当前思路下【是的因为第二个公式可以变形,以其他方式处理】,我们可以处理出等式两边的值,比如以v为第二条路径的终点时的dep[v]-dis(u,v)或者前者【这里我们是处理的后者】,而后直接对照公式的另一端。所以我们这里就没有必要处理出以某点为终点的人数了,我们只能直接对照值。写个桶来统计就比较方便了。仍旧有几个小细节——比如公式两端值可能为负,用桶差分的话可能会非法访问,所以要同时加上n;和第一条路径的处理相同的,虽然我们是以当前节点为终点,但是以当前节点为lca的也是不需要了的,维护差分要减去。

思路就是这样了!!!【反正这么细节而又巧妙的思路我是想不到的QwQ】下面看代码吧,也有详解【看了我一个上午QwQ】

#include<bits/stdc++.h>
#define maxn 300005
using namespace std;
int n, m;
int w[maxn];
int read()
{
	int x = 0, ch = getchar();
	while(!isdigit(ch)) ch = getchar();
	while(isdigit(ch)) x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
	return x;
}

struct edge
{
	int to, nxt;
	edge() {}
	edge(int t, int x) {to = t, nxt = x;}
}e[maxn << 1];
	
int head[maxn], k = 0;
void add(int u, int v) {e[k] = edge(v, head[u]); head[u] = k++;}

int fa[maxn], dep[maxn], size[maxn], son[maxn], top[maxn], depst = 0;
struct hld//树剖求lca,整体我就打包了区分一下 
{
	void dfs1(int u)
	{
		size[u] = 1; register int v;
		for(int i = head[u]; ~i; i = e[i].nxt)
		{
			v = e[i].to;
			if(v == fa[u]) continue;
			fa[v] = u; dep[v] = dep[u] + 1;
			depst = max(depst, dep[v]);
			dfs1(v);
			size[u] += size[v];
			if(size[v] > size[son[u]]) son[u] = v;
		}
	}
	
	void dfs2(int u, int tp)
	{
		register int v;
		top[u] = tp;
		if(son[u]) dfs2(son[u], tp);
		for(int i = head[u]; ~i; i = e[i].nxt)
		{
			v = e[i].to;
			if(v != fa[u] && v != son[u]) dfs2(v, v);
		}
	}
	
	int ask(int u, int v)
	{
		while(top[u] != top[v])
		{
			if(dep[top[u]] > dep[top[v]]) swap(u, v);
			v = fa[top[v]];
		}
		return dep[u] > dep[v]? v : u;
	}
}HLD;

struct node//存储每个人【每条路径】的信息 
{
	int u, v, lca, dis;
}a[maxn];

vector<int> v1[maxn], v2[maxn], v3[maxn];
int ans[maxn], cnt1[maxn], cnt2[maxn << 1], s[maxn];
void dfs_1(int u)//这两个dfs_是核心!!!!! dfs1处理u->lca路径
{
	register int v, x = w[u] + dep[u], tmp;//x为公式一中的涉及到的深度 
	if(x <= depst) tmp = cnt1[x];//注意x要判定合法性!!! depst在树剖初始化里更新的 
	for(int i = head[u]; ~i; i = e[i].nxt)
	{
		v = e[i].to;
		if(v != fa[u]) dfs_1(v);
	}	
	cnt1[dep[u]] += s[u];//累计在这个点出发的点 
	if(x <= depst) ans[u] = cnt1[x] - tmp;//tmp记录更新前的值 ,这里差分 
	for(int i = 0; i < v1[u].size(); i++)//这条路径在这里就拐弯了不贡献了
		cnt1[dep[v1[u][i]]]--;
}

void dfs_2(int u)//lca->v路径
{
	register int v, x = dep[u] - w[u] + n, tmp = cnt2[x];//x为目标值,tmp同样用于差分 
	for(int i = head[u]; ~i; i = e[i].nxt)//↑等式两边同时+n防止出现负数
	{
		v = e[i].to; 
		if(v != fa[u]) dfs_2(v);
	}
	for(int i = 0; i < v2[u].size(); i++) cnt2[v2[u][i] + n]++;//以u为终点,访问到了,这些值对应都存在了,先++用于调用 
	ans[u] += cnt2[x] - tmp;//ans加上目标值,差分 
	for(int i = 0; i < v3[u].size(); i++) cnt2[v3[u][i] + n]--;//路径二同理去掉这些路径
}

int main()
{
	memset(head, -1, sizeof head);
	n = read(), m = read();
	register int u, v;
	for(int i = 1; i < n; i++)
		u = read(), v = read(), add(u, v), add(v, u);
		
	for(int i = 1; i <= n; i++) w[i] = read();
	for(int i = 1; i <= m; i++) a[i].u = read(), a[i].v = read();
	
	HLD.dfs1(1); HLD.dfs2(1, 1);//加了HLD.的都是LCA的基本操作 
	
	for(int i = 1; i <= m; i++)
	{
		u = a[i].u, v = a[i].v;
		s[u]++;
		a[i].lca = HLD.ask(u, v);
		a[i].dis = dep[u] + dep[v] - (dep[a[i].lca] << 1);
		v1[a[i].lca].push_back(u);//路径一的细节处理 
		v2[a[i].v].push_back(dep[v] - a[i].dis);//路径二的答案对应累计用 
		v3[a[i].lca].push_back(dep[v] - a[i].dis);//路径二的细节处理 
	}
	
	dfs_1(1);
	dfs_2(1);
	for(int i = 1; i <= m; i++)//特殊处理一下:如果这条路径本就是一条链且起点or终点有重复计算,那么lca就多算了一次的 
		if(dep[a[i].u] - dep[a[i].lca] == w[a[i].lca]) ans[a[i].lca]--;
		
	for(int i = 1; i <= n; i++)
		printf("%d ", ans[i]);
	return 0;
}

终!于!写!完!了!!!树上差分的毒瘤题!!!【对我这种树上差分的新人太不友好了】

迎评:)
——End——

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值