最近公共祖先

\(\text{hdu-2874}\)

给定一张 \(n\) 个点 \(m\) 条边的有权无向图,对于每个询问 \((x, y)\),输出 \(x,y\) 之间的距离,或报告两个点不连通。

\(2 \le n \le 10^4\)\(0 \le m < 10^4\)\(1 \le q \le 10^6\)


用并查集判连通性,LCA 求两点的距离,注意细节。

注意:多测要清空 \(fa\) 数组,虽然看似不用清。

#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
#define MAXN 10005
#define pii pair<long long, long long>
#define fi first
#define se second

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, m, q, dep[MAXN], fa[MAXN][30], lg[MAXN], f[MAXN], a[MAXN];
vector<pii > v[MAXN];

long long find(long long x) { return (f[x] == x) ? x : f[x] = find(f[x]); }

void dfs(long long x, long long ff) {
	fa[x][0] = ff, dep[x] = dep[ff] + 1;
	for(int i = 1; i <= lg[dep[x]]; i ++)
		fa[x][i] = fa[fa[x][i - 1]][i - 1]; 
	for(auto it : v[x]) if(it.fi != ff) 
		a[it.fi] = a[x] + it.se, dfs(it.fi, x);
	return;
}

long long lca(long long x, long long y) {
	if(dep[x] < dep[y]) swap(x, y);
	while(dep[x] > dep[y]) 
		x = fa[x][lg[dep[x] - dep[y]] - 1];
	if(x == y) return x;
	for(int i = lg[dep[x]] - 1; i >= 0; i --)
		if(fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];
	return fa[x][0];
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	for(int i = 1; i < MAXN; i ++) lg[i] = lg[i >> 1] + 1;
	while(cin >> n >> m >> q) {
		for(int i = 1; i <= n; i ++) 
			f[i] = i, v[i].clear(), a[i] = dep[i] = 0;
		memset(fa, 0, sizeof fa);
		for(int i = 1; i <= m; i ++) {
			long long x, y, w; cin >> x >> y >> w;
			v[x].push_back({y, w}), v[y].push_back({x, w});
			f[find(x)] = find(y);
		}
		for(int i = 1; i <= n; i ++) if(!dep[i]) dfs(i, 0);
		while(q --) {
			long long x, y; cin >> x >> y;
			if(find(x) != find(y)) { cout << "Not connected\n"; continue; }
			cout << a[x] + a[y] - 2 * a[lca(x, y)] << "\n";
		}
	}
	return 0;
}

\(\text{hdu-3078}\)

给定一棵树,包含 \(n\) 个节点,每个点有权值 \(a_i\),有以下 \(q\) 次操作:

  • 0 x y 表示将 \(a_x\) 修改为 \(y\)
  • k x y 表示询问 \(x \to y\) 路径上第 \(k\) 大的点权。

对于第二个操作,若经过的点少于 \(k\) 个,则报告 invalid request!

\(0 \le n \le 8 \times 10^4\)\(0 \le q \le 3 \times 10^4\)


非常神秘的一道题,貌似数据太水了。

直接暴力把路径上的点存起来,然后排序就好了,但显然没法通过这样的数据范围。

但就是过了 \(\dots\)

确实不太知道如果数据强度够的话怎么写,大概是述链剖分吧。

#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
#define MAXN 80005

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, q, a[MAXN], b[MAXN], dep[MAXN], fa[MAXN][30], lg[MAXN];
vector<long long> v[MAXN];

void dfs(long long x, long long f) {
	fa[x][0] = f, dep[x] = dep[f] + 1;
	for(int i = 1; i <= lg[dep[x]]; i ++)
		fa[x][i] = fa[fa[x][i - 1]][i - 1]; 
	for(auto y : v[x]) if(y != f) dfs(y, x);
	return;
}

