线段树分治——学习笔记

题目的一些特征和思路

给你若干操作,每一个操作的作用时间范围为 [ l , r ] [l, r] ,然后让你求每一个时刻下 (题目要求) 的结果
或是没有说明操作时间,我们以操作个数为时间,当某些操作完全相同,则可以合并。

如第 1 , 2 , 3 1,2,3 个操作一样的,那么我们可以在 [ 1 , 3 ] [1, 3] 的时间上都做相同的修改,而不必分别在 [ 1 , 1 ] [1,1] , [ 2 , 2 ] [2,2] , [ 3 , 3 ] [3,3] 修改,即按时间建成线段树,然后在 [ 1 , 3 ] [1,3] 的节点上修改即可,而不必到达每一个叶节点。

那么按照这样的思路,如果我们要求每一个时间点的答案(即查询线段树叶节点),可以通过一次 d f s dfs 线段树来实现。why?
对于线段树的任意一个叶节点来说,他所进行的操作一定在访问到最后的叶节点之前就已经进行过了,以刚刚的例子来说,如要查询 [ 1 , 1 ] [1, 1] ,那么肯定已经经过了 [ 1 , 3 ] [1, 3] 这个点,所以查到的答案没问题。
这里有一个问题,既然我们要一次 d f s dfs 就完成所有点的查询(dfs先查左子树再右子树),那么当我们已经查完了当前节点的左右子树,要返回 d f s dfs 的上一层查另一个节点时就需要撤回操作
比如我们查完 [ 1 , 3 ] [1, 3] 后,要查 [ 4 , 6 ] [4, 6] 的时候,已经进行了 [ 1 , 3 ] [1,3] 的操作,该怎么办?撤回对 [ 1 , 3 ] [1, 3] 的所有操作。也就是每次 d f s dfs 搜完左右子树之后就将这个点进行的操作撤回,这样对后面查询的节点就没有影响了。而 d f s dfs 的过程就是不断进栈的过程,我们可以用堆栈来存储当前这个点的操作,然后结束之后撤回即可。

可以看到,这里我们需要操作是可以撤回的,对于一些不可撤回的操作这样就行不通了。

练习题

1. P5787【模板】线段树分治

题意

N N 个点, M M 条边在 [ 0 , K ] [0, K] 时间段内先后消失出现
问在每一个时刻这个图是否是二分图

分析

题目可以看做,给你 M M 个操作,每次在时间 [ l , r ] [l, r] 内加上一条边,当然这里每一条边都是不同的,所以是符合我们最开始说的线段树分治的题目特征
然后看看怎么判断一个图是否是二分图
这里可以用种类并查集来实现,不会的可以先写这个A Bug’s Life
那么这里我们的操作当然就是 m e r g e 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 个点 M M 条边的无向连通图,现在有 K K 个操作,每次删去 c i c_i 条边
问每次操作过后,图是否连通

分析

判断是否连通可以用带权并查集来解决,找任意一个点所在的集合元素个数是否为N即可
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;
}

最后,剩下还有些更难的,还没动,慢慢来

猜你喜欢

转载自blog.csdn.net/weixin_44282912/article/details/104403335