线段树

线段树

一种分治型结构,每个节点维护一段区间的信息,通过合并左右区间来完成查询。

节点数不会超过【比 2n1 大的最小的 2 的幂次】,所以通常把空间开到 4n.

使用条件

对于一个操作含区间修改、区间查询的问题,什么情况下可以使用线段树?

共有三个必须满足的条件。

  • 对于一次区间修改,若要某个节点打上标记,需要快速更新该节点的信息(update & pushdown)。
  • 标记在知晓前后顺序时可以合并(pushdown)。
  • 对于所有无标记的节点,可以根据左右子区间的信息,推出当前节点的信息(pushup)。

标记永久化

某些时候可以把标记一直放在节点不下传,修改时只更改被影响到的点的标记,询问时则一路累加路上的标记,这样的做法被称为标记永久化

当标记的合并具有交换律,即不知晓前后顺序也能合并标记时,就可以进行标记永久化。

eg:对于区间加区间求和问题,可以不下传标记,在查询的时候把答案累加上当前区间与答案区间的交的长度乘上区间 tag 的值。

线段树与二进制

对于与二进制(位运算)有关的区间查询问题,可以拆位后维护区间中每一位 01 的个数。

线段树上二分

给定一个数组,支持区间加、区间对 u 取 max,求出从左往右数,下标 x 的第一个值 y 的位置。

考虑线段树上二分

image

线段树合并

线段树合并是指建立一棵新的线段树,这棵线段树的每个节点都是两棵原线段树对应节点合并后的结果。因为重新建一颗满的线段树空间和时间都过大,所以要用动态开点线段树来维护。

线段树合并的过程本质上相当暴力:

  • 假设两颗线段树为 A 和 B,我们从两颗线段树的根节点同时开始递归合并。
  • 递归到某个节点时,如果 A 树或者 B 树上的对应节点为空,直接返回另一个树上对应节点,这里运用了动态开点线段树的特性。
  • 如果递归到叶子节点,我们合并两棵树上的对应节点。
  • 最后,根据子节点更新当前节点并且返回。

合并 n 颗线段树的复杂度通常是 O(nlogn) 级别的。

模板题 LG4556 代码:

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

const int N = 1e6 + 5;

int n, m, a, b, x, y, z, f[N][25], lg[N], dep[N], ans[N];
vector<int> g[N];

inline void dfs(int u, int fa)
{
	f[u][0] = fa, dep[u] = dep[fa] + 1;
	for(int i = 1; i <= lg[dep[u]]; i++)
		f[u][i] = f[f[u][i - 1]][i - 1];
	for(auto v : g[u])
		if(v != fa) dfs(v, u);
	return;
} 

inline int LCA(int a, int b)
{
	if(dep[a] > dep[b]) swap(a, b);
	for(int i = 20; i >= 0; i--)
		if(dep[f[b][i]] >= dep[a]) b = f[b][i];
	if(a == b) return a;
	for(int i = 20; i >= 0; i--)
		if(f[a][i] != f[b][i]) a = f[a][i], b = f[b][i];
	return f[a][0];
}

int ls[N << 3], rs[N << 3], val[N << 3], sum[N << 3], tot, root[N << 3];

void pushup(int x)
{
	int ln = ls[x], rn = rs[x];
	if(sum[ln] >= sum[rn]) val[x] = val[ln], sum[x] = sum[ln];
	else val[x] = val[rn], sum[x] = sum[rn];
	return;
}

int update(int l, int r, int p, int k, int x)
{
	if(!x) x = ++tot;
	if(l == r)
	{
		val[x] = p, sum[x] += k;
		return x;
	}
	int mid = (l + r) >> 1;
	if(p <= mid) ls[x] = update(l, mid, p, k, ls[x]);
	if(mid < p) rs[x] = update(mid + 1, r, p, k, rs[x]);
	pushup(x);
	return x;
}

int merge(int a, int b, int l, int r) // 把 B 合并到 A
{
	if(!a) return b;
	if(!b) return a;
	if(l == r)
	{
		sum[a] += sum[b];
		return a;	
	}
	int mid = (l + r) >> 1;
	ls[a] = merge(ls[a], ls[b], l, mid);
	rs[a] = merge(rs[a], rs[b], mid + 1, r);
	pushup(a);
	return a;
}

void dfs2(int u, int fa)
{
	for(auto v : g[u])
		if(v != fa) dfs2(v, u), root[u] = merge(root[u], root[v], 1, (int) 1e5);
	ans[u] = (bool) sum[root[u]] * val[root[u]];
	return;
}