long long lca(long long x, long long y) {
	if(dep[x] < dep[y]) swap(x, y);
	while(dep[x] > dep[y]) 
		x = fa[x][lg[dep[x] - dep[y]] - 1];
	if(x == y) return x;
	for(int i = lg[dep[x]] - 1; i >= 0; i --)
		if(fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];
	return fa[x][0];
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> q;
	for(int i = 1; i <= n; i ++) cin >> a[i];
	for(int i = 1; i < n; i ++) {
		long long x, y, w; cin >> x >> y;
		v[x].push_back(y), v[y].push_back(x);
	}
	for(int i = 1; i < MAXN; i ++) lg[i] = lg[i >> 1] + 1;
	dfs(1, 0);
	while(q --) {
		long long k, x, y, z, cnt = 0; 
		cin >> k >> x >> y; 
		if(!k) { a[x] = y; continue; }
		z = lca(x, y);
		for(int i = x; i != z; i = fa[i][0]) b[++ cnt] = a[i];
		for(int i = y; i != z; i = fa[i][0]) b[++ cnt] = a[i];
		b[++ cnt] = a[z];
		if(cnt < k) { cout << "invalid request!\n"; continue; }
		sort(b + 1, b + cnt + 1);
		cout << b[cnt - k + 1] << "\n";
	}
	return 0;
}

\(\text{hdu-3830}\)

小 X、小 Y 和小 Z 正在玩跳棋,这时小 Y 感到烦躁。他想把棋盘做得更大一些。虽然小 Z 坚持原版,但小 X 支持小 Y。他们把棋盘放大后,棋盘变成了一个无限线段。

现在棋盘就像是数轴,每个整数点都可以放一个棋子。初始状态下有三个不同整数点上各有一个棋子,游戏进行时,始终有三个棋子。每次,他们可以选择一个棋子 A 跳过一个中间棋子B到一个新位置(但是旧 A 和 B 之间的距离等于新A和B之间的距离,并且在[旧 A, 新 A]范围内除了 B 以外没有其他棋子)。

玩了一会儿后,他们想知道是否可以遵循规则,将给定状态 \(a,b,c\) 转移到 \(x,y,z\)。由于棋子被视为相同,\(a\) 必须跳到 \(x\) 是不必要的。

\(-10^9 \le a,b,c,x,y,z \le 10^9\)


以下部分题解来自于 HDU-3830 Checkers - dgsvygd - 博客园

隐式建图

考虑对排序好的 \(a,b,c\) 分类讨论:

  1. 如果 \(b−a = c−b\):只有两种情况转移,中间的往左边或者右边跳
  2. 如果 \(b−a \ne c−b\):除了上述的情况,还有两边通过中间跳

第二种情况显然可以被认为是其上一个状态,通过中间那个跳珠往两边跳造成的

把一个局面视为一个节点,我们则可以认为,如果中间往左边跳,会产生一个左子节点的状态;中间往右边跳,会产生一个右子节点的状态;如果是两边往中间跳,相等于往父节点走了一步;根节点为上述的情况 \(1\)

因此如果两个状态是可互相到达,说明他们的根是相同的。最短路径就是找他们的树上最短路径,即求解 lca。倍增计算的时候就不要建图了,构建一个函数来算他的 \(k\) 级祖先,通过整除的方式加速。


其实这种方式还挺好用的,不需要建图,但实际上有图的模型。

注意:本题多测,但题面没说,很阴。

#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
#define vc vector<long long>

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

vc v[2];

long long calc(vc &a, long long k) {
	long long res = 0, &x = a[0], &y = a[1], &z = a[2];
	while(k && (y - x) != (z - y)) {
		long long l = y - x, r = z - y, t = 0;
		if(l < r) t = min((r - 1) / l, k), x += t * l, y += t * l;
		else t = min((l - 1) / r, k), y -= t * r, z -= t * r;
		k -= t, res += t; 
	}
	return res;
}

long long lca(vc a, vc b) {
	vc ta = a, tb = b;
	long long depa = calc(ta, 1e18);
	long long depb = calc(tb, 1e18);
	if(ta != tb) return -1;
	if(depa < depb) swap(a, b), swap(depa, depb);
	long long res = depa - depb;
	depa -= calc(a, res);
	if(a == b) return res;
	for(int i = 40; i >= 0; i --) {
		vc pa = a, pb = b;
		long long t = calc(pa, 1ll << i);
		calc(pb, 1ll << i);
		if(pa != pb) a = pa, b = pb, res += (t << 1ll);
	}
	return res + 2;
}

