树上倍增LCA题集

树上倍增法可以用于求LCA,当然在其它场合也有应用,在很多时候看似不是树形的问题,我们也可以通过转化成树形结构,然后在树中进行操作,这种操作可以是求二者之间的最近状态,或者是二者之间的距离,如最后一题。
另外关于树的题型,最好还是根据样例画图思考,往往可以比空想获得更多思路。

点的距离

题意简述
给定一棵 n 个点的树,Q 个询问,每次询问点 x 到点 y 两点之间的距离。
解题思路
模板题,用树上倍增法,初始化时间为O(NlogN),查询时间O(logN)。

代码示例

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int M = 2e5+10;
int head[N],ver[M],nex[M],tot = 1;
void addEdge(int x,int y){
	ver[++tot] = y; nex[tot] = head[x]; head[x] = tot;
}
int n,q;
//anc[x][i]表示x往上走2^i步的祖先编号 
int anc[N][25], deep[N],dis[N];
int root = 1;	//这棵树的根,无特殊说明可以为1 
void dfs(int x){
	for(int i = 1;i <= 22;i++)
		anc[x][i] = anc[anc[x][i-1]][i-1];
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i] , z = 1;
		if(dis[y] || y == root) continue;
		deep[y] = deep[x]+1;
		dis[y] = dis[x] + z;
		anc[y][0] = x; dfs(y);
	}
}
int Lca(int u,int v){
	if(deep[u] < deep[v]) swap(u,v);
	for(int i = 22;i >= 0;i--)
		if(deep[anc[u][i]] >= deep[v]) u = anc[u][i];
	if(u == v) return u;
	for(int i = 22;i >= 0;i--)
		if(anc[u][i] != anc[v][i]){
			u = anc[u][i]; v= anc[v][i];
		}
	return anc[u][0];
}
int ask(int x,int y){
	/*logN时间内返回x与y之间的距离,注意是否会爆int*/ 
	return dis[x] + dis[y] - 2*dis[Lca(x,y)];
}
int main(){
	scanf("%d",&n);
	for(int i = 1,x,y;i < n;i++){
		scanf("%d%d",&x,&y);
		addEdge(x,y); addEdge(y,x);//双向边 
	}
	deep[0] = -1; dfs(root);//完成预处理 
	scanf("%d",&q);
	for(int i = 1,x,y;i <= q;i++){
		scanf("%d%d",&x,&y);
		printf("%d\n",ask(x,y));
	}
	return 0;
} 

暗的锁链*

题意简述
原题来自:POJ 3417

Dark 是一张无向图,图中有 N 个节点和两类边,一类边被称为主要边,而另一类被称为附加边。Dark 有 N–1 条主要边,并且 Dark 的任意两个节点之间都存在一条只由主要边构成的路径。另外,Dark 还有 M 条附加边。

你的任务是把 Dark 斩为不连通的两部分。一开始 Dark 的附加边都处于无敌状态,你只能选择一条主要边切断。一旦你切断了一条主要边,Dark 就会进入防御模式,主要边会变为无敌的而附加边可以被切断。但是你的能力只能再切断 Dark 的一条附加边。

现在你想要知道,一共有多少种方案可以击败 Dark。注意,就算你第一步切断主要边之后就已经把 Dark 斩为两截,你也需要切断一条附加边才算击败了 Dark。

解题思路
这是一道需要观察和转化题意的问题。
在树上如果有一条附加边,就形成了一个环,可知每个环内最多有一条附加边。如果一条主要边在环中,则称该主要边被覆盖 ,它在几个环内,就被覆盖了几次。可知一个主要边最多被覆盖2次。
如果一条主要边被覆盖了 2 次,那么我们无论怎么切都切不断 Dark,因为切断了主要边后,还是一个环,而切断一个环需要切断两条边。如果一条主要边被覆盖了 1 次,那么我们切断该主要边后,再切断它的附加边即可,即只有一种方案。如果一条主要边被覆盖0次,那么我们切完该主要边后,可以任意切一条附加边,共 m 中方案(m 是附加边的条数)。综上所述,我们只需要求出每条主要边被附加边覆盖次数即可,我们设 f[x] 为 x 到其父节点的主要边被覆盖的次数。