int main()
{
	ios :: sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> n >> m;
	for(int i = 2; i <= n; i++)
		lg[i] = lg[i >> 1] + 1;
	for(int i = 1; i < n; i++)
	{
		cin >> a >> b;
		g[a].push_back(b);
		g[b].push_back(a);
	}
	dfs(1, 0);
	for(int i = 1; i <= m; i++)
	{
		cin >> x >> y >> z;
		root[x] = update(1, (int) 1e5, z, 1, root[x]);
		root[y] = update(1, (int) 1e5, z, 1, root[y]);
		int lca = LCA(x, y);
		root[lca] = update(1, (int) 1e5, z, -1, root[lca]);
		root[f[lca][0]] = update(1, (int) 1e5, z, -1, root[f[lca][0]]);
	}
	dfs2(1, 0);
	for(int i = 1; i <= n; i++)
		cout << ans[i] << '\n';
	return 0;
}

线段树分治

LG5787 二分图 /【模板】线段树分治)维护一个图,一条边 (uj,vj) 只会在时间 i[lj,rj] 时存在。现在给出 q 次询问,第 i 次询问发生在时间 i,问图是否是二分图。

考虑线段树分治。以时间为下标建线段树,对每个节点维护其对应的时间范围内的所有存在的信息。从上向下 DFS 线段树,过程中维护答案,每遇到一个区间就将其信息贡献到答案中。递归到叶子结点(即满足 l=r)时,记录询问 l 的答案。

在此题中,即对每个节点维护对应的时间范围中一直存在的边的数量,并维护一个可删除的扩展域并查集,以判断某个时间区间内的边是否构成二分图。

具体内容可以看代码。

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

#define pii pair<int, int>
const int N = 4e5 + 5;

int n, m, k, _x[N], _y[N], l, r, fa[N], siz[N];
bool ans[N];

struct noti
{
	int u, f, s;
};

stack<noti> stk;

struct node
{
	int l, r;
	vector<pii> v;
} tree[N << 2];

void build(int l, int r, int x)
{
	tree[x] = {l, r, vector<pii>()};
	if(l == r) return;
	int mid = (l + r) >> 1;
	build(l, mid, x << 1);
	build(mid + 1, r, x << 1 | 1);
	return;
}

void update(int l, int r, int k, int x)
{
	if(l <= tree[x].l && tree[x].r <= r)
		return (void) (tree[x].v.push_back({_x[k], _y[k]}));
	int mid = (tree[x].l + tree[x].r) >> 1;
	if(l <= mid) update(l, r, k, x << 1);
	if(mid < r) update(l, r, k, x << 1 | 1);
	return;
}

inline int find(int x)
{
	return (x == fa[x] ? x : find(fa[x]));
}

void merge(int x, int y)
{
	if(siz[x] > siz[y]) swap(x, y);
	stk.push({x, fa[x], siz[x]});
	stk.push({y, fa[y], siz[y]});
	fa[x] = y, siz[y] += siz[x];
	return;
}

void rollback()
{
	noti st = stk.top(); stk.pop();
	fa[st.u] = st.f, siz[st.u] = st.s;
}

void dfs(int x, bool flag)
{
	int nsz = stk.size();
	for(auto p : tree[x].v)
	{
		int u = p.first, v = p.second;
		if(find(u) != find(v + n)) merge(find(u), find(v + n));
		if(find(u + n) != find(v)) merge(find(u + n), find(v));
		if(find(u) == find(u + n) || find(v) == find(v + n)) flag = 1;
	}
	if(tree[x].l == tree[x].r)
		return (void) (ans[tree[x].l] = flag);
	int mid = (tree[x].l + tree[x].r) >> 1;
	dfs(x << 1, flag);
	dfs(x << 1 | 1, flag);
	while(stk.size() != nsz) rollback();
	return;
}

int main()
{
	ios :: sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> n >> m >> k;
	build(0, k, 1);
	for(int i = 1; i <= m; i++)
	{
		cin >> _x[i] >> _y[i] >> l >> r;
		if(l == r) continue;
		update(l, r - 1, i, 1);
	}
	for(int i = 1; i <= 2 * n; i++) fa[i] = i, siz[i] = 1;
	dfs(1, 0);
	for(int i = 0; i < n; i++) 
		cout << (ans[i] ? "No" : "Yes") << '\n';
	return 0;
}

经典题目/trick

LG4198 楼房重建

小 A 的楼房外有一大片施工工地,工地上有 N 栋待建的楼房。每天,这片工地上的房子拆了又建、建了又拆。他经常无聊地看着窗外发呆,数自己能够看到多少栋房子。