int main() {
	long long x, y, z;
	while(cin >> x >> y >> z) {
		v[0].clear(), v[1].clear();
		v[0].push_back(x), v[0].push_back(y);
		v[0].push_back(z);
		for(int i = 1; i <= 3; i ++) {
			long long p; cin >> p;
			v[1].push_back(p);
		}
		sort(v[0].begin(), v[0].end());
		sort(v[1].begin(), v[1].end());
		long long ans = lca(v[0], v[1]);
		if(ans == -1) cout << "NO\n";
		else cout << "YES\n" << ans << "\n";
		
	}
	return 0;
}

\(\text{hdu-4338}\)

给定 \(n\) 个点 \(m\) 条边的无向图,可能有环也可能不连通。有 \(q\) 次询问,每次询问给定 \(x,y\),求 \(x\)\(y\) 的所有路径中,一定不经过的点的个数,这里的路径指简单路径,即一个点和一条边至多经过一次形成的路径。

\(x,y\) 之间不存在简单路径,则输出 \(n\)

注意:本题点的编号范围是 \(0 \sim n - 1\)

\(1 \le n \le 10^5\)\(0 \le m \le 2 \times 10^5\)\(1 \le q \le 10^5\)


非常非常毒瘤的一道题,花了我两个多小时,结果被卡常了,于是就不调了。

其实思路挺简单的。

首先原图可能有环,考虑把点双连通分量缩成一个点,但是仅把点双连边建图的话显然会出问题。比如,三个点双中间有一个割点,那么建图后三个点双会形成一条链,若此时询问最左侧点双和最右侧点双内的点,那么此时会认为三个点双内的点都能经过,但实际上只能经过割点而已,不能经过最中间点双内除割点以外的点,所以这样建图就出问题了。

通过反例我们可以发现,只需要把割点取出来,连接割点所在的所有点双,就解决了。

其实这样的图就是圆方树。其中割点为方点,点双为圆点。但是我写的时候并没有意识到,等发现的时候已经晚了,于是成功调了两个多小时,无果。

询问时若询问两个点,先把这两个点对应到缩点后的图上,然后 LCA 求出两个点之间路径经过的点数,这个需要推一下公式。下面是公式的全貌:

我们记缩点后点的大小为 \(sz_i\),点到根节点路径上经过的点数为 \(sm_i\),点到根节点的距离(也就是深度)为 \(dep_i\)\(x,y\) 的最近公共祖先为 \(z\)。实际上这个公式就是用容斥推出来的。

\[n-[(sm_x + sm_y - 2sm_z + sz_z) - (dep_x + dep_y - 2dep_z)] \]

除上述数据之外,重新建图的过程中我们还需要记录每个点对应新图上的点的编号。

还有一些特殊情况需要特判:

  • \(x = y\),输出 \(n-1\)
  • \(x,y\) 存在不是 \(0 \sim n - 1\) 的数,即 \(x \ge n\)\(y \ge n\),输出 \(n\)
  • \(x,y\) 不连通,输出 \(n\)。这一情况需要用并查集判。
  • \(x,y\) 在一个点双中,输出 \(n-sz_p\),其中 \(p\)\(x,y\) 所在的点双编号。

以及,这道题不能用邻接表存图,而且重构图可能会有 \(\ge n\) 个点,所以数组要开大点。

注意常数。


下面是一份没通过的代码,但是逻辑大概都是对的,卡常的问题。

以及代码最下面有两个较强的手捏样例。

#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
#include<stack>
using namespace std;
#define MAXN 200005

