[做题笔记 #2] 数据结构

[做题笔记 #2] 数据结构

DS 可爱!

2024.11.1 始。

注意事项

  1. 线段树算 mid 的时候如果 l + r < 0,不知道用 >> 1 会不会出问题,用 / 2 也许可以。如果 l + r 可能很大也要注意(不要炸 int 之类的)。
  2. 莫队 块长定值就对了是为啥?(yt 的代码)
  3. 注意分块、值域分块 空间。注意块长不固定的时候会不会数组越界。
  4. 开 long long 要开全。不要某些地方忘了。
  5. 注意快读快写是不是只处理 int。是的话不要用来处理 ll。
  6. 不要把[迭代变量](?) i 之类的和参数(初始的)p 之类的搞混了。(比如在树状数组里)
  7. 不要忘了预处理。(比如预处理 lg)
  8. 注意线段树维护的范围不能变,传参要传对。
  9. 注意线段树维护的范围可能不是 \([1, n]\),不要顺手无脑写 \(1, n\)
  10. 小心树状数组数组越界或到 \(0\)。可以集体 + 一个常数 C(把每次的位置 p 加 C),但要注意树状数组里的循环里范围 n 也要 + C,而且数组大小也要 + C。
  11. 注意线段树空间开多少。
  12. 线段树叶子结点不要 PushDown,也不要 PushUp。(不要用它的子结点)
  13. 线段树非叶子结点一定有两个实际存在的子结点,但平衡树就不一定了。要注意 PushUp 时的问题(也许也要注意 PushDown,不清楚)。
  14. 动态开点线段树,注意空间要根据建新点的数量开。
  15. 线段树合并要动态开点。

可能注意事项还没整理全。

P4137 Rmq Problem / mex

主席树,也可离线跑扫描线。

总结:对于与“一个数是否出现”相关的问题,可以考虑在每个位置维护每个数最近一次出现的位置,通过它来把出现的问题刻画成大小关系的问题。

扩展:对于与“一个数出现多少次”相关的问题,似乎也可以用类似的做法,即在每个位置维护每个数离当前第 \(k\) 次出现的位置。

再总结:“总结”和“扩展”中的做法本质都是对于每个数值去跑双指针,但可以用主席树或者离线下来用线段树跑扫描线 来同时维护所有数值的信息。

也可以用莫队 + 值域分块(应该是我之前的写法)。似乎也可以用回滚莫队(我还不会)。

2024.11.2

P4898 [IOI2018] seats 排座位

写了很长。有巨大常数,TLE。不知道复杂度错没错。待改。

参考(一部分基本是抄)XK 的课件。

由于我对这道题的这种做法还没有理解透,这里的描述可能有误。

对于一个矩形的判定,此题中直接用 xmin,xmax,ymin,ymax 和 面积(这里指的是其中点的个数)来判断不是很方便,因为一次交换操作就会改很多东西(???)。而且正确性我也不太确定。

考虑到矩形的特殊性(规则性),我们换一种判定方式。把一个前缀里的点全部染黑,其他的(包含未出现的点)全部染白。形成矩形且只形成一个矩形,那么就不能出现

这四种情况。

然而这只是必要不充分条件。发现还可以有多个矩形共存的情况。

至于两个合到一起是不是充要条件,[为什么两个合到一起就是充要条件了](?),我不知道。

于是我们分两部分思考:

  1. 第一种限制:每个白点周围不能有 \(\geq 2\) 个黑点。
  2. 第二种限制:左边和上面都有白点的黑点最多只有 \(1\) 个。

于是我们对每个前缀 \(1\)\(i\)(包含 \(1\)\(i\)),记 \(t _ i\) 表示周围黑点个数 \(\geq 2\) 的白点的个数,\(s _ i\) 表示左边和上面都有白点的黑点的个数。

那么 \(t _ i\) 最小为 \(0\)\(s _ i\) 最小为 \(1\),并且 \(t _ i = 0\)\(s _ i = 1\) 时前缀 \(i\) 合法。于是我们维护 \(s _ i + t _ i\),只要是 \(1\) 即可。

这样转化之后每次交换要修改的东西就比较少了,可以用线段树维护。

具体地,我们需要一棵线段树,支持区间加、区间求 min、区间求 min 的出现次数。

我的写法是修改时大力把相关的点全部都重新算贡献。但是这样有巨大常数。待改。

总结

  1. 此题中矩形判定转化的技巧可以积累。
  2. 遇到常规思路不好解决的判定性问题时不妨思考其他的判定方式(甚至哈希),可从特殊性入手。
  3. 如果无脑重算贡献常数很大的话,就要思考要不要无脑重算贡献,要平衡好 清晰思路、清晰简洁代码 和 小常数。

2024.11.2

upd 2024.11.6 :

在之前 jsh 和今天 lr 的帮助下过了。

  1. 不用处理左上方相邻(、右下方相邻)的点。
  2. 用 vector < vector < int > >(resize 初始化)而非 map 套 pair。
  3. 把要处理的点丢到数组里再在处理时用 [map](?)来跳过重复的似乎比直接用 map 来存,处理时遍历 map [快得多](?)。

P10641 BZOJ3252 攻略

XK 的课件里(第一种)直接来的贪心我不是很懂啊。怎么用树剖维护也不懂。还是说树剖是指直接 边带权值 长剖?

个人认为合并的思路更好理解。本质是一种贪心的合并,因为子树外的选择对子树内的最优选择是没有影响的,所以只需要把每棵子树贪心的结果合并起来就行了。

用线段树合并或者[可并堆](?)。我写的是可并堆,发现可并堆特别简洁,甚至目测比 快读+快写 还短(行数)。

总结:树上问题可以考虑合并,树上合并不一定是直接求值或者树形 DP,也可以是贪心。共性:子树外对子树内的统计 / 决策不影响。

2024.11.2

P10833 [COTS 2023] 下 Niz

统计满足某种条件的子段个数,考虑分治。

出现问题转[偏序](?)关系问题。对每个位置记这个位置上的数之前最近出现的位置,记为 \(lst _ i\)。一个区间 \([l, r]\) 满足条件就是每个位置的 \(lst _ i\)\(< l\),并且这个区间的 \(\max\)\(r - l + 1\)

考虑到题目里“\(1\) ~ \(r - l + 1\)”的限制,我们按每次区间的 \(\max\)(即限制里的 \(r - l + 1\))来分治。找到 \(\max\) 出现的一个位置 \(d\)。每次统计当前区间里包含(说“跨过”似乎不太准确) \(\max\) 的位置的合法子段的个数。为了复杂度更优秀,我们找 \(d\) 划分开更小的那段(相等就随意)来作为一个端点,考虑另一个端点有多少种取法。本题中,由于我们确定了 \(\max\),区间长度(\(r - l + 1\))也就确定了,因此另一个端点也是确定的,只需要用 \(st\) 表维护一下 \(lst _ i\) 的区间 \(\max\) 即可。要注意这样分治的层数可能很多,时间复杂度靠的不是层数,而是靠这种[启发式合并的逆过程](?);因此不能在 \([l, r]\) 中暴力枚举找 \(\max\),而应该用 \(st\) 表维护。

总结

  1. 统计满足某种条件的子段个数,考虑分治。

  2. 出现问题转[偏序](?)关系问题。对每个位置记这个位置上的数之前最近出现的位置

再转化成与之相关的[偏序问题](?)。

  1. 不用传统的 \(mid\) 分治,而是根据题目对子段的要求(限制),取特殊点(比如最值)来分治。这样处理限制更方便。
  2. 分治和合并过程的思维转化。常用来分析分治的时间复杂度。弄清时间复杂度是怎么保证的,从而决定每个地方是用暴力还是用数据结构。

拓展

jsh 给我看的一道类似的题。

固定一个端点后,可行的另一个端点可能不止一个。此时此题要 用主席树 或者 离线下来用 线段树或树状数组 跑扫描线。

还没想太清楚。这个“拓展”部分的话应该是对的。

2024.11.2

CF301D Yaroslav and Divisors

关注排列的特殊性,发现显然只有约 \(n \ln n\) 对符合条件的数对。于是直接二维数点即可。

注意[一个点自己和自己可以。](?)

代码实现上记每个数的出现位置。二维数点用 vector(桶)来做较方便。

总结

  1. 排列的特殊性:不重复、范围小、连续(这里用到前两个,目前想到这三个)。
  2. 调和级数暴力。
  3. 考虑题目中的 某个 / 某些 东西(满足条件的 / 不满足条件的)是不是较少,从较少的东西入手。

2024.11.5

CF1946F Nobody is needed

关注排列的特殊性,发现抓出一个合法子序列的一头一尾就只有大约 \(n \ln n\) 对。于是考虑计算每一对的答案,然后跑二维数点。

子序列 DP 是可以区间 DP 的!设 \(f _ {i, j}\) 表示以位置 \(i\) 开头且以位置 \(j\) 结尾的合法子序列个数。那么有转移方程

\[f _ {i, j} = \sum _ { i < k \leq j 且 a _ i \mid a _ k 且 a _ k \mid a _ j} f _ { k, j } ( i < j 且 a _ i \mid a _ j ) \]

这样转移其实是把子序列的第一个点取出来(枚举),那么后面的就是之前算过的东西。

另有初值

\[f _ { i, i } = 1 \]

在这个 DP 的表示下有用的状态数最多大约是 \(n \ln n\) 的,而转移就是先枚举合法的 \(k\) 再枚举合法的 \(j\)(都是枚举倍数),转移数最多(枚举到的数目)大约是 \(n \log ^ 2 n\) 的(我听说是,不会证),状态和转移都通过枚举倍数来实现即可。

代码实现上与上一题类似(注意上一题数对是无序的,而这题子序列是有序的);为了方便(为了不用管大量无用状态),代码里的 DP 状态和这里写的 DP 状态有所不同。

总结(与上一题重复的就没写):

  1. 枚举倍数再枚举倍数是大约 \(n \log ^ 2 n\) 的。可以考虑类似调和级数暴力做。
  2. 子序列 DP 问题(最长合法子序列、统计合法子序列个数)都可以区间 DP(设状态)。DP 的转移考虑去掉一个点,剩下的就是已经处理过的东西。
  3. DP 的状态数、转移数可能很少。可能需要利用。不要觉得设计的 DP 状态多就不用,可能有用的状态是很少的。
  4. 关于静态序列给若干个区间询问的问题:[总结(还没写)]。

2024.11.5

CF1746F Kazaee

[Sum Hashing](不太确定算不算).(我不清楚加不加 ing)

见 还没写的 [哈希博客](打算写 xor hashing、sum hashing、字符串哈希、([树哈希](如果我写的时候学懂了)))。

2024.11.5

P10856 【MX-X2-T5】「Cfz Round 4」Xor-Forces

异或具有结合律,于是我们可以不直接处理全局下标异或的操作,而是把异或的值异或起来,每次询问时询问异或当前的这个值后的结果,询问之间独立(即不是真正在序列上异或)。

至于如何用合并式的数据结构维护区间颜色段数:

  1. 可以维护反的,即维护区间中有多少对相邻的颜色不同的位置。
  2. 可以直接维护,即把子部分的答案加起来再减去新产生的相邻不同色的位置对的个数。

此题中我采用第二种。其实此题中两种的实现难度差不多。

\(n = 2 ^ k\) 和 下标的异或操作 提示我们要用和位有关的数据结构维护。可以尝试线段树(可结合 01-Trie 理解)、[按位分块](我自己随便取的名字)。这里使用线段树解决。

于是我们现在试图在线段树的每个结点上维护这个区间 xor 每个数的区间答案。但是这样是 \(n ^ 2\) 的。尝试利用已有信息。发现一个区间 xor 上 \(2\)\(\geq\) \(\log\) 区间长度 次幂都相当于把这个区间全部平移到线段树上的另一个结点所对应的区间,“平移”指的是区间内的点相对位置不变。

于是每个区间内维护 xor 0 ~ 区间长度 - 1 后的区间答案即可。可以直接合并求出(\(O(1)\))。

于是线段树上每个结点预处理的时间复杂度为\(O ( 区间长度 )\),那么预处理的总时间复杂度为 \(O(n \log n)\)(区间长度之和,共 [log](?) 层,每层的区间长度和为 n)。

预处理 lg。预处理一个数组来记录某一层某个左端点是哪个结点,用来跳到同层结点(前面提到的“平移”)。

利用异或的性质:

  1. 结合律。用来融合修改操作。
  2. xor x 两次就相当于不 xor x。可以用来求哪个数 xor x = y(这个数就是 x xor y)。

代码基本上抄了题解,那篇题解代码很简洁优美,对我来说很有学习价值。一些细节在我代码里。

总结

  1. 区间颜色段数有可加性(可合并),可以用合并式的数据结构维护,有两种维护方法。
  2. 关于下标上的修改,可以考虑用按位的数据结构来维护(如 01-Trie、维护的长度为 \(2\) 的幂的线段树 和 按位分块)
  3. 尝试不维护重复的信息,以“平移”的方式利用已有信息。
  4. 分治 或 分治类数据结构 的时空复杂度分析:可以把每一层的时空复杂度加起来。
  5. 当全局修改有结合律(如全局加、全局异或)或后面覆盖前面(如树的换根)时,可以不修改,而是在查询时看当前的修改值来处理。
  6. 利用好异或的性质:
    1. 结合律:用来融合修改操作。
    2. xor 同一个数 2 次 相当于不 xor 它:用来求哪个数 xor x = y(这个数就是 x xor y)。

2024.11.5

2024.11.6(总结)

P2779 [AHOI2016初中组] 黑白序列

(前面的部分是 DP 思路)

对于这种(序列)合法部分结合来扩大的 DP 题,有两种:

  1. 有包含关系:区间 DP。(?)
  2. 没有包含关系:子段划分 DP。(?)

这题里[没有包含关系](?),于是考虑子段划分。要求序列的方案数而非划分的方案数,于是我们先注意到一个确定的合法序列的划分是唯一的。那么就可以求划分的方案数了。子段划分 DP,但是时间复杂度较高。

发现前半段是黑后半段是白的限制不方便用数据结构直接优化,考虑转化成偏序关系。具体地,我们设 \(wt _ i\) 表示 \(i\) 往前填白最多能填多少个(包括它自己),\(bl _ i\) 表示 \(i\) 往后填黑最多能填多少个(包括它自己)。用这两个限制,小推一下式子(分离 i、j,把与 i 相关的放到一边,把与 j 相关的放到另一边),我们把问题转化为了[三维偏序](?)(加上 \(i < j\) 的限制)优化 DP。但是不急着直接用 CDQ 分治,我们发现有两维都可以直接通过顺序搞掉,剩下那一维用树状数组维护即可。

另外要求划分出来每个区间的右端点都是偶数,因为划分出来每段区间的长度都是偶数。

代码里写了注释。省略掉的具体思路(怎么转化成[偏序问题](?)以及怎么解决)见注释吧。

也有一位一位填的 DP,我没写,不知道对不对。这种 DP 的状态里要记是在填黑还是填白的阶段,填了多少个黑 / 还要填多少个白 什么的(我记不太清了,可能有问题),但优化有复制一整个 DP 数组这种操作,似乎可以主席树,但是我记得当时我觉得好像不能优化,XK 好像也觉得不太能优化。

总结

  1. 合法部分结合扩大的 DP 题:根据有无包含关系来决定要用 区间 DP 还是 子段划分 DP。也可以考虑一位一位填的 DP。
  2. 注意是求序列的方案数还是求划分的方案数。找二者之间的联系。
  3. 小推式子把不等式关系转化成偏序问题。(分离 i、j,把与 i 相关的放到一边,把与 j 相关的放到另一边)
  4. 三维偏序(高维偏序)不要急着 CDQ 分治或者其他什么,考虑特殊性,能不能有两维一起处理之类的。实际情况里可能有特殊性,可以用特殊做法解决。好写的做法:通过遍历顺序解决一些(/ 个)维度,通过数据结构解决另一些(/ 个)维度。

2024.11.6

#include <bits/stdc++.h>
#define pii pair < int, int > // ?
#define eb emplace_back
#define mp make_pair
using namespace std;

const int N = 500000, P = 1000000009;/*, C = 2; // C 防树状数组出问题,- 1 + 2 == 1*/

int n;
string s;
int bl[N + 2], wt[N + 1];
vector < vector < pii > > add(N + 1);

struct BIT{
//	int sum[N + 3]; // 3
	int sum[N + 2]; // 2
	inline int lowbit(int i) { return i & - i; }
	void Inc(int p, int v) { ++ p; for(; p <= n + 1; p += lowbit(p)) sum[p] = (sum[p] + v) % P; } // ++ p n + 1
	int Sum(int p) { ++ p; int res = 0; for(; p >= 1; p -= lowbit(p)) res = (res + sum[p]) % P; return res; } // ++ p
}tr;

void Solve()
{
	cin >> s;
	n = ((int)s.size()); s = " " + s;
	
	if(n & 1){ // 要特判 n 为奇数的情况 // 如果注释掉这个 if 及其中的东西的话,hack 数据:输入 B?B
		printf("0\n");
		return; //
	}
	
	wt[0] = 0;
	for(int i = 1; i <= n; ++ i) wt[i] = ((s[i] != 'B') ? (wt[i - 1] + 1) : 0);
	bl[n] = (s[n] != 'W');
	for(int i = n - 1; i >= 1; -- i) bl[i] = ((s[i] != 'W') ? (bl[i + 1] + 1) : 0);
	
	add[0].eb(mp(0, 1));
	if(0 + 2 * bl[0 + 1] <= n) add[0 + 2 * bl[0 + 1]].eb(mp(0, P - 1)); // if // P - 1 而非 - 1
	for(auto i : add[0]) tr.Inc(i.first, i.second);
	
	int f;
	for(int i = 1; i <= n; ++ i){
		// i -> j (i < j)(把 i < j 看成已保证的东西)
		// 要考虑区间长度是偶数。
		// 其实每个区间的右端点的下标一定是偶数,即要求 i、j 都是偶数
		// bl[i + 1] >= (j - i) / 2 // 要 + 1
		// wt[j] >= (j - i) / 2 // 不 + 1 也不 - 1
		// 上面两个 且 起来就是 i 能更新 j 的充要条件
		// i、j 混杂,不方便处理,拆开
		// j <= i + 2 * bl[i + 1]
		// j - 2 * wt[j] <= i
		// 加上 i < j 的限制
		// 其实本质是二维数点。考虑用树状数组维护,可以用修改的时刻和查询的范围来满足这些条件。
		// 具体地,顺序枚举以满足 i < j,在 i 和 i + 2 * bl[i + 1] 时修改以满足 i < j 和第二个限制,在 j 处用 j - 2 * wt[j] 查询以满足第三个限制。
		// 树状数组维护的范围是 0 ~ n(包含 0 和 n)
		if(i & 1) continue;
		f = (tr.Sum(n) - tr.Sum(i - 2 * wt[i] - 1) + P) % P; // (... + P) % P
		add[i].eb(mp(i, f));
		if(i < n) if(i + 2 * bl[i + 1] <= n) add[i + 2 * bl[i + 1]].eb(mp(i, (P - f) % P)); // 第二个 if n 而非 i + 1 // (P - f) % P 而非 - f // 这样套 if ?
		for(auto k : add[i]) tr.Inc(k.first, k.second);
	}
	printf("%d\n", f);
}

int main()
{
	Solve();
	return 0;
}
// 参考:https://www.luogu.com.cn/article/pryn1khz(主要是“状态表示”和“状态转移”,但这篇题解的式子有点问题,还有就是它里面的代码没特判字符串长度是奇数的情况,输入只有 B 时可以 hack 掉)
// 但还是膜拜这篇题解的思路。思路是从这篇题解来的。

P5298 [PKUWC2018] Minimax

列 DP 式子。考虑平衡树启发式合并优化,但似乎是两只 log。发现可以直接线段树合并优化,单 log。

具体地,线段树可以边合并边算前缀和、后缀和之类的。然后要求区间乘。

线段树合并是可以 PushDown 和 PushUp 的。不要局限于仅仅是把对应结点的权值加起来(本题中我也不是这样写的,而是直接 PushUp)。

线段树合并要动态开点。注意空间要根据建新点的数量开。

要离散化。

注意点的权值一开始是不取模的。

细节见代码。代码参考学习题解:https://www.luogu.com.cn/article/duj32nfk 。这篇题解的代码写得非常清晰巧妙。

总结

  1. 树上问题很可以考虑合并。
  2. 树形 DP 不要怕状态里除了 \(u\)(结点)以外的东西取值很多。状态可以很多。只要初值不多(比如 \(n\) 个),而[状态是合并得到的](?),就可以用线段树合并(或其他的合并,如 dsu on tree、启发式合并、可并堆)优化(即不是每个状态都会被处理到,可能有些会直接继承,可能有些会在线段树里的某些结点统一打标记维护)
  3. 线段树合并很强大,可以带标记带 PushDown 和 PushUp可以边合并边算信息
  4. (from nkp)线段树合并优化 DP 其实就是把 DP 式子拉通写,然后合并同类项之类的(对一些东西做同样的运算)。本题中体现为乘法交换律。stO nkp Orz

我的代码:

2024.11.8 upd:代码里 Mrg(合并)没有判叶子结点,因为这题特殊,没有相同的数,所以合并的时候到叶子结点就一定会有一棵线段树结点为空,此时就能直接返回了。但是如果有重复的叶子结点就要判了,否则就会出现叶子结点 PushDown(动态开点线段树好像没什么影响)和叶子结点 PushUp(会出问题!)的神秘行为。

#include <bits/stdc++.h>
#define gc getchar
#define pc putchar
//#define int long long //
using namespace std;

namespace FastIO{
	int rd()
	{
		int x = 0, f = 1;
		char c = getchar();
		while(c < '0' || c > '9'){
			if(c == '-') f = (- 1);
			c = getchar();
		}
		while(c >= '0' && c <= '9'){
			x = x * 10 + (c - '0');
			c = getchar();
		}
		return (x * f);
	}
	void wt(int x)
	{
		if(x < 0){
			x = (- x);
			pc('-');
		}
		if(x > 9) wt(x / 10);
		pc(x % 10 + '0');
	}
}
using namespace FastIO;

const int N = 3e5, P = 998244353;
const int ND = N * 30; // ?

int tmp[N + 1], cnt;

namespace SGT{
	int tot; // 点的编号从 1 开始
	int sum[ND + 1], mul[ND + 1], lc[ND + 1], rc[ND + 1];
	void Mo(int u, int V) { sum[u] = 1ll * sum[u] * V % P; mul[u] = 1ll * mul[u] * V % P; }
	void Up(int u) { sum[u] = (sum[lc[u]] + sum[rc[u]]) % P; } // sum[0] = 0 // 要 % P!
	void Dn(int u) { if(mul[u] != 1) { Mo(lc[u], mul[u]); Mo(rc[u], mul[u]); mul[u] = 1; } }
	inline int Mk() { ++ tot; mul[tot] = 1; return tot; } // inline ?
	void Mdf(int & u, int l, int r, int Pos, int V)
	{
		if(l > r || l > Pos || r < Pos) return;
		if(! u) u = Mk();
		if(l == Pos && r == Pos) { sum[u] = V; return; }
		int mid = ((l + r) >> 1);
		Mdf(lc[u], l, mid, Pos, V);
		Mdf(rc[u], mid + 1, r, Pos, V);
		Up(u);
	}
	int Mrg(int u, int v, int l, int r, int mulu, int mulv, int ppp)
	{
		if(! u || ! v) { Mo(u | v, ! u ? mulv : mulu); return u | v; } // ?
		int mid = ((l + r) >> 1);
		Dn(u); Dn(v); // 要放在下一行前面
		int sumlcu = sum[lc[u]], sumrcu = sum[rc[u]], sumlcv = sum[lc[v]], sumrcv = sum[rc[v]];
		lc[u] = Mrg(lc[u], lc[v], l, mid, (mulu + 1ll * (1 - ppp + P) % P * sumrcv % P) % P, (mulv + 1ll * (1 - ppp + P) % P * sumrcu % P) % P, ppp);
		rc[u] = Mrg(rc[u], rc[v], mid + 1, r, (mulu + 1ll * ppp * sumlcv % P) % P, (mulv + 1ll * ppp * sumlcu % P) % P, ppp);
		Up(u); // Merge 可以 PushUp
		return u; // 勿忘
	}
	int Qry(int u, int l, int r)
	{
		if(! u) return 0;
		if(l == r) return 1ll * l * tmp[l] % P * sum[u] % P * sum[u] % P;
		int mid = ((l + r) >> 1);
		Dn(u);
		return (Qry(lc[u], l, mid) + Qry(rc[u], mid + 1, r)) % P;
	}
}

int n;
int p[N + 1];
int ch[N + 1][2];
int rt[N + 1];

int Qpow(int x, int y)
{
	int res = 1;
	while(y){
		if(y & 1) res = 1ll * res * x % P;
		x = 1ll * x * x % P;
		y >>= 1;
	}
	return res;
}

void DFS(int u)
{
	if(! ch[u][0]) SGT::Mdf(rt[u], 1, cnt, p[u], 1);
	else if(! ch[u][1]) { DFS(ch[u][0]); rt[u] = rt[ch[u][0]]; }
	else { DFS(ch[u][0]); DFS(ch[u][1]); rt[u] = SGT::Mrg(rt[ch[u][0]], rt[ch[u][1]], 1, cnt, 0, 0, p[u]); }
	// 不要忘了 DFS
	// 线段树范围是到 cnt 而不是到 n
}

void Solve()
{
	n = rd();
	for(int i = 1; i <= n; ++ i) { int fa = rd(); if(! ch[fa][0]) ch[fa][0] = i; else ch[fa][1] = i; } // i == 1 时也不会出问题
	int inv = Qpow(10000, P - 2);
	for(int i = 1; i <= n; ++ i) { p[i] = rd(); if(ch[i][0]) p[i] = 1ll * p[i] * inv % P; else tmp[++ cnt] = p[i]; }
	sort(tmp + 1, tmp + cnt + 1);
	// 不用去重,因为保证不重
	for(int i = 1; i <= n; ++ i) { if(! ch[i][0]) p[i] = lower_bound(tmp + 1, tmp + cnt + 1, p[i]) - tmp; }
	DFS(1);
	wt(SGT::Qry(1, 1, cnt)); // 线段树范围是到 cnt 而不是到 n
}

int main()
{
	Solve();
	return 0;
}
// 参考:https://www.luogu.com.cn/article/duj32nfk

2024.11.6

CF710F String Set Queries

是之前学的二进制分组合并 trick 的板子题。终于补了。

因为这个 trick 大概算是 DS 的,所以就把这题的笔记丢到本文中了。

注意

  1. 关于 AC 自动机的 合并、插入字符串。
    1. 不要尝试直接合并经过 Build 的 AC 自动机,因为 ch 记的已经不只是 Trie 树上的边了。
    2. 也不要尝试往一个已经 Build 了的 AC 自动机插入字符串,同理。
  2. AC 自动机上在 [fail 树](?)上 DP(或者简单推)的东西不要写成 Trie 上的了。
  3. 往 AC 自动机里插入完字符串后一定要记得调用 Build!
  4. AC 自动机里 Build 的一个细节:算 nxt 时 \(u = 0\)\(fa = 0\) 的时候都不能算,\(nxt _ u\) 直接就是 \(0\)
  5. AC 自动机我的写法要以 \(0\) 为根,因为 nxt 和 ch 的初值都是全 \(0\)。如果不以 \(0\) 为根的话应该要[多](似乎有时可以全部都不手动初始化)手动初始化一些东西。
  6. 用 vector(动态)存 AC 自动机:
    1. 注意编号。初始化时 emplace_back 占位。
    2. [(ch 是 vector)绝对不要写 ch[u][...] = Mk();(Mk 里面要对 ch [emplace_back](?????))。](?????)先拿个 v 存 Mk(),再把 ch[u][...] 赋值成 v。

总结

  1. 多模式串,单文本串,匹配 -> AC 自动机。
  2. AC 自动机是静态的结构,要动态 -> 二进制分组合并。

2024.11.6


P5556 圣剑护符

总结

  1. 注意集合是什么的集合。注意集合可重还是不可重。
  2. 异或相关的多推式子(等式,两边同时异或,可消掉两边共有的,可把一边变成 \(0\) 或[一个数](还不知道有什么用(应该还没见过用到这个的)))。用来转化[能不能异或出什么东西的问题](较广义,比如本题)。
  3. 异或线性基就大概 \(\log\) 个格子,塞满了就塞满了,看能不能异或出 \(0\)(也可以用上面这条转化成其他的)就是看有没有数塞不进去,那么最多大概 \(\log\) 个数就能判断(多了就塞满了)。于是可以暴力添加数。
  4. 两个数异或想 01-Trie,多个数异或想线性基。(?)

2024.11.7


P4211 [LNOI2014] LCA

总结

  1. 经典 trick:求两点 LCA 的深度,可以把一个点到根的路径上的点权都 \(+ 1\)(也可以理解成标记这些点),再求另一个点到根的路径上的点的点权和就是答案。
  2. 分修改和查询两部分考虑。思考哪些作为修改,哪些作为查询。可以重复利用修改(离线/可持久化),可以重复利用查询(记)。

2024.11.7


P4175 [CTSC2008] 网络管理

总结

  1. 待修区间 \(k\) 大 / 小:树状数组套主席树。

  2. 数的出现次数有可减性。有可减性的树上简单路径信息可以考虑(有点权无边权):

    1. 每个点的线段树维护它到根的信息。自上而下用可持久化线段树维护。(不可修改)
    2. 仍是每个点的线段树维护它到根的信息。每个点对以它为根的子树算贡献,拍到 DFS 序上变成区间修改,用树状数组套主席树维护。(可修改)
    • 查询 \(u, v\) 时,是 \(val _ u + val _ v - val _ { LCA(u, v) } - val _ { fa _ { LCA(u, v) } }\)
  3. 第 2 条当然也可以考虑大力树剖,但是树剖的时间复杂度可能会更高。

2024.11.8(2024.11.7 做的这道题)


P1967 [NOIP2013 提高组] 货车运输

最小生成树、Kruskal 重构树的感觉。贪心,从大往小选边,建 Kruskal 重构树,查询时求 LCA 即可。

也可建最大生成树,查询树上简单路径的 \(\min\)。倍增求 LCA 的同时维护即可。

这两种做法差不多。

总结

  1. 这种最值最优化问题可以考虑贪心,因为最值会把其他值的影响覆盖掉。
  2. 树上倍增是处理树上无修改简单路径信息查询的利器,相比于树剖不需要线段树等数据结构的帮助,更快、更简洁。
  3. Kruskal 重构树的 LCA 的权值就是最小生成树路径上的 \(\max\)。理清 Kruskal 重构树和最值生成树之间的联系,[有时一个想不下去了可以考虑转化为另一个](???)。

2024.11.8


P1552 [APIO2012] 派遣

显然树上合并,在每个点处尝试更新答案。

可以尝试线段树合并,但没必要,因为所有薪水都是正的,因此合并的过程中一个忍者如果没被派遣就再也不会被派遣了。于是我们用左偏树,贪心地从大到小删除即可。注意左偏树里要维护 siz 和 sum,它们在合并的过程中需要 PushUp。

总结

  1. 非负:贪心。
  2. 删除了就不会再加回来:考虑更暴力的数据结构而不是二分。
  3. 左偏树里可以维护需要 PushUp 的东西,记得在合并时 PushUp。

2024.11.8


posted @ 2024-11-01 18:58  huangkxQwQ  阅读(16)  评论(0编辑  收藏  举报