线段树

海亮 OJ 题单

基本操作

区修区查:

  • 第 1、2、……k 小/大(\(k\) 不太大)
  • 区间和、积、异或和、区间矩阵乘法

维护差分信息

P4243 [JSOI2009] 等差数列

若要在序列上处理等差数列,可以考虑差分法。此时,我们不必将差分数组和数列中的元素一一对应(这会影响理解),而是将差分数组中的一个元素和原序列中对应的两个元素关联(我的理解盲区)。

这样,使用线段树时,对于(差分数组的下标)区间 \([l,r]\),我们可以记录 \([l,r]\)\([l,r-1]\)\([l+1,r]\)\([l+1,r-1]\) 的答案;合并时,通过考虑这些区间对应到原序列的哪些元素,对左右区间的重合部分统计额外贡献,不重合的情况直接相加。

对于较短的区间(\(len\le 2\)),如何处理边界条件?当 \(len=1\) 时,区间 \([l+1,r]\)\([l,r-1]\) 的答案在差分数组上无意义,但却能对应到原数组上的 \(1\) 个元素;对于 \([l+1,r-1]\),它在两个数组上都没有意义,因此直接赋值为 \(0\)\(\pm\infty\)

\(1\) 条差分信息 -> 约束两个位置
\(n\) 条差分信息 -> 约束 \(n+1\) 个位置

|
V

\(0\) 条差分信息 -> 代表一个位置

代码
#include<iostream>
using namespace std;
const int N = 1E5 + 10;
const int INF = 1E7 + 10;

struct Node {
	int mans, lans, rans, ans, tag;
	int ld, rd, len;
	Node(int _ma = 0, int _la = 0, int _ra = 0, int _a = 0, int _ld = 0, int _rd = 0, int _tg = 0, int _len = 0) {
		mans = _ma;
		lans = _la;
		rans = _ra;
		ans = _a;
		ld = _ld;
		rd = _rd;
		tag = _tg;
		len = _len;
	}
};

inline Node operator+(const Node &a, const Node &b) {
	Node res;
	res.mans = min(a.rans + b.lans - (a.rd == b.ld), min(a.mans + b.lans, a.rans + b.mans));
	res.lans = min(a.ans + b.lans - (a.rd == b.ld), min(a.lans + b.lans, a.ans + b.mans));
	res.rans = min(a.rans + b.ans - (a.rd == b.ld), min(a.mans + b.ans, a.rans + b.rans));
	res.ans = min(a.ans + b.ans - (a.rd == b.ld), min(a.lans + b.ans, a.ans + b.rans));
	res.ld = a.ld;
	res.rd = b.rd;
	return res;
}

int n, q; 
int a[N], d[N];

namespace Seg_T {
	
	inline int lc(int x) { return x << 1; }
	inline int rc(int x) { return x << 1 | 1; }
	Node tr[4 * N];
	inline void push_up(int p) {
		tr[p] = tr[lc(p)] + tr[rc(p)];
	}
	inline void move_tag(int p, int tg) {
		tr[p].ld += tg;
		tr[p].rd += tg;
		tr[p].tag += tg;
	}
	inline void push_down(int p) {
		if(!tr[p].tag) return;
		move_tag(lc(p), tr[p].tag);
		move_tag(rc(p), tr[p].tag);
		tr[p].tag = 0; 
	}
	void build(int p, int l, int r) {
		if(l == r) {
			tr[p] = {0, 1, 1, 1, d[l], d[l], 0};
			return;
		}
		int mid = (l + r) >> 1;
		build(lc(p), l, mid);
		build(rc(p), mid + 1, r);
		push_up(p); 
	} 
	void modify(int p, int l, int r, int q, int v) {
		if(l == r) {
			tr[p].ld += v;
			tr[p].rd += v;
			return;
		}
		push_down(p);
		int mid = (l + r) >> 1;
		if(mid >= q) modify(lc(p), l, mid, q, v);
		else modify(rc(p), mid + 1, r, q, v);
		push_up(p);
	}
	void modify(int p, int l, int r, int ql, int qr, int v) {
		if(ql <= l && r <= qr) {
			move_tag(p, v);
			return; 
		}
		push_down(p);
		int mid = (l + r) >> 1;
		if(mid >= ql) modify(lc(p), l, mid, ql, qr, v);
		if(mid < qr) modify(rc(p), mid + 1, r, ql, qr, v);
		push_up(p);
	}
	Node query(int p, int l, int r, int ql, int qr) {
		if(ql <= l && r <= qr) {
			return tr[p];
		}
		push_down(p);
		int mid = (l + r) >> 1;
		if(mid >= qr) return query(lc(p), l, mid, ql, qr);
		if(mid < ql) return query(rc(p), mid + 1, r, ql, qr);
		return query(lc(p), l, mid, ql, qr) + query(rc(p), mid + 1, r, ql, qr); 
	}
	
}

