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

之前就听说是好题,就想做,现在做完了,果然是好题。

P1600 天天爱跑步

题目大意:

给你一棵 \(n\) 个节点的树,\(m\) 个任务。

每一个任务有起点和终点,沿树上最短路径,都从 \(0\) 秒开始,每秒瞬移到下一个点。

每个节点有一个值 \(w\),代表当前节点会在 \(w\) 秒的时候统计当前节点一共有对少人。

问每个结点的答案。

思路:

1.转换

首先,上来会发现,这个任务相互之间是不可合并的,所以显然无法用树上差分之类的东西维护,所以无法直接做,考虑转化。

那么我们考虑对于每个点,统计对当前这个点有贡献的任务有多少。

显然,可以发现,如果一个任务对于当前节点有贡献的话,这个点一定是被夹在起始点或者结束点与它们的 \(lca\) 之间的,如下图:

\(s\)\(t\) 分别为起点和终点,\(lca\) 就是 \(s\)\(t\)\(lca\)

(求 \(lca\) 就不说了,有的是方法)

会发现,对某一点有贡献的任务一定是起点或者终点在这个点的子树中。

并且利用深度 \(dep\) 转化之后,我们可以发现,如果对某一点(设为 \(p\))有贡献,一定是:

  1. \(dep_s=dep_p+w_p\)
  2. \(len-dep_t=w_p-dep_p\)

\(len\)\(s\),\(t\) 之间的距离,就等于\(dep_s+dep_t-dep_{lca}\times 2\)

综上,就会发现有一个优美的性质,由于对某一点有贡献的任务一定是起点或者终点在这个点的子树中,并且经过转化后,每一个点作为起点或终点就独立了,所以我们可以对于每一个子树都作为子问题单独处理。

并且,又因为每个任务的起点和终点独立且做贡献的时候式子唯一,我们可以开两个桶 \(b1\)\(b2\),分别记录当前子树中的所有起点的 \(dep_s\) 和所有终点的 \(len-dep_t\) 的数量。

具体操作的时候,我们可以对于每个点开两个vector,记录所有以当前点作为起点的所有 \(dep_s\) 和作为终点的所有 \(len-dep_t\)

又因为作为起点的话,都是对桶的 \(dep_s\) 有贡献,所以可以直接统计某一点作为起点的数量就好了,就没必要开vector了。

递归到某一点将当前点的贡献加入桶中就彳亍了。

注意,由于在作为终点做贡献的时候会减出负数,所以可以统一加上一个大数防止溢出。

//递归到某个点
inline void dfs2(int now)
{
	…………
	……
	b1[dep[now]]+=st[now];
	for(int i=0;i<ed[now].size();i++)
		b2[ed[now][i]-dep[now]+ADD]++;
	…………
}

……
…………

int main()
{
	…………
	……
	//读入
	int u,v,lca,dist;
	while(m--)
	{
		u=read();	v=read();
		lca=LCA(u,v);	dist=dep[u]+dep[v]-(dep[lca]<<1);
		……
		…………
		st[u]++;
		ed[v].push_back(dist);
	}
	……
	…
}

2. 桶的使用

对于桶的使用,其实是有一个问题的。就是对于已经统计过的部分,无法保证只有当前树的子树,如下图:

在枚举到2子树时,1已经全部枚举完了,并且贡献都在桶中了,但是这一部分在统计2的答案的时候,不应该在桶里。

可要是都去除的话又不好,因为这一部分在统计整个树时候要用到,全部分开然后合并重算太费时间和空间。

怎么办呢?会发现对于某一棵子树,它的遍历是连续的(这也是几乎所有和子树有关的题目的做法依据),就是在刚进入这个点开始,到离开这个点结束,一定完全且只遍历了这个点的子树。

所以可以在刚刚进入这个点的时候,记录要统计的、桶内的贡献 \(num1\)\(num2\),之后在遍历完它的整个子树之后,将它们与现在的桶内贡献做差,就得到了这个子树内新增的贡献。

inline void dfs2(int now)
{
	int num1=b1[dep[now]+w[now]],num2=b2[w[now]-dep[now]+ADD];
	
	…………
	………………	//遍历子树
	…………
	
	ans[now]+=	(b1[dep[now]+w[now]]-num1)	+
				(b2[w[now]-dep[now]+ADD]-num2);
	
	……
}

3. 多余贡献

然后注意由于 \(p\) 必须是在 \(s\)\(t\)\(lca\) 之间,所以如果当前任务的 \(lca\) 在当前子树的内部,那么它就再也没有贡献了。(可以看图意会一下)

