☆ [NOIp2016] 天天爱跑步 「树上差分」

题目类型:LCA+思维

传送门:>Here<

题意:给出一棵树,有(M)个人在这棵树上跑步。每个人都从自己的起点(s[i])跑到终点(t[i]),跑过一条边的时间为1秒。现在每个节点都有一个观察员,节点(i)上的观察员会在第(W[i])秒进行观察,如果有(x)个人此时到达节点(i),则这个观察员能够观察到(x)个人。问所有人跑步结束以后每个观察员可以观察到多少人

解题思路

这道题是公认所有(NOIp)中最难的一道题。但其实这道题的数据约定能够给我们很大的提示。

其实这道题的正解就是对各个部分分的方法的汇总和整合。因此我们先一一分析部分分如何拿

子任务一 暴力

由于(N)(1000),可以暴力模拟每个人的行动轨迹。求一个(LCA)暴力走一下就好了,没有思维难度。这个部分分能够首先让你熟悉题目的意思

子任务二 树退化为链

乍一看很简单,其实由于(N=99994),也不是那么容易的。事实上,这一个部分分是整道题的精髓。

由于树已经退化为链,所以所有的路径要么从左往右,要么从右往左。我们可以记录一个桶(b[x])。当我们从左往右扫到(i)时,(b[x])表示以(x)为起点的所有路径中,能够一直延伸到(i)的路径数量。

