DS(2):从线段树开始的一些杂谈

从零开始

线段树可以维护的信息要求它满足双半群结构。如果满足交换律那么就可以使用标记永久化,标记永久化在可持久化线段树的区间标记中可以发挥效用,因为这样避免标记下放而产生的新增节点问题。(在普通的动态开点线段树这样是没问题的,因为一次最多新增 \(O(\log n)\) 个节点。但是放到可持久化线段树和线段树合并中就会爆炸)而从结合律的角度而言,可以从中引发出“子区间问题”的用猫树分治的形式,本质上是按照一个划分点分类,维护前缀后缀合并信息。

线段树的结构很特殊,它的常用变种有动态开点,可持久化,线段树合并。这三种往往是其它数据结构也需要支持的事情。同时线段树作为一个结构简洁,但是又略复杂于分块的数据结构,可以支持一些听起来不太可行的操作,如 seg-beat,楼房重建中的线段树(log pushup)。

线段树作为维护一个一维(当然,主席树可以维护特殊的二维平面)信息的结构,也常常搭配扫描线实现二维信息的查询。所以在这里介绍扫描线(虽然它是相当常用的思想方法)以及支配对。


双半群结构

所谓双半群结构,因为我并不懂学术上严谨的定义,通俗的来说就是。信息分成两类,一类是查询信息 \(A\),一类是修改信息 \(B\)。如果 \(A\) 自身可以与自身结合,信息仍然是 \(A\) 形式,修改信息也可以和自身结合,信息仍是 \(B\) 形式。\(B\) 可以作用于 \(A\),信息仍是 \(A\) 形式。举个例子,如以 \((k,b)\) 作为标记信息,代表 \(x\gets kx+b\)。如果要对 \(x\) 求和,因为和信息合并起来还是和,而 \((k_1, b_1),(k_2, b_2)\) 这两个信息也可以合并成 \((k_1k_2, b_1+k_1b_2)\)(请注意合并顺序,合并顺序对结果有影响,所以合并信息不一定满足交换律,但是一定满足结合律)。将 \((k, b)\) 标记作用与 \(x\) 上,\((kx+b)\) 仍然是实数的形式。这就是双半群结构。满足双半群结构的信息都可以用线段树维护。

双半群信息告诉我们,线段树要维护的查询和修改信息都必须满足结合律。如果修改信息同时满足交换律,那么我们可以使用标记永久化。


标记永久化

注意到线段树的过程实际上可以递归到查询区间 \([ql, qr]\) 的每一个包含它的线段树节点上。如果标记信息具有交换律,那么我们就可以直接计算这个信息对 \([ql, qr]\) 施加的影响。

如区间求和:

ll qry(int ql, int qr, int s, int t, int now, ll tg) {
	if(ql <= s && t <= qr) return sum[now] + tg;
	int mid = (s + t) >> 1;
	ll rest = 0;
	if(ql <= mid) rest += qry(ql, qr, s, mid, ls(now), tg);
	if(qr > mid) rest += qry(ql, qr, mid + 1, t, rs(now), tg);
	return rest + tag[now] * 1ll * max(min(qr, t) - max(ql, s) + 1, 0);;
}

往往是在在可持久化和线段树合并中才会发挥作用,可持久化上可见 To the Moon


二区间合并(猫爬架子)

这里不会详细的展开猫树分治内容。因为我也完全只是一个初学者,没有深入研究过。

为什么分治可以优化呢?因为分治维护了一个分界点 \(mid\),然后我们只需要用区间长度 \([l, r]\) 相关的代价处理出关于 \(mid\) 的前缀和后缀信息,通过合并这两个信息实现对子区间信息的求解。因此分治常常可能会运用到对于“全部子区间”问题的求解中。线段树本身也就是按照这个分治结构建立的。所以线段树可以很好的维护这件事情。

我个人认为把这个思路叫做“二区间合并”更加形象。毕竟这个最多也就算一个较为普及的 trick。为啥要叫猫树啊

利用这种思路,常常可以简化很多问题。固定端点后扫描可能可以大幅减小可能的答案数量,或者出于询问的需要,前缀后缀的合并不需要合并全部信息,让我们分别来看几个题。

CF1936D

这是一个涉及到“所有子区间”的问题,所以不妨考虑线段树的结构,分治解决。跨过中心点维护,又因为经典的,前缀或可能的取值最多只有 \(O(\log V)\) 个,解决了这一点这个问题几乎可以说是直接解决了。具体细节不是重点。

P6240

考虑线段树,直接合并两个背包的代价很大,但是如果只是在最后合并,只需要知道特定值的位置即可,那么合并背包就从 \(O(m^2)\) 变成了 \(O(m)\)

