[学习笔记]线段树分治

暂时先更到这儿,后面有时间还会补题。

主要写给自己看,不会很详细。

问题

主要针对一类“操作在时间轴上有存在区间”的问题,按时间轴分治进行查询。通过使用线段树分治,能将不同时间点间的公共信息进行继承,从而避免了暴力做法中对信息的销毁再重构造成的时间浪费。

流程

  • 建线段树
  • 将操作覆盖在线段树的时间区间上
  • dfs整棵线段树,到叶子结点的路径上将信息继承以得到子节点的信息,离开一个区间的时候就要撤销这个区间的操作
  • 一般会结合一些轻小的数据结构或算法使用,要求可撤销,如可撤销并查集、可持久化Trie、线性基等。

例题

P5787 二分图/【模板】线段树分治

经典问题:动态图。
判定二分图使用扩展域并查集,既然要撤销那就按秩合并。

#include <cstdio>
#include <vector>
using namespace std;
const int maxn = 1e5 + 10, maxm = 2e5 + 10, maxk = 1e5 + 10;
int n, m, k;
struct Edge
{
    int u, v;
} e[maxm];
vector<int> t[maxn << 2];
int fa[maxn << 1], sz[maxn << 1];
struct dat
{
    int x, y, add; //x并入y,y的sz加上了add
} st[maxn << 2];
int top;

void modify(int p, int l, int r, int x, int y, int v)
{
    if(l > y || r < x)
        return;
    if(x <= l && r <= y)
    {
        t[p].push_back(v);
        return;
    }
    int mid = (l + r) >> 1;
    modify(p << 1, l, mid, x, y, v);
    modify(p << 1 | 1, mid + 1, r, x, y, v);
    return;
}

int find(int x)
{
    while(fa[x] != x) x = fa[x];
    return x;
}

void hb(int x, int y)
{
    int a = find(x), b = find(y);
    if(sz[a] > sz[b]) swap(a, b);
    fa[a] = b; sz[b] += sz[a];
    st[++top] = (dat){a, b, sz[a]};
    return;
}

void solve(int p, int l, int r)
{
    int len = t[p].size(), lst = top;
    bool flag = true;
    for(int i = 0; i < len; ++i)
    /*刚写这题的时候NOI系列赛的新语言标准(-std=c++14)还没出来
    当时默认语言还是c++98
    所以就写了个这么丑陋的循环
    懒得改了*/
    {
        int u = e[t[p][i]].u, v = e[t[p][i]].v;
        int a = find(u), b = find(v);
        if(a == b)
        {
            flag = false;
            for(int k = l; k <= r; ++k)
            {
                puts("No");
            }
            break;
        }
        hb(u, v + n);
        hb(v, u + n);
    }
    if(flag)
    {
        if(l == r)
            puts("Yes");
        else
        {
            int mid = (l + r) >> 1;
            solve(p << 1, l, mid);
            solve(p << 1 | 1, mid + 1, r);
        }
    }
    while(top > lst)
    {
        dat d = st[top--];
        sz[d.y] -= d.add;
        fa[d.x] = d.x;
    }
    return;
}

int main()
{
    scanf("%d%d%d", &n, &m, &k);
    for(int i = 1; i <= m; ++i)
    {
        int l, r;
        scanf("%d%d%d%d", &e[i].u, &e[i].v, &l, &r);
        if(l + 1 <= k && l < r)
            modify(1, 1, k, l + 1, r, i);
    }
    for(int i = 1; i <= 2 * n; ++i)
    {
        fa[i] = i; sz[i] = 1;
    }
    solve(1, 1, k);
    return 0;
}

P4588 [TJOI2018]数学计算

由于 \(M\) 不一定为质数,会有很多逆元不存在的情况,用乘法逆元不便处理。

发现题目中的操作其实就是连乘再撤销的过程,相当于每一个乘法有一段存在时间。

这样就把删除的操作化为了每个添加操作有存在的时间区间。

快乐地使用线段树分治。

事实上这题还有一个更直接的做法:使用普通的线段树,乘法的区间修改,然后每个点单点查询。尽管在得到答案的过程中复杂度比线段树分治大,但前面区间 modify 的复杂度是相同的,所以总的复杂度没有变,都是\(O(q\log q)\)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxq = 1e5 + 5;
ll tr[maxq << 2];
int q;
ll Mod;
struct oper
{
    int l;
    int r;
    ll m;
};
oper a[maxq];
int cnt, id[maxq];

void build(int p, int l, int r)
{
    tr[p] = 1;
    if(l == r) return;
    int mid = (l + r) >> 1;
    build(p << 1, l, mid);
    build(p << 1 | 1, mid + 1, r);
    return;
}

