DS 进阶

李超线段树:

Problem: 维护一个一次函数集合 \(S\),支持两个操作:

  • 加入一条一次函数 \(f(x) = kx + b(l \le x \le r)\)
  • 给出 \(x\),求 \(\max_{f \in S}{f(x)}\)

首先对于一个一次函数,将 \([l, r]\) 拆成 \(\log n\) 个区间,然后把一次函数挂到这个节点上,每次询问对这些一次函数取个 \(\max\)。这个朴素算法有一个问题,就是可能一个节点可能挂了很多个函数,时间复杂度无法保证。

考虑改进这个朴素算法,我们考虑在每个节点上只保留一个函数。于是当两个函数同时挂到一个节点时,我们比较两个函数并得到他们的优势区间,显然只会有一个函数跨过区间,于是将另一个函数下放到它的优势区间。由于这是单边递归,时间复杂度显然是 \(O(\log n)\)。那么由于拆出来有 \(O(\log n)\) 的区间,单次操作时间复杂度 \(O(\log^2 n)\),查询 \(O(\log n)\)。于是总时间复杂度 \(O(m \log^2 n)\)

注意到某些需要斜率优化的 \(\rm DP\) 也等价于加入一次函数和单点求 \(\max\),而且每次都是全局加不需要拆分区间,时间复杂度 \(O(n \log n)\),吊打 \(\rm CDQ\) 和平衡树。

例题

code
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N = 1e5 + 10;
const double eps = 1e-9;

struct Segment{
	long double k, b;
	void init(int x0, int x1, int y0, int y1){
		if(x0 == y0) k = 0, b = max(y0, y1);
		else k = 1.0 * (y1 - y0) / (x1 - x0), b = y0 - 1.0 * x0 * k;
	}
	double val(int x){return 1.0 * k * x + b;}
}S[N];
// ask if x > y(x = y: 2)
int cmp(double x, double y){
	if(y - x > eps) return 0;
	if(x - y > eps) return 1;
	return 2;
}
// get maxid
int max(int id1, int id2, int x){
	int op = cmp(S[id1].val(x), S[id2].val(x));
	if(op == 2) return (id1 < id2 ? id1 : id2);
	return (op ? id1 : id2); 
}

struct Segtree{
	#define ls (o << 1)
	#define rs (o << 1 | 1)
	#define mid (l + r >> 1)
	int tag[N << 2];
	void upd(int o, int l, int r, int id1){
		if(!tag[o]){tag[o] = id1; return;}
		if(max(id1, tag[o], mid) == id1) swap(id1, tag[o]);
		if(l == r) return;
		if(max(id1, tag[o], l) == id1) upd(ls, l, mid, id1);
		if(max(id1, tag[o], r) == id1) upd(rs, mid + 1, r, id1);
	}
	void findSeg(int o, int l, int r, int s, int t, int id){
		if(s <= l && r <= t){upd(o, l, r, id); return;}
		if(s <= mid) findSeg(ls, l, mid, s, t, id);
		if(mid < t) findSeg(rs, mid + 1, r, s, t, id);
	}
	int qrymax(int o, int l, int r, int x){
		if(l == r) return tag[o];
		if(x <= mid) return max(tag[o], qrymax(ls, l, mid, x), x);
		else return max(tag[o], qrymax(rs, mid + 1, r, x), x); 
	}
}Seg;
int n, tot, siz = 39990;

void ADD(int& x, int y, int mod){x = (x + y - 1) % mod + 1;}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n; int lstans = 0;
	while(n--){
		int opt; cin >> opt;
		if(opt == 0){
			int pos; cin >> pos; ADD(pos, lstans, 39989);
			cout << (lstans = Seg.qrymax(1, 1, siz, pos)) << "\n";
		}
		else{
			int x0, y0, x1, y1; cin >> x0 >> y0 >> x1 >> y1;
			ADD(x0, lstans, 39989); ADD(x1, lstans, 39989);
			ADD(y0, lstans, 1e9); ADD(y1, lstans, 1e9);
			S[++tot].init(x0, x1, y0, y1);
			Seg.findSeg(1, 1, siz, min(x0, x1), max(x0, x1), tot);
		}
	}

	// system("pause");
	return 0;
}

线段树合并

Problem: 维护 \(n\) 个元素,初始时,每个元素自成一个集合。操作可以将两个集合合并,或对某个集合进行查询。

用动态开点线段树维护集合。合并两棵动态开点线段树时,只有双方重合的节点需要处理,耗时为“重合节点数目”。每次合并,总节点数都会减少“重合节点数目”,而总结点数是 \(O(n \log n)\) 的,故总复杂度均摊 \(O(n \log n)\)

这样讲可能过于抽象。来看例题 P3224 [HNOI2012] 永无乡

