题目的一些特征和思路
给你若干操作,每一个操作的作用时间范围为
[
l
,
r
]
[l, r]
[l,r],然后让你求每一个时刻下 (题目要求) 的结果
或是没有说明操作时间,我们以操作个数为时间,当某些操作完全相同,则可以合并。
如第 1 , 2 , 3 1,2,3 1,2,3个操作一样的,那么我们可以在 [ 1 , 3 ] [1, 3] [1,3]的时间上都做相同的修改,而不必分别在 [ 1 , 1 ] [1,1] [1,1], [ 2 , 2 ] [2,2] [2,2], [ 3 , 3 ] [3,3] [3,3]修改,即按时间建成线段树,然后在 [ 1 , 3 ] [1,3] [1,3]的节点上修改即可,而不必到达每一个叶节点。
那么按照这样的思路,如果我们要求每一个时间点的答案(即查询线段树叶节点),可以通过一次
d
f
s
dfs
dfs线段树来实现。why?
对于线段树的任意一个叶节点来说,他所进行的操作一定在访问到最后的叶节点之前就已经进行过了,以刚刚的例子来说,如要查询
[
1
,
1
]
[1, 1]
[1,1],那么肯定已经经过了
[
1
,
3
]
[1, 3]
[1,3]这个点,所以查到的答案没问题。
这里有一个问题,既然我们要一次
d
f
s
dfs
dfs就完成所有点的查询(dfs先查左子树再右子树),那么当我们已经查完了当前节点的左右子树,要返回
d
f
s
dfs
dfs的上一层查另一个节点时就需要撤回操作。
比如我们查完
[
1
,
3
]
[1, 3]
[1,3]后,要查
[
4
,
6
]
[4, 6]
[4,6]的时候,已经进行了
[
1
,
3
]
[1,3]
[1,3]的操作,该怎么办?撤回对
[
1
,
3
]
[1, 3]
[1,3]的所有操作。也就是每次
d
f
s
dfs
dfs搜完左右子树之后就将这个点进行的操作撤回,这样对后面查询的节点就没有影响了。而
d
f
s
dfs
dfs的过程就是不断进栈的过程,我们可以用堆栈来存储当前这个点的操作,然后结束之后撤回即可。
可以看到,这里我们需要操作是可以撤回的,对于一些不可撤回的操作这样就行不通了。
练习题
1. P5787【模板】线段树分治
题意
N
N
N个点,
M
M
M条边在
[
0
,
K
]
[0, K]
[0,K]时间段内先后消失出现
问在每一个时刻这个图是否是二分图
分析
题目可以看做,给你
M
M
M个操作,每次在时间
[
l
,
r
]
[l, r]
[l,r]内加上一条边,当然这里每一条边都是不同的,所以是符合我们最开始说的线段树分治的题目特征
然后看看怎么判断一个图是否是二分图
这里可以用种类并查集来实现,不会的可以先写这个A Bug’s Life
那么这里我们的操作当然就是
m
e
r
g
e
merge
merge(合并)加入边的两个顶点,并且这个操作要可撤回
所以这里的并查集不能用路径压缩,为了降低并查集的复杂度,我们需要在合并上花功夫,那么我们就按秩合并
代码
#include <bits/stdc++.h>
using namespace std;
#define lc u<<1
#define rc u<<1|1
#define m (l+r)/2
typedef pair<int, int> pii;
const int MAX = 1e5 + 10;
int N, M, K;
vector<pii> t[MAX << 2];//线段树
struct UnionFind {//并查集
private:
int rk[MAX << 1], pre[MAX << 1], totNode;//MAX为最大点数
stack<pii> st;//记录操作
public:
void init(int tot) {
totNode = tot;
for (int i = 1; i <= totNode * 2; i++)//种类并查集,分成两类
pre[i] = i, rk[i] = 1;
}
int find(int x) { while (x ^ pre[x]) x = pre[x]; return x; }//没有路径压缩
void merge(int x, int y) {//按秩合并
x = find(x), y = find(y);
if (x == y) return;
if (rk[x] < rk[y]) swap(x, y);
st.push(make_pair(y, rk[x]));//将操作存在栈中
pre[y] = x, rk[x] += rk[x] == rk[y];
}
int start() { return st.size(); }//当前点开始时栈中操作数
void end(int last) {//撤回merge操作
while (st.size() > last) {//一直到最开始为止
pii tp = st.top();
//这里按之前合并的时候反过来写就行了
rk[pre[tp.first]] -= tp.second;
pre[tp.first] = tp.first;
st.pop();
}
}
} uf;
void insert(int u, int l, int r, int ql, int qr, pii k) {//将加入的边插入[ql, qr]时间段即可
if (ql <= l && r <= qr) {
t[u].push_back(k);
return;
}
if (ql <= m) insert(lc, l, m, ql, qr, k);
if (qr > m) insert(rc, m + 1, r, ql, qr, k);
}
void dfs(int u, int l, int r) {
int now = uf.start(), flag = 0;//now记录最开始栈中元素个数
for (auto &i: t[u]) {
int x = i.first, y = i.second;
//我们要将x和y分成两种不同的类
if (uf.find(x) == uf.find(y)) {//如果x和y已经是同类了, 那么就有冲突了
flag = 1;
break;
}
//我们要将x和y分成两种不同的类
uf.merge(x, y + N); uf.merge(y, x + N);
}
if (flag) for (int i = l; i <= r; i++) printf("No\n");//有冲突,那么显然他的子树都不用在看了,肯定有冲突了
else if (l == r) printf("Yes\n");//如果到达子树都没有冲突,说明可以
else dfs(lc, l, m), dfs(rc, m + 1, r);
uf.end(now);//每次结束都撤回操作
}
int main() {
scanf("%d%d%d", &N, &M, &K);
while (M--) {
int x, y, ql, qr; scanf("%d%d%d%d", &x, &y, &ql, &qr); ql++;
if (ql <= qr) insert(1, 1, K, ql, qr, make_pair(x, y));
}
uf.init(N);
dfs(1, 1, K);
return 0;
}
2. P5227 [AHOI2013]连通图
题意
N
N
N个点
M
M
M条边的无向连通图,现在有
K
K
K个操作,每次删去
c
i
c_i
ci条边
问每次操作过后,图是否连通
分析
判断是否连通可以用带权并查集来解决,找任意一个点所在的集合元素个数是否为N即可
K
K
K个操作,每次删边,这样不好搞,我们不如反过来,每次加边
某个时刻删边等于边不存在,这样我们合并一下边存在的时间,然后插入线段树处理即可
#include <bits/stdc++.h>
using namespace std;
#define lc u<<1
#define rc u<<1|1
#define m (l+r)/2
typedef pair<int, int> pii;
const int MAX = 1e5 + 10;
int N, M, T;
vector<int> g[MAX << 1];
struct node {
int x, y, z;
};
struct edge {
int u, v;
} e[MAX << 1];
vector<int> t[MAX << 2];
struct UnionFind {
private:
int rk[MAX], pre[MAX], siz[MAX], totNode;//MAX为最大点数
stack<node> st;//node记录上次修改的内容,这里要多记录一个siz,所以开了一个node
public:
void init(int tot) {
totNode = tot;
for (int i = 1; i <= totNode; i++)
pre[i] = i, siz[i] = rk[i] = 1;
}
int find(int x) { while (x ^ pre[x]) x = pre[x]; return x; }
void merge(int x, int y) {//按秩合并
x = find(x), y = find(y);
if (x == y) return;
if (rk[x] < rk[y]) swap(x, y);
st.push(node{ y, rk[x], siz[y] });
pre[y] = x, rk[x] += rk[x] == rk[y], siz[x] += siz[y];
}
int start() { return st.size(); }
void end(int last) {//撤回merge操作
while (st.size() > last) {
node tp = st.top();
//合并时的操作反向进行
rk[pre[tp.x]] -= tp.y, siz[pre[tp.x]] -= tp.z;
pre[tp.x] = tp.x;
st.pop();
}
}
bool judge() { return siz[find(1)] == totNode; }//判断是否连通
} uf;
void insert(int u, int l, int r, int ql, int qr, int k) {//插入[ql, qr]时间段内存在的边
if (ql <= l && r <= qr) {
t[u].push_back(k);
return;
}
if (ql <= m) insert(lc, l, m, ql, qr, k);
if (qr > m) insert(rc, m + 1, r, ql, qr, k);
}
void dfs(int u, int l, int r) {
int now = uf.start();//开始
for (auto &i: t[u]) {
int x = e[i].u, y = e[i].v;
uf.merge(x, y);//连通x和y,即合并x和y
}
//uf.judge()判断任意点所在的集合元素个数是否为N
if (uf.judge()) for (int i = l; i <= r; i++) printf("Connected\n");//边是一条条出现的,所以到上面的节点都连通了,在加不加边都会连通
else if (l == r) printf("Disconnected\n");//到达叶节点都没连通
else dfs(lc, l, m), dfs(rc, m + 1, r);
uf.end(now);//撤回
}
int main() {
scanf("%d%d", &N, &M);
for (int i = 1; i <= M; i++)
scanf("%d%d", &e[i].u, &e[i].v);
scanf("%d", &T);
for (int i = 1; i <= T; i++) {
int c; scanf("%d", &c);
for (int j = 1; j <= c; j++) {
int x; scanf("%d", &x);
g[x].push_back(i);
}
}
for (int i = 1; i <= M; i++) {
if (g[i].empty()) insert(1, 1, T, 1, T, i);
else {
g[i].push_back(T + 1);
int len = g[i].size(), tmp = g[i].front();
if (tmp != 1) insert(1, 1, T, 1, tmp - 1, i);
for (int j = 1; j < len; j++)
if (g[i][j] != g[i][j - 1] + 1)
insert(1, 1, T, g[i][j - 1] + 1, g[i][j] - 1, i);
}
}
uf.init(N);
dfs(1, 1, T);
return 0;
}
最后,剩下还有些更难的,还没动,慢慢来