void modify(int p, int l, int r, int x, int y, ll m)
{
    if(r < x || l > y) return;
    if(x <= l && r <= y)
    {
        tr[p] = tr[p] * m % Mod;
        return;
    }
    int mid = (l + r) >> 1;
    modify(p << 1, l, mid, x, y, m);
    modify(p << 1 | 1, mid + 1, r, x, y, m);
    return;
}

void dfs(int p, int l, int r, ll res)
{
    res = res * tr[p] % Mod;
    if(l == r)
        printf("%lld\n", res);
    else
    {
        int mid = (l + r) >> 1;
        dfs(p << 1, l, mid, res);
        dfs(p << 1 | 1, mid + 1, r, res);
    }
    return;
}

void work()
{
    scanf("%d%lld", &q, &Mod);
    cnt = 0;
    fill(a + 1, a + q + 1, (oper){0, 0, 1});
    for(int i = 1; i <= q; ++i)
    {
        int opt; ll m;
        scanf("%d%lld", &opt, &m);
        if(opt == 1)
        {
            a[++cnt].m = m;
            a[cnt].l = i;
            id[i] = cnt;
        }
        else
            a[id[m]].r = i - 1;
    }
    for(int i = 1; i <= cnt; ++i)
        if(a[i].r == 0)
            a[i].r = q;
    build(1, 1, q);
    for(int i = 1; i <= cnt; ++i)
        modify(1, 1, q, a[i].l, a[i].r, a[i].m);
    dfs(1, 1, q, 1);
    return;
}

int main()
{
    int T;
    scanf("%d", &T);
    while(T--)
        work();
    return 0;
}

P5227 [AHOI2013]连通图

经典的动态图问题。
类似 [JSOI2008]星球大战 的思想,既然可以离线,那么把删边换为加边。
那么一条边的存在在时间轴上依然为区间形式。

顺理成章地使用线段树分治,维护连通性使用并查集。

#include <cstdio>
#include <utility>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int maxn = 1e5 + 10, maxk = 1e5 + 10, maxm = 2e5 + 10;
int n, m, k;
PII e[maxm];
int pre[maxm], tmp[7];
vector<int> t[maxk << 2];
int fa[maxn], sz[maxn], tot;
struct dat
{
    int x, y, add;
} st[maxm << 2];
int top;

void modify(int p, int l, int r, int x, int y, int v)
{
    if(l > y || r < x)
        return;
    if(x <= l && r <= y)
    {
        t[p].push_back(v);
        return;
    }
    int mid = (l + r) >> 1;
    modify(p << 1, l, mid, x, y, v);
    modify(p << 1 | 1, mid + 1, r, x, y, v);
    return;
}

int find(int x)
{
    while(fa[x] != x) x = fa[x];
    return x;
}

void hb(int a, int b)
{
    if(sz[a] > sz[b]) swap(a, b);
    tot++;
    fa[a] = b;
    sz[b] += sz[a];
    st[++top] = (dat){a, b, sz[a]};
    return;
}

void solve(int p, int l, int r)
{
    int lst = top;
    for(int i : t[p])
    {
        int a = find(e[i].first), b = find(e[i].second);
        if(a != b)
            hb(a, b);
    }
    if(l == r)
        puts(tot == n - 1 ? "Connected" : "Disconnected");
    else
    {
        int mid = (l + r) >> 1;
        solve(p << 1, l, mid);
        solve(p << 1 | 1, mid + 1, r);
    }
    while(top > lst)
    {
        dat d = st[top--];
        tot--;
        sz[d.y] -= d.add;
        fa[d.x] = d.x;
    }
    return;
}

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= m; ++i)
    {
        int u, v;
        scanf("%d%d", &u, &v);
        e[i] = make_pair(u, v);
        pre[i] = 1;
    }
    scanf("%d", &k);
    for(int i = 1; i <= k; ++i)
    {
        int c;
        scanf("%d", &c);
        for(int j = 1; j <= c; ++j)
            scanf("%d", tmp + j);
        for(int j = 1; j <= c; ++j)
        {
            if(i - 1 >= pre[tmp[j]])
                modify(1, 1, k, pre[tmp[j]], i - 1, tmp[j]);
            pre[tmp[j]] = i + 1;
        }
    }
    for(int i = 1; i <= m; ++i)
        if(k >= pre[i])
            modify(1, 1, k, pre[i], k, i);
    for(int i = 1; i <= n; ++i)
    {
        fa[i] = i;
        sz[i] = 1;
    }
    solve(1, 1, k);
    return 0;
}

