题目的一些特征和思路
给你若干操作,每一个操作的作用时间范围为
,然后让你求每一个时刻下 (题目要求) 的结果
或是没有说明操作时间,我们以操作个数为时间,当某些操作完全相同,则可以合并。
如第 个操作一样的,那么我们可以在 的时间上都做相同的修改,而不必分别在 , , 修改,即按时间建成线段树,然后在 的节点上修改即可,而不必到达每一个叶节点。
那么按照这样的思路,如果我们要求每一个时间点的答案(即查询线段树叶节点),可以通过一次
线段树来实现。why?
对于线段树的任意一个叶节点来说,他所进行的操作一定在访问到最后的叶节点之前就已经进行过了,以刚刚的例子来说,如要查询
,那么肯定已经经过了
这个点,所以查到的答案没问题。
这里有一个问题,既然我们要一次
就完成所有点的查询(dfs先查左子树再右子树),那么当我们已经查完了当前节点的左右子树,要返回
的上一层查另一个节点时就需要撤回操作。
比如我们查完
后,要查
的时候,已经进行了
的操作,该怎么办?撤回对
的所有操作。也就是每次
搜完左右子树之后就将这个点进行的操作撤回,这样对后面查询的节点就没有影响了。而
的过程就是不断进栈的过程,我们可以用堆栈来存储当前这个点的操作,然后结束之后撤回即可。
可以看到,这里我们需要操作是可以撤回的,对于一些不可撤回的操作这样就行不通了。
练习题
1. P5787【模板】线段树分治
题意
个点,
条边在
时间段内先后消失出现
问在每一个时刻这个图是否是二分图
分析
题目可以看做,给你
个操作,每次在时间
内加上一条边,当然这里每一条边都是不同的,所以是符合我们最开始说的线段树分治的题目特征
然后看看怎么判断一个图是否是二分图
这里可以用种类并查集来实现,不会的可以先写这个A Bug’s Life
那么这里我们的操作当然就是
(合并)加入边的两个顶点,并且这个操作要可撤回
所以这里的并查集不能用路径压缩,为了降低并查集的复杂度,我们需要在合并上花功夫,那么我们就按秩合并
代码
#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即可
个操作,每次删边,这样不好搞,我们不如反过来,每次加边
某个时刻删边等于边不存在,这样我们合并一下边存在的时间,然后插入线段树处理即可
#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;
}
最后,剩下还有些更难的,还没动,慢慢来