becoder 线段树分治题单

线段树分治

假如你需要维护一些信息,这些信息会在某一个时间段内出现,要求在离线的前提下回答某一个时刻的信息并,则可以考虑使用线段树分治。

典型的比如连通性,线性基等难以支持删除的信息。

简单概述 OJ 题单中 14 道题的做法,难度按个人认为的升序排序。

除此之外再提几道比较 educational 的线段树分治的题目,会打上 *。

「TJOI2018」数学计算

https://www.becoder.com.cn/problem/4413

因为 \(mod\) 不保证质数,所以不一定存在逆运算。

考虑线段树分治,然后只有乘法。

「BZOJ3237 Ahoi2013」连通图

https://www.becoder.com.cn/problem/17237

维护连通性板子,正常维护连通性是使用并查集的,但是这里需要撤销。

栈序撤销并查集

路径压缩过后并查集很难支持撤销,于是考虑通过启发式合并降低 find 的复杂度,把 fa 与 siz 的修改用栈存下来然后撤回。实现如下。

#define pi pair<int, int>
#define fi first
#define se second
const int N = 200020;
struct DSU {
	int fa[N], siz[N];
	stack<pi> stk;
	DSU() {
		rep(i, 0, N - 1)
			fa[i] = i, siz[i] = 1;
	}
	inline int find(int x) {
		while(fa[x] != x) x = fa[x];
		return x;
	}
	inline void merge(int x, int y) {
		x = find(x), y = find(y);
		if(x == y) return;
		if(siz[x] > siz[y]) swap(x, y);
		stk.push({x, y});
		fa[x] = y, siz[y] += siz[x];
	}
	inline void undo(int lst) {
		while(stk.size() > lst) {
			pi x = stk.top();
			stk.pop();
			fa[x.fi] = x.fi;
			siz[x.se] -= siz[x.fi];
		}
	}
} D;

注意复杂度是 \(n\log^2 n\) 的。

接着几个是维护并查集的例题

「CF1140F」Extending Set of Points

https://www.becoder.com.cn/problem/32792

首先通过线段树分治变成只加点。

然后考虑只有加入怎么求点的个数。

考虑构造一个无向图,有 \(n\) 个点 \(X_i\) 以及 \(n\) 个点 \(Y_i\),把集合加入一个点 \((a,b)\) 转化成给无向图中加一条边 \((x_a,Y_b)\) ,然后不难发现答案应该是无向图中每个连通块中 \(X\) 点与 \(Y\) 点个数的成绩,并查集维护即可。

「SHOI 2008」堵塞的交通

https://www.becoder.com.cn/problem/4931

维护连通性板子,不赘述。

「SHOI2014」神奇化合物

https://www.becoder.com.cn/problem/4056

也是板子,不赘述。

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

https://www.luogu.com.cn/problem/P5787

二分图判定同样也是可以通过并查集实现的,通过种类并查集将 \(i,j\) 颜色不等转化成连边 \((i+n,j),(i,j+n)\) ,然后判断是否对任意 \(i,i+n\) 均不在同一连通块中即可。

Painting Edges

https://www.becoder.com.cn/problem/23537

线段树分治维护二分图判定的板子题。

但是值得注意的是这个题中出现了加入加边不合法则无视该操作,处理的方法是假如一条边是不合法的,那么我们就先打上 tag,然后之后线段树分治的时候加边前先判断是否被打了 tag。


线性基同样也是可以支持栈序撤销的。

考虑用个栈存成功插入的元素,然后因为是栈序撤销,撤销时直接把插入位置清空即可。

当然有的时候因为线性基大小是很小的,直接暴力复制一个也是可以过的。


「BZOJ4184」 shallot

https://www.becoder.com.cn/problem/18184

线段树分治维护线性基板子。

贴个没有栈序撤销的代码。