P6109

只有一个询问,大家都会做:修改拆成关于 \(x\) 轴差分序列的两个影响,查询从 \(xl\)\(xr\) 扫描一下添加影响,维护历史最值。多个询问,考虑猫树分治,根据 \(x\) 轴划分为 \([l, mid]\)\([mid + 1, r]\) 两个部分。会一个询问了,剩下的就是一些细节。我的写法是进入 solve(l, r) 时保证加入的时 [1, l - 1]。

#include <bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
const int N = 5e4, M = 5e5;
const ll inf = 1e15;
int read() {
	int k = 0; char ch = getchar();
	while(ch < '0' || ch > '9') 
		ch = getchar();
	while(ch >= '0' && ch <= '9') 
		k = k * 10 + ch - '0', 
		ch = getchar();
	return k;
}
void write(ll x) {
	if(!x) {
		putchar('0');
		return ;
	}
	int tmp[30], len = 0;
	while(x) tmp[++len] = x % 10, x /= 10;
	for(int i = len; i >= 1; i--) putchar(char(tmp[i] + '0'));
}

int n, m, q;

struct dot {
	int yl, yr, val;
}; 
vector <dot> dvec[N + 10];
struct qyy {
	int xl, xr, yl, yr, ind;
};
namespace segt {
	il int ls(int x) {
		return x * 2;
	}
	il int rs(int x) {
		return x * 2 + 1;
	}
	struct info {
		ll cmax, hmax;
	};
	struct tg {
		ll c[2][2];
		void reset() {
			c[0][0] = c[1][1] = 0, c[1][0] = c[0][1] = -inf;
		}
		tg operator * (const tg &other) const {
			tg rest; rest.c[0][0] = rest.c[0][1] = rest.c[1][0] = rest.c[1][1] = -inf;
			rest.c[0][0] = max(c[0][0] + other.c[0][0], c[0][1] + other.c[1][0]);
			rest.c[0][1] = max(c[0][0] + other.c[0][1], c[0][1] + other.c[1][1]);
			rest.c[1][0] = max(c[1][0] + other.c[0][0], c[1][1] + other.c[1][0]);
			rest.c[1][1] = max(c[1][0] + other.c[0][1], c[1][1] + other.c[1][1]);
			return rest;
		}
	};
	info operator * (const tg &A, const info &B) {
		info tmp;
		tmp.cmax = max(A.c[0][0] + B.cmax, A.c[0][1] + B.hmax);
		tmp.hmax = max(A.c[1][0] + B.cmax, A.c[1][1] + B.hmax);
		return tmp;
	}
	info operator + (const info &A, const info &B) {
		return (info){max(A.cmax, B.cmax), max(A.hmax, B.hmax)};
	}
	info T[N * 4 + 10]; tg tag[N * 4 + 10];
	void push_up(int now) {
		T[now] = T[ls(now)] + T[rs(now)];
	}
	void push_tag(int now, const tg &t) {
		T[now] = t * T[now];
		tag[now] = t * tag[now];
	} 
	void push_down(int now) {
		if(tag[now].c[0][0] != 0 || tag[now].c[1][1] != 0 ||
			tag[now].c[0][1] > -inf || tag[now].c[1][0] > -inf) {
			push_tag(ls(now), tag[now]);
			push_tag(rs(now), tag[now]);
			tag[now].reset();
		}
	}