int read() {
	int x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

int n, m, q, cs, dfn[MAXN], low[MAXN], dn, s[MAXN], is[MAXN], sm[MAXN];
int fa[MAXN][30], lg[MAXN], dep[MAXN], sz[MAXN], f[MAXN], mp[MAXN];
vector<int> v[MAXN], g[MAXN];
vector<vector<int> > scc;
stack<int> st;
bool gd[MAXN];

int find(int x) { return (f[x] == x) ? x : f[x] = find(f[x]); }

void tarjan(int x, bool fp) {
	dfn[x] = low[x] = ++ dn;
	int son = 0; st.push(x);
	if(v[x].empty() && fp) { scc.push_back({x}); return; }
	for(auto y : v[x]) 
		if(!dfn[y]) {
			tarjan(y, 0), low[x] = min(low[x], low[y]);
			if(low[y] >= dfn[x]) {
				son ++;
				if(!fp || son >= 2) gd[x] = 1;
				int t = 0; scc.push_back({});
				do {
					t = st.top(), st.pop(), scc.back().push_back(t);
				} while(t != y);
				scc.back().push_back(x);
			}
		}
		else low[x] = min(low[x], dfn[y]);
	return;
}

void dfs(int x, int ff) {
	fa[x][0] = ff, dep[x] = dep[ff] + 1;
	sm[x] = sm[ff] + sz[x];
	for(int i = 1; i <= lg[dep[x]]; i ++)
		fa[x][i] = fa[fa[x][i - 1]][i - 1];
	for(auto y : g[x]) if(y != ff) dfs(y, x);
	return;
}

int lca(int x, int y) {
	if(dep[x] < dep[y]) swap(x, y);
	while(dep[x] > dep[y]) 
		x = fa[x][lg[dep[x] - dep[y]] - 1];
	if(x == y) return x;
	for(int i = lg[dep[x]] - 1; i >= 0; i --)
		if(fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];
	return fa[x][0];
}

int main() {
	for(int i = 1; i < MAXN; i ++) lg[i] = lg[i >> 1] + 1;
	while(cin >> n >> m) {
		for(int i = 1; i <= n; i ++) 
			v[i].clear(), g[i].clear(), sz[i] = sm[i] = 0;
		memset(fa, 0, sizeof fa);
		memset(mp, 0, sizeof mp);
		memset(dep, 0, sizeof dep);
		memset(dfn, 0, sizeof dfn); scc.clear();
		memset(low, 0, sizeof low); dn = 0;
		while(!st.empty()) st.pop();
		for(int i = 1; i <= n; i ++) f[i] = i;
		for(int i = 1; i <= m; i ++) {
			int x, y; cin >> x >> y; x ++, y ++;
			v[x].push_back(y), v[y].push_back(x);
			f[find(x)] = find(y);
		}
		for(int i = 1; i <= n; i ++) if(!dfn[i]) tarjan(i, 1);
		int res = 0;
		for(auto it : scc) {
			sz[++ res] = it.size();
			int id = res;
			for(auto x : it) 
				if(gd[x]) {
					if(!mp[x]) mp[x] = (++ res);
					g[id].push_back(mp[x]);
					g[mp[x]].push_back(id);
					sz[mp[x]] = 1;
				}
				else mp[x] = id;
		} 
		for(int i = 1; i <= res; i ++) if(!dep[i]) dfs(i, 0);
		cin >> q; cout << "Case #" << (++ cs) << ":\n";
		while(q --) {
			int x, y, z; cin >> x >> y;
			if(x == y) { cout << n - 1 << "\n"; continue; }
			if(x >= n || y >= n) { cout << n << "\n"; continue; }
			x ++, y ++;
			if(find(x) != find(y)) {
				cout << n << "\n"; 
				continue;
			}
			x = mp[x], y = mp[y];
			if(x == y) { cout << n - sz[x] << "\n"; continue; }
			z = lca(x, y); 
			int p1 = sm[x] + sm[y] - 2 * sm[z] + sz[z];
			int p2 = dep[x] + dep[y] - 2 * dep[z];
			cout << n - (p1 - p2) << "\n";
		} 
		cout << "\n";
	}
	return 0;
}

/*
11 11
0 1
0 2
1 2
1 3
1 4
2 5
2 6
7 8
7 9
8 9
8 10
5
1 2
3 5
3 4
0 3
7 10

9 12
0 1
0 2
1 2
2 3
2 4
3 4
3 5
3 6
5 6
4 7
4 8
7 8
6
0 2
0 3
0 5
2 5
3 5
5 6
*/
posted @ 2026-01-15 20:07  So_noSlack  阅读(0)  评论(0)    收藏  举报