P5214 [SHOI2014]神奇化合物

这题有跟这个化合物一样神奇的暴力做法,但在这里不讨论。

还是动态图,还是线段树分治。
每条边出现的时间在时间轴上形成一个区间。

#include <cstdio>
#include <utility>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int maxn = 5005, maxm = 2e5 + 10, maxq = 10005;
int d[maxn][maxn];
int fa[maxn], sz[maxn], tot;
int n, m, q;
char tmp[5];
vector<PII> t[maxq << 2];
int qe[maxq], cnt, ans[maxq];
struct dat
{
    int x, y, add;
} st[maxq << 2];
int top;

void modify(int p, int l, int r, int x, int y, const PII &v)
{
    if(l > y || r < x)
        return;
    if(x <= l && r <= y)
    {
        t[p].push_back(v);
        return;
    }
    int mid = (l + r) >> 1;
    modify(p << 1, l, mid, x, y, v);
    modify(p << 1 | 1, mid + 1, r, x, y, v);
    return;
}

int find(int x)
{
    while(x != fa[x]) x = fa[x];
    return x;
}

void hb(int a, int b)
{
    if(sz[a] > sz[b]) swap(a, b);
    fa[a] = b;
    sz[b] += sz[a];
    st[++top] = (dat){a, b, sz[a]};
    tot++;
    return;
}

void solve(int p, int l, int r)
{
    int lst = top;
    for(PII i : t[p])
    {
        int a = find(i.first), b = find(i.second);
        if(a != b)
            hb(a, b);
    }
    if(l == r)
        ans[l] = n - tot;
    else
    {
        int mid = (l + r) >> 1;
        solve(p << 1, l, mid);
        solve(p << 1 | 1, mid + 1, r);
    }
    while(top > lst)
    {
        dat d = st[top--];
        tot--;
        sz[d.y] -= d.add;
        fa[d.x] = d.x;
    }
    return;
}

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= m; ++i)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        d[a][b] = d[b][a] = 1;
    }
    scanf("%d", &q);
    ++q;
    for(int i = 2; i <= q; ++i)
    {
        scanf("%s", tmp);
        int x, y;
        if(tmp[0] == 'A')
        {
            scanf("%d%d", &x, &y);
            if(d[x][y] == 0)
                d[x][y] = d[y][x] = i;
        }
        else if(tmp[0] == 'D')
        {
            scanf("%d%d", &x, &y);
            modify(1, 1, q, d[x][y], i - 1, make_pair(x, y));
            d[x][y] = d[y][x] = 0;
        }
        else
        {
            qe[++cnt] = i;
        }
    }
    for(int i = 1; i <= n; ++i)
        for(int j = i + 1; j <= n; ++j)
            if(d[i][j])
                modify(1, 1, q, d[i][j], q, make_pair(i, j));
    for(int i = 1; i <= n; ++i)
    {
        fa[i] = i;
        sz[i] = 1;
    }
    solve(1, 1, q);
    for(int i = 1; i <= cnt; ++i)
        printf("%d\n", ans[qe[i]]);
    return 0;
}

P5631 最小mex生成树

没什么很好的思路能直接求,考虑枚举 \(mex\) 是多少并进行判定。

从小到大枚举 \(mex\),设当前枚举值为 \(w\),那么 \(w\)\(mex\) 意味着 \(w\) 可以不在生成树中出现,也就是去掉边权为 \(w\) 的所有边后依然有一棵生成树,而一张图有生成树等价于这张图连通。

所以只需要判定删掉所有边权为 \(w\) 的边后图是否连通即可。经典的动态图问题。

\(w\) 小的边权在前面的枚举中判定不合法,即任何一个生成树都含有所有比 \(w\) 小的边权,那么 \(w\) 满足 \(mex\) 的条件。

细节就是判定边权的值域是否连续。

《论动态图在线段树分治应用中的地位》

#include <cstdio>
#include <cctype>
#include <algorithm>
#include <vector>
using namespace std;
const int maxn = 1e6 + 10, maxm = 2e6 + 10, maxw = 1e5 + 10;
struct Edge
{
    int u, v, w;
} e[maxm];
vector<Edge> vec[maxw << 2];
int n, m;
bool con[maxw];
int fa[maxn], sz[maxn];
struct datum
{
    int r1, r2, ad;
};
datum stk[maxn];
int top, lim;
int read();

bool cmp(const Edge &x, const Edge &y)
{
    return x.w < y.w;
}