连边等价于合并两个联通块,用并查集维护。对于一个联通块,我们用权值线段树维护即可。每次合并两个联通块时,只合并重复的节点即可。这样时间复杂度和空间复杂度均为 \(O(n \log n)\)

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

const int N = 1e5 + 10;

int n, m, fa[N], rev[N];
int findfa(int x){return fa[x] = (fa[x] == x) ? x : findfa(fa[x]);}

struct node{
	int ls, rs, sum;
}t[N << 5];
int tot, rub[N << 5], tb;
int newnode(int val){
	int id = (tb ? rub[tb--] : (++tot));
	t[id] = {0, 0, 1}; return id;
}

struct Segtree{
	#define mid (l + r >> 1)
	int root;
	void pushup(int o){t[o].sum = t[t[o].ls].sum + t[t[o].rs].sum;}
	void init(int val){root = build(1, n, val);}
	int build(int l, int r, int val){
		int o = newnode(val); if(l == r) return o;
		if(val <= mid) t[o].ls = build(l, mid, val);
		else t[o].rs = build(mid + 1, r, val);
		pushup(o);
		return o;
	}
	int getkth(int o, int l, int r, int k){
		if(t[o].sum < k) return -1;
		if(l == r) return rev[l]; int lsiz = t[t[o].ls].sum;
		if(lsiz >= k) return getkth(t[o].ls, l, mid, k);
		else return getkth(t[o].rs, mid + 1, r, k - lsiz);  
	}
	int merge(int o, int rt, int l, int r){
		if((!o) || (!rt)) return o + rt;
		t[o].ls = merge(t[o].ls, t[rt].ls, l, mid);
		t[o].rs = merge(t[o].rs, t[rt].rs, mid + 1, r);
		pushup(o); rub[++tb] = rt; return o;
	} 
}Seg[N];

void merge(int x, int y){
	int fx = findfa(x), fy = findfa(y);
	if(fx == fy) return;
	fa[fy] = fx; Seg[fx].merge(Seg[fx].root, Seg[fy].root, 1, n); 
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n >> m;
	for(int i = 1; i <= n; i++){
		int x; cin >> x; rev[x] = i;
		Seg[i].init(x); fa[i] = i;
	}
	for(int i = 1; i <= m; i++){
		int x, y; cin >> x >> y;
		merge(x, y);
	}
	int T; cin >> T;
	while(T--){
		char opt; int x, y; cin >> opt >> x >> y;
		if(opt == 'Q') cout << Seg[findfa(x)].getkth(Seg[findfa(x)].root, 1, n, y) << "\n";
		else merge(x, y);
	}

	// system("pause");
	return 0;
}

线段树合并还可以用来优化树形 \(\rm DP\),比如 P6847 [CEOI2019] Magic Tree

显然有一个 \(O(nk)\) 的暴力 \(\rm DP\),设 \(f_{u, i}\) 设考虑以 \(u\) 为根的子树中,时间 \(\le i\) 的最多果汁数。有两种转移:

  • 不割 \(u\) 和父亲的连边:\(f_{u, i} = \sum_{v \in subtree(u)} f_{v, i}\)

  • \(u\) 和父亲的连边:\(\forall i \in [d_u, k], f_{u, i} = \max(f_{u, i}, \sum_{v \in subtree(u)} f_{v, d_u} + w_u)\)

于是用下标为时间的线段树维护 \(f_{u}\),于是我们首先将 \(u\) 的所有子树的线段树进行合并。第二个转移等价于将区间 \([d_u, k]\)\(\max\)。然后注意到 \(f_{u, i}\) 是单调不减的,于是每次等价于找到一个最大的 \(c\),使得 \(i \in [d_u, c], f_{u, i} \le val(val = \sum_{v \in subtree(u)} f_{v, d_u} + w_u)\),将 \([d_u, c]\) 全部置为 \(val\)。但是线段树合并的时间复杂度是均摊正确的,如果下传标记就会导致新建一堆节点增加很多节点,于是我们只能使用标记永久化的技巧,而标记永久化的标记需要满足可加性,但是赋值显然是不满足的,于是我们将赋值改成区间推平和区间加。区间推平就把整个区间删除即可。

code
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N = 1e5 + 10;

struct edge{
	int v, next;
}edges[N << 1];
int head[N], idx;
void add_edge(int u, int v){
	edges[++idx] = {v, head[u]};
	head[u] = idx;
}

int n, m, k, d[N], w[N];

struct node{
	int ls, rs, mx, mn, tag;
}t[N << 5];
int buc[N << 5], tot, tb;
int crenode(){return (tb ? buc[tb--] : (++tot));}
void delnode(int &id){
	if(id == 0) return;
	t[id] = {0, 0, 0, 0, 0}; buc[++tb] = id; id = 0;
}