int main() {
	
	cin >> n;
	for(int i = 1; i <= n; i++) {
		cin >> a[i];
		d[i] = a[i] - a[i - 1];
	}
	
	Seg_T::build(1, 1, n + 1);
	
	cin >> q;
	while(q--) {
		char op;
		cin >> op;
		if(op == 'A') {
			int l, r, a, b;
			cin >> l >> r >> a >> b;
			if(l == r) {
				Seg_T::modify(1, 1, n + 1, l, a);
				Seg_T::modify(1, 1, n + 1, r + 1, -a); 
				continue;
			}
			Seg_T::modify(1, 1, n + 1, l, a);
			Seg_T::modify(1, 1, n + 1, l + 1, r, b);
			Seg_T::modify(1, 1, n + 1, r + 1, -a - (r - l) * b); 
		} else {
			int l, r;
			cin >> l >> r;
			if(l == r) {
				cout << 1 << '\n';
				continue;
			} 
			cout << Seg_T::query(1, 1, n + 1, l + 1, r).ans << '\n'; 
		}
	}
	
	return 0;
}

主席树

P3919 【模板】可持久化线段树 1(可持久化数组)

主席树是可持久化权值线段树的简称,可以在 \(O(\log V)\) 的时间和空间复杂度内实现一次插入,同时可以保存所有历史版本。

模板代码
#include<iostream>
using namespace std;
const int N = 1E6 + 10;
const int LOGN = 25; 

int n, q;
int a[N], rt[N];

namespace Seg_T {
	
	int nn;
	int lc[N * LOGN], rc[N * LOGN], sum[N * LOGN];
	
	int addNode(int p) {
		int nw = ++nn;
		lc[nw] = lc[p];
		rc[nw] = rc[p];
		return nw;
	}
	
	inline void push_up(int p) {
		sum[p] = sum[lc[p]] + sum[rc[p]];
	}
	
	void build(int &p, int l, int r) {
		if(p == 0) p = ++nn;
		if(l == r) {
			sum[p] = a[l];
			return;
		}
		int mid = (l + r) >> 1;
		build(lc[p], l, mid);
		build(rc[p], mid + 1, r);
		push_up(p); 
	}
	
	int modify(int p1, int l, int r, int q, int v) {
		int p = addNode(p1);
		if(l == r) {
			sum[p] = v;
			return p;
		}
		int mid = (l + r) >> 1;
		if(mid >= q) lc[p] = modify(lc[p1], l, mid, q, v);
		else rc[p] = modify(rc[p1], mid + 1, r, q, v);
		push_up(p);
		return p;
	}
	
	int query(int p, int l, int r, int q) {
		if(l == r) {
			return sum[p];
		}
		int mid = (l + r) >> 1;
		if(mid >= q) return query(lc[p], l, mid, q);
		else return query(rc[p], mid + 1, r, q); 
	}
	
}

