线段树不要唱歌2

线段树不要唱歌第二弹。

本系列主要为线段树上处理(终于把线段树当树了,以前有事剪枝(分裂)又是嫁接(合并)的)

线段树就是树,所以可以 为所欲为 dp,分治(?)等等

线段树分治

此前我们需要讲一下可撤销并查集。

可撤销并查集

这个东西他不能路径压缩(差评,只会打路径压缩),但是可以按秩合并。

我们发现如果我们要硬改 fa 数组的话会导致错乱,所以我们要倒着改?

58ad612014caf

其实是如果要删除,就只能删上一次操作的那个。

我们可以用一个栈(毕竟倒着取出来) s 存下每次操作,在最后的时候,我们只需要一直删,删到你想要的位置就行了。

在删的时候应该是将 fa[find(u)] = u (指回自己), sz[find(v)] -= sz[find(v)] 删去合并时的贡献。

戳我喵~
struct node {
	int x, y, add;
};

stack<node> s;

int fa[N], sz[N];

int find(int x) {
	return fa[x] == x ? fa[x] : find(fa[x]);
}
int main() {
    int T = s.size();
	for (auto e : tree[p].edge) {
		int fu = find(e.first), fv = find(e.second);
		if (fu == fv) {
			s.push({-1, -1, -1});
			continue;
		}
		if (sz[fu] > sz[fv]) swap(fu, fv);
		s.push({fu, fv, sz[fu]}), fa[fu] = fv, sz[fv] += sz[fu];
	}
    while (s.size() > T) {
		node x = s.top(); s.pop();
		if (x.x == -1) continue;
		fa[x.x] = x.x, sz[x.y] -= x.add;
	}
}


线段树不本来就是分治 (自己b开) 吗?为什么还要分治?

且听我慢慢道来。

核心处理方式

主要是离线操作,线段树 \(=\) 时间戳,线段树的区间就是对应的时间,那和可撤销并查集又有什么关系呢?我们用一道例题来讲解。

「离线可过」动态图连通性

题目中说的是查询两个点是否连通,只有加边操作非常好做,但删边操作怎么做呢?

这时候我们的线段树分治就有用了。

我们观察到每一个边都是有一个有效时间的。这里记 \(l_{edge_i}\) 为第 \(i\) 条最后一次边加入时间,\(r_{edge_i}\) 为第 \(i\) 条边最后一次删去时间。

那么有效时间就是每一个 \([l_{edge_i},r_{edge_i}]\),我们可以视它为一个区间 update 进线段树(就是吧线段树当做时间的下标来做)。

而查询就再用一个变量 query 来记录查询的边。

我们将这些这些加完后,不要忘记有一些边没有删,那他的有效时间为 \([l_{edge_i},m]\)

我们在处理的时候用 dfs 暴力搜整颗线段树,当经过的点上有加入的边,就全加入并查集。

然后处理的时候就判 find(tree[p].query.first) 是否等于 find(tree[p].query.second)这里是用 pair 存了要查询的两个点。

然后递归下去,回溯的时候把这些在并查集里面的边全撤销了即可。

Q: 为什么可以用可撤销并查集?
A: 因为在回来的时候 dfs 时加的边已经被删去了,所以栈顶还是当前节点贡献的边。

戳我看代码喵~
struct Tree {
	int l, r;
	pii query;
	vector<pii> edge;
}tree[N * 4];

map<pii, int> lst; //记录上一次出现的位置
#define ls p << 1
#define rs p << 1 | 1

void build(int p, int l, int r) { //初始化
	tree[p].l = l, tree[p].r = r, tree[p].query = {0, 0};
	if (l == r) return ;
	int mid = l + r >> 1;
	build(ls, l, mid), build(rs, mid + 1, r);
}

void update_edge(int p, int l, int r, pii e) { //添加边的标记
	if (l <= tree[p].l && tree[p].r <= r) {
		tree[p].edge.push_back(e);
		return ;
	}
	int mid = tree[p].l + tree[p].r >> 1;
	if (l <= mid) update_edge(ls, l, r, e);
	if (r > mid) update_edge(rs, l, r, e);
}

void update_query(int p, int pos, pii e) { //添加查询标记
	if (tree[p].l == tree[p].r) {
		tree[p].query = e;
		return ;
	}
	int mid = tree[p].l + tree[p].r >> 1;
	if (pos <= mid) update_query(ls, pos, e);
	else update_query(rs, pos, e);
}

int fa[N], sz[N];

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

struct node {
	int x, y, add;
};

stack<node> s;

void dfs(int p) {
	int T = s.size(); //记录当前的长度,方便后面撤销
	for (auto e : tree[p].edge) {
		int fu = find(e.first), fv = find(e.second);
		if (fu == fv) {
			s.push({-1, -1, -1});
			continue;
		}
		if (sz[fu] > sz[fv]) swap(fu, fv);
		s.push({fu, fv, sz[fu]}), fa[fu] = fv, sz[fv] += sz[fu]; //普通并查集操作
	}
	if (tree[p].l == tree[p].r) {
		if (tree[p].query != make_pair(0, 0)) puts(find(tree[p].query.first) != find(tree[p].query.second) ? "N" : "Y"); //判断
	} else dfs(ls), dfs(rs); //往下走
	while (s.size() > T) { //撤销
		node x = s.top(); s.pop();
		if (x.x == -1) continue;
		fa[x.x] = x.x, sz[x.y] -= x.add;
	}
}

signed main() {
//    freopen(".in", "r", stdin);
//    freopen(".out", "w", stdout);
	int n = re, m = re;
	build(1, 1, m);
	iota(fa + 1, fa + 1 + n, 1)/*这个函数等同于for (int i = 1; i <= n; i++) fa[i] = i*/, fill(sz + 1, sz + 1 + n, 1); //初始化并查集
	for (int i = 1; i <= m; i++) {
		int op = re, x = re, y = re;
		pii edge = {min(x, y), max(x, y)}; //方便操作
		if (!op) lst[edge] = i;
		else if (op == 1) update_edge(1, lst[edge], i - 1, edge), lst.erase(edge);
		else update_query(1, i, edge);
	}
	for (auto X : lst) update_edge(1, X.second, m, X.first); //处理剩余的边
	dfs(1);
}

总结一下:

  1. 线段树分治主要用于存在性问题
  2. 只能离线:(
  3. 其实还可以做 dp 等其他算法。
  4. 后面忘了
posted @ 2026-07-14 20:58  OiLight  阅读(17)  评论(2)    收藏  举报