struct Segtree{
	int root; 
	#define mid (l + r >> 1)
	void pushup(int o){
		t[o].mx = max(t[t[o].ls].mx, t[t[o].rs].mx) + t[o].tag;
		t[o].mn = min(t[t[o].ls].mn, t[t[o].rs].mn) + t[o].tag;
	}
	// merge two tree
	void merge(int &o, int &rt, int l, int r){
		if((!o) || (!rt)){o = o + rt; return;}
		if(l == r){
			t[o].tag += t[rt].tag; delnode(rt);
			t[o].mx = t[o].mn = t[o].tag; return;
		}
		t[o].tag += t[rt].tag;
		merge(t[o].ls, t[rt].ls, l, mid); merge(t[o].rs, t[rt].rs, mid + 1, r);
		pushup(o); delnode(rt);
	}
	// set [s, e] = v (e <= v) 
	void modify(int &o, int l, int r, int s, int v){
		if(!o) o = crenode(); //cout << o << " " << l << " " << r << " " << s << " " << v << "\n";
		if(s <= l){
			if(t[o].mx <= v){
				delnode(t[o].ls); delnode(t[o].rs);
				t[o].tag = t[o].mx = t[o].mn = v;
				return;
			}
			else{
				v -= t[o].tag;
				if(t[t[o].ls].mn < v) modify(t[o].ls, l, mid, s, v);
				if(t[t[o].rs].mn < v) modify(t[o].rs, mid + 1, r, s, v);
				pushup(o); return;
			}
		}
		v -= t[o].tag;
		if(s <= mid) modify(t[o].ls, l, mid, s, v);
		modify(t[o].rs, mid + 1, r, s, v);
		pushup(o);
	}
	int qrysingle(int o, int l, int r, int x){
		if(!o) return 0;
		if(l == r) return t[o].tag;
		if(x <= mid) return t[o].tag + qrysingle(t[o].ls, l, mid, x);
		else return t[o].tag + qrysingle(t[o].rs, mid + 1, r, x);
	}
	int qryall(){return t[root].mx;}
}Seg[N];

void dfs(int u, int fa){
	for(int i = head[u]; i; i = edges[i].next){
		int v = edges[i].v; if(v == fa) continue;
		dfs(v, u); Seg[u].merge(Seg[u].root, Seg[v].root, 1, k);
	}
	if(d[u]){
		int s = w[u] + Seg[u].qrysingle(Seg[u].root, 1, k, d[u]);
		Seg[u].modify(Seg[u].root, 1, k, d[u], s);
		//cout << s << " " << Seg[u].qryall() << "\n";
	}
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n >> m >> k;
	for(int i = 2; i <= n; i++){
		int x; cin >> x;
		add_edge(x, i); add_edge(i, x);
	}
	for(int i = 1; i <= m; i++){
		int v; cin >> v;
		cin >> d[v] >> w[v];
	}
	dfs(1, 0); cout << Seg[1].qryall();

// 	system("pause");
	return 0;
}

线段树分治

线段树分治可以将 修改-查询-删除 这类操作以一个 \(\log\) 的代价变成 修改-查询-撤回。这种对于类似并查集的数据结构可以直接使用,方便操作。

先咕咕咕了。

猫树分治

对于一类可合并信息 \(U\) ,且两个 \(U_1, U_2\) 合并的代价比较大而将一个新元素加入 \(U\) 的代价不大的时候,可以考虑使用猫树分治来从而实现减少合并带来的时间复杂度开销。

猫树分治的具体思想就是对于一个区间 \([l, r]\),处理被 \([l, r]\) 完全包含的询问。不妨先处理 \([l, mid]\)\([mid + 1, r]\) 的子问题。然后考虑所有跨过中点 \(mid\) 的询问。不难发现每个跨过中点的询问都可以被一个以 \(mid - 1\) 为结尾的后缀和一个以 \(mid\) 为开头的前缀合并而成,先处理出以 \(mid - 1\) 为结尾的所有后缀的信息,还有前缀就可以了。

例题:

Problem: 给出长度为 \(n\) 的序列 \(a_i\) 和固定模数 \(m\)\(q\) 个询问 \([l_i, r_i]\),每次查询 \([l_i, r_i]\) 中有多少个子序列满足和是 \(m\) 的倍数。其中 \(n, q \le 2 \times 10^5, m \le 20\)

不难直接用线段树维护背包,时间复杂度 \(O(nm^2 \log n)\),无法通过。

接下来考虑猫树分治,显然对于一个背包加入一个点的时间复杂度是 \(O(m)\) 的,然后最后合并前后缀信息时不需要求出背包的所有信息,只求出和模 \(m\) 等于 \(0\) 的即可,这里也是 \(O(m)\) 的。于是这道题就以 \(O(nm \log n)\) 的时间复杂度解决了。