int main() {
	
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	
	cin >> n >> q;
	for(int i = 1; i <= n; i++) {
		cin >> a[i];
	} 
	
	Seg_T::build(rt[0], 1, n);
	
	for(int i = 1; i <= q; i++) {
		int ver, op, pos, val;
		cin >> ver >> op;
		if(op == 1) {
			cin >> pos >> val;
			rt[i] = Seg_T::modify(rt[ver], 1, n, pos, val);
		} else {
			cin >> pos;
			cout << Seg_T::query(rt[ver], 1, n, pos) << '\n';
			rt[i] = rt[ver]; 
		}
	}
	
	return 0;
}

P3834 【模板】可持久化线段树 2

查区间第 \(k\)

使用主席树我们可以对原数组的每一个前缀都“建”一棵权值线段树,通过对两棵不同下标对应的树作差,我们可以对任意下标区间 \([l,r]\) 使用线段树二分(即:得到了一棵不能查询最值,但可以查询区间和,区间绝对众数等的线段树)

时间复杂度 \(O(n\log V)\) - \(O(m\log V)\)

模板代码
#include<iostream>
#define int long long
using namespace std;
const int N = 2E5 + 10;
const int A = 1E9; 
const int LOGA = 20;

int n, m; 
int rt[N];

namespace Seg_T {
	
	int nn;
	int lc[N * LOGA], rc[N * LOGA], sum[N * LOGA];
	
	int addNode(int p1) {
		int p = ++nn;
		lc[p] = lc[p1];
		rc[p] = rc[p1];
		sum[p] = sum[p1];
		return p;
	}
	
	void push_up(int p) {
		sum[p] = sum[lc[p]] + sum[rc[p]];
	}
	
	int insert(int p1, int l, int r, int q, int v) {
		int p = addNode(p1);
		if(l == r) {
			sum[p] += v;
			return p;
		}
		int mid = (l + r) >> 1;
		if(mid >= q) lc[p] = insert(lc[p1], l, mid, q, v);
		else rc[p] = insert(rc[p1], mid + 1, r, q, v);
		push_up(p); 
		return p;
	}
	
	int queryKth(int pl, int pr, int l, int r, int k) {
		if(l == r) {
			return l;
		}
		int mid = (l + r) >> 1;
		if(k <= sum[lc[pr]] - sum[lc[pl]]) return queryKth(lc[pl], lc[pr], l, mid, k);
		else return queryKth(rc[pl], rc[pr], mid + 1, r, k - (sum[lc[pr]] - sum[lc[pl]]));
	}
	
}

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;
		rt[i] = Seg_T::insert(rt[i - 1], 0, A, x, 1);
	}
	
	for(int i = 1; i <= m; i++) {
		int l, r, k;
		cin >> l >> r >> k;
		cout << Seg_T::queryKth(rt[l - 1], rt[r], 0, A, k) << '\n';
	}
	
	return 0;
}

P7252 [JSOI2011] 棒棒糖

查询区间绝对众数

代码
#include<iostream>
using namespace std;
const int N = 5E4 + 10;
const int V = 5E4 + 10;
const int LOGV = 16;

int n, m; 
int c[N], rt[N];

namespace Seg_T {
	
	int sum[N * LOGV], lc[N * LOGV], rc[N * LOGV], nn;
	inline void push_up(int p) {
		sum[p] = sum[lc[p]] + sum[rc[p]];
	} 
	int insert(int p1, int l, int r, int q, int v) {
		int p = ++nn;
		lc[p] = lc[p1];
		rc[p] = rc[p1];
		sum[p] = sum[p1];
		if(l == r) {
			sum[p] += v;
			return p;
		}
		int mid = (l + r) >> 1;
		if(mid >= q) lc[p] = insert(lc[p1], l, mid, q, v);
		else rc[p] = insert(rc[p1], mid + 1, r, q, v); 
		push_up(p);
		if(sum[p] != sum[p1] + 1) throw -1; 
		return p; 
	}
	int query(int pl, int pr, int l, int r, int k) {
		if(l == r) {
			return l; 
		}
		int mid = (l + r) >> 1;
		if(sum[lc[pr]] - sum[lc[pl]] >= k) return query(lc[pl], lc[pr], l, mid, k);
		if(sum[rc[pr]] - sum[rc[pl]] >= k) return query(rc[pl], rc[pr], mid + 1, r, k);
		return 0; 
	}
	
}