void modify(int p, int l, int r, int a, int b, const Edge &e)
{
    if(r < a || l > b) return;
    if(a <= l && r <= b)
    {
        vec[p].push_back(e);
        return;
    }
    int mid = (l + r) >> 1;
    modify(p << 1, l, mid, a, b, e);
    modify(p << 1 | 1, mid + 1, r, a, b, e);
    return;
}

int find(int x)
{
    while(x != fa[x]) x = fa[x];
    return x;
}

void hb(int r1, int r2)
{
    if(sz[r1] > sz[r2]) swap(r1, r2);
    fa[r1] = r2;
    sz[r2] += sz[r1];
    stk[++top] = (datum){r1, r2, sz[r1]};
    return;
}

void dfs(int p, int l, int r, int cnt)
{
    if(cnt == 1)
    {
        for(int i = l; i <= r; ++i)
            con[i] = true;
        return;
    }
    int lst = top;
    for(Edge edg : vec[p])
    {
        int u = edg.u, v = edg.v, w = edg.w;
        int r1 = find(u), r2 = find(v);
        if(r1 != r2)
        {
            hb(r1, r2);
            cnt--;
        }
    }
    if(l == r)
        con[l] = (cnt == 1);
    else
    {
        int mid = (l + r) >> 1;
        dfs(p << 1, l, mid, cnt);
        dfs(p << 1 | 1, mid + 1, r, cnt);
    }
    while(top > lst)
    {
        datum tmp = stk[top--];
        fa[tmp.r1] = tmp.r1;
        sz[tmp.r2] -= tmp.ad;
    }
    return;
}

int main()
{
    n = read(); m = read();
    for(int i = 1; i <= m; ++i)
    {
        e[i].u = read();
        e[i].v = read();
        e[i].w = read();
    }
    sort(e + 1, e + m + 1, cmp);
    if(e[1].w > 0)
    {
        puts("0");
        return 0;
    }
    int pos = m;
    for(int i = 1; i < m; ++i)
        if(e[i + 1].w - e[i].w > 1)
        {
            pos = i;
            break;
        }
    lim = e[pos].w;
    int cur;
    if(lim > 0)
        for(cur = 1; e[cur].w == 0; ++cur)
            modify(1, 0, lim, 1, lim, e[cur]);
    for( ; e[cur].w < lim; ++cur)
    {
        modify(1, 0, lim, 0, e[cur].w - 1, e[cur]);
        modify(1, 0, lim, e[cur].w + 1, lim, e[cur]);
    }
    if(lim > 0)
        for( ; e[cur].w == lim; ++cur)
            modify(1, 0, lim, 0, lim - 1, e[cur]);
    for( ; cur <= m; ++cur)
        modify(1, 0, lim, 0, lim, e[cur]);
    for(int i = 1; i <= n; ++i)
    {
        fa[i] = i;
        sz[i] = 1;
    }
    dfs(1, 0, lim, n);
    for(int i = 1; i <= pos; ++i)
        if(con[e[i].w])
        {
            printf("%d\n", e[i].w);
            return 0;
        }
    printf("%d\n", e[pos].w + 1);
    return 0;
}

int read()
{
    int x = 0; bool f = true; char ch = getchar();
    while(!isdigit(ch)) { if(ch == '-') f = false; ch = getchar(); }
    while(isdigit(ch)) { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); }
    return f ? x : -x;
}

P4585 [FJOI2015] 火星商店问题

终于不是动态图了~

本题有两个限制:可购买的商品的时间限制,以及商店编号的限制。

可购买的商品的时间是个区间,在时间轴上进行线段树分治。

商店编号的限制使用可持久化\(\text{01Trie}\),即可得到异或最大值。

要往时间轴上铺设的是火星人的查询操作。

为了保证商店的编号是在 \([l,r]\) 内的,我们需要按编号对商品排序,然后再将符合时间要求的丢到对应的分治区间里。

撤销可持久化 Trie 比较难,不妨直接暴力重建。

剩下的详见代码吧,这题比较难,写了点儿注释。时间复杂度 \(O(n\log ^2 n)\)

FJOI 没有部分分,题面描述恶心,好评

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10, maxb = 18;
int n, m;
struct import  //购进商品的操作
{
    int s, v, t;
} ad[maxn], tmp1[maxn], tmp2[maxn];
int totad;  //操作0的总数
struct ask  //火星人购买商品的操作
{
    int l, r, x, tl, tr, id;
} q[maxn];
int totq;  //操作1的总数
int ans[maxn];
int day;  //当前天数,事实上day和totad相等
struct Trie
{
    int ch[2];
    int w;
} tr[maxn * maxb];
int root[maxn], tot;
vector<int> dat[maxn << 2];
int que[maxn];