我们可以利用树上差分来求 f 数组,树上差分的思想就不额外介绍了,具体做法是如果有一条附加边(x, y),那么f[x]++ ,f[y]++,同时 f[ Lca(x, y) ] -= 2,这样当我们添加完附加边后,将 f 数组从叶子节点向上累加,即可更新完毕 f 数组。

代码示例

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10;
const int M = 2e5+10;
int head[N],ver[M],nex[M], tot = 1;
void addEdge(int x,int y){
	ver[++tot] = y; nex[tot] = head[x];head[x] = tot;
}
int n,m,f[N];
int dis[N],deep[N],anc[N][24];
void dfs(int x){
	for(int i = 1;i <= 22;i++) 
		anc[x][i] = anc[anc[x][i-1]][i-1];
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i], z = 1;
		if(dis[y] || y == 1) continue;
		deep[y] = deep[x] + 1;
		dis[y] = dis[x] + z;
		anc[y][0] = x; dfs(y);
	}
}
int Lca(int x,int y){
	if(deep[x] < deep[y]) swap(x,y);
	for(int i = 22;i >= 0;i--)
		if(deep[anc[x][i]] >= deep[y]) x = anc[x][i];
	if(x == y) return x;
	for(int i = 22;i >= 0;i--)
		if(anc[x][i] != anc[y][i]){
			x = anc[x][i]; y = anc[y][i];
		}
	return anc[x][0];
}
int vis[N];
int dfs2(int x){
	vis[x] = true;
	for(int i = head[x];i;i = nex[i]){
		int y = ver[i];
		if(vis[y]) continue;
		f[x] += dfs2(y);
	}
	return f[x];
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i = 1,x,y;i < n;i++){
		scanf("%d%d",&x,&y);
		addEdge(x,y); addEdge(y,x);
	}
	dfs(1);
	for(int i = 1,x,y;i <= m;i++){
		scanf("%d%d",&x,&y);
		f[x]++; f[y]++; f[Lca(x,y)] -= 2;
	}
	dfs2(1); int ans = 0;
	for(int i = 2;i <= n;i++)
		if(!f[i]) ans += m;
		else if(f[i] == 1) ans++;
	printf("%d\n",ans);
	return 0;
}

异象石*

题意简述
原题来自:Contest Hunter Round #56

在 Adera 的异时空中有一张地图。这张地图上有 N 个点,有 N−1 条双向边把它们连通起来。起初地图上没有任何异象石,在接下来的 M 个时刻中,每个时刻会发生以下三种类型的事件之一:

地图的某个点上出现了异象石(已经出现的不会再次出现);

地图某个点上的异象石被摧毁(不会摧毁没有异象石的点);

向玩家询问使所有异象石所在的点连通的边集的总长度最小是多少。

请你作为玩家回答这些问题。下图是一个例子,灰色节点表示出现了异象石,加粗的边表示被选为连通异象石的边集。

解题思路
我也不太能理解这题的思路是怎么发现的。
很自然的想法就是求所有 k 个异象石两两之间的距离和,然后 / (k-1) 即为答案,但显然会超时。
正解是利用一个结论,该结论题解没给出证明,搞懂了再补上。
对这颗树先进行dfs遍历,给每个节点打上一个时间戳,设节点 x 的时间戳为 dfn[x];设已存在的异象石中,dfn序是 x 前驱的节点编号为 pre,后继节点编号为 nex,那么在 x 处插入异象石后, ans += Dis(pre , x) + Dis(x, nex) - Dis(pre, nex)。
删除一个异象石同理,减去就好。
证明暂时没有,另外这个要做法记住,是用来求树上多个节点之间的最短总路径用的。

还要注意set的用法,如果set为空,那么begin() = end(),此时如果用指向end()的迭代器自增或自减都是不合法的。