为了简化问题,我们考虑这些事件发生在一个二维平面上。小 A 在平面上 (0,0) 点的位置,第 i 栋楼房可以用一条连接 (i,0)(i,Hi) 的线段表示,其中 Hi 为第 i 栋楼房的高度。如果这栋楼房上存在一个高度大于 0 的点与 (0,0) 的连线没有与之前的线段相交,那么这栋楼房就被认为是可见的。

施工队的建造总共进行了 M 天。初始时,所有楼房都还没有开始建造,它们的高度均为 0。在第 i 天,建筑队将会将横坐标为 Xi 的房屋的高度变为 Yi(高度可以比原来大—修建,也可以比原来小—拆除,甚至可以保持不变—建筑队这天什么事也没做)。请你帮小 A 数数每天在建筑队完工之后,他能看到多少栋楼房?

对于 100% 的数据,1XiN1Yi1091N,M105

设每个点与原点连线的斜率为 ai,题目实际上是让我们维护 1n 中从第一项开始,每一个大于前一项的必选,小于等于前一项的不选,所的得到的严格上升序列的长度。

用线段树对每个区间维护这个上升序列的长度,难点在于区间的合并(pushup)。

考虑将区间 [L,R] 中的答案记为 val([L,R]).

对于一个大区间 [L,R],其左区间 [L,M] 中所有对答案造成贡献的位置在现在的区间中仍然能产生贡献,所以可以直接把答案加上 val([L,M]);而右区间 [M+1,R] 中能造成贡献的位置个数就是从右区间中第一个 >max([L,M]) 的位置开始计算得到的答案。

可以设计一个类似线段树二分的递归函数 f(l,r,k),表示当前查询某个区间 (l,r) 中从第一个 >k 的值开始计算得到的答案。

  • l=r,则直接返回 [al>k]
  • 否则,若 max([l,mid])<=k,说明左边的所有点都没有用处了,则返回 f(mid,r,k)
  • max([l,mid])>k,左边的情况是未知的;对 [mid+1,r] 中的每个在 val([l,r]) 中产生贡献的点,它都一定能在答案中产生贡献(可以理解为右区间已经被 max([l,mid]) 挡住,所以不会再被更小的 k 影响),所以返回 val([l,r])val([l,mid])+f(l,mid,k).

(注意:此处讨论的区间 [l,r] 是递归函数中的参数,而上文中的区间 [L,R] 指需要 pushup 到的那一个区间,即原区间。)

第三种情况中有一个坑点:右区间的贡献不是 val([mid+1,r]) 而是 val([l,r])val([l,mid]). 前者是只考虑右区间的答案,而后者才是区间 [l,r] 中的贡献,可以参考下图。

这种在线段树 pushup 中利用类二分的递归做法来合并的 trick 比较经典,值得一记。

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

const int N = 1e5 + 5;

int x, y, n, m;
double h[N];

struct node
{
	int l, r;
	double maxi;
	int val;
} tree[N << 2];

int query(double k, int x)
{
	if(tree[x].l == tree[x].r) return (tree[x].maxi > k);
	
	if(tree[x << 1].maxi <= k) return query(k, x << 1 | 1);
	else return tree[x].val - tree[x << 1].val + query(k, x << 1);
}

void pushup(int x)
{
	tree[x].maxi = max(tree[x << 1].maxi, tree[x << 1 | 1].maxi);
	tree[x].val = tree[x << 1].val + query(tree[x << 1].maxi, x << 1 | 1);	
}

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

void update(int p, double k, int x)
{
	if(tree[x].l == tree[x].r && tree[x].l == p)
		return (void) (tree[x].maxi = h[p], tree[x].val = 1);
	int mid = (tree[x].l + tree[x].r) >> 1;
	if(p <= mid) update(p, k, x << 1);
	if(mid < p) update(p, k, x << 1 | 1);
	pushup(x);
	return;
}

int main()
{
	ios :: sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> n >> m;
	build(1, n, 1);
	for(int i = 1; i <= m; i++)
	{
		cin >> x >> y;
		h[x] = (double) y / x;
		update(x, h[x], 1);
		cout << tree[1].val << '\n';
	}
	return 0;
}

LG2824 [HEOI2016/TJOI2016] 排序

2016 年,佳媛姐姐喜欢上了数字序列。因而她经常研究关于序列的一些奇奇怪怪的问题,现在她在研究一个难题,需要你来帮助她。