对于这种任务,我们需要在桶内去除它的贡献。

可以发现,这种任务最后的贡献是在 \(p\) 即为 \(lca\) 的时候,之后就再不可能有贡献。所以,在递归的时候,可以对于所有 \(lca\) 为当前点的任务从桶中去除。

具体操作的时候可以对于每个点都开一个vector,存储所有以当前点为 \(lca\) 的任务,在统计完这个点的答案之后将这些任务去除就好了。

inline void dfs2(int now)
{
	…………
	……
	…………
	……
	for(int i=0;i<lst[now].size();i++)
	{
		b1[dep[lst[now][i].s]]--;
		b2[lst[now][i].dist-dep[lst[now][i].t]+ADD]--;
	}
}

……
…………

int main()
{
	…………
	……
	//读入
	int u,v,lca,dist;
	while(m--)
	{
		……
		lst[lca].push_back({u,v,dist});
		……
	}
	……
	…
}

4. 重复贡献

还有一种情况,就是在某一个任务对于 \(s\)\(t\)\(lca\) 有贡献的时候,这个贡献会计算两次,所以进行特判。

产生的原因可以靠下图意会,就是相当于两个红点合二为一了。当然通过转换后的式子也可以发现。

具体操作时其实很简单,就只需要在读入的时候减掉就好了。

int main()
{
	…………
	……
	//读入
	int u,v,lca,dist;
	while(m--)
	{
		……
		ans[lca]-=(dep[u]==w[lca]+dep[lca]);
		……
	}
	……
	…
}

Code:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
inline int read(){
	int rt=0;	char g=getchar();
	while(g<'0'||g>'9')	g=getchar();
	while(g>='0'&&g<='9')	rt=(rt<<3)+(rt<<1)+g-'0',g=getchar();
	return rt;
}
int n,m;
vector<int>t[300005];
int f[300005][19];
int dep[300005];
inline void dfs(int now,int fa,int deep)
{
	f[now][0]=fa;	dep[now]=deep;
	for(int i=0,to;i<t[now].size();i++)
	{
		if(t[now][i]==fa)	continue;
		dfs(t[now][i],now,deep+1);
	}
}
inline void init()
{
	for(register int j=1;j<=18;j++)
		for(register int i=1;i<=n;i++)
			f[i][j]=f[f[i][j-1]][j-1];
}
inline int LCA(int x,int y)
{
	if(dep[x]<dep[y])	swap(x,y);
	for(int i=18;i>=0;i--)
		if(dep[f[x][i]]>=dep[y])
			x=f[x][i];
	if(x==y)	return x;
	for(int i=18;i>=0;i--)
		if(f[x][i]!=f[y][i])
			x=f[x][i],y=f[y][i];
	return f[x][0];
}

int w[300005];
int st[300005];
vector<int>ed[300005];
struct node{int s,t,dist;};
vector<node>lst[300005];

#define ADD 300000
int ans[300005];
int b1[300005],b2[600010];
inline void dfs2(int now)
{
	int num1=b1[dep[now]+w[now]],num2=b2[w[now]-dep[now]+ADD];
	for(int i=0,to;i<t[now].size();i++)
	{
		if(t[now][i]==f[now][0])	continue;
		dfs2(t[now][i]);
	}
	b1[dep[now]]+=st[now];
	for(int i=0;i<ed[now].size();i++)
		b2[ed[now][i]-dep[now]+ADD]++;
	ans[now]+=b1[dep[now]+w[now]]-num1+b2[w[now]-dep[now]+ADD]-num2;
	for(int i=0;i<lst[now].size();i++)
	{
		b1[dep[lst[now][i].s]]--;
		b2[lst[now][i].dist-dep[lst[now][i].t]+ADD]--;
	}
}

int main()
{
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n=read();m=read();
	for(int i=1,u,v;i<n;i++)
	{
		u=read();	v=read();
		t[u].push_back(v);
		t[v].push_back(u);
	}
	dfs(1,0,1);	init();
	for(int i=1;i<=n;i++)	w[i]=read();
	int u,v,lca,dist;
	while(m--)
	{
		u=read();	v=read();
		lca=LCA(u,v);	dist=dep[u]+dep[v]-(dep[lca]<<1);
		lst[lca].push_back({u,v,dist});
		ans[lca]-=(dep[u]-dep[lca]==w[lca]);
		st[u]++;
		ed[v].push_back(dist);
	}
	dfs2(1);
	for(int i=1;i<=n;i++)	printf("%d ",ans[i]);
	return 0;
}
posted @ 2024-07-11 15:05  YT0104  阅读(134)  评论(0编辑  收藏  举报