记录这个有什么用呢?对于节点(i),由于路径从左往右,所以如果想要一条路径在走到(i)的时候被观察到,那么这条路径的起点(S)一定满足:$$S=i-W[i]$$因此我们在考虑节点(i)时,只需要考虑以(i-W[i])为起点的能够延伸到(i)的路径条数,这些路径一定能够在(i)点被观察到。因此我们需要统计的就是(b[i-W[i])。那么从右往左的路径也类似,我们只需要统计(b[i+W[i]])就可以了

那么如何维护(b)数组呢?我们可以首先记录每个点的起点个数,然后过了终点以后减去终点对应的那些起点。例如(2)开头只有(3)条路径:((2,4) (2,3) (2,7))。我们扫描到(2)的时候让(b[2]+=3),代表从(2)开始能够延伸到(2)的路径有(3)条。然而扫描到(4)的时候事实上只有(2)条路径满足了,因此在统计完(3)以后应当(--b[2]),因为((2,3))这条路径不可能延伸到(4)了。但是如果还存在一条路径((1,3))呢?一个终点可能对应多个起点,所以我们需要用一个(vector)来维护每个终点对应的所有起点,在扫描到这个终点的时候扫一遍(vector)全部减掉

事实上,这就是差分。只不过普通差分只需要统计次数,而不会规定起点的深度。对于这个规定起点深度的问题,自然需要排除深度不正确的起点。所以才会需要用(vector)来记录

子任务三 (S=1)

我们发现,当(S=1)时,所有路径都是从根节点往下。这样的路径有什么特点?对于任意一个点(u),由于它的路径一定是从根节点出发的,因此根节点到它的路径长度就是它的深度(根节点深度为0)。也就是说,这要满足(dep[u]=W[u]),这个点就是能够被观察到的

那么到底会被观察到几次呢?这就取决于根节点到它有几条路径。或者用更便于统计的方法:(u)及它的子树内有多少个终点

子任务四 (T=1)

和前者反了一下,但也更巧妙。所有路径都是从下往上到根节点的

我们发现一条路径上的一个点如果要被观察到,首先应该满足$$dep[S]-dep[u]=W[u]$$移项我们发现,也就是$$dep[u]+W[u]=dep[S]$$也就是说我们需要找的答案等价于,在节点(u)及其子树中,有多少起点的深度为(dep[u]+W[u])

联系链状那一部分的做法,其实就是把一维的拓展为了树。由于所有路径都是向上的,也就等价于链状中所有路径都是从右向左的。我们记录一个桶(b[x])表示深度为(x)的起点有多少个。因此我们还是一样,每遇到一个起点就++。特别的幸运的是,我们还不需要开一个(vector),遇到终点就减去对应起点,因为终点一定是根节点。每一次统计的答案也就是(b[dep[u]+W[u]])

但是有一个问题,我们这样的做法的正确性必须保证是在(u)的子树内。然而深度为(x)的起点有可能是之前在别的子树内统计的。怎么办?其实我们是从下往上走,也就是要在处理完(u)的全部子树以后才来处理(u),于是我们其实只需要考虑新增加的部分就可以了。也就是在(DFS)下去之前先记录一个(b[dep[u]+W[u]])存为(tmp),在(DFS)结束以后的答案也就是(b[dep[u]+W[u]]-tmp)

附上如上子任务的代码 Code

正解

对于一般性的数据,所有路径一定是从下往上,经过(LCA),再从上往下。而数据提示了我们(S=1)(T=1)。这就是在暗示我们将路径分开来考虑!

因此,就像链的情况,正解就是需要判断起点深度的树上差分!

首先考虑所有的((S,LCA))的路径。这些路径的特性与(T=1)的路径是一样的。都需要满足(dep[u]+W[u]=dep[S]),唯一的不同就在于需要像链状一样,维护一个(vector)记录终点对应的起点,在过终点时减去即可。注意,这里的终点不是指(T),而是(LCA)

最最难理解的就是((LCA,T))这一部分了。我们依旧分析路劲的特征。发现被观察到的点一定满足$$W[u]+dep[T]-dep[u]=dis(S,T)$$移项得到$$dep[u]-W[u]=dep[T]-dis(S,T)$$。但是不像之前的(dep[S]),这个(dep[T]-dis(S,T))是在树上是没有实际意义的(一定要说有的话,那就是将向上的半截路径翻到上面去的深度)。

于是没有办法,我们需要类比((S,LCA))部分的做法来处理这个部分。我们依然是从下往上做,与之前相反,每遇到一个(T)就代表着进入了一条新的路径,每遇到一个(LCA)就意味着离开了一条路径。并且我们每遇到一个终点,标记的不是对应起点,而是对应的(dep[T]-dis(S,T));每遇到一个(LCA),减去的也是该路径对应的(dep[T]-dis(S,T))。因此我们此时需要两个(vector)来存了

综上就是算法的基本思想。这里还有两个细节需要分析:

  • 注意到(dep[T]-dis(S,T))很有可能是个负数,这让(c++)开通特别麻烦。一个比较粗暴的方式就是全部一律右移(MAXN)

  • 我们将一条路径剖为了((S,LCA) 和 (LCA,T))。如果(LCA)恰好是能够被观察到的,不就重复统计了一遍?所以我们需要在最后结算的时候进行修改,如果当前路径的(LCA)会被看到,也就是满足$$dep[S]-dep[LCA]=W[LCA]$$那么就应当减去一个(1)

Code

倍增的数组开太小了调试了近一个小时(qwq)

/*By DennyQi 2018*/
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
#define  r  read()
using namespace std;
typedef long long ll;
const int MAXN = 300010;
const int MAXM = 600010;
const int INF = 1061109567;
inline int Max(const int a, const int b){ return (a > b) ? a : b; }
inline int Min(const int a, const int b){ return (a < b) ? a : b; }
inline int read(){
    int x = 0; int w = 1; register char c = getchar();
    for(; c ^ '-' && (c < '0' || c > '9'); c = getchar());
    if(c == '-') w = -1, c = getchar();
    for(; c >= '0' && c <= '9'; c = getchar()) x = (x<<3) + (x<<1) + c - '0'; return x * w;
}
int N,M,x,y,len;
int first[MAXM],nxt[MAXM],to[MAXM],cnt;
int f[MAXN][25],dep[MAXN],W[MAXN],s[MAXN],t[MAXN],lca[MAXN],nums[MAXN],b[MAXN],bk[MAXN*2],ans[MAXN];
vector <int> S[MAXN];
vector <int> T1[MAXN], T2[MAXN];
inline void add(int u, int v){
	to[++cnt]=v, nxt[cnt]=first[u], first[u]=cnt;
}
void BZ_INIT(int u, int _f, int d){
	int v;
	dep[u] = d; f[u][0] = _f;
	for(int i = 1; (1 << i) <= d; ++i){
		f[u][i] = f[f[u][i-1]][i-1];
	}
	for(int i = first[u]; i; i = nxt[i]){
		if((v = to[i]) == _f) continue;
		BZ_INIT(v, u, d+1);
	}
}
inline int LCA(int a, int b){
	if(dep[a] < dep[b]) swap(a, b);
	for(int i = 20; i >= 0; --i){
		if(dep[a] - (1 << i) >= dep[b]) a = f[a][i];
	}
	if(a == b) return a;
	for(int i = 20; i >= 0; --i){
		if(f[a][i] == f[b][i]) continue;
		a = f[a][i], b = f[b][i];
	}
	return f[a][0];
}
void DFS1(int u, int _f){
	int v, tmp = b[dep[u] + W[u]];
	for(int i = first[u]; i; i = nxt[i]){
		if((v = to[i]) == _f) continue;
		DFS1(v, u);
	}
	b[dep[u]] += nums[u];
	ans[u] += b[dep[u] + W[u]] - tmp;
	for(int i = 0, sz = S[u].size(); i < sz; ++i){
		--b[S[u][i]];
	}
}
void DFS2(int u, int _f){
	int v, tmp = bk[dep[u] - W[u] + MAXN];
	for(int i = first[u]; i; i = nxt[i]){
		if((v = to[i]) == _f) continue;
		DFS2(v, u);
	}
	for(int i = 0, sz = T1[u].size(); i < sz; ++i){
		++bk[T1[u][i] + MAXN];
	}
	ans[u] += bk[dep[u] - W[u] + MAXN] - tmp;
	for(int i = 0, sz = T2[u].size(); i < sz; ++i){
		--bk[T2[u][i] + MAXN];
	}
}
int main(){
	N = r, M = r;
	for(int i = 1; i < N; ++i){
		x = r, y = r;
		add(x, y);
		add(y, x);
	}
	BZ_INIT(1, 0, 0);
	for(int i = 1; i <= N; ++i){
		W[i] = r;
	}
	for(int i = 1; i <= M; ++i){
		s[i] = r, t[i] = r;
		lca[i] = LCA(s[i], t[i]);
		len = dep[s[i]] + dep[t[i]] - 2 * dep[lca[i]];
		++nums[s[i]];
		S[lca[i]].push_back(dep[s[i]]);
		T1[t[i]].push_back(dep[t[i]] - len);
		T2[lca[i]].push_back(dep[t[i]] - len);
	}
	DFS1(1, 0);
	DFS2(1, 0);
	for(int i = 1; i <= M; ++i){
		if(dep[s[i]] - dep[lca[i]] == W[lca[i]]){
			--ans[lca[i]];
		}
	}
	for(int i = 1; i <= N; ++i){
		printf("%d ", ans[i]);
	}
	return 0;
}
原文地址:https://www.cnblogs.com/qixingzhi/p/9522068.html