	void upd(int ql, int qr, int s, int t, int now, tg tgt) {
		if(ql <= s && t <= qr) {
			push_tag(now, tgt);
			return ;
		}
		int mid = (s + t) >> 1;
		push_down(now);
		if(ql <= mid) upd(ql, qr, s, mid, ls(now), tgt);
		if(qr > mid) upd(ql, qr, mid + 1, t, rs(now), tgt);
		push_up(now);
	}
	info qry(int ql, int qr, int s, int t, int now) {
		if(ql <= s && t <= qr) return T[now];
		int mid = (s + t) >> 1;
		push_down(now);

		if(ql > mid) return qry(ql, qr, mid + 1, t, rs(now));
		if(qr <= mid) return qry(ql, qr, s, mid, ls(now));
		return (qry(ql, qr, s, mid, ls(now)) + qry(ql, qr, mid + 1, t, rs(now)));
	}
	void build(int l, int r, int now) {
		tag[now].reset();
		T[now].cmax = T[now].hmax = 0;
		if(l == r) return ;
		int mid = (l + r) >> 1;
		build(l, mid, ls(now));
		build(mid + 1, r, rs(now));
	}
}
segt::tg up;
vector <qyy> qyl[N + 10], qyr[N + 10];
ll ans[M + 10];
void solve(int l, int r, vector <qyy> &vect) {
	if(l >= r) {
		segt::tg tmp;
		for(int i = 0; i < dvec[l].size(); i++) {
			tmp.c[0][0] = dvec[l][i].val, tmp.c[1][0] = tmp.c[0][1] = -inf, tmp.c[1][1] = 0;
			segt::upd(dvec[l][i].yl, dvec[l][i].yr, 1, n, 1, tmp);
		}
		for(int i = 0; i < vect.size(); i++)
			ans[vect[i].ind] = max(ans[vect[i].ind], segt::qry(vect[i].yl, vect[i].yr, 1, n, 1).cmax);
		return ;
	}
	vector <qyy> vecta, vectb;
	int mid = (l + r) >> 1;
	for(int i = 0; i < vect.size(); i++)
		if(vect[i].xr <= mid) vecta.push_back(vect[i]);
		else if(vect[i].xl > mid) vectb.push_back(vect[i]);
	solve(l, mid, vecta);

	for(int i = 0; i < vect.size(); i++) 
		if(vect[i].xl <= mid && vect[i].xr > mid)
			qyl[vect[i].xl].push_back(vect[i]),
			qyr[vect[i].xr].push_back(vect[i]);

	segt::tg tmp;
	tmp.c[0][0] = 0, tmp.c[0][1] = -inf, tmp.c[1][0] = 0, tmp.c[1][1] = -inf;
	segt::upd(1, n, 1, n, 1, tmp);
	for(int i = mid; i >= l; i--) {
		for(int j = 0; j < qyl[i].size(); j++)
			ans[qyl[i][j].ind] = max(ans[qyl[i][j].ind], segt::qry(qyl[i][j].yl, qyl[i][j].yr, 1, n, 1).hmax);
		for(int j = 0; j < dvec[i].size(); j++) {
			tmp.c[0][0] = -dvec[i][j].val, tmp.c[1][0] = tmp.c[0][1] = -inf, tmp.c[1][1] = 0;
			segt::upd(dvec[i][j].yl, dvec[i][j].yr, 1, n, 1, tmp);
		}
		segt::upd(1, n, 1, n, 1, up);
	}
	for(int i = l; i <= mid; i++) {
		for(int j = 0; j < dvec[i].size(); j++) {
			tmp.c[0][0] = dvec[i][j].val, tmp.c[1][0] = tmp.c[0][1] = -inf, tmp.c[1][1] = 0;
			segt::upd(dvec[i][j].yl, dvec[i][j].yr, 1, n, 1, tmp);
		}
	}
	tmp.c[0][0] = 0, tmp.c[0][1] = -inf, tmp.c[1][0] = -inf, tmp.c[1][1] = -inf;
	segt::upd(1, n, 1, n, 1, tmp);
	for(int i = mid + 1; i <= r; i++) {
		for(int j = 0; j < dvec[i].size(); j++) {
			tmp.c[0][0] = dvec[i][j].val, tmp.c[1][0] = tmp.c[0][1] = -inf, tmp.c[1][1] = 0;
			segt::upd(dvec[i][j].yl, dvec[i][j].yr, 1, n, 1, tmp);
		}	
		segt::upd(1, n, 1, n, 1, up);
		for(int j = 0; j < qyr[i].size(); j++)
			ans[qyr[i][j].ind] = max(ans[qyr[i][j].ind], segt::qry(qyr[i][j].yl, qyr[i][j].yr, 1, n, 1).hmax);
	}
	for(int i = r; i >= mid + 1; i--) {
		for(int j = 0; j < dvec[i].size(); j++) {
			tmp.c[0][0] = -dvec[i][j].val, tmp.c[1][0] = tmp.c[0][1] = -inf, tmp.c[1][1] = 0;
			segt::upd(dvec[i][j].yl, dvec[i][j].yr, 1, n, 1, tmp);
		}	
	}

	for(int i = l; i <= mid; i++) qyl[i].clear();
	for(int i = mid + 1; i <= r; i++) qyr[i].clear();
	
	solve(mid + 1, r, vectb); 
}
int main() {
	n = read(), m = read(), q = read();
	for(int i = 1, a, b, c, d, e; i <= m; i++) {
		a = read(), c = read(), b = read(), d = read(), e = read();
		dvec[a].push_back((dot){c, d, e});
		dvec[b + 1].push_back((dot){c, d, -e});
	}

	vector <qyy> vect;
	for(int i = 1, a, b, c, d; i <= q; i++) {
		a = read(), c = read(), b = read(), d = read();
		vect.push_back((qyy){a, b, c, d, i});
		ans[i] = -inf;
	}
	segt::build(1, n, 1);

	up.c[0][0] = up.c[1][0] = up.c[1][1] = 0, up.c[0][1] = -inf;
	solve(1, n, vect);

	for(int i = 1; i <= q; i++)
		write(ans[i]), putchar('\n');
}