代码示例

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10;
const int M = 2e5+10;
int head[N],ver[M],nex[M], tot = 1;
void addEdge(int x,int y){
	ver[++tot] = y; nex[tot] = head[x];head[x] = tot;
}
int n,m,f[N];
int dis[N],deep[N],anc[N][24];
void dfs(int x){
	for(int i = 1;i <= 22;i++) 
		anc[x][i] = anc[anc[x][i-1]][i-1];
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i], z = 1;
		if(dis[y] || y == 1) continue;
		deep[y] = deep[x] + 1;
		dis[y] = dis[x] + z;
		anc[y][0] = x; dfs(y);
	}
}
int Lca(int x,int y){
	if(deep[x] < deep[y]) swap(x,y);
	for(int i = 22;i >= 0;i--)
		if(deep[anc[x][i]] >= deep[y]) x = anc[x][i];
	if(x == y) return x;
	for(int i = 22;i >= 0;i--)
		if(anc[x][i] != anc[y][i]){
			x = anc[x][i]; y = anc[y][i];
		}
	return anc[x][0];
}
int vis[N];
int dfs2(int x){
	vis[x] = true;
	for(int i = head[x];i;i = nex[i]){
		int y = ver[i];
		if(vis[y]) continue;
		f[x] += dfs2(y);
	}
	return f[x];
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i = 1,x,y;i < n;i++){
		scanf("%d%d",&x,&y);
		addEdge(x,y); addEdge(y,x);
	}
	dfs(1);
	for(int i = 1,x,y;i <= m;i++){
		scanf("%d%d",&x,&y);
		f[x]++; f[y]++; f[Lca(x,y)] -= 2;
	}
	dfs2(1); int ans = 0;
	for(int i = 2;i <= n;i++)
		if(!f[i]) ans += m;
		else if(f[i] == 1) ans++;
	printf("%d\n",ans);
	return 0;
}

次小生成树*

这是阳哥哥负责的题型,我就不越界了。

Dis

题意简述
给出 n 个点的一棵树,多次询问两点之间的最短距离。
注意:边是双向的。
解题思路
这个就是标准的模板题了,和第一题一样,注意一下细节即可。

代码示例

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int M = 2e5+10;
int head[N],ver[M],nex[M],edge[M],tot = 1;
void addEdge(int x,int y,int z){
	ver[++tot] = y; nex[tot] = head[x];
	edge[tot] = z; head[x] = tot;
}
int n,q;
int anc[N][25], deep[N],dis[N];
void dfs(int x){
	for(int i = 1;i <= 22;i++)
		anc[x][i] = anc[anc[x][i-1]][i-1];
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i] , z = edge[i];
		if(dis[y] || y == 1) continue;
		deep[y] = deep[x]+1;
		dis[y] = dis[x] + z;
		anc[y][0] = x; dfs(y);
	}
}
int Lca(int u,int v){
	if(deep[u] < deep[v]) swap(u,v);
	for(int i = 22;i >= 0;i--)
		if(deep[anc[u][i]] >= deep[v]) u = anc[u][i];
	if(u == v) return u;
	for(int i = 22;i >= 0;i--)
		if(anc[u][i] != anc[v][i]){
			u = anc[u][i]; v= anc[v][i];
		}
	return anc[u][0];
}
int ask(int x,int y){
	return dis[x] + dis[y] - 2*dis[Lca(x,y)];
}
int main(){
	scanf("%d%d",&n,&q);
	for(int i = 1,x,y,z;i < n;i++){
		scanf("%d%d%d",&x,&y,&z);
		addEdge(x,y,z); addEdge(y,x,z);
	}
	dfs(1);//完成预处理
	for(int i = 1,x,y;i <= q;i++){
		scanf("%d%d",&x,&y);
		printf("%d\n",ask(x,y));
	}
	return 0;
} 

祖孙询问