#include <bits/stdc++.h>
#define rep(i, x, y) for (int i = x; i <= y; ++i)
#define drep(i, x, y) for (int i = x; i >= y; --i)
#define ll long long
#define pb push_back
#define IOS ios::sync_with_stdio(false), cin.tie(0), cout.tie(0)
using namespace std;
#define ui unsigned int
const int N = 500050;
struct D {
    ui f[32];
    D() { rep(i, 0, 30) f[i] = 0; }
    inline void ins(ui x) {
        drep(i, 30, 0) {
            if (x >> i & 1) {
                if (f[i]) {
                    x ^= f[i];
                    continue;
                }
                f[i] = x;
                return;
            }
        }
    }
    inline ui Q() {
        ui res = 0;
        drep(i, 30, 0) if ((res ^ f[i]) > res) res ^= f[i];
        return res;
    }
};
int n, a[N];
unordered_map<int, int> lst;
ui ans[N];
vector<ui> T[N << 2];
void ins(int ql, int qr, int x, int p = 1, int l = 1, int r = n) {
    if (qr < l || r < ql)
        return;
    if (ql <= l && r <= qr)
        return void(T[p].pb(x));
    int mid = (l + r) >> 1;
    ins(ql, qr, x, p << 1, l, mid);
    ins(ql, qr, x, p << 1 | 1, mid + 1, r);
}
void solve(int p, int l, int r, D x) {
    for (ui y : T[p]) x.ins(y);
    if (l ^ r) {
        int mid = (l + r) >> 1;
        solve(p << 1, l, mid, x);
        solve(p << 1 | 1, mid + 1, r, x);
    } else {
        ans[l] = x.Q();
    }
}
int main() {
    IOS;
    cin >> n;
    rep(i, 1, n) {
        cin >> a[i];
        if (a[i] < 0)
            ins(lst[-a[i]], i - 1, -a[i]), lst[-a[i]] = 0;
        else
            lst[a[i]] = i;
    }
    for (auto x : lst) {
        if (x.second != 0)
            ins(x.second, n, x.first);
    }
    D ep;
    solve(1, 1, n, ep);
    rep(i, 1, n) cout << ans[i] << '\n';
    return 0;
}

「BZOJ4644」经典傻逼题

题目名字怎么骂藏?

https://www.becoder.com.cn/problem/18644

但是确实是 sb 题,难点在读题。

定义 \(val_x\) 为与 \(x\) 相连的所有边的边权的异或和,然后不难发现答案应该是 \(val\) 的最大异或和,因为有修改,线段树分治即可。


假如你做过 [WC2011] 最大XOR和路径 那么就会有并查集与线性基同时使用的题目。

「HAOI2017」八纵八横

https://www.becoder.com.cn/problem/4164

根据 [WC2011] 最大XOR和路径 的做法,应当每次把环的边权和加入线性基中。

于是考虑并查集,维护每个点到根的边权异或和,然后再用栈序撤销线性基就行。

这个题输入输出比较恶心,注意一下。

「CF1442D」选数

https://www.becoder.com.cn/problem/31218

运用了线段树分治的思想,因为 \(t_i\ge n\) ,于是可以考虑这样的策略,枚举 \(i\) ,然后求出除了 \(i\) 之外的栈的背包 dp,然后枚举当前栈选几个。

然后直接线段树分治加背包就行,这个题性质比较好,不需要写插入,直接分治即可。

「雅礼集训 2018 Day10」贪玩蓝月

https://www.becoder.com.cn/problem/5503

经典题,不难发现线段树分治是可做的,但是过不去。

不难发现我们实际上要维护这样的一个问题,有一个序列,要在开头加入删除或者末尾加入删除,然后维护序列的和但是只满足结合律,要做到线性。

做法是双栈,具体双栈的介绍以及复杂度说明可以看这个文章 :https://www.becoder.com.cn/article/4282

「FJOI2015」火星商店问题

https://www.becoder.com.cn/problem/6483

个人认为这东西应该用线段树套 Trie 树是比较直接的。然后假如要优化空间可以离线一下空间少一只 \(\log\)

假如有线段树分治做法敲我,可能是我太菜了。

「BZOJ4311」向量

https://www.becoder.com.cn/problem/18311

经典的一个套路,求与 \((a,b)\) 点乘的最大值其实 \(ax+by=a(x+\frac b ay)\) ,然后不难发现后边是个直线 \(kx+b\) 的形式,于是可以用李超线段树解决。

因为有删除可以考虑给李超线段树栈序撤销或者可持久化。

李超线段树是可以可持久化的哦,空间也是 \(1\log\)

「CTSC2016」时空旅行

https://www.becoder.com.cn/problem/4933

很大一坨东西是定值,然后发现问题变成了 「BZOJ4311」向量 ,直接套上去就行了。

但是这样做有点卡空间。


OJ 上边的题就这些了,接着提下一些其它比较有价值的东西。

首先线段树是可以栈序撤销的,用个栈存修改的点的下标然后回撤回去。

假如只有单点修改空间是可以做到线性的,直接把修改的叶子节点存下来,然后撤回时先回撤叶子节点,然后将叶子到根的路径重新 pushup 更新一遍。

*P11619 [PumpkinOI Round 1] 种南瓜

https://www.luogu.com.cn/problem/P11619

不难发现题意应该是询问是否存在两个区间只相交但互不包含。

假如只有加入是好做的,我们每次加入 \([l,r]\) ,要判断是否有 \([L,R]\) 满足 \(L < l \leq R < r\) 或者 \(l < L \leq r < R\) ,对 \(R\) 为下标维护区间 \(L\) 的最小值,对 \(L\) 为下标维护区间 \(R\) 最大值,两颗线段树即可。