可持久化

动态开点没什么好说的。也许需要注意可以当作离散化?但是可持久化用处很大。线段树相当于维护一个线,而可持久化线段树相当于维护一个平面,用 \(\log\) 的代价换来了一个新的维度。你可以认为是如果相邻两个状态之间相差很小,那么就可以用可持久化线段树实现记录矩形信息的任务。

区间 kth 问题就是一个极好的例子。


线段树合并

线段树合并的关键在于考虑“一个空节点和一个有值节点取交集的时候的变化”和“两个叶子节点如何合并”。

P5298

暴力式子为

\[f[u, k] = p_u(f[l, k]\sum\limits_{c = 1}^kf[r, c] + f[r, k]\sum\limits_{c = 1}^kf[l, c])\\ +(1 - p_i)(f[l, k]\sum\limits_{c = k}^mf[r, c]+ f[r, k]\sum\limits_{c = k}^mf[l, c]) \]

为什么 UT 老师断言这个可以线段树合并呢?我不明白。

现在我们尝试明白:

  • 观察一下这个式子,实际上两部分只有一部分有值,另外一部分是 \(0\)
  • 我们现在对于线段树不知道维护任何东西,只知道叶子节点应当是 \(f[u, x]\)
  • 考虑合并的过程,merge (x, y, l, r) 这个事情
    • 线段树合并通常要考虑叶子结点的合并,和只有一个点有值的合并。
    • 叶子节点 \(l = r\) 处,此时 \(x, y\) 只有一个点有值,所以都是“只有一个点有值”的合并。
    • \([l, r]\) 内只有 \(x\) 有值,这时候这些位置上相当于 \(f[l\sim r]\) 要乘上 \(p_u\sum\limits_{c = 1}^{l - 1}f[r, c] + (1 - p_u)\sum\limits_{c = r + 1}^mf[r, c]\)。要注意的事情是 \([l, r]\) 内只有 \(x\) 有值,\(y\) 的值都是 \(0\)
    • 发现这个事情是一个区间乘法,可以直接刻画。

扫描线

这里仅仅讨论序列相关的内容。树上的统计会写的怎么样才能成为扫描线大师呢?很多时候会被简单的扫描线击杀,扫描线它背后一定有它的苦衷。

扫描线的常用通用技巧有:

  1. 用合适的手法刻画“可以被统计的合法点对”,往往是利用某些结构投射为点对问题,然后仅仅从点对问题上出发,画到二维平面上方便考虑。
  2. 不断变换枚举的对象。
  3. 支配对。用来减少点对数量。

常见的模型有:

  1. 对于一个区间的所有子区间求解。对于询问 \([l, r]\) 离线,扫描 \(r\),对于 \(l\) 位置上面记录 \([l, r]\) 的答案,然后记录历史和。
  • 其它的思路有:直接分治,考虑跨国中间部分。如最大和子序列。
  • 也有可能进化称猫树分治,如 P6240
  1. 涉及到 \(\max, \min\) 的问题,用一个栈,维护 \((i, l, r)\) 代表区间 \([l, r]\) 的极值为 \(a_i\),然后可以增量的维护这个栈。
  • 其它的思路有建立笛卡尔树。
  • 一般来说,如果只涉及一个序列的就建立笛卡尔树,多个序列或者多个 \(\min, \max\) 就考虑扫描线。

loj3033

刻画合法点对条件:

  • \(|i - j| \le [A_i, B_i] \cap [A_j, B_j]\)

要求解的是:\(|H_i - H_j|\) 的最大值。

首先尝试刻画这个合法条件。难点主要在于如何求解这个交集,因为此时 \(i\) 可以到达的 \(j\) 是散的,不能表示为常数个区间的形式。因为似乎没有好的形式,所以我们直接从化简式子,分离常数未知量入手。

钦定 \(i> j\),枚举 \(i\) 考虑 \(j\)。于是应有:

  • \(i \in [A_j + j, B_j + j]\)
  • \(j \in[i - B_i, i - A_i]\)