void insert(int &p, int pre, int x, int bit)
{
    tr[p = ++tot] = tr[pre];
    tr[p].w++;
    if(bit == -1) return;
    int c = (x >> bit) & 1;
    insert(tr[p].ch[c], tr[pre].ch[c], x, bit - 1);
    return;
}

int query(int r1, int r2, int x)
{
    int res = 0;
    for(int i = 16; i >= 0; --i)
    {
        int c = ((x >> i) & 1) ^ 1;
        int cnt = tr[tr[r2].ch[c]].w - tr[tr[r1].ch[c]].w;
        if(cnt > 0)
        {
            res |= (1 << i);
            r2 = tr[r2].ch[c];
            r1 = tr[r1].ch[c];
        }
        else
        {
            r2 = tr[r2].ch[1 ^ c];
            r1 = tr[r1].ch[1 ^ c];
        }
    }
    return res;
}

bool cmp(const import &x, const import &y)
{
    return x.s < y.s;
}

void modify(int p, int tl, int tr, int tx, int ty, int v)
{
    if(tr < tx || tl > ty) return;
    if(tx <= tl && tr <= ty)
    {
        dat[p].push_back(v);
        return;
    }
    int mid = (tl + tr) >> 1;
    modify(p << 1, tl, mid, tx, ty, v);
    modify(p << 1 | 1, mid + 1, tr, tx, ty, v);
    return;
}

void calc(int p, int a, int b)
{
    if(dat[p].empty()) return;
    tot = 0;
    int tail = 0;
    for(int i = a; i <= b; ++i)
    {
        que[++tail] = ad[i].s;
        insert(root[tail], root[tail - 1], ad[i].v, 16);
    }
    for(int i : dat[p])
    {
        int l = lower_bound(que + 1, que + tail + 1, q[i].l) - que;
        int r = upper_bound(que + 1, que + tail + 1, q[i].r) - que - 1;
        ans[q[i].id] = max(ans[q[i].id], query(root[l - 1], root[r], q[i].x));
    }
    return;
}

void dfs(int p, int tl, int tr, int a, int b)
//[tl, tr]是时间区间,[a,b]是排序后的商品编号区间
{
    if(a > b) return;
    calc(p, a, b);
    if(tl == tr) return;
    int mid = (tl + tr) >> 1;
    int p1 = 0, p2 = 0;
    for(int i = a; i <= b; ++i)  //丢到对应的时间区间
    {
        if(ad[i].t <= mid)
            tmp1[++p1] = ad[i];
        else
            tmp2[++p2] = ad[i];
    }
    for(int i = a; i <= a + p1 - 1; ++i)
        ad[i] = tmp1[i - a + 1];
    for(int i = a + p1; i <= b; ++i)
        ad[i] = tmp2[i - a - p1 + 1];
    dfs(p << 1, tl, mid, a, a + p1 - 1);
    dfs(p << 1 | 1, mid + 1, tr, a + p1, b);
    return;
}

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; ++i)
    {
        int x;
        scanf("%d", &x);
        insert(root[i], root[i - 1], x, 16);
    }  //特殊商品,另行计算
    day = 1;  //坑点一:样例开始输入时就是第一天,此时可能就是操作1而非操作0
    for(int i = 1; i <= m; ++i)
    {
        int opt;
        scanf("%d", &opt);
        if(opt == 0)
        {
            if(i != 1) day++;  //对应坑点一
            ad[++totad].t = day;
            scanf("%d%d", &ad[totad].s, &ad[totad].v);
        }
        else
        {
            int d;
            totq++;
            scanf("%d%d%d%d", &q[totq].l, &q[totq].r, &q[totq].x, &d);
            q[totq].tr = day;
            q[totq].tl = max(1, day - d + 1);
            q[totq].id = totq;
            ans[totq] = query(root[q[totq].l - 1], root[q[totq].r], q[totq].x);  //考虑购买特殊商品
        }
    }
    sort(ad + 1, ad + totad + 1, cmp);  //按照商店编号对商品排序
    for(int i = 1; i <= totq; ++i)
        if(q[i].tl <= q[i].tr)  //坑点二:存在d=0的情况,此时只能购买特殊商品
            modify(1, 1, day, q[i].tl, q[i].tr, i);
    dfs(1, 1, day, 1, totad);
    for(int i = 1; i <= totq; ++i)
        printf("%d\n", ans[i]);
    return 0;
}
posted @ 2022-07-12 17:29  Andrewzdm  阅读(87)  评论(0)    收藏  举报