经典永流传

子区间(子串)问题

RMQ(静态区间最值)

  • \(O(n\log n)\) ~ \(O(1)\)

st 表,猫树均可。一般来说 st 表就足够了。

  • \(O(n)\) ~ \(O(1)\)

重点。

  1. 四毛子树(RMQ 转 LCA 转 \(\pm1 RMQ\)),还有一大堆 \(RMQ\)\(LCA\) 的算法我就不一一列举了。

缺点:难写。

  1. 分块 st 表(请允许我这么称呼)

\(\log n\) 为块长分块,因为只有 \(\dfrac n {\log n}\) 个块了,现在预处理块间 st 表是 \(O(\dfrac n {\log n} \log n)=O(n)\) 的了,这是算法核心思路。

当然还需要预处理一下每个块的前缀和后缀 max,现在跨过块的询问就没问题了。

块内的询问还有点麻烦。先考虑离线怎么做。

可以维护一个单调栈,栈内元素单调递减,然后维护下标。

扫到 r 的时候,处理右端点在 r 上的询问。发现答案就是单调栈内,下标大于 \(l\) 的第一个(最靠左)元素。

现在又有两个问题,

1. 查询上面这个东西很容易带 log 
1. 离线很麻烦。可持久化更麻烦

有一个人类智慧的做法,就是把一个块内的单调栈直接装压起来。状压就是普通的装压,比如出现了 x 这个数,那么状压出来的二进制的 x 位就是 1。

由于每个块长都是 \(\log n\) ,所以甚至用 int 就能状压一个块。

对于每个 r 维护一个状压的单调栈,然后查询就是 l + __builtin_ctz(st[r]>>l) 。这个状压非常的有意思呀,感觉可拓展性非常强。

最大子段和

是可合并信息。全局可以直接 dp。静态用猫树,动态用线段树。

9.8 CQYC 的联考 T3 是求最小步数将一个区间修改为最大子段和,详见联考总结。

子序列问题

最长上升子序列

  • 普通 LIS

有一个二分的神秘做法,不如 BIT 优化 dp。

  • LIS 计数

把 dp 的转移的路径当成边,然后网络流建模。

  • 区间 LIS

P2075 区间 LIS - 洛谷

内容:对于任意有限偏序集,其最大反链中元素的数目必等于最小链划分中链的数目。此定理的对偶形式亦真。

可以认为,满足 \(a_i>a_j,i<j\)\(i,j\) 构成偏序关系,于是最长上升子序列就是最长反链,最长反链等于最小链划分,这等价于最小能划分出多少个下降子序列。所以 LISDilworth 很有关系。例题.

  • 树上 LIS(写得很简略,仅供参考)

可以在一个路径的 LCA 处统计答案。

然后长剖。

首先需要记录一个向上的 LIS 片段和向下的 LIS 片段,具体来说是记录 dp,\(dp_{u,i,0/1}\) 还是表示以 u 节点向上/向下长度为 i 的 LIS 末尾最小/最大的数是多少。

考虑如何合并。

枚举 LIS 在短链中的答案,然后二分或双指针查询拼到长链中的 LIS 长度。用完就插入到长链中。

考虑如何继承/拓展。

就是普通 LIS 拼接该怎么搞怎么搞。

好题:

  1. AT_jag2018summer_day2_k Short LIS - 洛谷

特殊的信息

区间众数

在线做法:

先离散化,发现区间众数不好合并,然后直接莫队的话,删除需要带 log。考虑分块,预处理出每两个块之间的答案,处理出每种颜色在以每个块右端点为界的前缀的出现次数,这个可以做到 \(O(n\sqrt n)\) 预处理。

对于查询,答案要么是整块的答案,要么是散块中出现的数。可以通过遍历散块来求出这些数出现次数(差分出这些数在整块中出现次数)来比大小。

  • 弱化1:序列 a 单调

可以做到 log 预处理,O(1) 查询。不知道有没有 O(1) 预处理的做法,反正我不会

找出每个值相同的连续区间,然后给每个位置标记一下这个数所处区间的左右端点和区间长度,称区间长度为该点权值。维护一个区间权值 max 的 st 表。

查询直接查询左右端点所在块和中间剩下的块的答案。

  • 弱化2:查询区间出现次数大于区间长度一半的数。(过半众数)

用主席树可做到一个 log。

在值域上维护可持久化线段树(主席树),对序列的每个位置开历史版本。通过差分,判断值域上值域在 [l,mid] 的数的出现次数之和是否大于区间长度的一半,否者看 [mid+1,r] 。可以直接这么做是由于保证了是过半众数。

思考:为什么这个做法只能做过半众数?因为主席树维护的信息需要通过在 r 和 l-1 两个版本上做差分得来的,取 max 的操作不被支持。只能维护区间和。

过半众数如果带修,还可以用摩尔根投票法的合并来做,对于判断找到的数是否大于区间长度的一般,可以对每种颜色开个平衡树来维护一下这个数在 [l,r] 上出现次数。例题:P3765 总统选举 - 洛谷

摩尔根投票:因为过半众数过半,所以可以让两个不同的数两两抵消,抵消的顺序任意,这样一定会只剩下一种数。如果有一个数出现次数大于一半,那么这样操作以后这个数一定还有剩,又因为上述操作之会剩下一个数,于是剩下的那个数就是过半众数。一个性质是,不管是哪种顺序抵消数,如果某个数在最后没有剩下,那么出现次数一定不过半。看懂了刚才的描述后可以发现证明显然。实现的话只用一个计数器即可。