题意简述
已知一棵 n 个节点的有根树。有 m 个询问,每个询问给出了一对节点的编号 x 和 y,询问 x 与 y 的祖孙关系。
解题思路
简单的模板题,需要注意的是如果 x == y,那么是属于其它情况哦。
另外还需要注意一下,deep[0] = -1,否则会和根节点数值相等而出错。

代码示例

#include<bits/stdc++.h>
using namespace std;
int n,m;
const int N = 1e5+10;
const int M = 2e5+10;
int head[N],ver[M],edge[M],nex[M],tot = 1;
void addEdge(int x,int y,int z){
	ver[++tot] = y; edge[tot] = z;
	nex[tot] = head[x]; head[x] = tot;
}
int anc[N][25], dis[N],deep[N];
int root;
void dfs(int x){
	for(int i = 1;i <= 22;i++)
		anc[x][i] = anc[anc[x][i-1]][i-1];
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i], z = edge[i];
		if(dis[y] || y == root) continue;
		deep[y] = deep[x]+1;
		dis[y] = dis[x] + 1;
		anc[y][0] = x; dfs(y);
	}
}
int Lca(int x,int y){
	if(deep[x] < deep[y]) swap(x,y);
	for(int i = 22;i >= 0;i--)
		if(deep[anc[x][i]] >= deep[y]) x = anc[x][i];
	if(x == y) return x;
	for(int i = 22;i >= 0;i--)
		if(anc[x][i] != anc[y][i]){
			x = anc[x][i]; y = anc[y][i];
		}
	return anc[x][0];
}
int main(){
	scanf("%d",&n);
	for(int i = 1,x,y;i <= n;i++){
		scanf("%d%d",&x,&y);
		if(y == -1) root = x;
		else addEdge(x,y,1), addEdge(y,x,1);
	}
	/* 因为deep[root] = 0,如果deep[0] = 0,则会认为 0 号节点也是根 */
	deep[0] = -1; dfs(root);
	scanf("%d",&m);
	for(int i = 1,x,y;i <= m;i++){
		scanf("%d%d",&x,&y);
		int par = Lca(x,y);
		if(x == y) puts("0");
		else if(par == x) puts("1");
		else if(par == y) puts("2");
		else puts("0");
	}
	return 0;
}

聚会*

题意简述
原题来自:AHOI 2008

Y 岛风景美丽宜人,气候温和,物产丰富。Y 岛上有 N 个城市,有 N−1 条城市间的道路连接着它们。每一条道路都连接某两个城市。幸运的是,小可可通过这些道路可以走遍 Y 岛的所有城市。神奇的是,乘车经过每条道路所需要的费用都是一样的。

小可可,小卡卡和小 YY 经常想聚会,每次聚会,他们都会选择一个城市,使得三个人到达这个城市的总费用最小。

由于他们计划中还会有很多次聚会,每次都选择一个地点是很烦人的事情,所以他们决定把这件事情交给你来完成。他们会提供给你地图以及若干次聚会前他们所处的位置,希望你为他们的每一次聚会选择一个合适的地点。

解题思路
在一棵树上求三个点 (x, y ,z) 之间的最短路径,我们求出dis[x, y] + dis[x , z] + dis[y , z],画图就会发现每一段路径都被重复计算了 2 次,于是我们再将答案 / 2就得到了三个点之间的路径。
假设这个聚会点为 p ,那么显然 p 是三条路径的交汇点,否则路径和肯定不会是最短。下一步的目标就是求出这个交汇点的坐标。

如果我们以某个点为根,为每个节点更新深度deep(就像我们在求LCA时一样),那么显然 p 点就是 Lca[x, y] , Lca[x , z] , Lca[y , z] 三者中deep最大(最深)的点,这也可以通过画图观察出来,也很容易证明。

于是我们就可以利用树上倍增法求出Lca,在O(NlogN)时间内解决该题,但是常数不太优秀,极限数据还是会被卡掉,如果想满分需要用树链剖分,不过该思路已经可以通过一本通oj上的数据了。
代码示例

