☆ [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;
}
posted @ 2018-08-23 10:03  DennyQi  阅读(337)  评论(0编辑  收藏  举报