线段树不要唱歌2
线段树不要唱歌第二弹。
本系列主要为线段树上处理(终于把线段树当树了,以前有事剪枝(分裂)又是嫁接(合并)的)
线段树就是树,所以可以 为所欲为 dp,分治(?)等等
线段树分治
此前我们需要讲一下可撤销并查集。
可撤销并查集
这个东西他不能路径压缩(差评,只会打路径压缩),但是可以按秩合并。
我们发现如果我们要硬改 fa 数组的话会导致错乱,所以我们要倒着改?

其实是如果要删除,就只能删上一次操作的那个。
我们可以用一个栈(毕竟倒着取出来) 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);
}
总结一下:
- 线段树分治主要用于存在性问题
- 只能离线:(
- 其实还可以做 dp 等其他算法。
- 后面忘了

浙公网安备 33010602011771号