有删除加上线段树分治即可,复杂度是 \(q\log q\log n\) 的。

至于回撤操作,用栈序撤销的线段树即可,写 zkw 的话常数极小,不怕被卡常。代码也很简单。

#include <bits/stdc++.h>
#define rep(i, x, y) for (int i = x; i <= y; ++i)
#define drep(i, x, y) for (int i = x; i >= y; --i)
#define ll long long
#define pb push_back
#define IOS ios::sync_with_stdio(false), cin.tie(0), cout.tie(0)
using namespace std;
const int N = 200020;
int n, q, lst[N], l[N], r[N], op[N], stk[N], tp; vector<int> T[N << 2];
struct zkw {
	int t[N << 2], m, tp; pair<int, int> stk[N << 3]; // Min SegTree
	inline void build() { m = 1, memset(t, 0x3f, sizeof t); while(m <= n) m <<= 1; }
	inline void pushup(int p) { t[p] = min(t[p << 1], t[p << 1 | 1]); }
	inline void up(int p) { while(p >>= 1) pushup(p); }
	inline void upd(int x, int y) { x += m, stk[++tp] = {x, t[x]}, t[x] = min(t[x], y), up(x); }
	inline void undo(int lst) { while(tp > lst) t[stk[tp].first] = stk[tp].second, up(stk[tp].first), --tp; }
	inline int qry(int l, int r) { int res = 1e9; if(l > r) return res;
		for(l += m - 1, r += m + 1; l ^ r ^ 1; l >>= 1, r >>= 1) {
			if(~l & 1) res = min(res, t[l ^ 1]); if(r & 1) res = min(res, t[r ^ 1]);
		} return res;
	}
} t1, t2;
inline void insert(int ql, int qr, int x, int p = 1, int l = 1, int r = q) {
	if(qr < l || r < ql) return; if(ql <= l && r <= qr) return void(T[p].pb(x));
	int mid = (l + r) >> 1; insert(ql, qr, x, p << 1, l, mid);
	insert(ql, qr, x, p << 1 | 1, mid + 1, r); }
inline void Ins(int l, int r) {
	++tp, stk[tp] = stk[tp - 1];
	if(t1.qry(l, r - 1) < l) stk[tp] = 1;
	if(-t2.qry(l + 1, r) > r) stk[tp] = 1;
	t1.upd(r, l), t2.upd(l, -r);
}
inline void solve(int p = 1, int l = 1, int r = q) {
	int lst = tp; for(int x : T[p]) Ins(::l[x], ::r[x]);
	if(l == r) cout << (stk[tp] ? "No\n" : "Yes\n");
	else { int mid = (l + r) >> 1;
		solve(p << 1, l, mid), solve(p << 1 | 1, mid + 1, r);
	} t1.undo(lst), t2.undo(lst), tp = lst;
}
int main() {
	IOS; cin >> n >> q, t1.build(), t2.build();
	rep(i, 1, q) {
		cin >> op[i];
		if(op[i] == 1) cin >> l[i] >> r[i];
		else cin >> l[i], lst[l[i]] = i; 
	}
	rep(i, 1, q) if(op[i] == 1) insert(i, (!lst[i] ? q : lst[i] - 1), i);
	solve(); return 0;
}

*P10611 故事结局

https://www.luogu.com.cn/problem/P10611

线段树分治是非正解,需要卡常,我还没调出来,但还是提一下

先颜色均摊,然后变成只有加入,删除。

接着以行为下标建立线段树,然后把查询拆成 \(q\log n\) 个。

然后考虑子问题,即区间插入一个数 \(x\),删除之前一次操作,以及区间最值。

两种做法,一种直接标记永久化 + set,一种是再离线一遍然后线段树分治。

\(n,q\) 同阶,总复杂度是 \(n\log^3n\) 的,难以通过。


*CF500F New Year Shopping

https://www.luogu.com.cn/problem/CF500F

线段树分治维护背包板子,复杂度 \(nm\log n+q\)\(m\) 是背包容量。

常数很小直接 300ms 飞过去。

*P3206 [HNOI2010] 城市建设

https://www.luogu.com.cn/problem/P3206

维护动态最小生成树。

两种做法,一种是只保留有用边然后递归下去,细节比较繁琐,一种比较无脑,考虑线段树分治过后只有加边,然后变成 xxxx 经典题,用 LCT 维护生成树即可。

总结

个人认为线段树分治是一个特别实用的技巧,最近的 CF div2 有场的 F 也是线段树分治。在维护连通性时在代码难度上会远低于 LCT ,很多时候代码难度低,用途广,特别适合拿部分分以及辅助解决一些不支持删除的问题。

posted @ 2025-03-24 17:39  yzq_yzq  阅读(86)  评论(0)    收藏  举报