code
#pragma GCC optimize(3, "Ofast", "inline")
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N = 2e5 + 10, mod = 1e9 + 7;

int n, m, Q, a[N], ans[N];

struct node{
	int val[20];
	node(){val[0] = 1; for(int i = 1; i < 20; i++) val[i] = 0;} 
}bg[N];

void ADD(int& x, int y){x = (x + y) % mod;}
void ADDbag(node& bg, int x){
	node ret;
	for(int i = 0; i < 20; i++) ret.val[i] = bg.val[i];
	for(int i = 0; i < 20; i++) ADD(ret.val[(i + x) % m], bg.val[i]);
	bg = ret;
}

struct query{
	int l, r, id;
};
namespace cattree{
	#define ls (o << 1)
	#define rs (o << 1 | 1)
	#define mid (l + r >> 1)
	vector<query> vec[N << 2], Ql[N], Qr[N];
	void clr(vector<query>& vvvv){vector<query> qwq; swap(qwq, vvvv);}
	void ADDqry(int o, int l, int r, int s, int t, int id){
		if((s <= mid && mid < t) || l == r){vec[o].push_back((query){s, t, id}); return;}
		if(s <= mid) ADDqry(ls, l, mid, s, t, id);
		else ADDqry(rs, mid + 1, r, s, t, id);
	}
	void solve(int o, int l, int r){
		if(l == r){
			for(int i = 0; i < vec[o].size(); i++) ans[vec[o][i].id] = 1 + (a[l] == 0);
			return;
		}
		solve(ls, l, mid); solve(rs, mid + 1, r);
		for(int i = 0; i < vec[o].size(); i++) Ql[vec[o][i].l].push_back(vec[o][i]), Qr[vec[o][i].r].push_back(vec[o][i]);
		node lft, rht;
		for(int i = mid; i >= l; i--){
			ADDbag(lft, a[i]);
			for(int j = 0; j < Ql[i].size(); j++) bg[Ql[i][j].id] = lft;
		}
		for(int i = mid + 1; i <= r; i++){
			ADDbag(rht, a[i]);
			for(int j = 0; j < Qr[i].size(); j++){
				int id = Qr[i][j].id;
				for(int k = 0; k < m; k++)
					ADD(ans[id], bg[id].val[k] * rht.val[(m - k) % m] % mod);
			}
		}
		for(int i = 0; i < vec[o].size(); i++) clr(Ql[vec[o][i].l]), clr(Qr[vec[o][i].r]);
	}
}


signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n >> m; for(int i = 1; i <= n; i++) cin >> a[i], a[i] = a[i] % m;
	cin >> Q;
	for(int i = 1; i <= Q; i++){
		int l, r; cin >> l >> r;
		cattree::ADDqry(1, 1, n, l, r, i);
	}
	cattree::solve(1, 1, n);
	for(int i = 1; i <= Q; i++) cout << ans[i] << "\n";

	return 0;
}
/**/

Seg-beats

\(\rm Segbeats\) 主要用来解决一类区间和定值取 \(\min\) / \(\max\) 的操作。即每次对于给定的 \([l, r]\)\(v\)\(\forall l \le i \le r, a_i \gets \min(a_i, v)\)。每次查询区间和之类的东西。

普通的线段树似乎好像不能快速维护这种东西。而我们伟大的吉如一老师给出了一个很优美的剪枝。(下面以区间取 \(\max\) 为例,取 \(\min\) 同理)对于一个线段树上的节点 \(o\)(对应区间 \([l_o, r_o]\)),维护 \([l_o, r_o]\) 中的最小值 \(mn_o\)严格次小值 \(se_o\)。然后对区间 \([l, r]\) 执行操作时,有以下三种情况:

  • \(mn_o \ge v\):无事发生。

  • \(mn_o \le v < se_o\):令 \(mn_o \gets v\) 即可。

  • \(se_o \le v\):直接对这个节点的左右儿子进行暴力递归更新。

这样的时间复杂度是对的吗?事实上,对于 \(O(n)\) 组询问次数,这个算法的时间复杂度是 \(O(n \log n)\) 的,即单次操作均摊 \(O(\log n)\)。为什么呢?不难发现只有时间复杂度只跟出发最后一种情况的次数有关,而最后一种情况触发一定会导致这个区间中的数种类数减一。由于线段树每个节点所对应的区间的数的种类数的和是 \(O(n \log n)\) 级别的,从而最多触发 \(O(n \log n)\) 次 第三种情况。

Segbeats 结合区间加操作时间复杂度是 \(O(n \log ^2 n)\) 的,但是我不会证明/fad。

posted @ 2024-08-18 20:02  Little_corn  阅读(16)  评论(0编辑  收藏  举报