int main() {
	
	cin >> n >> m;
	for(int i = 1; i <= n; i++) {
		cin >> c[i];
		rt[i] = Seg_T::insert(rt[i - 1], 1, V, c[i], 1);
	}
	
	while(m--) {
		int l, r;
		cin >> l >> r;
		cout << Seg_T::query(rt[l - 1], rt[r], 1, V, (r - l + 1) / 2 + 1) << '\n'; 
	}
	
	return 0; 
} 

CF786C Till I Collapse

子问题:求任意区间内不同数字的个数(重复的数字算一种);

考虑从左向右给每个前缀都建一棵线段树,线段树下标和原数组对应(意思是,这些线段树不是权值线段树),并使前缀中相同的数字只有最右边的一个 \(val=1\)。我们可以考虑用 last 数组记录每个数字上一次出现的位置,每次插入新数字时,若发现其左边有重复的数字,则对重复的位置 \(-1\) 去掉贡献。这样,我们在前缀 \([1,r]\) 的线段树中查询 \([l,r]\) 的区间和,就可以知道区间 \([l,r]\) 中有多少种不同的数字。

本题还需要从左往右做贪心,同时还需要线段树上二分,故需要把上面的过程左右颠倒一下,但总体思路不变。

代码
#include<iostream>
using namespace std;
const int N = 1E5 + 10;
const int LOGN = 20;

int n;
int rt[N], a[N], last[N];

namespace Seg_T {
	
	int sum[2 * N * LOGN], lc[2 * N * LOGN], rc[2 * N * LOGN], nn;
	
	inline void push_up(int p) { sum[p] = sum[lc[p]] + sum[rc[p]]; }
	int insert(int p1, int l, int r, int q, int v) {
		int p = ++nn;
		lc[p] = lc[p1];
		rc[p] = rc[p1];
		sum[p] = sum[p1];
		if(l == r) {
			sum[p] += v;
			return p; 
		}
		int mid = (l + r) >> 1;
		if(mid >= q) lc[p] = insert(lc[p1], l, mid, q, v);
		else rc[p] = insert(rc[p], mid + 1, r, q, v);
		push_up(p);
		return p;
	}
	int query(int p, int l, int r, int k) {
		if(l == r) {
			return l;
		}
		int mid = (l + r) >> 1;
		if(sum[lc[p]] > k) return query(lc[p], l, mid, k);
		return query(rc[p], mid + 1, r, k - sum[lc[p]]); 
	}
	
}

int main() {
	
	cin >> n;
	for(int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	
	for(int i = n; i >= 1; i--) {
		rt[i] = Seg_T::insert(rt[i + 1], 1, n + 1, i, 1);
		if(last[a[i]]) {
			rt[i] = Seg_T::insert(rt[i], 1, n + 1, last[a[i]], -1);
		}
		last[a[i]] = i;
	}
	
	for(int k = 1; k <= n; k++) {
		int cur = 1, ans = 0;
		while(cur <= n) {
			cur = Seg_T::query(rt[cur], 1, n + 1, k);
			ans++;
		}
		cout << ans << ' ';
	}
	cout << endl;
	
	return 0;
}

线段树优化建图

优化最短路松弛

若使用 BFS 等最短路算法,每个节点只会考虑一次。因此在总边数很多的情况下,我们可以使用线段树优化枚举出边的过程,并且满足每次松弛结束,该节点就不会再被松弛,我们就可以用线段树把这个节点标记为无效,优化后面的松弛,从而达到较低的均摊时间复杂度。

posted @ 2025-01-19 18:38  Simon火腿肠  阅读(11)  评论(1编辑  收藏  举报