摩尔根投票的合并:如果两个子区间都把不同的数两两抵消并且剩下一个数。如果一种数不是这两个子区间剩下的数,那么它的出现次数一定小于一半。所以只用考虑两个区间剩下那两个数即可。发现还要记录一下那个数出现次数,然后就能继续维护抵消的过程了。

因为可以合并,所以随便拿什么数据结构维护都可以,静态问题摩尔根投票用猫树直接爆杀上文主席树做法。有一个静态区间绝对众数 \(O(n)-O(1)\) 的科技,但我不会。

过半众数似乎可以用随机化乱搞。。。

不同颜色数

属于不可差分信息(?),不容易合并(合并大概率只能使用 bitset \(\dfrac n\omega\)

  1. 可以离线 : 扫描线扫颜色。 如果上树则需要莫队。
  2. 在线:序列上可以预处理块与块之间的(类似于区间众数)注意这种分块预处理复杂度是 \(\dfrac {n^2}{\omega}\),或许可以树套树。
  3. 可以考虑根号分治,对每个询问查大颜色,小颜色支持对每个点对计算贡献。

树上问题

Dfs 序 LCA

普通倍增求 LCA 的时代早已过去!时空大常数并且代码大坨的四毛子树和 Tarjian 的光辉渐渐褪去。新的时代,就要有新的 LCA 求法!dfn 序求 LCA,同时兼备码量巨小,常数(特别是空间常数)巨小,查询 O(1) ,好理解,等众多好处,将 DFS 序求 LCA 发扬光大,让欧拉序求 LCA 成为时代的眼泪!

大量参考:https://www.luogu.com.cn/article/pu52m9ue

假设现在在求 \(u\),\(v\) 的 LCA \(d\),可以发现其在 dfs 序下有许多性质。

不妨让 \(dfn_u < dfn_v\),显然 u 不会是 v 的儿子。可以分类讨论:

  1. u 不是 v 的祖先。那么 dfs 时就是 \(d -> u -> d -> v\) 的顺序,这里虽然经过了 d ,但是显然不会给 d 编号。考虑 d -> v 路径上,只有点 d 不在遍历的过程中标号,假设 d -> v 路径上第二个点为 v',可以发现 v' 是 dfs 序在 \([dfn_u,dfn_v]\) 之间深度最小的点。所以只需要找到 dfs 序在 \([dfn_u,dfn_v]\) 之间深度最小的点 v',它的父亲就是 d。

  2. u 是 v 的祖先。显然可以判断子树的包含关系,但是有一种更加简洁的方法,那就是找到 dfs 序在 \([dfn_{u + 1},dfn_v]\) 之间深度最小的点 v',它的父亲就是 d。这对于情况 1 其实也是适用的。

使用 st 表维护区间深度最小的点,复杂度 \(O(n\log n)~O(1)\)。当然使用分块 st 表是 \(O(n)\) 预处理。

还要记得特判 u == v 的边界情况。

代码短小精悍:

void dfs(int u, int f) {
    stmin[dfn[u] = ++ DFN][0] = f;
    for(int v : e[u]) if(v ^ f) dfs(v, u);
}
inline int dfnmin(int x, int y) {return dfn[x] < dfn[y] ? x : y;}
void preworkstmin() {For(i, 1, n) for(int j = 1; i - (1 << j) + 1 >= 1; ++j) stmin[i][j] = dfnmin(stmin[i][j - 1], stmin[i - (1 << (j - 1))][j - 1]);} 
inline int LCA(int x, int y) {
    if(x == y) return x;
    if((x = dfn[x]) > (y = dfn[y])) swap(x, y);
    int s = __lg(y - x ++); // y - (x + 1) + 1
    return dfnmin(stmin[x + (1 << s) - 1][s], stmin[y][s]);
}	
int main() {
	n = read(), m = read(), rt = read();
	For(i, 1, n - 1) {
		int u = read(), v = read();
		e[u].pb(v), e[v].pb(u);
	}
	dfs(rt, 0), preworkstmin();
    For(i, 1, m) wt(LCA(read(), read()));
	return 0;
}

树上k级祖先

  • 直接倍增/树剖

显然是 \(O(n\log n)\)

  • 长链剖分

时间复杂度为 O(nlogn) 预处理, O(1) 查询。

长链剖分的经典应用。

注意到,一个点向上跳 n 步以后,所处长链的长度一定大于 n (显然),于是可以先跳一个 \(2^{upbit(k)}\) , 然后在长链上条剩下的。

预处理

  1. 普通的长链剖分

  2. 每个点 \(2^n\) 级祖先(倍增套路求法)

  3. 对于长度为len的链,处理出顶点向上len个祖先和向下走len个位置, \(O(n)\)

  4. 处理出每个数二进制下最高位 \(h_i\) (似乎用高科技,不处理也可以O(1)查?)

查询

  1. 利用倍增数组先将 x 跳到 x 的 \(2^{h_k}\), 剩下的步数 k' 小于 \(2^{h_k}\)
  2. 由于长链长度 > k',因此可以先将 x 跳到 x 所在链的顶点,若之后剩下的级数为正,则利用向上的数组求出答案,否则利用向下的数组求出答案。

树上背包

for (int i = min(sz[u], m); i >= 1; i--) {
	for (int j = min(sz[v], m - i); j >= 1; j--) {
		dp[u][i + j] = max(dp[u][i + j], dp[u][i] + dp[v][j]);
	}
}

树上背包时间复杂度证明 - 洛谷专栏

复杂度 \(\sum_{u\rightarrow v} \min(m,pre_v)\times \min(m,siz_v) = O(nm)\).非常的神奇。

posted @ 2025-09-15 20:20  花子の水晶植轮daisuki  阅读(15)  评论(0)    收藏  举报
https://blog-static.cnblogs.com/files/zouwangblog/mouse-click.js