Welcome to Liam's Blog
About me
-
My id : lnw143
-
Year of Birth : 2009
-
Location : Zhongshan, Guangdong Province, CHN
Motto
Nothing.
Links
后缀数组
后缀数组是处理字符串的利器。
定义
对于长度为 \(n\) 的字符串 \(s = s_1 s_2 \cdots s_n\),定义 \(s[i,j] = s_i s_{i+1} \cdots s_j\),即 \(s\) 的第 \(i\) 个到第 \(j\) 个字符形成的子串。
定义 sa
为 \(s\) 的后缀数组,sa[i]
表示第 \(i\) 小的后缀的编号。
定义 rk[sa[i]]=i
,即 rk[i]
表示后缀 \(i\) 的 rank。
求解
考虑最暴力的做法,取出所有后缀并排序,空间复杂度 \(O(n ^ 2)\),时间复杂度 \(O(n^2 \log n)\)。
考虑倍增,设 rk[j][i]
表示在所有长度为 \(2^j\) 的后缀中,后缀 \(s[i,i+2^j-1]\) 的 rank(超出 \(n\) 的部分记为空字符,小于任何非空字符)。
显然我们可以 naively 得到 rk[0][i]
,考虑如何由 rk[j-1]
得到 rk[j]
。
发现可以将所有二元组 (rk[j-1][i],rk[j-1][i+(1<<(j-1))]
排序,即把每段后缀分为前后两段,并以前一半的 rank 作为第一关键字,后一半的 rank 作为第二关键字,超出 \(n\) 的部分记为 0
,排序后得到 rk[j]
。
总共倍增 \(\mathrm O(\log n)\) 次,每次排序 \(\mathrm O(n \log n)\),总时间复杂度 \(\mathrm O(n \log^2 n)\)。
发现 rk
的值域是 \(\mathrm O(n)\) 的,因此可以用桶排做到 \(O(n \log n)\)。
在算法竞赛中这已经足够了,如果追求更优的时间复杂度,可以考虑 SA-IS / DC3 算法,线性时间复杂度,但实现十分复杂,在竞赛中不常用,因此不再赘述。
显然通过 rk
可以 \(\mathrm O(n)\) 得到 sa
。
height 数组
定义 ht[i]
为后缀 sa[i]
与 sa[i-1]
的最长公共前缀的长度,并设 ht[1]=0
。
一眼看来似乎没有线性做法,但是我们将尝试证明以下引理:
Lemma: ht[rk[i]]
\(\ge\) ht[rk[i-1]]-1
。
Proof:
ht[rk[i-1]]
\(\le 1\) 的情况是 naive 的。
若 ht[rk[i-1]]
\(\ge 2\),即 sa[rk[i-1]-1]
与 i-1
的最长公共前缀长度 \(\ge 2\),设这个最长公共前缀为 \(aA\),其中 \(a\) 是单个字符,\(A\) 是一个非空字符串。
那么设后缀 sa[rk[i-1]-1]
为 \(aAB\),i-1
为 \(aAC\),则 \(B \lt C\),且后缀 sa[rk[i-1]-1]+1
为 \(AB\),i
为 \(AC\)。
后缀 sa[rk[i]-1]
因为只比 i
前一名,所以是小于 i
中最大的,又因为 \(AB \lt AC\),所以 \(AB \le\) 后缀 sa[rk[i]-1]
\(\lt AC\),容易看出后缀 i
和后缀 sa[rk[i]-1]
有公共前缀 \(A\)。
于是 ht[rk[i]]
$\ge |A| = $ ht[rk[i-1]]-1
。
\(\square\)
由此,我们从 ht[rk[i-1]]-1
开始暴力拓展 ht[rk[i]]
,在 \(\mathrm O(n)\) 的时间复杂度内得到 ht
。
ht
数组有这样一个性质:后缀 i,j
的最长公共前缀长度等于 ht[rk[i],rk[j])
中的最小值。(钦定 rk[i]
\(\lt\) rk[j]
)
因此我们可以用 ST 表 \(\mathrm O(n \log n)\) 预处理后 \(\mathrm O(1)\) 询问任意后缀的最长公共前缀长度。
例题
UOJ 35.后缀排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
|
VSCode 配置
前言
作为 VSCode 资深用户,从第一次下载到如今,踩了一个又一个坑,在这里记录一下我的配置方法以及遇到的一些问题。
准备
在官网下载后,点开安装程序,建议勾选以下所有选项:
等待安装完毕即可。
插件
插件是 VSCode 的核心,其强大的插件开发社区是一大亮点。
打开左侧的扩展管理器,搜索并点击安装便可将插件添加到 VSCode。
如搜索 chinese
,安装中文语言包。
可以在终端用 code --install-extension <extension-id>
或按 F1 / Ctrl+Shift+P
打开命令面板,搜索 ext install
并选择 Extensions: Install Extension
,输入插件名称或 ID 安装插件。
附录 有一些常用的插件。
配置
VSCode 默认是不支持编译运行 C++
的,需要安装插件 C/C++
并配置文件。
Ctrl+K Ctrl+O
打开一个工作文件夹,并新建 .vscode
文件夹,并新建两个文件 tasks.json
和 launch.json
。
Tasks
tasks.json
文件用来配置编译任务,可以配置多个编译任务,每个任务对应一个编译命令。
尝试在 tasks.json
文件中添加以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
|
其中 label
字段是任务的名称,command
字段是编译器,args
字段是编译命令,cwd
字段是编译命令的运行目录,isDefault
字段是是否为默认编译任务。
之所以输出为 a.exe
,是为了兼容源代码文件名中的中文字符,如果不需要,可以改为 ${fileBasenameNoExtension}.exe
。
Warning
非必要不要更改 cwd
,type
,problemMatcher
等字段,否则可能导致任务执行失败。
Launch
launch.json
文件用来配置调试任务,可以配置多个调试任务,每个任务对应一个调试命令。
尝试在 launch.json
文件中添加以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
其中一般只需要配置 preLaunchTask
与 program
字段,前者指定编译任务,后者指定运行程序。
全局配置
你是否有这样的烦恼:每次新建 VSCode 工作文件夹都要配置 tasks.json
和 launch.json
?每次编译都会自动生成 .vscode
文件夹?
VSCode 的全局 tasks.json
配置位于 %APPDATA%\Code\User\tasks.json
,可以编辑该文件来配置全局编译任务。
而 launch.json
配置位于 %APPDATA%\Code\User\settings.json
,在其中加入以下内容:
1 |
|
这样即可实现一次配置,终身受益(
附录
常用插件
Tools
Themes
8113. 【2022.10.7联考noip模拟】Talulah
题意
给定长度为 \(n\) 的排列 \(p\),定义集合 \(S_i = \{j \mid j \ge i \land \max_{k \in [i,j]} p_k = p_j\}\)。
给定 \(q\) 次询问 \(l,r\),求 \(\sum_{x,y \in [l,r]} |S_x \cap S_y|\)。
\(n,q \le 2.5 \times 10^5\)
思考
考虑钦定 \(x<y\),如下图所示:
发现 \(\forall i \ge y\),\(i \in S_x \Rightarrow i \in S_y\)。
Proof: 因为 \(i \in S_x\),所以 \(\max_{k \in [x,i]} p_k = p_i\),因此显然 \(\max_{k \in [y,i]} p_k = p_i\),即 \(i \in S_y\)。\(\square\)
因此 \(|S_x \cap S_y| = |S_x \cap [x,y)| + |S_y|\)。
考虑把这两个部分分开处理,发现所有 \(|S_y|\) 即为 \(\sum_{i \in [l,r]} (i-l)|S_i|\),可以用前缀和 \(\mathrm O(1)\) 计算。
考虑计算 \(\sum_{l \le x \lt y \le r} |S_x \cap [x,y)|\),如下图所示:
正难则反,考虑抛弃 \(y\) 的限制,而是计算 \(i \in S_x\) 的贡献。
发现 \(\forall i \in S_x \cap [x,r]\),对于 \(\forall y \gt i\) 都有 \(1\) 的贡献,即原式等于 \(\sum_{i \in S_x \cap [x,r]} r-i\)。
发现这个不好维护,于是正难则反,考虑计算 \(\forall i \in [l,r]\) 对 \(x \le i\) 的贡献。
记 \(\mathrm{pre}_i = \max_{j \lt i} {j \mid p_j \gt p_i}\),即 \(i\) 前第一个大于 \(i\) 的元素的位置,并记 \(p_0 = \infty\)。
发现 \(\forall i \in [l,r]\),\(i\) 对 \(x \in (\mathrm{pre}_i,i]\) 有 \(r-i\) 的贡献。
由于我们不清楚 \(r\) 的值,因此我们维护 \(r\) 的系数即可。
如上图,考虑用线段树维护区间加与区间和。
考虑 \(i \in [l,r]\) 这条限制,容易发现当 \(i \lt l\) 时,\(i\) 对 \([l,r]\) 无贡献;当 \(i \gt r\) 时,如上图的 \(j\),会对 \([l,r]\) 产生多余的贡献。
因此我们把询问离线并按右端点排序,依次加点即可。
最后别忘了我们计算的是 \(x \lt y\) 的答案,剩下部分是 naive 的。
如果强制在线的话,可以用可持久化线段树 + 标记永久化解决,时间复杂度同为 \(\mathrm O((n+q) \log n)\)。
图论
这里是图论相关的算法笔记。
行列式
定义
定义 \(n\) 阶方阵 \(A\) 的行列式为:
其中 \(\{p_1,p_2,\cdots,p_n\}\) 是一个 \(n\) 的排列,\(\tau(p_1,p_2,\cdots,p_n)\) 表示 \(\{p_1,p_2,\cdots,p_n\}\) 的逆序数。
引理
Lemma 1
读者自证不难。
Lemma 2
读者自证不难。
Lemma 3
若矩阵有相同的两行,则行列式为零。
Proof: 交换相同的两行后矩阵不变,且由 Lemma 2 得行列式变号,即 \(|A|=-|A|\),所以 \(|A|=0\)。\(\square\)
Lemma 4
若矩阵某一行是另一行的倍数,则行列式为零。
Proof: 由 Lemma 1 & Lemma 3 易证。
Lemma 5
若 \(\forall j,a_{i,j} = b_j + c_j\),则
读者自证不难。
Lemma 6
Proof: 设
由 Lemma 4 得 \(|B|=0\),由 Lemma 5 得 \(|C| = |A| + |B| = |A|\)。\(\square\)
Lemma 7
即上三角矩阵的行列式为主对角线元素的乘积。
Proof: 由行列式的定义得若 \(\exists i, a_{i,p_i} = 0\),那么整个排列对行列式无贡献。
因此要想对行列式有贡献,\(p_n\) 必等于 \(n\),同理得 \(\forall i, p_i = i\)。\(\square\)
求解
显然有 \(\mathrm O(n! n)\) 的 naive 方法,但如果这样上面的 \(\LaTeX\) 就白写了(
考虑把矩阵转化为上三角矩阵并由 Lemma 7 \(\mathrm O(n)\) 计算行列式。
这个过程形如高斯消元,于是考虑用高斯消元法来求解行列式。
假设目前处理到第 \(i\) 行,要使得 \(\forall j > i, a_{j,i} = 0\)。
显然可以让第 \(j\) 行加上第 \(i\) 行乘 \(- \frac{a_{i,j}}{a_{i,i}}\),但对于任意模数情况下 \(a_{i,i}\) 不一定有逆元。
因此,我们考虑用辗转相除法。
每次使得 \(a_{j,i} \gets a_{j,i} \bmod a_{i,i}\),并交换第 \(i\) 行与第 \(j\) 行,直到 \(a_{i,i}\) 为 \(0\)。
即让第 \(j\) 行加上第 \(i\) 行乘 \(- \lfloor \frac{a_{i,j}}{a_{i,i}} \rfloor\)。
时间复杂度看似是 \(\mathrm O(n^3 \log P)\),但可以证明是 \(\mathrm O(n^3)\),其中 \(P\) 是模数。
参考代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
应用
- 矩阵树定理
8081.【2024年SD省队集训 day2】反转了 (flip)
题意
给定长度为 \(N\) 的数组 \(A\),定义 \(S_0 = 0\),\(S_i = \sum_{j=1}^i A_j(i \in [1,N])\)。
对 \(K \in [0,N]\),求解:
- 至多反转 \(K\) 个元素的符号,求 \(\max_{i \in [0,N]} {S_i}\) 的最大值。
朴素解法
暴力枚举 \(K\),并枚举 \(i \in [0,N]\),假定 \(S_i = \max_{j \in [0,N]} {S_j}\),显然最优方案为反转前 \(i\) 个元素中绝对值最大的 \(K\) 个元素。
设 \(B_i = \max(0,-A_i)\),那么第 \(i\) 段前缀对 \(K\) 的贡献为 \(C(i,K) = S_i + 2 W(i,K)\),其中 \(W(i,K)\) 为 \(\{B_1,B_2,\dots,B_i\}\) 中的前 \(K\) 大元素的和。
答案即为 \(\max{C(i,K)}\)。
注意当 \(i \lt K\) 时,\(W(i,K) = \sum_{j=1}^i B_j\)。
\(W(i,K)\) 可以用主席树在 \(O(\log N)\) 时间内求得。
总时间复杂度 \(O(N ^ 2 \log N)\),期望得分 30。
优化
设 \(f_K\) 表示 \(\min \{i | \forall j,C(i,K) \ge C(j,K)\}\),即使得 \(C(i,K)\) 最大的最小的 \(i\) 值。
容易发现 \(f\) 非严格单调递增。
于是便如 ABC348G 一样,用分治 + 主席树解决。
单调性证明
实践是检验真理的唯一标准
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
考虑使用反证法,假设 \(\exists i,f_i > f_{i+1}\),设 \(f_i = x,f_{i+1} = y\)。
因为 \(f_i = x\),于是有
因为 \(f_{i+1} = y\),于是有
这显然是矛盾的,因为 \(y<x\),\(W(y,i+1) - W(y,i)\) 相当于 \(\{B_1,B_2,\dots,B_x\}\) 中的第 \(i+1\) 大的元素,但是 \(\{B_1,B_2,\dots,B_y\} \subset \{B_1,B_2,\dots,B_x\}\),于是必有 \(W(y,i+1) - W(y,i) \le W(x,i+1) - W(x,i)\),矛盾。
因此,\(f\) 是非严格单调递增的。