这个难题是这样子的:给出一个 1n 的排列,现在对这个排列序列进行 m 次局部排序,排序分为两种:

  • 0 l r 表示将区间 [l,r] 的数字升序排序;
  • 1 l r 表示将区间 [l,r] 的数字降序排序。

注意,这里是对下标在区间 [l,r] 内的数排序。
最后询问第 q 位置上的数字。
对于 100% 的数据,n,m1051qn.

区间排序是不好维护的,所以先考虑一个简化版:对一个只有 0,1 的序列做区间排序。

容易发现只需要用线段树维护区间 1 的个数,每次升序排序的时候把 0 放前面 1 放后面做区间覆盖即可,降序的情况同理。

注意到将原序列中 ans 的元素设为 1,其他位置的元素设为 0,完成排序后 aq 的值一定为 1. 而这个性质显然是有单调性的,也就是说,在某个临界点之前 aq=0,某个临界点之后 aq=1,而这个临界点恰好就是答案。

二分答案即可。

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

const int N = 2e5 + 5;
int n, m, a[N], opt[N], l[N], r[N], q;

struct node
{
	int l, r, sum1, tag;
} tree[N << 2];

#define getlen(x) tree[x].r - tree[x].l + 1

void pushup(int x)
{
	tree[x].sum1 = tree[x << 1].sum1 + tree[x << 1 | 1].sum1;
}

void build(int l, int r, int x)
{
	tree[x] = {l, r, 0, -1};
	if(l == r) return;
	int mid = (l + r) >> 1;
	if(l <= mid) build(l, mid, x << 1);
	if(mid < r) build(mid + 1, r, x << 1 | 1);
	pushup(x);
	return;
}

void pushdown(int x)
{
	if(tree[x].tag == -1) return;
	if(tree[x].tag == 1)
	{
		tree[x << 1].sum1 = getlen(x << 1), tree[x << 1].tag = 1;
		tree[x << 1 | 1].sum1 = getlen(x << 1 | 1), tree[x << 1 | 1].tag = 1;
	}
	else
	{
		tree[x << 1].sum1 = 0, tree[x << 1].tag = 0;
		tree[x << 1 | 1].sum1 = 0, tree[x << 1 | 1].tag = 0;
	}
	tree[x].tag = -1;
}

void update(int l, int r, int k, int x)
{
	if(l <= tree[x].l && tree[x].r <= r)
	{
		// cout << '.';
		if(k == 1) tree[x].sum1 = getlen(x), tree[x].tag = 1;
		else tree[x].sum1 = 0, tree[x].tag = 0;
		return;
	}
	int mid = (tree[x].l + tree[x].r) >> 1;
	pushdown(x);
	if(l <= mid) update(l, r, k, x << 1);
	if(mid < r) update(l, r, k, x << 1 | 1);
	pushup(x);
}

int query(int l, int r, int x)
{
	if(l <= tree[x].l && tree[x].r <= r)
		return tree[x].sum1;
	int mid = (tree[x].l + tree[x].r) >> 1, res = 0;
	pushdown(x);
	if(l <= mid) res += query(l, r, x << 1);
	if(mid < r) res += query(l, r, x << 1 | 1);
	return res;
}

bool check(int x)
{
	// cout << x << ' '; cout << '\n';
	for(int i = 1; i <= n; i++)
		update(i, i, (a[i] >= x), 1);
	// cout << '\n';
	
	// for(int i = 1; i <= n; i++) cout << query(i, i, 1) << ' ';
	// cout << '\n';
	for(int i = 1; i <= m; i++)
	{
		int s1 = query(l[i], r[i], 1), len = r[i] - l[i] + 1, s0 = len - s1;
		if(opt[i] == 0)
		{
			update(l[i], l[i] + s0 - 1, 0, 1);
			update(l[i] + s0, r[i], 1, 1);
		}
		else
		{
			update(l[i], l[i] + s1 - 1, 1, 1);
			update(l[i] + s1, r[i], 0, 1);
		}
	}
	// for(int i = 1; i <= n; i++) cout << query(i, i, 1) << ' ';
	// cout << '\n';
	return query(q, q, 1);
}

int main()
{
	ios :: sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> n >> m;
	for(int i = 1; i <= n; i++) cin >> a[i];
	for(int i = 1; i <= m; i++)
		cin >> opt[i] >> l[i] >> r[i];
	cin >> q;
	build(1, n, 1);
	int L = 1, R = n, ans = 0;
	while(L <= R)
	{
		int mid = (L + R) >> 1;
		if(check(mid)) ans = mid, L = mid + 1;
		else R = mid - 1;
	}
	cout << ans;
	return 0;
}
posted @   心灵震荡  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示