线段树时间分治

线段树时间分治解决的问题是一类可离线的,要求支持先修改后撤销,查询某一时间点前修改的总贡献的问题。大致的思路是,在时间轴上建一棵线段树,把 l 时刻修改,r+1 时刻撤销的操作,“区间加”到 [l,r] 区间上。然后遍历线段树所有节点(时间区间),O(nlogn) 地统计出每个时间的答案。

以洛谷模板题 P5787 二分图 /【模板】线段树分治 为例:有一张 n 个节点 m 条边的无向图。但 m 条边在时间 [1,k] 内不是一直存在的——第 i 条边 (ui,vi) 在时间 li 开始出现,到 ri 消失。你需要对每个时间点 i[1,k] 判断此时这个图是否为二分图。

“先出现后消失”可以转换成 li 时刻加边,ri 时刻撤销。而对于每个时间是否是二分图,可以用扩展域并查集进行判定。它支持动态维护,且可以撤销,符合线段树分治的条件(也因此不能使用染色法)。

我们把这个操作“加”到线段树上去。具体写法是在线段树的每个节点上开一个 vector,然后

void insert(int p, int l, int r, int x) {
    if (l <= t[p].l && t[p].r <= r) {
        t[p].v.emplace_back(x); // x是修改操作的编号
        return;
    }
    if (l <= mid) insert(ls, l, r, x);
    if (r > mid) insert(rs, l, r, x);
}

把所有的修改放上线段树之后,每个节点的 vector 中存储的就是所有在整个这个时间段内存在的边。此时可以从上到下遍历整个线段树,并维护一个扩展域并查集。当走到一个时间段,把这个节点上的边加入并查集,发现出现了矛盾,则这个时间段内一定不是二分图,不需要再向下递归了,直接把这个区间全部标记为 No;否则继续向下递归。回溯的时候,需要撤销当前时间段内加上的边,可以使用可撤销化并查集(用栈记录合并,复原现场)。因为使用了可撤销化,不能路径压缩,必须按秩合并保证复杂度。

下面是遍历部分的代码:

int find(int x) { // 路径压缩会丢失父亲信息,导致无法撤销
    while (x != fa[x]) x = fa[x];
    return x;
}

stack<pair<int, int>> memo;

void merge(int x, int y) {
    if (x == y) return;
    if (d[x] > d[y]) swap(x, y); // 按秩合并:树高小的往大的上面合并(连到根节点)
    memo.emplace(x, d[x] == d[y]); // 记录合并
    fa[x] = y;
    d[y] += d[x] == d[y]; // 如果树高相等,总树高+1
}

void dfs(int p, int l, int r) {
    int siz = memo.size();
    for (auto i : t[p].v) {
        int x = find(u[i]), y = find(v[i]);
        if (x == y) { // 如果出现矛盾(两个节点属于同一部)
            for (int j = l; j <= r; j++) cout << "No\n";
            goto DRAW_BACK;
        }
        merge(x, find(v[i] + n));
        merge(y, find(u[i] + n));
    }
    if (l == r) {
        cout << "Yes\n";
    } else {
        dfs(ls, l, mid);
        dfs(rs, mid + 1, r);
    }
DRAW_BACK:
    while (memo.size() > siz) { // 把这一部分新加的边全部撤销掉
        auto [x, v] = memo.top();
        memo.pop();
        d[find(x)] -= v, fa[x] = x;
    }
}
// main()
for (int i = 1; i <= n * 2; i++) fa[i] = i;
dfs(1, 1, k);

今年辽宁省赛的 K 题可重集合也是线段树分治,当时被卡住不会处理错失前三,现在可以补了。题目要求维护一个可重集合,支持元素的增加和删除,在每一次操作后输出集合中元素通过加法组合能生成多少个不同的正整数。

注意到可以背包 dp,设 dpi 表示数字 i 能否被表示出来,显然 dpi{0,1} 且有转移方程 dp[i]∣=dp[ia[j]],复杂度 O(mn)。这是 bitset 优化背包的模板,直接转化为 dp∣=dpi,复杂度 O(nmw)

因为有删除操作,所以上线段树分治。根据插入删除的时间确定每个元素在时间轴上存在的区间,递归处理即可。

#include <bits/stdc++.h>
using namespace std;

const int N = 5e3 + 10;
const int M = 5e5 + 10; // x[i]的上界
int n, x[N], ans[N];
unordered_map<int, vector<int>> s;
bitset<M> dp;

struct segtree {
    int l, r;
    vector<int> v;
} t[N << 2];

#define ls p << 1
#define rs p << 1 | 1
#define mid ((t[p].l + t[p].r) >> 1)

void build(int p, int l, int r) {
    t[p].l = l, t[p].r = r;
    if (l == r) return;
    build(ls, l, mid), build(rs, mid + 1, r);
}

void insert(int p, int l, int r, int x) {
    if (l <= t[p].l && t[p].r <= r) {
        t[p].v.emplace_back(x);
        return;
    }
    if (l <= mid) insert(ls, l, r, x);
    if (r > mid) insert(rs, l, r, x);
}

void dfs(int p, int l, int r) {
    bitset<M> tmp = dp; // 记录状态
    for (int i : t[p].v) dp |= dp << i; // bitset优化背包
    if (l == r) {
        ans[l] = dp.count() - 1; // 有多少个1就是有多少个数能表示(-1减去0)
        dp = tmp;
        return;
    }
    dfs(ls, l, mid);
    dfs(rs, mid + 1, r);
    dp = tmp; // 回溯
}

int main() {
    cin.tie(0)->ios::sync_with_stdio(0);
    cin >> n;
    build(1, 1, n);
    for (int i = 1, op; i <= n; i++) {
        cin >> op >> x[i];
        if (op == 1) {
            s[x[i]].emplace_back(i); // 记录可重集内所有x[i]的下标(起点)
        } else {
            insert(1, s[x[i]].back(), i - 1, x[i]); // 遇到删除操作,把到这为止的一段加进去
            s[x[i]].pop_back();
        }
    }
    for (auto& [i, v] : s) {
        if (v.empty()) continue;
        for (int j : v) insert(1, j, n, i); // 没有删除的,直接覆盖到最后
    }
    dp[0] = 1; // 初始化,0一定能被表示
    dfs(1, 1, n);
    for (int i = 1; i <= n; i++) {
        cout << ans[i] << '\n';
    }
    return 0;
}
posted @   XYukari  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示