#include<bits/stdc++.h>
using namespace std;
const int N = 5e5+10;
const int M = 1e6+10;
int head[N],ver[M],edge[M],nex[M], tot = 1;
void addEdge(int x,int y,int z){
	ver[++tot] = y, edge[tot] = z;
	nex[tot] = head[x] , head[x] = tot;
} 
int n,m;
int getInt(){
	int res = 0;
	bool neg = false;
	char c = getchar();
	while(c != '-' && (c < '0' || c > '9')) c = getchar();
	if(c == '-') neg = true,c = getchar();
	while(c >= '0' && c <= '9') 
		res = res*10+c-'0', c = getchar();
	return neg?-res:res;
}
int root = 1;
int deep[N],anc[N][24],dis[N];
void dfs(int x){
	for(int i = 1;i <= 22;i++)
		anc[x][i] = anc[anc[x][i-1]][i-1];
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i], z = edge[i];
		if(dis[y] || y == root) continue;
		deep[y] = deep[x] + 1;
		dis[y] = dis[x] + 1;
		anc[y][0] = x; dfs(y);
	}
}
int Lca(int x,int y){
	if(deep[x] < deep[y]) swap(x,y);
	for(int i = 22;i >= 0;i--)
		if(deep[anc[x][i]] >= deep[y]) x = anc[x][i];
	if(x == y) return x;
	for(int i = 22;i >= 0;i--)
		if(anc[x][i] != anc[y][i]){
			x = anc[x][i]; y = anc[y][i];
		}
	return anc[x][0];
}
int Dis(int x,int y){
	return dis[x] + dis[y] - 2*dis[Lca(x,y)];
}
void print(int x){
	if(!x) return;
	print(x/10);
	putchar(x%10+'0');
}
int main(){
	n = getInt(); m = getInt();
	for(int i = 1,x,y;i < n;i++){
		x = getInt(); y = getInt();
		addEdge(x,y,1); addEdge(y,x,1);
	}
	deep[0] = -1; dfs(root);
	for(int i = 1,x,y,z;i <= m;i++){
		x = getInt(); y = getInt(); z = getInt();
		int par = Lca(x,y); 
		int p2 = Lca(x,z) , p3 = Lca(y,z);
		if(deep[par] < deep[p2]) par = p2;
		if(deep[par] < deep[p3]) par = p3;
		int res = Dis(x,y) + Dis(x,z) + Dis(y,z) >> 1;
		print(par); putchar(' '); print(res); puts("");
	}
	return 0;
} 

跳跳棋**

题意简述
原题来自:BZOJ 2144

跳跳棋是在一条数轴上进行的。棋子只能摆在整点上。每个点不能摆超过一个棋子。我们用跳跳棋来做一个简单的游戏:棋盘上有三颗棋子,分别在 a,b,c 这三个位置。我们要通过最少的跳动把他们的位置移动成 x,y,z(注意:棋子是没有区别的)。

跳动的规则很简单,任意选一颗棋子,对一颗中轴棋子跳动。跳动后两颗棋子距离不变。一次只允许跳过一颗棋子。