于是对于 \(j\) 记录 \((j, l_j, r_j)\) 其中 \(l_j = A_j + j, r_j = B_j + j\)。对于 \(i\) 则记录 \((i, L_i, R_i)\),其中 \(L_i = i - B_i, R_i = i - A_i\)。放到二维平面上,以 \(i\) 为横轴 \(j\) 为纵轴,画出线段有交则产生贡献的对,如图:

于是扫描 \(i\) 的同时记录当前可能产生贡献的线段 \((l_j, r_j)\),用一个数据结构记录当前仍然会产生贡献的纵坐标以及值,用线段树即可。

zr3379

好题啊!

首先考虑充要条件,一个很直接的想法是:令 \(|A|\le |B|\),充要条件为 \(|B| - \operatorname{lcp}(A, B) - \operatorname{lcs}(A, B) \le k\)。测一下样例就发现不对,\(k = 1, A = \texttt{XY}, B = \texttt{XYXY}\)。因为我不能删完。

上面的做法在 \(\operatorname{lcp}(A, B) + \operatorname{lcs}(A, B) \le |A|\) 时显然必然是正确的。因此我们只要考虑超过了 \(A\) 的情况。此时我们最多可以保留一个 \(|A|\),于是应当有 \(|B| - |A| \le k\)。合并两点显然也成立。

  • 对于 \(A, B(|A|\le |B|)\),合法的充要条件为:
  • \(|B| - |A| \le k\)
  • \(\operatorname{lcp}(A, B) + \operatorname{lcs}(A, B)\ge |B| - k\)

如果没有第一条约束,对于第二条约束怎么做?

多串 lcp 的直观想法是挂到 trie 上,或者 hash+binarysearch。枚举 \(|B|\),按照字符串大小从小到大排序扫描加入 \(S\) 中,枚举 \(\operatorname{lcs}(A, B) = w\)。记录之前串的所有后缀哈希值,排个序那么可能合法的串就在一个区间内了(特别的只能保留倒数第 \((w+1)\) 个字符不同的情况,而相同的情况要单独计算,不过也类似)。这样的话,\(A\) 就是前缀 trie 树上 \(B\) 的某一层级祖先的子树中的节点,写到 dfn 序上,问题转化为矩形内数点数量。离线跑一下做完了。

CF526F

容易等价为:计算有多少区间 \([l, r]\) 满足 \(\max\limits_{i \in [l, r]}x_i - \min\limits_{i \in[l, r]}x_i = r - l + 1\)

两个想法:笛卡尔树或者扫描线。笛卡尔树实际上不太可行,因为这实际上涉及到两颗笛卡尔树,所以考虑扫描线。(这似乎也是很重要的想法,结合下面一道题,太多笛卡尔树我们就直接考虑扫描线吧)

对于 \(r\) 如何统计 \(l\) 呢?扫描线的时候维护 \(a_i\) 作为最大值所管理的区间,这个可以用一个单调栈 \((i, l, r)\) 代表 \(i\)\([l, r]\) 内最大的数,放到一个栈里面。每次加入一个点 \(i\) 就往前推平这些区间,就可以维护 \(\max\limits_{i \in [l, r]}x_i - \min\limits_{i \in [l, r]}x_i\)

这是一个常用的维护 \(\max, \min\) 的技巧,扫描线的时候用一个栈维护 \((i, l, r)\) 代表 \(a_i\)\([l, r]\) 内极值,如果要修改的话直接往前推平即可。

arc067_d

同理。

P8868

经典永流传了属于是。糅合了两个常用的技巧。

采用扫描线中扫描 \(r\) 维护 \(l\) 位置答案的手法。计算 \(r\) 对这些区间的贡献。\([l, r]\) 的贡献就是 \((\max\limits_{i \in [l, r]}a_i)(\max\limits_{i \in [l, r]}b_i)\)。可以用上述的技巧线段树把这个问题变成两个区间推平。这是一个大概的思路。

如何用线段树维护这种看上去十分困难的东西呢?一点一点来,不要着急。

对于位置 \(l\) 我们需要维护:

  1. \(c_l = \max\limits_{l \le i \le r}a_i, d_l = \max\limits_{l \le i \le r} b_i\)
  2. \(s_l = c_ld_l\)
  3. \(s_l\) 的历史和 \(h_l\)

\(c_l,d_l\) 的维护可以看作是区间推平。如果每次只选择一个推平,那么 \(s_l\) 中仍然有一个是常数,可以看作是一次的。历史和不用多说。于是可以想到用高贵的矩阵来刻画转移。最后是向量之和,所以我们记录向量 \((\sum c, \sum d, \sum s, \sum h, len)\)。转移矩阵写一下即可。

posted @ 2025-10-28 16:49  CatFromMars  阅读(4)  评论(0)    收藏  举报