写一个程序,首先判断是否可以完成任务。如果可以,输出最少需要的跳动次数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NV2YSO6W-1570623629210)(en-resource://database/5334:0)]
解题思路
这题有好几个难点,思路也不是那么容易想到。

首先比较容易想到的是,有哪些点是可以达到的;设a,b,c升序,l1 = b-a, l2 = c-b,那么(l1, l2)可以转移到 (l1, l2-l1) 和 (l1-l2 , l2)。
那么每次转移了多少长度,向哪个方向转移的我们都知道了,自然就可以得到点的转移。就比如 (l1 , l2) --> (l1 , l2 - l1) ,就对应着(a ,b ,c)–>(b, 2b-a, c)。

既然我们知道点如何转移,那么问题就变成了判断 (a , b, c) 和 (x , y , z) 是否可以转移到同一个状态。

我们上面只讨论了两端节点向中间跳(对应着合法长度的相减,a跳到b、c中间,c跳到a、b中间),没考虑中间节点向两边跳(a关于c跳,或c关于a跳,或b关于a、c跳,对应着合法长度的相加),实际上两者都成立,比如(l1 , l2)–>(l1, l1+l2)对应着(a ,b ,c) --> (b , a , c) ,但是如果我们将其中两点之间的距离不断变大,会终止于正无穷,但如果不断缩小,就会终止于某一个非负整数;基于此我们只考虑“两边的点向中间跳”这种情况,这对应着开始介绍的两个减法转移。

到此就有些明朗了,每个状态看作一个节点,转移一次就看作向上走一步,最终一定会终止于某一个状态,这个状态就可以看作根节点;我们将(a ,b ,c) 与 (x, y , z)同时“向上”转移,如果它们终止于同一个状态,就说明它们可以互相抵达,否则则不能。

接下来就是要判断最小的步数,即(a ,b ,c) 到 (x , y , z) 的最少转移次数。这就相当于求二者的LCA,因为它们有共同的“根节点”,并且有着到根节点的唯一路径,所以可以将它们的转移看作一棵树,边权为1,求着两个状态之间的距离。

由于我们利用的是合法长度的转移来对应着点的状态的转移,即(l1, l2) --> (l1 , l2-l1)或(l1-l2, l2)。这和更相减损术 gcd(a, b) = gcd(a, b-a) = gcd(a-b, a)相同,而欧几里得又告诉我们 gcd(a ,b) = gcd(b ,a%b),于是本来 O(N) 的减法变成了 O(log N)的除法(取模)。

我们可以在O(logN)的时间内转移到任意一个父状态,但是我们不知道该向上转移几步才是最少步数,于是我们要先将二者的深度调到相同,然后同时向上走 k 步,这可以用倍增实现,也可以用二分搜索实现。求深度我们可以在求第一问时顺便求解,同时深度也是最长路径,用于二分。

代码示例

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
int a[5],b[5];
struct Node{
	int x,y,z;
	Node(){}
	Node(int x,int y,int z):x(x),y(y),z(z){}
	bool operator != (const Node& B) const{
		return !(x == B.x && y == B.y && z == B.z);
	}
};
int deep;
Node jump(Node h, int step){
	/*返回从位置h向上跳step步后的状态,顺便记录一下当前深度(距根距离)*/
	Node res = h;
	int t1 = h.y-h.x, t2 = h.z - h.y;
	if(t1 == t2) return res; 
	if(t1 < t2){
		int t = min(step,(t2-1)/t1);//用除法优化 
		step -= t; deep += t;	//记录一下深度 
		res.y += t*t1; res.x += t*t1;
	}else{
		int t = min(step,(t1-1)/t2);
		step -= t; deep += t;
		res.y -= t*t2; res.z -= t*t2;
	}
	if(step) return jump(res,step);
	return res;
}
bool check(int x,Node h1,Node h2){
	return !(jump(h1,x) != jump(h2,x));
}
int main(){
	for(int i = 1;i <= 3;i++) scanf("%d",a+i);
	for(int i = 1;i <= 3;i++) scanf("%d",b+i);
	sort(a+1,a+4); sort(b+1,b+4);
	Node h1(a[1],a[2],a[3]) , h2(b[1],b[2],b[3]);
	Node aa = jump(h1,INF); int d1 = deep; deep = 0;
	Node bb = jump(h2,INF); int d2 = deep; deep = 0;
	if(aa != bb){
		puts("NO"); return 0;
	}
	puts("YES");
	/*计算步数:先调整到同一深度,再二分一起跳*/
	if(d1 < d2) swap(d1,d2),swap(h1,h2);
	h1 = jump(h1,d1-d2);
	int l = 0, r = d1;
	while(l <= r){
		int mid = l+r>>1;
		if(check(mid,h1,h2)) r = mid-1;
		else l = mid+1;
	}
	printf("%d\n",2*l+d1-d2);
	return 0;
}
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 程序猿惹谁了 设计师:白松林 返回首页