<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>Jerry&apos;s Blog</title><description>Stay hungry, stay foolish</description><link>https://blog.jerrylab.top</link><item><title>差分和前缀和</title><link>https://blog.jerrylab.top/blog/ds/prefix-sum</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/ds/prefix-sum</guid><description>区间修改和区间查询</description><pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前缀和&lt;/h1&gt;
&lt;p&gt;前缀和用于查询区间和．&lt;/p&gt;
&lt;p&gt;定义 $s[i]$ 表示区间 $[1,i]$ 之间所有元素的和（即前 $i$ 个数的和），不难发现，区间 $[l,r]$ 的和为 $s[r] - s[l-1]$．&lt;/p&gt;
&lt;p&gt;计算 $s$ 数组可以使用递推的方式，有 $s[i] = s[i-1] + a[i]$．&lt;/p&gt;
&lt;h1&gt;差分&lt;/h1&gt;
&lt;p&gt;差分用于区间修改，是前缀和的逆运算．&lt;/p&gt;
&lt;p&gt;定义 $d[i]$ 表示 $a[i]$ 相对 $a[i-1]$ 变化了多少，即 $d[i] = a[i] - a[i-1]$．&lt;/p&gt;
&lt;p&gt;当想要对 $[l, r]$ 增加 $x$ 时，只需要将 $d[l]$ 增加 $x$，$d[r+1]$ 减少 $x$．&lt;/p&gt;
&lt;p&gt;如果想要将差分数组还原成原数组，可以使用下面的公式：$a[i] = a[i-1] + d[i]$．&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>快速幂</title><link>https://blog.jerrylab.top/blog/other/fast-pow</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/other/fast-pow</guid><description>快速进行幂运算</description><pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;快速幂用于在 $O(\log n)$ 的速度计算一个数的 $n$ 次幂．&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- P1226 【模板】快速幂&lt;/p&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;给你三个整数 $a,b,p$，求 $a^b \bmod p$．&lt;/p&gt;
&lt;h3&gt;输入格式&lt;/h3&gt;
&lt;p&gt;输入只有一行三个整数，分别代表 $a,b,p$．&lt;/p&gt;
&lt;h3&gt;输出格式&lt;/h3&gt;
&lt;p&gt;输出一行一个字符串 &lt;code&gt;a^b mod p=s&lt;/code&gt;，其中 $a,b,p$ 分别为题目给定的值， $s$ 为运算结果．&lt;/p&gt;
&lt;h3&gt;数据范围&lt;/h3&gt;
&lt;p&gt;对于 $100%$ 的数据，保证 $0\le a,b &amp;#x3C; 2^{31}$，$a+b&gt;0$，$2 \leq p \lt 2^{31}$．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;快速幂使用倍增的思想．&lt;/p&gt;
&lt;p&gt;对于任意一个指数 $b$，将其用二进制表示，可以拆解为 $2^{a_1} + 2^{a_2} + \cdots + 2^{a_n}$ 的形式，其中 $a_i$ 为互不相同的非负整数．不难得出，设 $b$ 二进制表示中的第 $k$ 位为 1，则所有 $a_i$ 和所有 $k-1$ 对应相等．&lt;/p&gt;
&lt;p&gt;根据幂的相关性质可知，$a^b=a^{2^{k_1} + 2^{k_2} + \cdots + 2^{k_n}}=a^{2^{k_1}}\times a^{2^{k_2}}\times \cdots \times a^{2^{k_n}}$，即指数的二进制表示中，&lt;strong&gt;所有位数值为 1&lt;/strong&gt; 的位，记该位的权值为 $p$，答案就是 $\sum_{p_i \in P}a^p$，其中，$P$ 是所有满足要求的 $p$ 的取值．&lt;/p&gt;
&lt;p&gt;例如，对于 $a=3$，$b=10$ 的情况，由于 $10_{10}$ 的二进制为 $1010_2$，其中，为 1 的位是右起第 2 位（对应权值为 $2^{2-1}=2$）和右起第四位（对应权值 $2^{4-1}=8$），所以，$P={2,8}$，$3^{10} = 3^2\times 3^8$．&lt;/p&gt;
&lt;p&gt;因此，检查 $b$ 二进制中的每一位．并对于权值为 $p$ 的位，通过递推记录当前位的 $a^p$ ，若 $b$ 二进制的该位为 1，则将 $a^p$ 计入答案．如何通过递推记录？只需要在从权值为 $p$ 的一位转移到左边一位时，由于左边一位的权值是 $2p$，将 $a^p$ 平方为 $a^{2p}$ 即可．&lt;/p&gt;
&lt;p&gt;可以写出基本流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;检查 $b$ 二进制右起第一位是否为 1，若是，则将答案乘上 $a$．&lt;/li&gt;
&lt;li&gt;将 $b$ 左移一位，露出 $b$ 的二进制右起第二位．&lt;/li&gt;
&lt;li&gt;由于 $b$ 左移了一位，所以目前 $b$ 右起第一位的权值翻了倍，需要让 $a$ 平方才能让权值指数也翻倍．&lt;/li&gt;
&lt;li&gt;若当前 $b$ 大于等于 1，则回到步骤 1．&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;核心代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int exp = b; // 记录目前剩余的指数
int ans = 1; // 记录答案
int now = a; // 记录现在位的权值
while (exp &gt;= 1) {
	if (exp % 2) // 如果当前位是1
		ans *= now; // 就将当前位的权值记录答案
	exp /= 2; // 指数右移一位
	now *= now; // 权值对应增加
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此外，这道题还要注意取模和开 &lt;code&gt;long long&lt;/code&gt;．&lt;/p&gt;
&lt;p&gt;标程如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

int a, b, p;

int main() {
	cin &gt;&gt; a &gt;&gt; b &gt;&gt; p;
	long long exp = b, ans = 1, now = a;
	while (exp &gt;= 1) {
		if (exp % 2)
			(ans *= now) %= p;
		exp /= 2;
		(now *= now) %= p;
	}
	cout &amp;#x3C;&amp;#x3C; a &amp;#x3C;&amp;#x3C; &quot;^&quot; &amp;#x3C;&amp;#x3C; b &amp;#x3C;&amp;#x3C; &quot; mod &quot; &amp;#x3C;&amp;#x3C; p &amp;#x3C;&amp;#x3C; &quot;=&quot; &amp;#x3C;&amp;#x3C; ans;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>树状数组</title><link>https://blog.jerrylab.top/blog/ds/fenwick</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/ds/fenwick</guid><description>查询序列中的信息</description><pubDate>Sun, 05 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;树状数组是用来实现单点查询，区间修改的数据结构，虽然功能上可以被线段树完全覆盖，但是容易实现．&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- P3374 【模板】树状数组 1&lt;/p&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;如题，已知一个数列，你需要进行下面两种操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将某一个数加上 $x$；&lt;/li&gt;
&lt;li&gt;求出某区间每一个数的和．&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;输入格式&lt;/h3&gt;
&lt;p&gt;第一行包含两个正整数 $n,m$，分别表示该数列数字的个数和操作的总个数．&lt;/p&gt;
&lt;p&gt;第二行包含 $n$ 个用空格分隔的整数，其中第 $i$ 个数字表示数列第 $i$ 项的初始值．&lt;/p&gt;
&lt;p&gt;接下来 $m$ 行每行包含 $3$ 个整数，表示一个操作，具体如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 x k&lt;/code&gt; 含义：将第 $x$ 个数加上 $k$；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2 x y&lt;/code&gt; 含义：输出区间 $[x,y]$ 内每个数的和．&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;输出格式&lt;/h3&gt;
&lt;p&gt;输出包含若干行整数，即为所有操作 $2$ 的结果．&lt;/p&gt;
&lt;h3&gt;数据范围&lt;/h3&gt;
&lt;p&gt;对于 $100%$ 的数据，$1\le n,m \le 5\times 10^5$，$1\le x\le y\le n$，$-2^{31}\le k&amp;#x3C;2^{31}$．&lt;/p&gt;
&lt;p&gt;数据保证对于任意时刻，$a$ 的任意子区间（包括长度为 $1$ 和 $n$ 的子区间）和均在 $[-2^{31}, 2^{31})$ 范围内．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;基础树状数组&lt;/h1&gt;
&lt;h2&gt;构造&lt;/h2&gt;
&lt;p&gt;我们让树状数组 &lt;code&gt;c&lt;/code&gt; 的每一项 &lt;code&gt;c[i]&lt;/code&gt; 都存储以 i 为最终位置，以 &lt;code&gt;lowbit(i)&lt;/code&gt; 为长度的区间中所有数的和，即 &lt;code&gt;c[i]&lt;/code&gt; 存储区间 $[i-lowbit(i)+1, i]$．&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- 什么是 &lt;code&gt;lowbit&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;lowbit&lt;/code&gt;，即低位，是指一个数的二进制从右起的第一个 1 和这个 1 右边的所有 0 所构成的新的二进制数．或者说是最右边的 1 所对应的权值．&lt;/p&gt;
&lt;p&gt;如，$lowbit(12) = lowbit(1100_2)=100_2=4$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例如，一个 16 个元素的数组，其树状数组的管辖区间示意如下：（摘自 &lt;a href=&quot;https://oi-wiki.org&quot;&gt;oi-wiki&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../pic/fenwick.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果写成二进制，更直观地：&lt;/p&gt;
&lt;p&gt;令 $i$ 的二进制表示中，从右起第一个 &lt;code&gt;1&lt;/code&gt; 位于第 $k$ 位（即 $lowbit(i) = 2^{k-1}$），则 $c[i]$ 管辖的区间由其本身 $i$ 和所有满足以下条件的正整数 $m$ 组成：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$m$ 的二进制中，第 $k$ 位及比 $k$ 更高的所有位与 $i$ 对应相等，低于第 $k$ 位的部分可以是任意值．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例：对于 $i=88$，二进制为 &lt;code&gt;101 1000&lt;/code&gt;，从右起第一个 &lt;code&gt;1&lt;/code&gt; 在第 4 位．&lt;/p&gt;
&lt;p&gt;因此 $c[88]$ 存储的数为所有形如 &lt;code&gt;101 xxxx&lt;/code&gt; 但实际数值不超过 $88$ 的正整数——即固定高三位 &lt;code&gt;101&lt;/code&gt;，低四位从 &lt;code&gt;0000&lt;/code&gt; 变到 &lt;code&gt;1000&lt;/code&gt;，对应区间 $[81, 88]$．&lt;/p&gt;
&lt;p&gt;根据二进制知识可知，&lt;code&gt;lowbit(i) = i &amp;#x26; -i&lt;/code&gt;．&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- 为什么？
由于 &lt;code&gt;-i&lt;/code&gt; 在计算机里是补码，等于 &lt;code&gt;~i + 1&lt;/code&gt;（按位取反再加 1）．&lt;/p&gt;
&lt;p&gt;这样操作后，若记 &lt;code&gt;i&lt;/code&gt; 最右边的一个 1 为 $k$，由于取反，$k$ 右边的所有位都会变成 1，在加一后由于进位，再次变成 0．$k$ 在取反时变成 0，但由于进位变回 1．$k$ 左边的所有位都由于取反和原来不一样．这样，在按位与后，除了 $k$ 必然为 1 之外，其余的位必然都为 0．&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;例子&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;i = 6&lt;/code&gt; → 二进制 &lt;code&gt;110&lt;/code&gt;，&lt;code&gt;-i&lt;/code&gt; 是 &lt;code&gt;010&lt;/code&gt;（补码计算：&lt;code&gt;~110 = 001&lt;/code&gt;，加 1 得 &lt;code&gt;010&lt;/code&gt;）．&lt;/p&gt;
&lt;p&gt;&lt;code&gt;110 &amp;#x26; 010 = 010&lt;/code&gt; → 正是最右边的 1（二进制 &lt;code&gt;10&lt;/code&gt;，即 2）．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;查询操作&lt;/h2&gt;
&lt;p&gt;想要对区间 $[l, r]$ 查询，只需要知道区间 $[1,l-1]$ 和区间 $[1, r]$ 的和，之后用后者减去前者即可．&lt;/p&gt;
&lt;p&gt;如何进行前缀查询呢？&lt;/p&gt;
&lt;p&gt;例如，我想要查询区间 $[1, 7]$，先看 $c[7]$，能管辖 $[7,7]$，那 6 及之前的怎么办呢？$c[i]$ 肯定包含 $i$ 的，于是找到 $c[6]$，能管辖 $[5,6]$，同样的，那 4 及之前的怎么办呢，再看到 $c[4]$，能管辖 $[1,4]$，不剩了．&lt;/p&gt;
&lt;p&gt;再例如，我想要查询区间 $[1, 5]$，先看 $c[5]$，能管辖 $[5,5]$，于是再找到 $c[4]$，能管辖 $[1,4]$，不剩了，故答案为 $c[4]+c[5]$．&lt;/p&gt;
&lt;p&gt;一般的，我想要查询区间 $[1, n]$，先看 $c[n]$，发现 $c[n]$ 所能够管辖到的最小值为 $n-lowbit(n)+1$，在将 $c[n]$ 加和到 $ans$ 中后，原问题就转化为查询区间 $[1,n-lowbit(n)]$，边界条件是 $[1,0] = 0$．&lt;/p&gt;
&lt;p&gt;时间复杂度 $O(\log n)$．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 查询区间[1,ed]
int query(int ed) {
	int ans = 0;
	for (int i = ed; i &gt;= 1; i -= i &amp;#x26; -i) {
		ans += c[i];
	}
	return ans;
}
// 查询区间[l,r]
int query(int l, int r) {
	return query(r) - query(l - 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;修改操作&lt;/h2&gt;
&lt;p&gt;我们现在尝试修改序列的第 $i$ 项．&lt;/p&gt;
&lt;p&gt;我们知道，树状数组是一棵树的形态，因此，修改一个元素，只需要更新记录这个元素的节点即可．&lt;/p&gt;
&lt;p&gt;通过观察上图，所有被影响的节点会组成一条链的形状，且对于节点 $a[i]$，最底端（即下标最小）的节点为 $c[i]$．只需要找出 $c[i]$ 及所有的祖先节点，将它们都同步进行更改即可．&lt;/p&gt;
&lt;p&gt;注意到，节点 $c[i]$ 的父节点总是 $c[i+lowbit(i)]$．（节点 $c[i]$ 的父节点为 $i$ 最小的、真包含 $c[i]$ 的节点）&lt;/p&gt;
&lt;p&gt;因此，得出修改操作的一般步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;令 $k$ 初始化为 $i$&lt;/li&gt;
&lt;li&gt;修改 $c[k]$&lt;/li&gt;
&lt;li&gt;将 $k$ 设定为 $k+lowbit(k)$，若 $k\leq n$，回到步骤 2&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;时间复杂度 $O(\log n)$．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void add(int t, int k) {
	for (int i = t; i &amp;#x3C;= n; i += i &amp;#x26; -i) {
		c[i] += k;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;建树&lt;/h2&gt;
&lt;p&gt;一般来说，建树只需要转化为 $n$ 次单点增加就可以了．时间复杂度 $O(n\log n)$．&lt;/p&gt;
&lt;p&gt;但是有没有 $O(n)$ 建树的方法呢？&lt;/p&gt;
&lt;p&gt;前面提到，$c[i] = sum{a_i | i \in [i-lowbit(i)+1, i]}$，可以使用前缀和优化．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for (int i = 1; i &amp;#x3C;= n; i++) {
	cin &gt;&gt; a[i];
	sum[i] = sum[i - 1] + a[i];
}
for (int i = 1; i &amp;#x3C;= n; i++) {
	c[i] = sum[i] - sum[i - (i &amp;#x26; -i)];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;标程&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

const int N = 5e5 + 100;

int n, m, c[N], a[N], sum[N];

class Tree {
public:
	int query(int ed) {
		int ans = 0;
		for (int i = ed; i &gt;= 1; i -= i &amp;#x26; -i) {
			ans += c[i];
		}
		return ans;
	}
	int query(int l, int r) {
		return query(r) - query(l - 1);
	}
	void add(int t, int k) {
		for (int i = t; i &amp;#x3C;= n; i += i &amp;#x26; -i) {
			c[i] += k;
		}
	}
};

int main() {
	cin &gt;&gt; n &gt;&gt; m;
	Tree tree;
	for (int i = 1; i &amp;#x3C;= n; i++) {
		cin &gt;&gt; a[i];
		sum[i] = sum[i - 1] + a[i];
	}
	for (int i = 1; i &amp;#x3C;= n; i++) {
		c[i] = sum[i] - sum[i - (i &amp;#x26; -i)];
	}
	for (int i = 1; i &amp;#x3C;= m; i++) {
		int op, x, k;
		cin &gt;&gt; op &gt;&gt; x &gt;&gt; k;
		if (op == 1)
			tree.add(x, k);
		else
			cout &amp;#x3C;&amp;#x3C; tree.query(x, k) &amp;#x3C;&amp;#x3C; endl;
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;树状数组变体&lt;/h1&gt;
&lt;p&gt;如果需要单点查询，区间修改，又该怎么做呢？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- P3368 【模板】树状数组 2&lt;/p&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;如题，已知一个数列，你需要进行下面两种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;将某区间每一个数加上 $x$；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;求出某一个数的值．&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;输入格式&lt;/h3&gt;
&lt;p&gt;第一行包含两个整数 $N$、$M$，分别表示该数列数字的个数和操作的总个数．&lt;/p&gt;
&lt;p&gt;第二行包含 $N$ 个用空格分隔的整数，其中第 $i$ 个数字表示数列第 $i $ 项的初始值．&lt;/p&gt;
&lt;p&gt;接下来 $M$ 行每行包含 $2$ 或 $4$ 个整数，表示一个操作，具体如下：&lt;/p&gt;
&lt;p&gt;操作 $1$： 格式：&lt;code&gt;1 x y k&lt;/code&gt; 含义：将区间 $[x,y]$ 内每个数加上 $k$；&lt;/p&gt;
&lt;p&gt;操作 $2$： 格式：&lt;code&gt;2 x&lt;/code&gt; 含义：输出第 $x$ 个数的值．&lt;/p&gt;
&lt;h3&gt;输出格式&lt;/h3&gt;
&lt;p&gt;输出包含若干行整数，即为所有操作 $2$ 的结果．&lt;/p&gt;
&lt;h3&gt;数据范围&lt;/h3&gt;
&lt;p&gt;对于 $100%$ 的数据：$1 \leq N, M\le 5\times10^5$，$1 \leq x, y \leq n$，保证任意时刻序列中任意元素的绝对值都不大于 $2^{30}$．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;很简单，只要使用差分来维护就可以了．&lt;/p&gt;
&lt;p&gt;树状数组的主体不用改变，在建树时，由维护数组值改为维护差分值．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for (int i = 1; i &amp;#x3C;= n; i++) {
	cin &gt;&gt; a[i];
	tree.add(i, a[i] - a[i - 1]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于查询第 $x$ 项，等同于区间查询差分数组 $[1, x]$．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;cin &gt;&gt; x;
cout &amp;#x3C;&amp;#x3C; tree.query(1, x) &amp;#x3C;&amp;#x3C; endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于区间修改，设第 $k_1$ 到 $k_2$ 项增加 $x$，等同于 $c[k_1]$ 增加 $x$，$c[k_2+1]$ 减少 $x$．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;cin &gt;&gt; x &gt;&gt; y &gt;&gt; k;
tree.add(x, k);
tree.add(y + 1, -k);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;标程&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

const int N = 5e5 + 100;

int n, m, c[N], a[N], sum[N];

class Tree {
public:
	int query(int ed) {
		int ans = 0;
		for (int i = ed; i &gt;= 1; i -= i &amp;#x26; -i) {
			ans += c[i];
		}
		return ans;
	}
	int query(int l, int r) {
		return query(r) - query(l - 1);
	}
	void add(int t, int k) {
		for (int i = t; i &amp;#x3C;= n; i += i &amp;#x26; -i) {
			c[i] += k;
		}
	}
};

int main() {
	cin &gt;&gt; n &gt;&gt; m;
	Tree tree;
	for (int i = 1; i &amp;#x3C;= n; i++) {
		cin &gt;&gt; a[i];
		tree.add(i, a[i] - a[i - 1]);
	}
	for (int i = 1; i &amp;#x3C;= m; i++) {
		int op, x, y, k;
		cin &gt;&gt; op;
		if (op == 1) {
			cin &gt;&gt; x &gt;&gt; y &gt;&gt; k;
			tree.add(x, k);
			tree.add(y + 1, -k);
		}
		else {
			cin &gt;&gt; x;
			cout &amp;#x3C;&amp;#x3C; tree.query(1, x) &amp;#x3C;&amp;#x3C; endl;
		}
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>博弈论</title><link>https://blog.jerrylab.top/blog/count/game-theory</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/count/game-theory</guid><description>已成定局的游戏</description><pubDate>Sun, 29 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;什么是博弈论&lt;/h1&gt;
&lt;p&gt;博弈论，即组合博弈，指一种游戏，这种游戏一般有以下性质：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;两人游戏，且轮流行动&lt;/li&gt;
&lt;li&gt;对等、完全
双方得知信息对等，双方可以进行的行动完全相同．&lt;/li&gt;
&lt;li&gt;确定性
游戏没有随机性的变量（如骰子、随机数等）&lt;/li&gt;
&lt;li&gt;无平局&lt;/li&gt;
&lt;li&gt;有限性
游戏无法无限继续，最终在一定时间内一定有一名玩家胜利，一名玩家失败．&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;相关定义&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;博弈论：研究&lt;strong&gt;绝对理性&lt;/strong&gt;决策者之间战略互动的数学模型．&lt;/li&gt;
&lt;li&gt;$\color{green}{\mathcal{N}}$ 必胜态（Next player wins）：&lt;strong&gt;至少存在&lt;/strong&gt;一种可能使得对方处于 $\color{red}{\mathcal{P}}$ 的一种游戏状态被称为必胜态．&lt;/li&gt;
&lt;li&gt;$\color{red}{\mathcal{P}}$ 必败态（Previous player wins）：&lt;strong&gt;存在任意&lt;/strong&gt;一种方法让对方处于 $\color{green}{\mathcal{N}}$ 的一种游戏状态被称为必败态．&lt;/li&gt;
&lt;li&gt;终局：处于终局时，有且仅有一名玩家获得立即胜利．
如 FPS 游戏中，仅剩 1 名玩家存活就是一个终局．&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;解决博弈论的一般步骤&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;明确终局&lt;/li&gt;
&lt;li&gt;进行倒推&lt;/li&gt;
&lt;li&gt;寻找规律&lt;/li&gt;
&lt;li&gt;进行数学证明（可以省略）&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;典例：&lt;/h1&gt;
&lt;h2&gt;巴什博弈&lt;/h2&gt;
&lt;p&gt;考虑总共有 $n$ 个物品，两名玩家，每次玩家必须至少取 $1$ 个，最多取 $m$ 个．最后一个物品十分值钱，取到最后一个物品的玩家获胜．&lt;/p&gt;
&lt;p&gt;首先，检查这是不是博弈论：&lt;/p&gt;
&lt;p&gt;| 项目         | 结果                         |
| ---------- | -------------------------- |
| 两人游戏，且轮流行动 | 符合，是两个人轮流行动                |
| 对等、完全      | 符合，每个人只能拿物品，且所有人都知道当前有多少物品 |
| 确定性        | 符合，没有随机因素                  |
| 无平局        | 符合，取到最后一个物品的玩家获胜，不会出现平局    |
| 有限性        | 符合，每次玩家必须至少取 1 个，不会出现取不完   |&lt;/p&gt;
&lt;p&gt;所以下定结论，这是博弈论．&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;明确终局&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;很显然，终局是场上只剩下一个物品时，此时先手玩家 $\color{green}{\mathcal{N}}$．&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;进行倒推&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们尝试推演当 $m=2$ 时的情景，其中，红色节点表示先手玩家 $\color{red}{\mathcal{P}}$，绿色节点代表先手玩家 $\color{green}{\mathcal{N}}$．&lt;/p&gt;
&lt;p&gt;先选定一个起始节点，起始节点不宜太大，太大则会难于分析，需要选取适中的起始节点．在这里，选择起始节点 $n=7$．&lt;/p&gt;
&lt;p&gt;尝试分析起始节点的胜负性．&lt;/p&gt;
&lt;p&gt;首先，列举其所有可能的游戏发展子状态．例：$n=7$ 的子状态是 $n=6$（拿一个） 和 $n=5$（拿两个）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
7(n=7)--&quot;-1&quot;--&gt;6(n=6)
7--&quot;-2&quot;--&gt;5(n=5)

classDef N fill:#67C23A
classDef P fill:#F56C6C
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再列举起始节点所有可能的 游戏发展子状态（这里指 $n\in {5,6}$）的游戏发展子状态，一直下去，知道抵达终局（这里指 $n=1$）．&lt;/p&gt;
&lt;p&gt;列举完之后如下图：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
7(n=7)--&quot;-1&quot;--&gt;6(n=6)
6--&quot;-1&quot;--&gt;5(n=5)
7--&quot;-2&quot;--&gt;5
6--&quot;-2&quot;--&gt;4(n=4)
5--&quot;-1&quot;--&gt;4
4--&quot;-1&quot;--&gt;3(n=3)
5--&quot;-2&quot;--&gt;3
4--&quot;-2&quot;--&gt;2(n=2)
3--&quot;-1&quot;--&gt;2
2--&quot;-1&quot;--&gt;1(n=1)
3--&quot;-2&quot;--&gt;1

classDef N fill:#67C23A
classDef P fill:#F56C6C
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，给每一个节点染色．若处于当前状态时先手玩家 $\color{red}{\mathcal{P}}$ 则将其染成红色，先手玩家 $\color{green}{\mathcal{N}}$ 则将其染成绿色．&lt;/p&gt;
&lt;p&gt;染色规则如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;终局节点根据游戏规则染色（此题中为 $\color{green}{\mathcal{N}}$）&lt;/li&gt;
&lt;li&gt;若一个节点其所有子节点全为 $\color{green}{\mathcal{N}}$，则该节点为 $\color{red}{\mathcal{P}}$，否则该节点为 $\color{green}{\mathcal{N}}$．&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;染色完如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
7(n=7):::N--&quot;-1&quot;--&gt;6(n=6):::P
6--&quot;-1&quot;--&gt;5(n=5):::N
7--&quot;-2&quot;--&gt;5
6--&quot;-2&quot;--&gt;4(n=4):::N
5--&quot;-1&quot;--&gt;4
4--&quot;-1&quot;--&gt;3(n=3):::P
5--&quot;-2&quot;--&gt;3
4--&quot;-2&quot;--&gt;2(n=2):::N
3--&quot;-1&quot;--&gt;2
2--&quot;-1&quot;--&gt;1(n=1):::N
3--&quot;-2&quot;--&gt;1

classDef N fill:#67C23A
classDef P fill:#F56C6C
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;列下表：&lt;/p&gt;
&lt;p&gt;| m   | n   | 局势                           |
| --- | --- | ---------------------------- |
| 2   | 1   | $\color{green}{\mathcal{N}}$ |
| 2   | 2   | $\color{green}{\mathcal{N}}$ |
| 2   | 3   | $\color{red}{\mathcal{P}}$   |
| 2   | 4   | $\color{green}{\mathcal{N}}$ |
| 2   | 5   | $\color{green}{\mathcal{N}}$ |
| 2   | 6   | $\color{red}{\mathcal{P}}$   |
| 2   | 7   | $\color{green}{\mathcal{N}}$ |&lt;/p&gt;
&lt;p&gt;不难发现，当 $m=2$ 时，对于所有的 $n$，当且仅当 $3 \mid n$（$n$ 被 3 整除）时先手是 $\color{red}{\mathcal{P}}$ 的，此外，先手都是 $\color{green}{\mathcal{N}}$ 的．&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数学证明&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设整数 $k$ 满足 $k= n \bmod 3$，当 $k \in {1,2}$ 时，只需要拿取 $k$ 个，就可以转换为 $k=0$ 的情况，从而先手玩家 $\color{red}{\mathcal{P}}$．当 $k=0$ 时，无论拿 1 个还是 2 个，都会转化为 $k \in {1,2}$ 的形式，从而先手玩家必败．&lt;/p&gt;
&lt;p&gt;推广到 $m \neq 2$ 的情况：当 $(m+1) \mid n$ 时，先手是 $\color{red}{\mathcal{P}}$ 的，此外的所有情况，先手都是 $\color{green}{\mathcal{N}}$ 的．&lt;/p&gt;
&lt;h2&gt;Nim 游戏&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- P2197 Nim 游戏
甲，乙两个人玩 Nim 取石子游戏．&lt;/p&gt;
&lt;p&gt;Nim 游戏的规则是这样的：地上有 n 堆石子，每人每次可从任意一堆石子里取出任意多枚石子扔掉，可以取完，不能不取．每次只能从一堆里取．最后没石子可取的人就输了．假如甲是先手，且告诉你这 n 堆石子的数量，他想知道是否存在先手必胜的策略．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;显然，这是博弈论．&lt;/p&gt;
&lt;p&gt;终局条件是场上没有物品时，此时先手玩家 $\color{red}{\mathcal{P}}$．&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;进行倒推&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当 $n=1$ 时，先手玩家只要把这一堆石子取走就可以．故此时，先手玩家 $\color{green}{\mathcal{N}}$．&lt;/p&gt;
&lt;p&gt;当 $n=2$ 时，为了方便表示，将 &lt;code&gt;x_y&lt;/code&gt; 表示为第一堆剩下 &lt;code&gt;x&lt;/code&gt; 个，第二堆剩下 &lt;code&gt;y&lt;/code&gt; 个的情况．$x\geq y$&lt;/p&gt;
&lt;p&gt;显然，当有一堆时 0 时，先手玩家可以直接拿走那一堆，故此时，先手玩家 $\color{green}{\mathcal{N}}$．&lt;/p&gt;
&lt;p&gt;选取 &lt;code&gt;3,2&lt;/code&gt; 为初始节点，列表：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TD
    classDef N fill:#67C23A
    classDef P fill:#F56C6C

    %% 节点声明（顺序也会影响布局）
    0_0:::P
    1_0:::N
    1_1:::P
    2_0:::N
    2_1:::N
    2_2:::P
    3_0:::N
    3_1:::N
    3_2:::N

    %% 从小局势开始连接，逐步到大局势
    %% (1,1) 出发
    1_1--&quot;-1&quot;--&gt;1_0

    %% (2,1) 出发
    2_1--&quot;-1&quot;--&gt;1_1
    2_1--&quot;-2&quot;--&gt;1_0
    2_1--&quot;-1&quot;--&gt;2_0

    %% (2,2) 出发
    2_2--&quot;-1&quot;--&gt;2_1
    2_2--&quot;-2&quot;--&gt;2_0

    %% (3,1) 出发
    3_1--&quot;-1&quot;--&gt;2_1
    3_1--&quot;-1&quot;--&gt;3_0
    3_1--&quot;-2&quot;--&gt;1_1
    3_1--&quot;-3&quot;--&gt;1_0

    %% (3,2) 出发
    3_2--&quot;-1&quot;--&gt;2_2
    3_2--&quot;-1&quot;--&gt;3_1
    3_2--&quot;-2&quot;--&gt;2_1
    3_2--&quot;-2&quot;--&gt;3_0
    3_2--&quot;-3&quot;--&gt;2_0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好像当且仅当 $x=y$ 时为 $\color{red}{\mathcal{P}}$，其他时候都是 $\color{green}{\mathcal{N}}$．&lt;/p&gt;
&lt;p&gt;证明也很简单，所有非 $x=y$ 的状态都可以一步转化为 $x=y$ 这个 $\color{red}{\mathcal{P}}$ 的状态．&lt;/p&gt;
&lt;p&gt;那么，如何推广呢？&lt;/p&gt;
&lt;h3&gt;Nim 和&lt;/h3&gt;
&lt;p&gt;定义一组数 $a_1,a_2,a_3,\cdots, a_n$ 的 &lt;strong&gt;Nim 和&lt;/strong&gt;为 $a_1\oplus a_2\oplus\cdots\oplus a_n$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!info]+ 异或运算 $\oplus$
异或是一种逻辑运算符，其法则为相同为 0，不同为 1，异或可以视为不进位加法运算．&lt;/p&gt;
&lt;p&gt;其真值表如下：&lt;/p&gt;
&lt;p&gt;|$A$|$B$|$A\oplus B$|
|----|----|----|
|0|0|0|
|0|1|1|
|1|0|1|
|1|1|0|&lt;/p&gt;
&lt;p&gt;对于两个数之间的异或运算，需要将它们转化为二进制之后对每一位进行异或．&lt;/p&gt;
&lt;p&gt;计算：$5\oplus 3$&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
5\oplus 3
&amp;#x26;=(101)_2 \oplus (11)_2\
&amp;#x26;=(110)_2\
&amp;#x26;=6\
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;除了交换律、结合律外，异或还拥有一些性质：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;异或具有自反性：$k\oplus a \oplus a = k$&lt;/li&gt;
&lt;li&gt;异或拥有恒等律：$k\oplus 0 = k$&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;那么，当一个状态满足其 &lt;strong&gt;Nim 和&lt;/strong&gt;为 0 是，当前状态为 $\color{red}{\mathcal{P}}$．&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!info]- 证明
把每堆石子数写成二进制（比如 3 写成 &lt;code&gt;11&lt;/code&gt;，5 写成 &lt;code&gt;101&lt;/code&gt;），然后靠右对齐每一位，数一下这一位上一共有几个 1．&lt;/p&gt;
&lt;p&gt;如果每一位上 1 的个数都是偶数，定义这个状态为“平衡”（Nim 和为 0）．如果至少有一位上 1 的个数是奇数，定义这个状态为“不平衡”（Nim 和不为 0）．&lt;/p&gt;
&lt;p&gt;举个例子：两堆，3 和 5&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;3 → &lt;code&gt;011&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;5 → &lt;code&gt;101&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对齐（假设三位）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 0 位（最右）：3 有 1，5 有 1 → 共 2 个 1（偶数）&lt;/li&gt;
&lt;li&gt;第 1 位：3 有 1，5 有 0 → 共 1 个 1（奇数）&lt;/li&gt;
&lt;li&gt;第 2 位：3 有 0，5 有 1 → 共 1 个 1（奇数）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以不平衡（Nim 和不为 0）．&lt;/p&gt;
&lt;p&gt;如果两堆都是 3：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;3 → &lt;code&gt;011&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;3 → &lt;code&gt;011&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每位 1 的个数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 0 位 2 个（偶）&lt;/li&gt;
&lt;li&gt;第 1 位 2 个（偶）&lt;/li&gt;
&lt;li&gt;第 2 位 0 个（偶）→ 平衡（Nim 和为 0）．&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于这个平衡状态，有两条性质：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;性质 1：从平衡状态出发，无论你如何操作，只要操作是合法的，一定会变成不平衡状态．&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为什么？因为你想保持平衡，就得让每一堆的每一二进制位上的 1 的个数都还是偶数．但当你只改一堆时，那堆的某些位从 1 变 0 或 0 变 1，必然导致至少一位上 1 的个数从偶数变成奇数．所以&lt;strong&gt;平衡 → 不平衡&lt;/strong&gt;是必然的．&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;性质 2：从不平衡状态出发，总有一种拿法，让它变回平衡状态．&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;怎么找？找出二进制中“最高那位出现奇数个 1 的位”（如在两堆分别是 3 和 5 时，是右起第三位），然后选一堆在那一位上是 1 的，从这堆里拿走恰好能让所有位都变偶数的石子数．这个操作一定可行（因为那堆原来的石子数足够多）．所以&lt;strong&gt;不平衡 → 平衡&lt;/strong&gt;是可以做到的．&lt;/p&gt;
&lt;p&gt;终局时所有堆都是 0，写出来全是 0，每一位上 1 的个数都是 0（偶数），所以终局是&lt;strong&gt;平衡状态&lt;/strong&gt;．&lt;/p&gt;
&lt;p&gt;而且谁面对终局谁就输了（因为没石子可拿）．&lt;/p&gt;
&lt;p&gt;因此，假设开局是平衡状态（Nim 和为 0）．先手只能把它变成不平衡（性质 1）．后手面对不平衡，可以把它变回平衡（性质 2）．如此循环：平衡→不平衡→平衡→不平衡……因为终局是平衡状态，所以最后一定是&lt;strong&gt;后手&lt;/strong&gt;把局面变成终局（平衡），然后轮到先手时无石子可拿，先手输．&lt;/p&gt;
&lt;p&gt;如果开局是不平衡状态，先手第一步就可以把它变成平衡，然后自己扮演上面“后手”的角色，最终让对手面对终局，所以先手赢．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由以上证明可以推导出博弈论证明的基本方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;明确一个必输终局有的性质，记之为 $R$.&lt;/li&gt;
&lt;li&gt;对于 $R$，如果所有操作都能破环性质 $R$，且所有不符合性质 $R$ 的局面都至少有 1 种操作能使局面具有 $R$ 性质．那么 $R$ 就是所有必输局面的共同性质．&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;阶梯 Nim&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note] 题目简介
共有 $n$ 堆石子，第 $i$ 堆有 $a_i$ 枚石子．两名玩家轮流操作，每次操作中，要么取走第 1 堆石子中的任意多枚，要么将第 $i &gt; 1$ 堆石子中的任意多枚移动到第 $i-1$ 堆，但不能不做任何操作．取走最后一枚石子的玩家取胜．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在问题中，处于偶数堆的石子可以视为不存在，因为当先手将偶数堆的石子向下移移到奇数堆，后手可以再将这些石子再向下又移到偶数堆．直到从第 1 堆拿走．&lt;/p&gt;
&lt;p&gt;因此，影响战局的只会是奇数堆．再次观察，每一次移动奇数堆的石子，都会使其移动到偶数堆上，又因为偶数堆上的石子可以视为没有，所以，每一次移动奇数堆上棋子到偶数堆，等同于将这些石子移出游戏．这样，阶梯 Nim 游戏就相当于以每一个奇数堆为一个有效堆的 Nim 游戏．&lt;/p&gt;
&lt;h1&gt;SG 函数&lt;/h1&gt;
&lt;p&gt;定义一个局面 $x$ 的 &lt;strong&gt;SG 值&lt;/strong&gt; 为 $sg(x)$，则有&lt;/p&gt;
&lt;p&gt;$$
sg(x) = mex{sg(a_i) \mid a_i \rightarrow x}
$$&lt;/p&gt;
&lt;p&gt;，其中，$a_i\rightarrow x$ 的意思是能从局面 $a_i$ 通过一次合法的操作转换为 $x$．&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note] mex
mex 指在一个序列中，最小的、不在这个序列中的自然数．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此，不难得出，当 $sg(x) = 0$，则局面 $x$ 先手必败，否则先手必胜．&lt;/p&gt;
&lt;p&gt;举个例子，就拿巴氏博弈（$m=2$）来说，可列下表：&lt;/p&gt;
&lt;p&gt;| $x$ | $a_i$         | $sg(a_i)$     | $sg(x)$ | 先手局势                         |
| --- | ------------- | ------------- | ------- | ---------------------------- |
| 0   | $\varnothing$ | $\varnothing$ | 0       | $\color{red}{\mathcal{P}}$   |
| 1   | ${0}$       | ${0}$       | 1       | $\color{green}{\mathcal{N}}$ |
| 2   | ${0,1}$     | ${0,1}$     | 2       | $\color{green}{\mathcal{N}}$ |
| 3   | ${1,2}$     | ${1,2}$     | 0       | $\color{red}{\mathcal{P}}$   |
| 4   | ${2,3}$     | ${2,0}$     | 1       | $\color{green}{\mathcal{N}}$ |
| 5   | ${3,4}$     | ${0,1}$     | 2       | $\color{green}{\mathcal{N}}$ |
| 6   | ${4,5}$     | ${1,2}$     | 0       | $\color{red}{\mathcal{P}}$   |&lt;/p&gt;
&lt;p&gt;对于多个相对独立的游戏同时进行（例如：考虑总共 $n$ 堆物品，每堆有 $a_i$ 个物品，两名玩家，每次玩家必须至少取 $1$ 个，最多取 $m$ 个，且不能跨堆取．最终，无法操作的玩家落败．这是多个巴士博弈的游戏同时进行．），则总游戏的 SG 值与其分游戏的 SG 值有如下关系：&lt;/p&gt;
&lt;p&gt;$$
sg(a_1, a_2, \cdots, a_n) = sg(a_1) \oplus sg(a_2) \oplus \cdots \oplus sg(a_n)
$$&lt;/p&gt;
&lt;p&gt;Nim 游戏就是一个典型的多个相对独立的游戏同时进行的游戏，以下是每个独立游戏，即为单堆石子可列表格：&lt;/p&gt;
&lt;p&gt;| $x$           | $a_i$                | $sg(a_i)$            | $sg(x)$ | 先手局势                         |
| ------------- | -------------------- | -------------------- | ------- | ---------------------------- |
| 0             | $\varnothing$        | $\varnothing$        | 0       | $\color{red}{\mathcal{P}}$   |
| 1             | ${0}$              | ${0}$              | 1       | $\color{green}{\mathcal{N}}$ |
| 2             | ${0,1}$            | ${0,1}$            | 2       | $\color{green}{\mathcal{N}}$ |
| 3             | ${0,1,2}$          | ${0,1,2}$          | 3       | $\color{green}{\mathcal{N}}$ |
| 4             | ${0,1,2,3}$        | ${0,1,2,3}$        | 4       | $\color{green}{\mathcal{N}}$ |
| 5             | ${0,1,2,3,4}$      | ${0,1,2,3,4}$      | 5       | $\color{green}{\mathcal{N}}$ |
| $n\mid n &gt; 0$ | ${0,1,\cdots,n-1}$ | ${0,1,\cdots,n-1}$ | $n$     | $\color{green}{\mathcal{N}}$ |&lt;/p&gt;
&lt;p&gt;因此，对于一个具有 $n$ 堆石子，第 $i$ 堆有 $a_i$ 个的 Nim 游戏，整体游戏的 SG 值如下：&lt;/p&gt;
&lt;p&gt;$$
sg(a_i\mid 1\leq i\leq n) = a_1 \oplus a_2\oplus\cdots\oplus a_n
$$&lt;/p&gt;
&lt;p&gt;不难发现，Nim 游戏的 SG 值就等于其 Nim 和，则从侧面证明了 Nim 游戏策略的正确性．&lt;/p&gt;
&lt;h1&gt;解决博弈论问题的一般策略&lt;/h1&gt;
&lt;p&gt;了解了 SG 函数，接下来就可以总结博弈论问题的一般策略．&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;理解题意，提炼出博弈论模型．&lt;/li&gt;
&lt;li&gt;根据规则，手推或者计算机打表求出一些小数据的 SG 值．&lt;/li&gt;
&lt;li&gt;寻找规律．&lt;/li&gt;
&lt;li&gt;根据规律编程解决问题．&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- P4018 Roy&amp;#x26;October 之取石子&lt;/p&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;Roy 和 October 两人在玩一个取石子的游戏．共有 $n$ 个石子，两人每次都只能取 $p^k$ 个（ $p$ 为质数，$k$ 为自然数，且 $p^k$ 小于等于当前剩余石子数），谁取走最后一个石子，谁就赢了．&lt;/p&gt;
&lt;p&gt;现在 October 先取，问她有没有必胜策略．&lt;/p&gt;
&lt;p&gt;若她有必胜策略，输出一行 &lt;code&gt;October wins!&lt;/code&gt;；否则输出一行 &lt;code&gt;Roy wins!&lt;/code&gt;．&lt;/p&gt;
&lt;h3&gt;输入格式&lt;/h3&gt;
&lt;p&gt;第一行一个正整数 $T$，表示测试点组数．&lt;/p&gt;
&lt;p&gt;第 $2$ 行 $\sim$ 第 $T+1$ 行，一行一个正整数 $n$，表示石子个数．&lt;/p&gt;
&lt;h3&gt;输出格式&lt;/h3&gt;
&lt;p&gt;$T$ 行，每行分别为 &lt;code&gt;October wins!&lt;/code&gt; 或 &lt;code&gt;Roy wins!&lt;/code&gt;．&lt;/p&gt;
&lt;h3&gt;数据范围&lt;/h3&gt;
&lt;p&gt;对于 $100%$ 的数据，$1\leq n\leq 5\times 10^7$, $1\leq T\leq 10^5$．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;博弈论模型不难提炼．根据规则，可以写出如下的打表程序：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

int n, sg[10000];
vector&amp;#x3C;int&gt; prime;

int cal_sg(int n) {
	if (n == 0) return 0;
	if (sg[n] != -1) return sg[n];
	set&amp;#x3C;int&gt; s;
	s.insert(cal_sg(n - 1));
	for (auto pr : prime) {
		int now = pr;
		while (now &amp;#x3C;= n) {
			s.insert(cal_sg(n - now));
			now *= pr;
		}
	}
	for (int i = 0; ; i++) {
		if (s.find(i) == s.end()) {
			sg[n] = i;
			return i;
		}
	}
}

int main() {
	memset(sg, -1, sizeof(sg));
	// 预处理质数
	for (int i = 2; i &amp;#x3C;= 100000; i++) {
		bool is_prime = true;
		for (int j = 2; j &amp;#x3C;= sqrt(i); j++) {
			if (i % j == 0)
				is_prime = false;
		}
		if (is_prime) {
			prime.push_back(i);
		}
	}

	for (int i = 1; i &amp;#x3C;= 100; i++) {
		if (cal_sg(i) == 0)
			cout &amp;#x3C;&amp;#x3C; i &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; cal_sg(i) &amp;#x3C;&amp;#x3C; &quot; &quot;;
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里，由于只有 1 个独立游戏，因此，我们只关心 SG 值是否为 0．在这里，我打印出了所有 SG 值为 0 的情况．&lt;/p&gt;
&lt;p&gt;输出如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;6 12 18 24 30 36 42 48 54 60 66 72 78 84 90 96
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不难发现，某一局面的 SG 值为 0 当且仅当 $n$ 是 6 的倍数．&lt;/p&gt;
&lt;p&gt;因此，可以编写程序：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

int main() {
	int T;
	cin &gt;&gt; T;
	while (T--) {
		int n;
		cin &gt;&gt; n;
		if (n % 6 == 0)
			cout &amp;#x3C;&amp;#x3C; &quot;Roy wins!&quot; &amp;#x3C;&amp;#x3C; endl;
		else
			cout &amp;#x3C;&amp;#x3C; &quot;October wins!&quot; &amp;#x3C;&amp;#x3C; endl;
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>并查集</title><link>https://blog.jerrylab.top/blog/ds/dsu</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/ds/dsu</guid><description>快速完成合并和查询</description><pubDate>Mon, 16 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;并查集是一种松散的数据结构，在求连通问题时很好用．&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- 题目：P3367 【模板】并查集&lt;/p&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;如题，现在有一个并查集，你需要完成合并和查询操作．&lt;/p&gt;
&lt;h3&gt;输入格式&lt;/h3&gt;
&lt;p&gt;第一行包含两个整数 $N,M$ ,表示共有 $N$ 个元素和 $M$ 个操作．&lt;/p&gt;
&lt;p&gt;接下来 $M$ 行，每行包含三个整数 $Z_i,X_i,Y_i$ ．&lt;/p&gt;
&lt;p&gt;当 $Z_i=1$ 时，将 $X_i$ 与 $Y_i$ 所在的集合合并．&lt;/p&gt;
&lt;p&gt;当 $Z_i=2$ 时，输出 $X_i$ 与 $Y_i$ 是否在同一集合内，是的输出&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Y&lt;/code&gt; ；否则输出 &lt;code&gt;N&lt;/code&gt; ．&lt;/p&gt;
&lt;h3&gt;输出格式&lt;/h3&gt;
&lt;p&gt;对于每一个 $Z_i=2$ 的操作，都有一行输出，每行包含一个大写字母，为 &lt;code&gt;Y&lt;/code&gt; 或者 &lt;code&gt;N&lt;/code&gt; ．&lt;/p&gt;
&lt;h3&gt;数据范围&lt;/h3&gt;
&lt;p&gt;对于 $100%$ 的数据，$1\le N\le 2\times 10^5$，$1\le M\le 10^6$，$1 \le X_i, Y_i \le N$，$Z_i \in { 1, 2 }$．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;思路&lt;/h1&gt;
&lt;p&gt;首先，我们假定每一个集合都有一个节点作为代表，我们称那个节点是集合内其余节点的为&lt;strong&gt;领导节点&lt;/strong&gt;．&lt;/p&gt;
&lt;p&gt;我们定义一个数组 $f$ ，其中，$f[i]$ 表示的意义为：若 $f[i] = i$，即节点 $i$ 是一个领导节点，则 $f[i]$ 表示节点 $i$ 的领导节点．若 $f[i] \neq i$，则节点 $i$ 的领导节点就是节点 $f[i]$ 的领导节点．&lt;/p&gt;
&lt;p&gt;我们可以使用递归来求一个节点 $a$ 的领导节点，在求的同时，我们可以将求到的值保存到 $f[a]$ 中，下次只需要使用 $O(1)$ 的复杂度就可以求解了，这个行为叫做&lt;strong&gt;路径压缩&lt;/strong&gt;．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 查找节点i的领导节点，并进行路径压缩
int find(int t)
{
	if (f[t] != t)
		f[t] = find(f[t]);
	return f[t];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们很容易就可以想到，对于两个节点 $a$ 和 $b$，如果想要合并，则可以让 $f[b]$ 设定为 $a$ 的领导节点．如果想要判断是否在同一集合中，只需要判断节点 $a$ 和 $b$ 的领导节点是否相同即可．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 查询两个节点是否属于同一节点
bool query(int a, int b)
{
	// 查询两个节点的领导节点是否一致
	return find(a) == find(b);
}
// 合并两个节点
void merge(int a, int b)
{
	int x = find(a), y = find(b);
	if (x != y)
		f[x] = y;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，千万不要忘了初始化，在最开始，所有节点的领导节点都是它自己．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 初始化，一开始所有节点的领导节点都是它自己
void init()
{
	for (int i = 1; i &amp;#x3C;= n; i++)
		f[i] = i;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度分析&lt;/h2&gt;
&lt;p&gt;首先，&lt;code&gt;find&lt;/code&gt; 函数的复杂度看似好像是最坏 $O(n)$ 的，但是由于有了路径压缩，每一次的 &lt;code&gt;find&lt;/code&gt; 都会大大减少下一次 &lt;code&gt;find&lt;/code&gt; 相同节点的时间，最终无限趋近于 $O(1)$，但常常带有较大的常数．&lt;/p&gt;
&lt;p&gt;因此，查询和合并的复杂度也是 $O(1)$，综合一下，$m$ 次操作的总复杂度约为 $O(m\log n)$．&lt;/p&gt;
&lt;h1&gt;标程&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
const int N = 2e5 + 6;
int n, m;

// 定义一个并查集
class DSU
{
private:
	// f[i]表示第i个节点的领导节点
	int f[N];
	// 查找节点i的领导节点，并进行路径压缩
	int find(int t)
	{
		if (f[t] != t)
			f[t] = find(f[t]);
		return f[t];
	}

public:
	DSU()
	{
		this-&gt;init();
	}
	// 初始化，一开始所有节点的领导节点都是它自己
	void init()
	{
		for (int i = 1; i &amp;#x3C;= n; i++)
			f[i] = i;
	}
	// 查询两个节点是否属于同一节点
	bool query(int a, int b)
	{
		// 查询两个节点的领导节点是否一致
		return find(a) == find(b);
	}
	// 合并两个节点
	void merge(int a, int b)
	{
		int x = find(a), y = find(b);
		if (x != y)
			f[x] = y;
	}
} dsu;

int main()
{
	cin &gt;&gt; n &gt;&gt; m;
	dsu.init();
	for (int i = 1; i &amp;#x3C;= m; i++)
	{
		int z, x, y;
		cin &gt;&gt; z &gt;&gt; x &gt;&gt; y;
		if (z == 1)
		{
			dsu.merge(x, y);
		}
		else
		{
			cout &amp;#x3C;&amp;#x3C; (dsu.query(x, y) ? &quot;Y&quot; : &quot;N&quot;) &amp;#x3C;&amp;#x3C; endl;
		}
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>线段树</title><link>https://blog.jerrylab.top/blog/ds/seg</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/ds/seg</guid><description>区间修改和查询</description><pubDate>Mon, 16 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;线段树的使用场景&lt;/h1&gt;
&lt;p&gt;线段树是一种常用来维护 &lt;strong&gt;区间信息&lt;/strong&gt; 的数据结构&lt;/p&gt;
&lt;p&gt;​可以在极快的时间内实现单点修改、区间修改、单点查询、区间查询（区间求和，求区间的最大值，求区间的最小值）等操作&lt;/p&gt;
&lt;p&gt;线段树的时间复杂度为 $O(nlogn)$，但具有很大的常数，一般能够适用于 $10^6$ 数据规模的题．&lt;/p&gt;
&lt;p&gt;线段树的空间复杂度为 $O(n)$，但也具有很大的常数（一般大于 10）&lt;/p&gt;
&lt;h1&gt;基础线段树&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- 题目：P3372 线段树 1&lt;/p&gt;
&lt;p&gt;如题，已知一个数列 ${a_i}$ 共 $n$ 项，你需要进行 $m$ 次下面两种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将某区间每一个数加上 $k$．&lt;/li&gt;
&lt;li&gt;求出某区间每一个数的和．&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于 $100%$ 的数据：$1 \le n, m \le {10}^5$，$a_i,k$ 为正数，且任意时刻数列的和不超过 $2\times 10^{18}$．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;思路&lt;/h2&gt;
&lt;p&gt;​如下图所示，线段树是建立在区间基础上的树，树的每个节点都代表着一段区间【L，R】，之后，在程序中维护每一个节点的信息．&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../pic/seg_tree.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;将所有的节点从 $1$ 到 $2n-1$ 进行标号，则不难得出以下性质：&lt;/p&gt;
&lt;p&gt;对于节点 $t$，设其区间为 $[l,r]$，定义 $mid = (l + r) \div 2$，则其子节点有两个，其一为节点 $2t$，其区间为 $[l, mid]$，其一为节点 $2t+1$，其区间为 $[mid+1, r]$，此外，当且仅当 $l = r$ 时，该节点为叶子节点，没有子节点．&lt;/p&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;h3&gt;建树&lt;/h3&gt;
&lt;p&gt;首先，我们在建树的时候，可以考虑递归建树．&lt;/p&gt;
&lt;p&gt;对于每一个节点，如果它是叶子节点，可以直接通过 &lt;code&gt;a&lt;/code&gt; 数组设置基础信息，如果其下还有节点，则将该区间从中点处分割为两个子区间，并分别进入左右节点的递归建树，最后将两个子节点的信息 &lt;code&gt;pushup&lt;/code&gt; 到父节点，即收集子节点的所有 &lt;code&gt;sum&lt;/code&gt; 之和，来刷新父节点的元素和．&lt;/p&gt;
&lt;p&gt;额外要注意的是，节点数组需要开至少 4 倍，由于线段树的二分特性，所以线段树的&lt;strong&gt;有效节点&lt;/strong&gt;是 $2n-1$ ，但是，在下文的一些操作中，可能会访问到叶子节点的子节点，所以至少开 4 倍，这里建议开 8 倍，为了防止不必要的下标越界．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const int N = 1e5 + 100;
int a[N];
int sum[N * 8], add[N * 8];
// sum[i]表示第i个节点内，所有数之和
// add[i]（标记）表示在第i个节点内，需要对每一个数增加的数量，需要注意的是，在add数组更改时，sum数组也会同步更改，实际上，add数组储存的是该节点的所有子节点所需要增加的数量
void build(int t, int l, int r)
{
	// 需要：使用a数组来初始化sum数组
	// 如是最底层，单个的，则直接赋值
	if (l == r)
	{
		sum[t] = a[l];
		return;
	}
	// 否则，使用递归来为sum赋值
	int mid = (l + r) / 2;
	build(2 * t, l, mid);
	build(2 * t + 1, mid + 1, r);
	// 下层节点的贡献汇入本节点
	pushup(t);
}
// 下层节点的贡献汇入本节点
void pushup(int t)
{
	sum[t] = sum[2 * t] + sum[2 * t + 1];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;区间修改&lt;/h3&gt;
&lt;p&gt;对于一个节点 $t$，我们可以尝试实现一个函数，用于让该节点和目标区间重合的部分进行修改．在此题中，我们需要刷新第 $t$ 个节点，其起屹位置分别为 $l$ 和 $r$，使得区间 $[x,y] \cap [l,r]$ 中的每一个数增加 $k$&lt;/p&gt;
&lt;p&gt;首先的首先，我们需要让节点 $t$ 没有向下处理的所有标记（即记录在 &lt;code&gt;add&lt;/code&gt; 数组中的）都推到节点 $t$ 的两个子节点上，并且要让子节点根据 &lt;code&gt;add&lt;/code&gt; 来更新 &lt;code&gt;sum&lt;/code&gt;，这个操作叫做 &lt;code&gt;pushdown&lt;/code&gt;．更加普遍的，&lt;code&gt;pushdown&lt;/code&gt; 就是让一个节点的所有的&lt;strong&gt;修改&lt;/strong&gt;标记都释放给子节点，并且为子结点结算所有受到影响的&lt;strong&gt;查询&lt;/strong&gt;标记．一个节点的修改标记会在施加后立即对查询标记生效，千万不要重复生效．&lt;/p&gt;
&lt;p&gt;如果节点 $t$ 完全包含在了目标区间中（也就是说，$[l,r]\subseteq[x,y]$，即满足 $l \geq x \land r \leq y$），我们就要对该节点的 &lt;code&gt;sum&lt;/code&gt; 值进行修正，此节点的每一个值都要增加 $k$，则这个节点的 &lt;code&gt;sum&lt;/code&gt; 值就要有几个数就加几个 $k$，即需要增加的值 $\Delta = k(r-l+1)$，按道理来说，我需要将节点 $t$ 的所有子节点都进行更改，但是先放一放，暂且记在 &lt;code&gt;add&lt;/code&gt; 中，作为标记．&lt;/p&gt;
&lt;p&gt;如果节点 $t$ 与目标区间毫不相干（也就是说，$[x,y] \cap [l,r] = \varnothing$，即满足 $l &gt; y \lor r &amp;#x3C; x$），则无需做任何处理，直接 &lt;code&gt;return&lt;/code&gt;．&lt;/p&gt;
&lt;p&gt;否则，表示节点 $t$ 与目标区间有重合部分，也间接表明了节点 $t$ 不是叶子节点，则可以递归处理，最后再将两个子节点的信息 &lt;code&gt;pushup&lt;/code&gt; 到父节点．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void pushdown(int t, int l, int r)
{
	if (add[t])
	{
		// 处理add数组
		add[t * 2] += add[t];
		add[t * 2 + 1] += add[t];
		// 连带处理sum数组
		int mid = (l + r) / 2;
		sum[t * 2] += (mid - l + 1) * add[t];
		sum[t * 2 + 1] += (r - mid) * add[t];
		add[t] = 0;
	}
}
/// @brief 尝试在t=&gt;(l, r)这个范围内为[x,y]增加k
void Modify(int t, int l, int r, int x, int y, int k)
{
	// 先清理干净
	pushdown(t, l, r);
	// 检查能否完全包含
	if (l &gt;= x &amp;#x26;&amp;#x26; r &amp;#x3C;= y)
	{
		add[t] += k;
		sum[t] += k * (r - l + 1);
		return;
	}
	// 检查是否毫不相干
	if (l &gt; y || r &amp;#x3C; x)
	{
		return;
	}
	// 否则，尝试委托给子节点
	int mid = (l + r) / 2;
	Modify(t * 2, l, mid, x, y, k);
	Modify(t * 2 + 1, mid + 1, r, x, y, k);
	// 收获成果
	pushup(t);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;区间查询&lt;/h3&gt;
&lt;p&gt;和修改很类似．&lt;/p&gt;
&lt;p&gt;对于一个节点 $t$，我们可以尝试实现一个返回值为 &lt;code&gt;int&lt;/code&gt; 的函数，用于让该节点和目标区间重合的部分进行修改．在此题中，我们需要查询第 $t$ 个节点，其起屹位置分别为 $l$ 和 $r$，获得区间 $[x,y] \cap [l,r]$ 中的每一个数增加 $k$&lt;/p&gt;
&lt;p&gt;首先的首先，我们需要先进行一次 &lt;code&gt;pushdown&lt;/code&gt;，将自己身上撇干净．&lt;/p&gt;
&lt;p&gt;如果节点 $t$ 完全包含在了目标区间中，我们就需要返回该节点中的元素的和，即 &lt;code&gt;sum[t]&lt;/code&gt;．&lt;/p&gt;
&lt;p&gt;如果节点 $t$ 与目标区间毫不相干，则直接 &lt;code&gt;return 0&lt;/code&gt;．&lt;/p&gt;
&lt;p&gt;否则，表示节点 $t$ 与目标区间有重合部分，也间接表明了节点 $t$ 不是叶子节点，则可以递归处理，最后再将两个子节点的信息相加返回．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/// @brief 尝试得到[l,r]和[x,y]之间的并集的元素之和
int Query(int t, int l, int r, int x, int y)
{
	// 先清理干净
	pushdown(t, l, r);
	// 检查能否完全包含
	if (l &gt;= x &amp;#x26;&amp;#x26; r &amp;#x3C;= y)
	{
		return sum[t];
	}
	// 检查是否毫不相干
	if (l &gt; y || r &amp;#x3C; x)
	{
		return 0;
	}
	// 否则，尝试委托给子节点
	int mid = (l + r) / 2;
	return Query(t * 2, l, mid, x, y) + Query(t * 2 + 1, mid + 1, r, x, y);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;标程&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

const int N = 1e5 + 100;

#define int long long

int n, m, a[N];
int sum[N * 8], add[N * 8];

class SegTree
{
private:
	// 初始化节点
	void build(int t, int l, int r)
	{
		// 需要：使用a数组来初始化sum数组
		// 如是最底层，单个的，则直接赋值
		if (l == r)
		{
			sum[t] = a[l];
			return;
		}
		// 否则，使用递归来为sum赋值
		int mid = (l + r) / 2;
		build(2 * t, l, mid);
		build(2 * t + 1, mid + 1, r);
		// 下层节点的贡献汇入本节点
		pushup(t);
	}
	// 下层节点的贡献汇入本节点
	void pushup(int t)
	{
		sum[t] = sum[2 * t] + sum[2 * t + 1];
	}
	// 将当前节点的add委派给子节点，调整子节点的add &amp;#x26; sum
	void pushdown(int t, int l, int r)
	{
		if (add[t])
		{
			// 处理add数组
			add[t * 2] += add[t];
			add[t * 2 + 1] += add[t];
			// 连带处理sum数组
			int mid = (l + r) / 2;
			sum[t * 2] += (mid - l + 1) * add[t];
			sum[t * 2 + 1] += (r - mid) * add[t];
			add[t] = 0;
		}
	}

public:
	void Init()
	{
		build(1, 1, n);
	}
	/// @brief 尝试在t=&gt;(l, r)这个范围内为[x,y]增加k
	void Modify(int t, int l, int r, int x, int y, int k)
	{
		// 先清理干净
		pushdown(t, l, r);
		// 检查能否完全包含
		if (l &gt;= x &amp;#x26;&amp;#x26; r &amp;#x3C;= y)
		{
			add[t] += k;
			sum[t] += k * (r - l + 1);
			return;
		}
		// 检查是否毫不相干
		if (l &gt; y || r &amp;#x3C; x)
		{
			return;
		}
		// 否则，尝试委托给子节点
		int mid = (l + r) / 2;
		Modify(t * 2, l, mid, x, y, k);
		Modify(t * 2 + 1, mid + 1, r, x, y, k);
		// 收获成果
		pushup(t);
	}
	/// @brief 尝试得到[l,r]和[x,y]之间的并集的元素之和
	int Query(int t, int l, int r, int x, int y)
	{
		// 先清理干净
		pushdown(t, l, r);
		// 检查能否完全包含
		if (l &gt;= x &amp;#x26;&amp;#x26; r &amp;#x3C;= y)
		{
			return sum[t];
		}
		// 检查是否毫不相干
		if (l &gt; y || r &amp;#x3C; x)
		{
			return 0;
		}
		// 否则，尝试委托给子节点
		int mid = (l + r) / 2;
		return Query(t * 2, l, mid, x, y) + Query(t * 2 + 1, mid + 1, r, x, y);
	}
};

signed main()
{
	cin &gt;&gt; n &gt;&gt; m;
	for (int i = 1; i &amp;#x3C;= n; i++)
		cin &gt;&gt; a[i];
	SegTree st;
	st.Init();
	for (int i = 1; i &amp;#x3C;= m; i++)
	{
		int op;
		cin &gt;&gt; op;
		if (op == 1)
		{
			int x, y, k;
			cin &gt;&gt; x &gt;&gt; y &gt;&gt; k;
			st.Modify(1, 1, n, x, y, k);
		}
		if (op == 2)
		{
			int x, y;
			cin &gt;&gt; x &gt;&gt; y;
			cout &amp;#x3C;&amp;#x3C; st.Query(1, 1, n, x, y) &amp;#x3C;&amp;#x3C; endl;
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;带有多种修改和查询操作的线段树&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- 题目：P3373 线段树 2
已知一个数列 $n$ 个数，你需要进行 $q$ 次下面三种操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将区间 $[x,y]$ 每一个数乘上 $k$；&lt;/li&gt;
&lt;li&gt;将区间 $[x,y]$ 每一个数加上 $k$；&lt;/li&gt;
&lt;li&gt;求出某区间每一个数的和对 $m$ 取模的结果．&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于 $100%$ 的数据：$1 \le n \le 10^5$，$1 \le q \le 10^5,1\le k\le 10^4,m = 571373$．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;思路&lt;/h2&gt;
&lt;p&gt;对于多种查询的线段树，对每一种查询，给每一个节点设立一个对应的编辑，然后在 &lt;code&gt;pushup&lt;/code&gt; 和 &lt;code&gt;pushdown&lt;/code&gt; 中分别维护其添加、生效及移除即可．&lt;/p&gt;
&lt;p&gt;对于多种改动的线段树，对每一种改动，先找出改动的优先级，然后再找到改动后对其余两种改动的影响．&lt;/p&gt;
&lt;p&gt;就像此题，此题有两种改动，加法和乘法，如果对于某一个节点，其乘法标记增加了 $k$，那么其加法标记就会增加 $xk$，其中，$x$ 是原本就有的加法标记数量．&lt;/p&gt;
&lt;p&gt;容易发现，乘法对加法的影响很容易确定，但是，如果对于某一个节点，其加法标记增加了 $k$，那么我们就很难确定其乘法标记增加多少．（也许是 $(x+k)\div k$，其中，$x$ 是原本就有的加法标记数量，但却大概率是个小数）&lt;/p&gt;
&lt;p&gt;所以，我们在处理加法标记时，最好没有乘法标记（即乘法标记为 1），因此，乘法的优先级就大于加法．&lt;/p&gt;
&lt;p&gt;与此同时，如果有一个操作“覆盖”，能进行区间的覆盖操作，那么很容易就可以知道，覆盖操作的优先级是大于乘法的．&lt;/p&gt;
&lt;p&gt;因此，我们在 &lt;code&gt;pushup&lt;/code&gt; 中只处理子节点提供给父节点的&lt;strong&gt;查询&lt;/strong&gt;标记，而在 &lt;code&gt;pushdown&lt;/code&gt; 中依照优先级处理父节点传递给子节点的所有标记及其影响，并清空父节点的标记．&lt;/p&gt;
&lt;p&gt;对于此题，每一个节点都有 1 个&lt;strong&gt;查询&lt;/strong&gt;标记 &lt;code&gt;sum&lt;/code&gt; 和 2 个&lt;strong&gt;修改&lt;/strong&gt;标记 &lt;code&gt;add&lt;/code&gt; 与 &lt;code&gt;mul&lt;/code&gt;．&lt;/p&gt;
&lt;h2&gt;标程&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
const int N = 1e5 + 10;
int n, q, m;
int a[N], sum[N * 8], add[N * 8], mul[N * 8];
// sum[i]表示第i个区间内，所有数之和
// add[i]（标记）表示在第i个区间内，需要对每一个数增加的数量，需要注意的是，在add数组更改时，sum数组也会同步更改，实际上，add数组储存的是该节点的所有子节点所需要增加的数量
// mul[i]（标记）与add类似，储存的是该节点的所有子节点所需要乘的倍数
// 此外，由于在访问左节点和右节点时，可能会访问到边界之外的值，所以，建议上述三个数组都要开8倍最大n的大小

// 定义一个类，用来表示线段树
class SegTree
{
private:
	/// @brief 通过t节点的所有子节点的sum值，将节点t的sum值进行刷新（收作业）
	/// @param t 目标节点
	/// @param l 节点t的左边界
	/// @param r 节点t的右边界
	void pushup(int t, int l, int r)
	{
		sum[t] = sum[t * 2] + sum[t * 2 + 1];
	}
	/// @brief 将t节点所有的标记都下放到其子节点，并在其子节点进行标记的结算
	/// @param t 目标节点
	/// @param l 节点t的左边界
	/// @param r 节点t的右边界
	void pushdown(int t, int l, int r)
	{
		// 由于mul[t]的默认值为1，所以不要写成以下代码
		// if (mul[t])
		if (mul[t] != 1) // 如果存在乘法标记
		{
			// 将乘法标记下放
			mul[t * 2] += mul[t];
			mul[t * 2 + 1] += mul[t];

			// 对应的，刷新add标记
			add[t * 2] *= mul[t];
			add[t * 2 + 1] *= mul[t];

			// 对应的，刷新sum标记
			sum[t * 2] *= mul[t];
			sum[t * 2 + 1] *= mul[t];

			// mul[t]恢复默认值
			mul[t] = 1;
		}

		if (add[t]) // 如果存在加法标记
		{
			// 将加法标记下放
			add[t * 2] += add[t];
			add[t * 2 + 1] += add[t];

			// 对应的，刷新sum标记
			int mid = (l + r) / 2;
			sum[t * 2] += add[t] * (mid - l + 1);
			sum[t * 2 + 1] += add[t] * (r - mid);

			// add标记恢复默认
			add[t] = 0;
		}
	}

public:
	/// @brief 构造一棵线段树的节点t
	/// @param t 目标节点
	/// @param l 节点t的左边界
	/// @param r 节点t的右边界
	void Build(int t, int l, int r)
	{
		// 标记恢复默认
		sum[t] = 0;
		add[t] = 0;
		mul[t] = 1;
		// 当节点t是底层节点时，记录sum值
		if (l == r)
		{
			sum[t] = a[l];
			return;
		}
		// 向下递归
		int mid = (l + r) / 2;
		Build(t * 2, l, mid);
		Build(t * 2 + 1, mid + 1, r);
		// 结算结果
		pushup(t, l, r);
	}

	/// @brief 对于节点t，在[x,y]的范围内增加k
	/// @param t 目标节点
	/// @param l 节点t的左边界
	/// @param r 节点t的右边界
	/// @param x 如brief
	/// @param y 如brief
	/// @param k 如brief
	void Add(int t, int l, int r, int x, int y, int k)
	{
		// 清空自己身上的标记
		pushdown(t, l, r);
		// 当节点t完全包含在[x,y]中时
		if (l &gt;= x &amp;#x26;&amp;#x26; r &amp;#x3C;= y)
		{
			// 对节点t进行add操作
			add[t] += k;
			sum[t] += k * (r - l + 1);
			return;
		}
		// 如果搭不上边，那么就不处理
		if (l &gt; y || r &amp;#x3C; x)
		{
			return;
		}
		// 递归向下处理
		int mid = (l + r) / 2;
		Add(t * 2, l, mid, x, y, k);
		Add(t * 2 + 1, mid + 1, r, x, y, k);
		// 结算结果
		pushup(t, l, r);
	}

	/// @brief 对于节点t，在[x,y]的范围内的每个数都乘上k
	/// @param t 目标节点
	/// @param l 节点t的左边界
	/// @param r 节点t的右边界
	/// @param x 如brief
	/// @param y 如brief
	/// @param k 如brief
	void Mul(int t, int l, int r, int x, int y, int k)
	{
		// 清空自己身上的标记
		pushdown(t, l, r);
		// 当节点t完全包含在[x,y]中时
		if (l &gt;= x &amp;#x26;&amp;#x26; r &amp;#x3C;= y)
		{
			// 对节点t进行add操作
			mul[t] *= k;
			add[t] *= k; // 可省略
			sum[t] *= k;
			return;
		}
		// 如果搭不上边，那么就不处理
		if (l &gt; y || r &amp;#x3C; x)
		{
			return;
		}
		// 递归向下处理
		int mid = (l + r) / 2;
		Mul(t * 2, l, mid, x, y, k);
		Mul(t * 2 + 1, mid + 1, r, x, y, k);
		// 结算结果
		pushup(t, l, r);
	}

	/// @brief 对于节点t，得到在[x,y]的范围内的每个数的和
	/// @param t 目标节点
	/// @param l 节点t的左边界
	/// @param r 节点t的右边界
	/// @param x 如brief
	/// @param y 如brief
	int Query(int t, int l, int r, int x, int y)
	{
		// 清空自己身上的标记
		pushdown(t, l, r);
		// 当节点t完全包含在[x,y]中时
		if (l &gt;= x &amp;#x26;&amp;#x26; r &amp;#x3C;= y)
		{
			// 计数
			return sum[t];
		}
		// 如果搭不上边，那么就记0
		if (l &gt; y || r &amp;#x3C; x)
		{
			return 0;
		}
		// 递归向下处理
		int mid = (l + r) / 2;
		return Query(t * 2, l, mid, x, y) + Query(t * 2 + 1, mid + 1, r, x, y);
	}
};

int main()
{
	cin &gt;&gt; n &gt;&gt; q &gt;&gt; m;
	for (int i = 1; i &amp;#x3C;= n; i++)
	{
		cin &gt;&gt; a[i];
	}

	SegTree st;

	st.Build(1, 1, n);

	for (int i = 1; i &amp;#x3C;= q; i++)
	{
		int op;
		int x, y, k;
		cin &gt;&gt; op;
		switch (op)
		{
		case 1:
			cin &gt;&gt; x &gt;&gt; y &gt;&gt; k;
			st.Mul(1, 1, n, x, y, k);
			break;

		case 2:
			cin &gt;&gt; x &gt;&gt; y &gt;&gt; k;
			st.Add(1, 1, n, x, y, k);
			break;

		default:
			cin &gt;&gt; x &gt;&gt; y;
			cout &amp;#x3C;&amp;#x3C; st.Query(1, 1, n, x, y) &amp;#x3C;&amp;#x3C; endl;
			break;
		}
	}

	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;复杂线段树——辅助标记的添加与应用&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- 题目：P2572 序列操作&lt;/p&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;lxhgww 最近收到了一个 $01$ 序列，序列里面包含了 $n$ 个数，下标从 $0$ 开始．这些数要么是 $0$，要么是 $1$，现在对于这个序列有五种变换操作和询问操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0 l r&lt;/code&gt; 把 $[l, r]$ 区间内的所有数全变成 $0$；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 l r&lt;/code&gt; 把 $[l, r]$ 区间内的所有数全变成 $1$；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2 l r&lt;/code&gt; 把 $[l,r]$ 区间内的所有数全部取反，也就是说把所有的 $0$ 变成 $1$，把所有的 $1$ 变成 $0$；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;3 l r&lt;/code&gt; 询问 $[l, r]$ 区间内总共有多少个 $1$；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;4 l r&lt;/code&gt; 询问 $[l, r]$ 区间内最多有多少个连续的 $1$．&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于每一种询问操作，lxhgww 都需要给出回答，聪明的程序员们，你们能帮助他吗？&lt;/p&gt;
&lt;h3&gt;输入格式&lt;/h3&gt;
&lt;p&gt;第一行两个正整数 $n,m$，表示序列长度与操作个数．&lt;/p&gt;
&lt;p&gt;第二行包括 $n$ 个数，表示序列的初始状态．&lt;/p&gt;
&lt;p&gt;接下来 $m$ 行，每行三个整数，表示一次操作．&lt;/p&gt;
&lt;h3&gt;输出格式&lt;/h3&gt;
&lt;p&gt;对于每一个询问操作，输出一行一个数，表示其对应的答案．&lt;/p&gt;
&lt;h3&gt;数据范围&lt;/h3&gt;
&lt;p&gt;对于 $100%$ 的数据，$1\le n,m \le 10^5$．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们想要实现对于每一种修改，每一种查询都能进行变化．&lt;/p&gt;
&lt;p&gt;对于覆盖，两种查询都很好求，但对于取反，第二种查询就比较复杂．&lt;/p&gt;
&lt;p&gt;首先，为了计算出区间内连续 1 的个数，我们需要知道其子区间&lt;strong&gt;从左边开始数的连续的 1 的数量&lt;/strong&gt;&lt;code&gt;lv1&lt;/code&gt; 以及&lt;strong&gt;从右边开始数的连续的 1 的数量&lt;/strong&gt;&lt;code&gt;rv1&lt;/code&gt;，那么，其区间内连续 1 的个数 &lt;code&gt;con1&lt;/code&gt; 的转换公式为 $fa.con1 = max(left.con1, right.con1, left.rv1 + right.lv1)$，其中节点 $fa$ 的左右子节点分别为 $left$ 和 $right$ ．&lt;/p&gt;
&lt;p&gt;但是，对于 &lt;code&gt;lv1&lt;/code&gt; 和 &lt;code&gt;rv1&lt;/code&gt;，甚至 &lt;code&gt;con1&lt;/code&gt;，也很难知道其取反后的结果，所以，我们为这两个辅助标记再定义辅助标记，我们需要知道其子区间&lt;strong&gt;从左边开始数的连续的 0 的数量&lt;/strong&gt;&lt;code&gt;lv0&lt;/code&gt;、&lt;strong&gt;从右边开始数的连续的 0 的数量&lt;/strong&gt;&lt;code&gt;rv0&lt;/code&gt; 以及&lt;strong&gt;区间内 0 的个数&lt;/strong&gt;&lt;code&gt;con0&lt;/code&gt;，所以，对于一次取反操作，我们只需要分别交 &lt;code&gt;lv1&lt;/code&gt; 和 &lt;code&gt;rv1&lt;/code&gt;，&lt;code&gt;lv0&lt;/code&gt; 和 &lt;code&gt;rv0&lt;/code&gt;，&lt;code&gt;con1&lt;/code&gt; 和 &lt;code&gt;con0&lt;/code&gt; 就可以了．&lt;/p&gt;
&lt;p&gt;对于这么多的标记，我们可以定义一个结构体来进行储存，并且重载 + 运算符来表示合并．&lt;/p&gt;
&lt;p&gt;最后，我们只需要修改 &lt;code&gt;pushdown&lt;/code&gt;、&lt;code&gt;pushup&lt;/code&gt; 以及所有的改动函数来初始化和维护这些变量就可以了．&lt;/p&gt;
&lt;h2&gt;标程&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;

using namespace std;

const int N = 1e5 + 100;

struct Node
{
	// 修改标记（懒标记）
	bool rev;  // 是否取反
	int cover; // 覆盖成何值
	bool need; // 是否需要覆盖

	// 查询标记
	int sum;  // 记录当前区间内1的数量
	int len;  // 记录当前区间的长度
	int con1; // 记录当前最长的连续的一段1
	int con0; // 记录当前最长的连续的一段0
	int lv1;  // 记录从左边开始数的连续的1的数量
	int rv1;  // 记录从右边开始数的连续的1的数量
	int lv0;  // 记录从左边开始数的连续的0的数量
	int rv0;  // 记录从右边开始数的连续的0的数量
	friend Node operator+(Node a, Node b)
	{
		Node c;
		c.len = a.len + b.len;
		c.sum = a.sum + b.sum;
		c.con1 = max(max(a.con1, b.con1), a.rv1 + b.lv1);
		c.con0 = max(max(a.con0, b.con0), a.rv0 + b.lv0);
		c.lv1 = (a.sum == a.len) ? a.len + b.lv1 : a.lv1;
		c.lv0 = (a.sum == 0) ? a.len + b.lv0 : a.lv0;
		c.rv1 = (b.sum == b.len) ? b.len + a.rv1 : b.rv1;
		c.rv0 = (b.sum == 0) ? b.len + a.rv0 : b.rv0;
		return c;
	}
};

Node node[N * 4]; // 线段树节点数组，4倍空间足够维护区间

struct SegTree
{
	// pushup: 合并左右儿子的区间信息，更新父节点
	void pushup(int t, int l, int r)
	{
		// 先保存父节点的懒标记（need/cover/rev），再合并左右儿子的区间信息
		Node c = node[t];
		node[t] = node[t * 2] + node[t * 2 + 1]; // 区间合并
		node[t].need = c.need;
		node[t].cover = c.cover;
		node[t].rev = c.rev;
	}

	// hcover: 把节点t的区间全部赋成v（0或1），并设置懒标记
	void hcover(int t, int v)
	{
		node[t].need = true; // 标记该区间需要被覆盖
		node[t].cover = v;	 // 记录要覆盖成的值
		node[t].rev = 0;	 // 覆盖后不再需要取反
		// 直接更新区间信息
		node[t].sum = node[t].len * v;
		node[t].con1 = node[t].len * v;
		node[t].con0 = node[t].len * !v;
		node[t].lv1 = node[t].len * v;
		node[t].lv0 = node[t].len * !v;
		node[t].rv1 = node[t].len * v;
		node[t].rv0 = node[t].len * !v;
	}

	// hrev: 把节点t的区间全部取反，并设置懒标记
	void hrev(int t)
	{
		node[t].rev = !node[t].rev;				 // 取反标记异或
		node[t].sum = node[t].len - node[t].sum; // 1变0，0变1
		swap(node[t].con1, node[t].con0);		 // 连续1和连续0交换
		swap(node[t].lv1, node[t].lv0);
		swap(node[t].rv1, node[t].rv0);
	}

	// pushdown: 下传懒标记，将父节点的标记传递给左右儿子
	void pushdown(int t, int l, int r)
	{
		if (l == r)
			return; // 叶子节点无需下传
		// 如果有区间赋值标记，优先下传
		if (node[t].need)
		{
			hcover(t * 2, node[t].cover);
			hcover(t * 2 + 1, node[t].cover);
			node[t].need = false;
			node[t].cover = 0;
		}
		// 如果有区间取反标记，下传
		if (node[t].rev)
		{
			hrev(t * 2);
			hrev(t * 2 + 1);
			node[t].rev = 0;
		}
	}

	// Build: 建树，递归初始化每个节点的区间信息
	void Build(int t, int l, int r, int *v)
	{
		if (l == r)
		{
			// 叶子节点，直接赋值
			node[t].need = false;
			node[t].cover = 0;
			node[t].rev = 0;
			node[t].len = 1;
			node[t].sum = v[l];
			node[t].con1 = v[l];
			node[t].con0 = !v[l];
			node[t].lv1 = v[l];
			node[t].lv0 = !v[l];
			node[t].rv1 = v[l];
			node[t].rv0 = !v[l];
			return;
		}
		int mid = (l + r) / 2;
		Build(t * 2, l, mid, v);
		Build(t * 2 + 1, mid + 1, r, v);
		pushup(t, l, r);
	}

	// 区间赋值操作：将[x, y]区间全部赋成k（0或1）
	void Cover(int t, int l, int r, int x, int y, int k)
	{
		pushdown(t, l, r); // 递归前先下传懒标记
		if (x &amp;#x3C;= l &amp;#x26;&amp;#x26; r &amp;#x3C;= y)
		{
			hcover(t, k); // 完全覆盖，直接赋值
			return;
		}
		if (y &amp;#x3C; l || r &amp;#x3C; x)
			return; // 无交集，直接返回
		int mid = (l + r) / 2;
		Cover(t * 2, l, mid, x, y, k);
		Cover(t * 2 + 1, mid + 1, r, x, y, k);
		pushup(t, l, r); // 回溯时合并区间信息
	}

	// 区间取反操作：将[x, y]区间全部取反
	void Rev(int t, int l, int r, int x, int y)
	{
		pushdown(t, l, r); // 递归前先下传懒标记
		if (x &amp;#x3C;= l &amp;#x26;&amp;#x26; r &amp;#x3C;= y)
		{
			hrev(t); // 完全覆盖，直接取反
			return;
		}
		if (y &amp;#x3C; l || r &amp;#x3C; x)
			return; // 无交集，直接返回
		int mid = (l + r) / 2;
		Rev(t * 2, l, mid, x, y);
		Rev(t * 2 + 1, mid + 1, r, x, y);
		pushup(t, l, r); // 回溯时合并区间信息
	}

	// 区间查询：返回[x, y]区间的信息
	Node Query(int t, int l, int r, int x, int y)
	{
		pushdown(t, l, r); // 递归前先下传懒标记
		if (x &amp;#x3C;= l &amp;#x26;&amp;#x26; r &amp;#x3C;= y)
			return node[t]; // 完全覆盖，直接返回
		int mid = (l + r) / 2;
		if (y &amp;#x3C;= mid)
			return Query(t * 2, l, mid, x, y); // 只在左子树
		if (x &gt; mid)
			return Query(t * 2 + 1, mid + 1, r, x, y); // 只在右子树
		// 跨越左右子树，合并结果
		Node q1 = Query(t * 2, l, mid, x, y);
		Node q2 = Query(t * 2 + 1, mid + 1, r, x, y);
		return q1 + q2;
	}
};

int a[N];

// 此处的read()是快读函数，可以极快速的读入一个整数
int read()
{
	int s = 0, c = getchar(), a = 0;
	while (!isdigit(c))
		s |= c == &apos;-&apos;, c = getchar();
	while (isdigit(c))
		a = a * 10 + c - &apos;0&apos;, c = getchar();
	return s ? -a : a;
}

int main()
{
	int n, m;
	// 读入序列长度n和操作次数m
	n = read(), m = read();
	SegTree tree;
	// 读入初始01序列，下标从1开始，a[1]~a[n]
	for (int i = 1; i &amp;#x3C;= n; ++i)
		a[i] = read();
	// 建树，根节点编号为1，维护区间[1, n]的信息
	tree.Build(1, 1, n, a);
	// 依次处理m个操作
	for (int i = 1; i &amp;#x3C;= m; ++i)
	{
		int op, l, r;
		// 读入操作类型op和区间[l, r]，题目输入下标从0开始，这里+1转为1开始
		op = read(), l = read() + 1, r = read() + 1;
		if (op == 0)
			// 区间[l, r]赋值为0
			tree.Cover(1, 1, n, l, r, 0);
		if (op == 1)
			// 区间[l, r]赋值为1
			tree.Cover(1, 1, n, l, r, 1);
		if (op == 2)
			// 区间[l, r]取反
			tree.Rev(1, 1, n, l, r);
		if (op == 3)
		{
			// 查询区间[l, r]内1的个数，输出结果
			Node res = tree.Query(1, 1, n, l, r);
			cout &amp;#x3C;&amp;#x3C; res.sum &amp;#x3C;&amp;#x3C; endl;
		}
		if (op == 4)
		{
			// 查询区间[l, r]内最长连续1的长度，输出结果
			Node res = tree.Query(1, 1, n, l, r);
			cout &amp;#x3C;&amp;#x3C; res.con1 &amp;#x3C;&amp;#x3C; endl;
		}
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>堆与平衡树</title><link>https://blog.jerrylab.top/blog/ds/heap-and-tree</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/ds/heap-and-tree</guid><description>查询序列中的信息</description><pubDate>Mon, 16 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;堆&lt;/h1&gt;
&lt;p&gt;堆用来解决一些需要快速排序，并且每次只关心排序后最值的问题．&lt;/p&gt;
&lt;p&gt;C++ STL 中的优先队列就是用堆实现的．&lt;/p&gt;
&lt;p&gt;堆是特殊的二叉树，有一个最重要的性质：对于堆中的任何一个节点，都保证其子节点的值严格大于父节点．&lt;/p&gt;
&lt;p&gt;例如下面一张图就符合堆的性质&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A((3)) --&gt; B((5))
    A --&gt; C((6))
    B --&gt; D((8))
    B --&gt; E((10))
    C --&gt; F((11))
    C ~~~ Empty(( ))
    style Empty fill:#fff,stroke-width:0px
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;堆的操作&lt;/h2&gt;
&lt;h3&gt;节点添加&lt;/h3&gt;
&lt;p&gt;设需要在上图中添加一个元素 4，我们可以这么做：&lt;/p&gt;
&lt;p&gt;首先，我们先将要添加的节点放到最下一层最右边的叶子之后．如果最下一层已满，就新增一层．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A((3)) --&gt; B((5))
    A --&gt; C((6))
    B --&gt; D((8))
    B --&gt; E((10))
    C --&gt; F((11))
    C --&gt; G((4))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而后，尝试让新的节点和其父节点交换位置．在这个例子中，显然，4 是比 6 要小的，为了保证堆的性质，我们要让 4 和 6 换个位置．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A((3)) --&gt; B((5))
    A --&gt; C((4))
    B --&gt; D((8))
    B --&gt; E((10))
    C --&gt; F((11))
    C --&gt; G((6))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显然，3 是比 4 要小的，已经满足了堆的性质，这一次添加操作就此结束．&lt;/p&gt;
&lt;h3&gt;堆的删除&lt;/h3&gt;
&lt;p&gt;堆支持删除最顶上的一个节点．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A((3)) --&gt; B((5))
    A --&gt; C((4))
    B --&gt; D((8))
    B --&gt; E((10))
    C --&gt; F((11))
    C --&gt; G((6))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了对上面一张图的顶点（即值为 3 的点）进行删除，我们可以这样做：&lt;/p&gt;
&lt;p&gt;首先，把这个点和最后一个点交换位置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A((6)) --&gt; B((5))
    A --&gt; C((4))
    B --&gt; D((8))
    B --&gt; E((10))
    C --&gt; F((11))
    C --&gt; G((3))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再把最后一个点删掉&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A((6)) --&gt; B((5))
    A --&gt; C((4))
    B --&gt; D((8))
    B --&gt; E((10))
    C --&gt; F((11))
    C ~~~ Empty(( ))
    style Empty fill:#fff,stroke-width:0px
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，由于这个图不满足堆的定义，所以我们从他的子节点中选择一个最小的，和根节点交换，作为新的根节点．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A((4)) --&gt; B((5))
    A --&gt; C((6))
    B --&gt; D((8))
    B --&gt; E((10))
    C --&gt; F((11))
    C ~~~ Empty(( ))
    style Empty fill:#fff,stroke-width:0px
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后一直交换，直到其所有子节点都大于父节点，或没有子节点为止．&lt;/p&gt;
&lt;h3&gt;查询最小值&lt;/h3&gt;
&lt;p&gt;这个最简单，此时堆顶的元素最小，没有比它更小的了．&lt;/p&gt;
&lt;h2&gt;复杂度分析&lt;/h2&gt;
&lt;p&gt;节点添加操作平均是 $O(\log n)$ 的，但最坏 $O(n)$．&lt;/p&gt;
&lt;p&gt;查询最小值 $O(1)$．&lt;/p&gt;
&lt;p&gt;删除操作平均是 $O(\log n)$ 的，但最坏 $O(n)$．&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- 题目：P3378 【模板】堆&lt;/p&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;给定一个数列，初始为空，请支持下面三种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;给定一个整数 $x$，请将 $x$ 加入到数列中．&lt;/li&gt;
&lt;li&gt;输出数列中最小的数．&lt;/li&gt;
&lt;li&gt;删除数列中最小的数（如果有多个数最小，只删除 $1$ 个）．&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;输入格式&lt;/h3&gt;
&lt;p&gt;第一行是一个整数，表示操作的次数 $n$．
接下来 $n$ 行，每行表示一次操作．每行首先有一个整数 $op$ 表示操作类型．&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若 $op = 1$，则后面有一个整数 $x$，表示要将 $x$ 加入数列．&lt;/li&gt;
&lt;li&gt;若 $op = 2$，则表示要求输出数列中的最小数．&lt;/li&gt;
&lt;li&gt;若 $op = 3$，则表示删除数列中的最小数．如果有多个数最小，只删除 $1$ 个．&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;输出格式&lt;/h3&gt;
&lt;p&gt;对于每个操作 $2$，输出一行一个整数表示答案．&lt;/p&gt;
&lt;h3&gt;数据范围&lt;/h3&gt;
&lt;p&gt;对于 $100%$ 的数据，保证 $1 \leq n \leq 10^6$，$1 \leq x \lt 2^{31}$，$op \in {1, 2, 3}$．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;标程&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
#define fa(x) (x / 2)
#define lson(x) (2 * x)
#define rson(x) (2 * x + 1)

const int N = 1e6 + 100;
const int root = 1;

int n;

class Heap
{
private:
    int size = 0;
    int a[N];

public:
    Heap()
    {
        memset(a, 0x3f, sizeof(a));
    }
    void push(int x)
    {
        a[++size] = x;
        int now = size;
        while (now &gt; root)
        {
            if (a[now] &amp;#x3C; a[fa(now)])
            {
                swap(a[now], a[fa(now)]);
                now = fa(now);
            }
            else
                break;
        }
    }
    int query()
    {
        return a[root];
    }
    void remove()
    {
        int now = root;
        a[root] = a[size];
        size--;
        while (lson(now) &amp;#x3C;= size)
        {
            int minn = lson(now);
            if (rson(now) &amp;#x3C;= size &amp;#x26;&amp;#x26; a[rson(now)] &amp;#x3C; a[minn])
                minn = rson(now);
            if (a[now] &gt; a[minn])
            {
                swap(a[now], a[minn]);
                now = minn;
            }
            else
                break;
        }
    }
};

signed main()
{
    cin &gt;&gt; n;
    Heap heap;
    for (int i = 1; i &amp;#x3C;= n; i++)
    {
        int op;
        cin &gt;&gt; op;
        if (op == 1)
        {
            int x;
            cin &gt;&gt; x;
            heap.push(x);
        }
        if (op == 2)
            cout &amp;#x3C;&amp;#x3C; heap.query() &amp;#x3C;&amp;#x3C; endl;
        if (op == 3)
            heap.remove();
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;二叉搜索树&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- 题目：P3369【模板】普通平衡树&lt;/p&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;您需要动态地维护一个可重集合 $M$，并且提供以下操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;向 $M$ 中插入一个数 $x$．&lt;/li&gt;
&lt;li&gt;从 $M$ 中删除一个数 $x$（若有多个相同的数，应只删除一个）．&lt;/li&gt;
&lt;li&gt;查询 $M$ 中有多少个数比 $x$ 小，并且将得到的答案加一．&lt;/li&gt;
&lt;li&gt;查询如果将 $M$ 从小到大排列后，排名位于第 $x$ 位的数．&lt;/li&gt;
&lt;li&gt;查询 $M$ 中 $x$ 的前驱（前驱定义为小于 $x$，且最大的数）．&lt;/li&gt;
&lt;li&gt;查询 $M$ 中 $x$ 的后继（后继定义为大于 $x$，且最小的数）．&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于操作 $3,5,6$，&lt;strong&gt;不保证&lt;/strong&gt;当前可重集中存在数 $x$．&lt;/p&gt;
&lt;p&gt;对于操作 $5,6$，保证答案一定存在．&lt;/p&gt;
&lt;h3&gt;输入格式&lt;/h3&gt;
&lt;p&gt;第一行为 $n$，表示操作的个数，下面 $n$ 行每行有两个数 $\text{opt}$ 和 $x$，$\text{opt}$ 表示操作的序号（$1 \leq \text{opt} \leq 6$）．&lt;/p&gt;
&lt;h3&gt;输出格式&lt;/h3&gt;
&lt;p&gt;对于操作 $3,4,5,6$ 每行输出一个数，表示对应答案．&lt;/p&gt;
&lt;h3&gt;数据范围&lt;/h3&gt;
&lt;p&gt;对于 $100%$ 的数据，$1\le n \le 10^5$，$|x| \le 10^7$．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;二叉搜索树是一种特殊的树，其特点为：对于一个节点，其左子树上的节点总是小于父节点，而其右子树上的节点总是大于父节点．&lt;/p&gt;
&lt;p&gt;例如下面一张图就符合二叉搜索树的性质&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A((10)) --&gt; B((8))
    A --&gt; C((15))
    B --&gt; D((5))
    B --&gt; E((9))
    C --&gt; F((11))
    C ~~~ Empty(( ))
    style Empty fill:#fff,stroke-width:0px
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于每一个节点，我们需要记录一些信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;v&lt;/code&gt;：该节点的值．&lt;/li&gt;
&lt;li&gt;&lt;code&gt;num&lt;/code&gt;：为了让二叉搜索树能够存储重复的数据，我们将数值相同的节点合并．&lt;code&gt;num&lt;/code&gt; 表示树中共有 &lt;code&gt;num&lt;/code&gt; 个数值为 &lt;code&gt;v&lt;/code&gt; 的值，即该节点是由 &lt;code&gt;num&lt;/code&gt; 个节点合并而来的．&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lson&lt;/code&gt;：该节点的左子节点．&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rson&lt;/code&gt;：该节点的右子节点．&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size&lt;/code&gt;：以本节点为根的子树中一共有多少个数（此处不是节点，是因为每一个节点可能储存多个相同的数），包括根节点．&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Node {
	int v;
	int num;
	int size;
	int lson;
	int rson;

	Node() : v(0), num(0), size(0), lson(0), rson(0) {}

	Node(int v, int num = 1) {
		init(v, num);
	}

	void init(int v, int num) {
		this-&gt;v = v;
		this-&gt;num = num;
		this-&gt;size = num;
	}

	// 左节点
	int lsize();
	// 右节点
	int rsize();
	// 计算大小
	void push_up() {
		size = lsize() + rsize() + num;
	}
} node[N];

int Node::lsize() {
	return node[lson].size;
}
int Node::rsize() {
	return node[rson].size;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;定义一个类用来存放操作函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Tree {
public:
	// 当前根节点
	int ROOT = 0;
	// 当前树中节点数量
	int tot = 0;
	queue&amp;#x3C;int&gt; free_node;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;插入&lt;/h2&gt;
&lt;p&gt;插入很简单，只需要从根节点向下遍历一遍，找到最适合的位置．&lt;/p&gt;
&lt;p&gt;对于一个节点 $k$，要在以该节点为根的子树中插入一个元素 $x$，有两种情况&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若节点 $k$ 是空的，则将 $x$ 放在 $k$ 的位置．&lt;/li&gt;
&lt;li&gt;若节点 $k$ 非空，则
&lt;ul&gt;
&lt;li&gt;$x = k$，将节点 $k$ 的 &lt;code&gt;num&lt;/code&gt; 值增加即可．&lt;/li&gt;
&lt;li&gt;$x\lt k$，可以转化为在节点为 $k_{lson}$ 为根的子树下插入元素 $x$．&lt;/li&gt;
&lt;li&gt;$x \gt k$，可以转化为在节点为 $k_{rson}$ 为根的子树下插入元素 $x$．&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后，在统计 &lt;code&gt;size&lt;/code&gt; 时，可以使用类似于线段树的 &lt;code&gt;pushup&lt;/code&gt; 方法，将左右子树的 &lt;code&gt;size&lt;/code&gt; 相加即可．&lt;/p&gt;
&lt;p&gt;时间复杂度为 $O(h)$，$h$ 为高度，最坏 $O(n)$&lt;/p&gt;
&lt;p&gt;由于有删除有新增，所以需要一个函数来动态管理内存．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Tree {
public:
	// 当前空的节点
	// 会在删除时，将删除后空出来的位置记录
	queue&amp;#x3C;int&gt; free_node;
	// 获得下一个空位
	int next_node_pos() {
		if (!free_node.empty()) {
			// 有空位
			int u = free_node.front();
			free_node.pop();
			return u;
		}
		// 开辟新的位子
		return ++tot;
	}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下代码使用递归进行插入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void insert(int x, int&amp;#x26; now) { // now是int&amp;#x26;，&amp;#x26;不能忘记
	if (now == 0)
	{
		// 当前节点是空的
		// 申请新的空间
		now = next_node_pos();
		// 登记挂载到父节点上
		node[now] = Node(x);
	}
	else {
		if (node[now].v == x) {
			node[now].num++;
		}
		else if (node[now].v &amp;#x3C; x) {
			insert(x, node[now].rson);
		}
		else {
			insert(x, node[now].lson);
		}
	}
	node[now].push_up();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;查询排名&lt;/h2&gt;
&lt;p&gt;依旧的，进行一次搜索．&lt;/p&gt;
&lt;p&gt;如果你想要查询 $x$ 在树中是排第几位的，那么只需要算出比 $x$ 小的数有几个，而后，将结果加一，即为排名．&lt;/p&gt;
&lt;p&gt;特殊的，如果 $x$ 不存在，则假想 $x$ 存在，即定义 $x$ 的排名为比 $x$ 小的数的个数加一．&lt;/p&gt;
&lt;p&gt;为了统计比 $x$ 小的数有几个，如果你当前遍历到节点 $k$，那么有以下三种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$x \lt k$，比 $x$ 小的数一定都在 $k$ 的左子树上，可以舍弃右子树，继续遍历左子树．&lt;/li&gt;
&lt;li&gt;$x=k$，其实和 $x \lt k$ 的情况是一样的．&lt;/li&gt;
&lt;li&gt;$x \gt k$，此时左子树中的一定都是比 $x$ 小的，右子树中有部分．将左子树和 $k$ 计入答案，继续遍历右子树．&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int rank(int x) {
	int now = ROOT;
	int ans = 0;
	while (now) {
		if (x &amp;#x3C;= node[now].v) {
			now = node[now].lson;
		}
		else if (x &gt; node[now].v) {
			ans += node[now].lsize() + node[now].num;
			now = node[now].rson;
		}
	}
	return ans + 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;查询第几大&lt;/h2&gt;
&lt;p&gt;为了查询第 $x$ 大的数，如果你当前遍历到节点 $k$，若定义 $k$ 的左子树大小为 $k_{lsize}$，那么有以下三种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$x \le k_{lsize}$，即第 $x$ 大的数在 $k$ 的左子树，只需要继续遍历左子树即可．&lt;/li&gt;
&lt;li&gt;$k_{lsize} \gt x \le k_{lsize} + k_{num}$，即第 $x$ 大的数就是 $k$&lt;/li&gt;
&lt;li&gt;否则，第 $x$ 大的数在 $k$ 的右子树，将 $x$ 减去左子树和 $k$ 点的数量后，继续遍历右子树．&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int kth(int x) {
	int now = ROOT;
	while (now) {
		if (node[now].lsize() &gt;= x) {
			now = node[now].lson;
		}
		else if (node[now].lsize() + node[now].num &gt;= x) {
			return node[now].v;
		}
		else {
			x -= node[now].lsize() + node[now].num;
			now = node[now].rson;
		}
	}
	return -1; // 不会抵达
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;查询前驱/后继&lt;/h2&gt;
&lt;p&gt;计算前驱（即最大的比 $x$ 小的数），可以得知 $x$ 的排名后，再将排名减一后用 &lt;code&gt;kth&lt;/code&gt; 查询即可．&lt;/p&gt;
&lt;p&gt;计算后继（即最小的比 $x$ 大的数），显然，后继一定比原数至少大 1，可以通过得知 $x+1$ 的排名后，通过这个排名来用 &lt;code&gt;kth&lt;/code&gt; 查询．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int pre(int x) {
	return kth(rank(x) - 1);
}
int succ(int x) {
	return kth(rank(x + 1));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;删除&lt;/h2&gt;
&lt;p&gt;为了删除 $x$，如果你当前遍历到节点 $k$，那么有以下三种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$x \lt k$，$x$ 一定在 $k$ 的左子树上，可以继续遍历左子树．&lt;/li&gt;
&lt;li&gt;$x \gt k$，$x$ 一定在 $k$ 的右子树上，可以继续遍历右子树．&lt;/li&gt;
&lt;li&gt;$x=k$
&lt;ul&gt;
&lt;li&gt;如果 $k_{num} \ne 1$，即还有多个此种数字，那么直接让 $k_{num}-1$ 即可．&lt;/li&gt;
&lt;li&gt;否则，就需要删除这个节点
&lt;ul&gt;
&lt;li&gt;如果节点 $k$ 没有子节点，直接删&lt;/li&gt;
&lt;li&gt;如果节点 $k$ 只有左子节点或右子节点，那么将其子节点覆盖父节点．&lt;/li&gt;
&lt;li&gt;如果 $k$ 有两个子节点，那么，寻找 $k$ 的前驱或后继，将其&lt;strong&gt;覆盖&lt;/strong&gt;节点 $k$（注意，一定是&lt;strong&gt;覆盖&lt;/strong&gt;而非&lt;strong&gt;替换&lt;/strong&gt;，因为如果替换就会破坏平衡树的大小规则）．可以证明，节点 $k$ 的前驱和后继分别在 $k$ 的左子树和右子树上．并且，$k$ 的前驱和后继必然大于原来 $k$ 的左子树上的节点且小于原来 $k$ 右子树上的节点．&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void remove(int x, int&amp;#x26; now) {
	if (!now) return;
	if (x &amp;#x3C; node[now].v) {
		remove(x, node[now].lson);
	}
	else if (x &gt; node[now].v) {
		remove(x, node[now].rson);
	}
	else {
		// 找到要删除的节点
		if (node[now].num &gt; 1) {
			// 节点有大于1个元素，直接删
			node[now].num--;
		}
		else if (node[now].lson == 0 &amp;#x26;&amp;#x26; node[now].rson == 0) {
			// 节点左右都没有节点，直接删
			// 由于删除之后这个位置会空缺，所以记录
			free_node.push(now);
			// 在父节点登记
			now = 0;
		}
		else if (node[now].lson == 0) {
			// 节点没有左节点
			// 由于删除之后这个位置会空缺，所以记录
			free_node.push(now);
			// 在父节点登记
			now = node[now].rson;
		}
		else if (node[now].rson == 0) {
			// 节点没有右节点，和左节点相同
			free_node.push(now);
			now = node[now].lson;
		}
		else {
			// 节点又有左节点，又有右节点
			// 获取后继的值
			int suc = node[now].rson;
			// 获取值对应的下标
			while (node[suc].lson) suc = node[suc].lson;
			// 覆盖
			node[now].v = node[suc].v;
			node[now].num = node[suc].num;
			// 防止删不掉
			node[suc].num = 1;
			// 在右节点范围进行删除
			remove(node[suc].v, node[now].rson);
		}
	}
	// 由于对节点进行了改动，需要重新计算大小
	// 一定要避免对节点0的push_up
	if (now) node[now].push_up();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;时间复杂度&lt;/h2&gt;
&lt;p&gt;每一项操作的时间复杂度都是 $O(h)$，其中 $h$ 是深度，共有 $n$ 次操作，综合最坏 $O(n^2)$．&lt;/p&gt;
&lt;h2&gt;标程&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

const int N = 1e5 + 100;
int n;

struct Node {
	int v;
	int num;
	int size;
	int lson;
	int rson;

	Node() : v(0), num(0), size(0), lson(0), rson(0) {}

	Node(int v, int num = 1) {
		init(v, num);
	}

	void init(int v, int num) {
		this-&gt;v = v;
		this-&gt;num = num;
		this-&gt;size = num;
	}

	int lsize();
	int rsize();
	void push_up() {
		size = lsize() + rsize() + num;
	}
} node[N];

int Node::lsize() {
	return node[lson].size;
}
int Node::rsize() {
	return node[rson].size;
}

class Tree {
public:
	int ROOT = 0;
	int tot = 0;
	queue&amp;#x3C;int&gt; free_node;

	int next_node_pos() {
		if (!free_node.empty()) {
			int u = free_node.front();
			free_node.pop();
			return u;
		}
		return ++tot;
	}

	void insert(int x, int&amp;#x26; now) {
		if (now == 0)
		{
			now = next_node_pos();
			node[now] = Node(x);
		}
		else {
			if (node[now].v == x) {
				node[now].num++;
			}
			else if (node[now].v &amp;#x3C; x) {
				insert(x, node[now].rson);
			}
			else {
				insert(x, node[now].lson);
			}
		}
		node[now].push_up();
	}

	int rank(int x) {
		int now = ROOT;
		int ans = 0;
		while (now) {
			if (x &amp;#x3C;= node[now].v) {
				now = node[now].lson;
			}
			else if (x &gt; node[now].v) {
				ans += node[now].lsize() + node[now].num;
				now = node[now].rson;
			}
		}
		return ans + 1;
	}

	int kth(int x) {
		int now = ROOT;
		while (now) {
			if (node[now].lsize() &gt;= x) {
				now = node[now].lson;
			}
			else if (node[now].lsize() + node[now].num &gt;= x) {
				return node[now].v;
			}
			else {
				x -= node[now].lsize() + node[now].num;
				now = node[now].rson;
			}
		}
		return -1;
	}

	int pre(int x) {
		return kth(rank(x) - 1);
	}
	int succ(int x) {
		return kth(rank(x + 1));
	}

	void remove(int x, int&amp;#x26; now) {
		if (!now) return;
		if (x &amp;#x3C; node[now].v) {
			remove(x, node[now].lson);
		}
		else if (x &gt; node[now].v) {
			remove(x, node[now].rson);
		}
		else {
			if (node[now].num &gt; 1) {
				node[now].num--;
			}
			else if (node[now].lson == 0 &amp;#x26;&amp;#x26; node[now].rson == 0) {
				free_node.push(now);
				now = 0;
			}
			else if (node[now].lson == 0) {
				free_node.push(now);
				now = node[now].rson;
			}
			else if (node[now].rson == 0) {
				free_node.push(now);
				now = node[now].lson;
			}
			else {
				int suc = node[now].rson;
				while (node[suc].lson) suc = node[suc].lson;
				node[now].v = node[suc].v;
				node[now].num = node[suc].num;
				node[suc].num = 1;
				remove(node[suc].v, node[now].rson);
			}
		}
		if (now) node[now].push_up();
	}
};

int main() {
	cin &gt;&gt; n;
	Tree tree;
	for (int i = 1; i &amp;#x3C;= n; i++) {
		int opt, x;
		cin &gt;&gt; opt &gt;&gt; x;
		if (opt == 1) {
			tree.insert(x, tree.ROOT);
		}
		if (opt == 2) {
			tree.remove(x, tree.ROOT);
		}
		if (opt == 3) {
			cout &amp;#x3C;&amp;#x3C; tree.rank(x) &amp;#x3C;&amp;#x3C; endl;
		}
		if (opt == 4) {
			cout &amp;#x3C;&amp;#x3C; tree.kth(x) &amp;#x3C;&amp;#x3C; endl;
		}
		if (opt == 5) {
			cout &amp;#x3C;&amp;#x3C; tree.pre(x) &amp;#x3C;&amp;#x3C; endl;
		}
		if (opt == 6) {
			cout &amp;#x3C;&amp;#x3C; tree.succ(x) &amp;#x3C;&amp;#x3C; endl;
		}
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;二叉平衡树&lt;/h1&gt;
&lt;p&gt;显然最坏 $O(n^2)$ 的时间复杂度无法通过，所以需要一些优化．&lt;/p&gt;
&lt;h2&gt;替罪羊树&lt;/h2&gt;
&lt;p&gt;很显然，之所以会有最坏 $O(n^2)$，是因为失衡，使其退化成了接近链的形式．&lt;/p&gt;
&lt;p&gt;因此，只需要在插入之后进行一次重构，就可以解决这个问题．&lt;/p&gt;
&lt;p&gt;究竟在何时重构呢？在这里要引入一个常数 $\alpha \in (0.5, 1)$（常取 0.75），当左子树节点数 $siz_1$ 和右子树节点数 $siz_2$ 和当前字数总节点数 $siz_0$ 满足 $max(siz_1, siz_2) &gt; \alpha siz_0$ 时，需要重构．&lt;/p&gt;
&lt;p&gt;为了实现，我们需要定义：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Node {
	int v;
	int num;
	int size;
	// 用于计算当前字数中有多少个节点
	int tot; // [!code highlight]
	int lson;
	int rson;

	Node() : v(0), num(0), size(0), lson(0), rson(0), tot(0) {}

	Node(int v, int num = 1) {
		init(v, num);
	}

	void init(int v, int num) {
		this-&gt;v = v;
		this-&gt;num = num;
		this-&gt;size = num;
		this-&gt;tot = 1;
	}

	int lsize();
	int rsize();
	int ltot();
	int rtot();
	void push_up() {
		size = lsize() + rsize() + num;
		// 注意比对size和tot之间更新的差异
		tot = ltot() + rtot() + 1; // [!code highlight]
	}
} node[N];

int Node::lsize() {
	return node[lson].size;
}
int Node::rsize() {
	return node[rson].size;
}
int Node::ltot() {
	return node[lson].tot;
}
int Node::rtot() {
	return node[rson].tot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在插入时，我们需要在回溯时判断这个节点是否平衡，如果不平衡，将其记录．&lt;/p&gt;
&lt;p&gt;每一次插入，只有一个节点，即最上面的一个不平衡的节点需要被视为重构的根节点．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int insert(int x, int&amp;#x26; now) {
	// 记录重构的节点
	// 此处的null是一个常数，代表没有节点需要重构
	// 可以将其定义为-1或INT_MIN
	int ref_node = null; // [!code highlight]
	if (now == 0)
	{
		now = next_node_pos();
		node[now] = Node(x);
	}
	else {
		if (node[now].v == x) {
			node[now].num++;
		}
		else if (node[now].v &amp;#x3C; x) {
			ref_node = insert(x, node[now].rson);
		}
		else {
			ref_node = insert(x, node[now].lson);
		}
	}
	node[now].push_up();
	// 如果该节点不平衡，就记录该节点．
	// [!code highlight:3]
	if (max(node[now].ltot(), node[now].rtot()) &gt; node[now].tot * ALPHA) 
		ref_node = now;
	return ref_node;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;main&lt;/code&gt; 函数中处理 &lt;code&gt;insert&lt;/code&gt; 的返回值．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;if (opt == 1) {
	int ref_node = tree.insert(x, tree.ROOT);
	if (ref_node != null) {
		tree.rebuild(ref_node);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当需要重构时，调用重构函数．重构主要需要完成以下几件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;获取中序遍历结果．&lt;/li&gt;
&lt;li&gt;删除子树中的所有节点．&lt;/li&gt;
&lt;li&gt;用像线段树一样的方法去重建树．&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;public:
	void rebuild(int root) {
		// 获取中序遍历结果，并删除节点
		// 这里pair的第一个参数表示值（v），第二个参数表示数量（num）
		vector&amp;#x3C;pair&amp;#x3C;int, int&gt; &gt; list;
		flatten(node[root].lson, list);
		list.push_back({ node[root].v, node[root].num });
		flatten(node[root].rson, list);
		// 重建树
		build(root, list, 0, list.size() - 1);
	}

private:
	// 展平操作
	// 将以root为根的子树中序遍历并完全删除，结果储存到list中
	// 这里list是引用类型，&amp;#x26;不要忘了加
	void flatten(int root, vector&amp;#x3C;pair&amp;#x3C;int, int&gt; &gt;&amp;#x26; list) {
		// 防止根节点是空的
		if (root == 0) return;
		// 展平左节点
		flatten(node[root].lson, list);
		// 加入自身
		list.push_back({ node[root].v, node[root].num });
		// 展平右节点
		flatten(node[root].rson, list);
		// 删除自身
		free_node.push(root);
	}
	// 使用list中[l,r]的部分以root为根节点重建
	// 此处的list一定要是引用类型，因为如果是值传递，频繁的拷贝vector会导致复杂度急剧上升
	void build(int root, const vector&amp;#x3C;pair&amp;#x3C;int, int&gt; &gt;&amp;#x26; list, int l, int r) {
		// 获取root应为的值和数量，即list[mid]
		int mid = (l + r) / 2;
		node[root] = Node(list[mid].first, list[mid].second);
	
		if (l &amp;#x3C;= mid - 1)
		{
			// 构建左子树
			node[root].lson = next_node_pos();
			build(node[root].lson, list, l, mid - 1);
		}
		if (mid + 1 &amp;#x3C;= r)
		{
			// 构建右子树
			node[root].rson = next_node_pos();
			build(node[root].rson, list, mid + 1, r);
		}
	
		// 由于构建操作更改了节点，所以需要push_up
		node[root].push_up();
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;时间复杂度&lt;/h3&gt;
&lt;p&gt;由于使用了重构，所以单次操作时间复杂度是均摊 $O(\log n)$ 的，总时间复杂度 $O(n\log n)$&lt;/p&gt;
&lt;h3&gt;标程&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

const int N = 1e5 + 100;
const double ALPHA = 0.75;
const int null = INT_MIN;
int n;

struct Node {
	int v;
	int num;
	int size;
	int tot;
	int lson;
	int rson;

	Node() : v(0), num(0), size(0), lson(0), rson(0), tot(0) {}

	Node(int v, int num = 1) {
		init(v, num);
	}

	void init(int v, int num) {
		this-&gt;v = v;
		this-&gt;num = num;
		this-&gt;size = num;
		this-&gt;tot = 1;
	}

	int lsize();
	int rsize();
	int ltot();
	int rtot();
	void push_up() {
		size = lsize() + rsize() + num;
		tot = ltot() + rtot() + 1;
	}
} node[N];

int Node::lsize() {
	return node[lson].size;
}
int Node::rsize() {
	return node[rson].size;
}
int Node::ltot() {
	return node[lson].tot;
}
int Node::rtot() {
	return node[rson].tot;
}

class Tree {
public:
	int ROOT = 0;
	int tot = 0;
	queue&amp;#x3C;int&gt; free_node;

	int next_node_pos() {
		if (!free_node.empty()) {
			int u = free_node.front();
			free_node.pop();
			return u;
		}
		return ++tot;
	}

	int insert(int x, int&amp;#x26; now) {
		int ref_node = null;
		if (now == 0)
		{
			now = next_node_pos();
			node[now] = Node(x);
		}
		else {
			if (node[now].v == x) {
				node[now].num++;
			}
			else if (node[now].v &amp;#x3C; x) {
				ref_node = insert(x, node[now].rson);
			}
			else {
				ref_node = insert(x, node[now].lson);
			}
		}
		node[now].push_up();
		if (max(node[now].ltot(), node[now].rtot()) &gt; node[now].tot * ALPHA) {
			ref_node = now;
		}
		return ref_node;
	}

	int rank(int x) {
		int now = ROOT;
		int ans = 0;
		while (now) {
			if (x &amp;#x3C;= node[now].v) {
				now = node[now].lson;
			}
			else if (x &gt; node[now].v) {
				ans += node[now].lsize() + node[now].num;
				now = node[now].rson;
			}
		}
		return ans + 1;
	}

	int kth(int x) {
		int now = ROOT;
		while (now) {
			if (node[now].lsize() &gt;= x) {
				now = node[now].lson;
			}
			else if (node[now].lsize() + node[now].num &gt;= x) {
				return node[now].v;
			}
			else {
				x -= node[now].lsize() + node[now].num;
				now = node[now].rson;
			}
		}
		return -1;
	}

	int pre(int x) {
		return kth(rank(x) - 1);
	}
	int succ(int x) {
		return kth(rank(x + 1));
	}

	void remove(int x, int&amp;#x26; now) {
		if (!now) return;
		if (x &amp;#x3C; node[now].v) {
			remove(x, node[now].lson);
		}
		else if (x &gt; node[now].v) {
			remove(x, node[now].rson);
		}
		else {
			if (node[now].num &gt; 1) {
				node[now].num--;
			}
			else if (node[now].lson == 0 &amp;#x26;&amp;#x26; node[now].rson == 0) {
				free_node.push(now);
				now = 0;
			}
			else if (node[now].lson == 0) {
				free_node.push(now);
				now = node[now].rson;
			}
			else if (node[now].rson == 0) {
				free_node.push(now);
				now = node[now].lson;
			}
			else {
				int suc = node[now].rson;
				while (node[suc].lson) suc = node[suc].lson;
				node[now].v = node[suc].v;
				node[now].num = node[suc].num;
				node[suc].num = 1;
				remove(node[suc].v, node[now].rson);
			}
		}
		if (now) node[now].push_up();
	}

	void rebuild(int root) {
		vector&amp;#x3C;pair&amp;#x3C;int, int&gt; &gt; list;
		flatten(node[root].lson, list);
		list.push_back({ node[root].v, node[root].num });
		flatten(node[root].rson, list);
		build(root, list, 0, list.size() - 1);
	}
private:
	void flatten(int root, vector&amp;#x3C;pair&amp;#x3C;int, int&gt; &gt;&amp;#x26; list) {
		if (root == 0) return;
		flatten(node[root].lson, list);
		list.push_back({ node[root].v, node[root].num });
		flatten(node[root].rson, list);
		free_node.push(root);
	}
	void build(int root, const vector&amp;#x3C;pair&amp;#x3C;int, int&gt; &gt;&amp;#x26; list, int l, int r) {
		int mid = (l + r) / 2;
		node[root] = Node(list[mid].first, list[mid].second);

		if (l &amp;#x3C;= mid - 1)
		{
			node[root].lson = next_node_pos();
			build(node[root].lson, list, l, mid - 1);
		}
		if (mid + 1 &amp;#x3C;= r)
		{
			node[root].rson = next_node_pos();
			build(node[root].rson, list, mid + 1, r);
		}

		node[root].push_up();
	}
};

int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin &gt;&gt; n;
	Tree tree;
	for (int i = 1; i &amp;#x3C;= n; i++) {
		int opt, x;
		cin &gt;&gt; opt &gt;&gt; x;
		if (opt == 1) {
			int ref_node = tree.insert(x, tree.ROOT);
			if (ref_node != null) {
				tree.rebuild(ref_node);
			}
		}
		if (opt == 2) {
			tree.remove(x, tree.ROOT);
		}
		if (opt == 3) {
			cout &amp;#x3C;&amp;#x3C; tree.rank(x) &amp;#x3C;&amp;#x3C; endl;
		}
		if (opt == 4) {
			cout &amp;#x3C;&amp;#x3C; tree.kth(x) &amp;#x3C;&amp;#x3C; endl;
		}
		if (opt == 5) {
			cout &amp;#x3C;&amp;#x3C; tree.pre(x) &amp;#x3C;&amp;#x3C; endl;
		}
		if (opt == 6) {
			cout &amp;#x3C;&amp;#x3C; tree.succ(x) &amp;#x3C;&amp;#x3C; endl;
		}
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Meet in the middle</title><link>https://blog.jerrylab.top/blog/thought/mitm</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/thought/mitm</guid><description>一种特殊的搜索</description><pubDate>Wed, 04 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Meet in the middle，或称折半搜索，是一种搜索的优化方法．&lt;/p&gt;
&lt;p&gt;折半搜索用于普通的搜索数据量过大，可以将时间复杂度中的变量减半．&lt;/p&gt;
&lt;p&gt;使用折半搜索，有以下几步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确认一个起点，一个终点&lt;/li&gt;
&lt;li&gt;由起点开始搜索，但是不要搜索完全，只需要进行&lt;strong&gt;一半&lt;/strong&gt;的决策，记录下状态．&lt;/li&gt;
&lt;li&gt;由终点开始反着搜索，倒着进行一半的决策，如果所得的状态在第二步中有记录，那么则可以将其作为答案（或答案的一个贡献）．&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;使用朴素搜索，如果有 $n$ 个决策点，每个决策点有 $m$ 个选择，则其时间复杂度为 $O(m^n)$，如果使用折半搜索，那么时间复杂度会降为 $O(m^{\frac{n}{2}})$&lt;/p&gt;
&lt;h1&gt;例题&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;有 $n$ 个灯泡，每个灯泡有亮和暗两种状态，初始每一盏灯都是暗的．&lt;/p&gt;
&lt;p&gt;你可以对一盏灯进行一次操作，使得其&lt;strong&gt;改变状态&lt;/strong&gt;（即亮的灯变暗，暗的灯变量）．与此同时，每一盏灯还有它的&lt;strong&gt;伴随灯&lt;/strong&gt;，当你对灯泡 $i$ 进行操作时，所有灯泡 $i$ 的伴随灯 $p_{i,j}$ 都会改变状态，但是 $p_{i,j}$ 的伴随灯不会改变状态．&lt;/p&gt;
&lt;p&gt;你需要将所有灯都变成亮的，求最小操作次数．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;输入格式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第一行一个正整数 $n$，表示一共的灯泡数量&lt;/p&gt;
&lt;p&gt;接下来 $n$ 行，第 $i$ 行先有一个整数 $k_i$，表示灯泡 $i$ 的伴随灯数量，接下来 $k_i$ 个整数，表示灯泡 $i$ 的所有伴随灯编号．&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输出格式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一行一个整数，表示最小的操作数．&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据约束&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对于 $50%$ 的数据，$n \leq 18$&lt;/p&gt;
&lt;p&gt;对于 $100%$ 的数据，$n \leq 35$，且对于所有的 $i$（$1\leq i\leq n$），都有 $k_i \lt n$&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;50pts 的做法很简单，只需要从全部都是暗的开始 DFS 搜索，对于每一盏灯，都有操作和不操作两种状态（对一盏灯操作两次等同于没操作），这样依次对每一盏灯进行决策．可以传递一个参数 $s$，$s$ 的二进制的从低到高第 $i$ 为表示第 $i$ 盏灯亮（用 1 表示）还是灭（用 0 表示），直到所有灯都决策完后，如果 $s$ 的每一位都是 1，那么将这种开关方式算作合法的开关方式，将操作次数记录．这种解决问题方式的复杂度是 $O(2^n)$&lt;/p&gt;
&lt;p&gt;那么如何解决这道题呢？使用折中搜索．&lt;/p&gt;
&lt;p&gt;首先，确定起点和终点，起点是&lt;strong&gt;全部灯都是暗的&lt;/strong&gt;，此时 $s$ 的所有位都是 0．终点是&lt;strong&gt;全部灯都是亮的&lt;/strong&gt;，此时 $s$ 的所有位都是 1．&lt;/p&gt;
&lt;p&gt;而后，我们进行第一次 DFS，在这第一次中，我们将前 $\lceil\frac{n}{2}\rceil$ 个灯泡进行决策，将决策结果记录在一个 map 中，map 的键是状态 $s$，map 的值是前 $\lceil\frac{n}{2}\rceil$ 个灯泡中选择操作的数量．&lt;/p&gt;
&lt;p&gt;最后，我们从全部灯都是亮的的状态进行一次倒着的 DFS，我们将后 $\lfloor\frac{n}{2}\rfloor$ 个灯泡进行决策，将决策后的状态 $s$ 放在 map 中比较，看有没有对应的状态，如果有，将前后两次的操作次数相加，作为一种可行的方案，最后在所有可行的方案中，取操作数量最少的一个．&lt;/p&gt;
&lt;p&gt;这种想法的时间复杂度是 $O(2^{\frac{n}{2}})$，可以通过．&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>取模与逆元</title><link>https://blog.jerrylab.top/blog/math/mod</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/math/mod</guid><description>关于取模的数学知识</description><pubDate>Wed, 04 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;定义&lt;/h1&gt;
&lt;p&gt;对于两个数 $a$ 和 $b$，$a,b\in \mathbb{Z}$，则如果存在整数 $c$，$d$，使得 $bc+d=a$，且保证 $0\leq \lvert{d}\rvert \lt b$，则称 $c$ 是 $a$ 除以 $b$ 的商，$d$ 是 $a$ 除以 $b$ 的余数，也被称为 $a$ 模 $b$ 的结果，记作 $a\bmod{b}=d$．&lt;/p&gt;
&lt;p&gt;对于两个整数 $a$ 和 $b$，如果有整数 $p$，使得 $a\bmod{p} = b\bmod{p}$，则称 $a$ 和 $b$ 对模 $p$ &lt;strong&gt;同余&lt;/strong&gt;，记作 $a\equiv b\pmod p$，像这样的式子叫做&lt;strong&gt;同余方程&lt;/strong&gt;．&lt;/p&gt;
&lt;p&gt;对于两个整数 $a$ 和 $p$，如果 $a\equiv 0\pmod p$，则 $p$ &lt;strong&gt;整除&lt;/strong&gt; $a$，记作 $p \mid a$ ．&lt;/p&gt;
&lt;h1&gt;性质&lt;/h1&gt;
&lt;h2&gt;同余的基本性质&lt;/h2&gt;
&lt;p&gt;对于两个整数 $a,b$，如果有 $a\equiv b\pmod p$，则可得 $p \mid a - b$&lt;/p&gt;
&lt;p&gt;证明：&lt;/p&gt;
&lt;p&gt;由已知可得：&lt;/p&gt;
&lt;p&gt;$$
\left{ \begin{aligned} &amp;#x26; k_1p+n=a \ &amp;#x26; k_2p+n=b\end{aligned} \right.
$$&lt;/p&gt;
&lt;p&gt;其中，&lt;/p&gt;
&lt;p&gt;$$
n,k_1,k_2\in\mathbb{Z}
$$&lt;/p&gt;
&lt;p&gt;所以：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
a-b
&amp;#x26;=k_1p+n-k_2p-n\
&amp;#x26;=k_1p-k_2p\
&amp;#x26;=p(k_1-k_2)\
&amp;#x26;\Rightarrow a-b\equiv 0\pmod p \
&amp;#x26;\Rightarrow p \mid a-b
\end{aligned}
$$&lt;/p&gt;
&lt;h2&gt;加法性质&lt;/h2&gt;
&lt;p&gt;对于整数 $a,b,c,d,p$，如果有：&lt;/p&gt;
&lt;p&gt;$$
\left{ \begin{aligned} &amp;#x26; a\equiv b\pmod p \ &amp;#x26; c\equiv d\pmod p\end{aligned} \right.
$$&lt;/p&gt;
&lt;p&gt;则有：&lt;/p&gt;
&lt;p&gt;$$
(a+c) \equiv (b+d)\pmod p
$$&lt;/p&gt;
&lt;p&gt;证明：&lt;/p&gt;
&lt;p&gt;由已知条件可得：&lt;/p&gt;
&lt;p&gt;$$
\left{ \begin{aligned} &amp;#x26; k_1p+n=a \ &amp;#x26; k_2p+n=b \ &amp;#x26; k_3p+m=c \ &amp;#x26; k_4p+m=d\end{aligned} \right.
$$&lt;/p&gt;
&lt;p&gt;其中，&lt;/p&gt;
&lt;p&gt;$$
n,m,k_1,k_2,k_3,k_4\in\mathbb{Z}
$$&lt;/p&gt;
&lt;p&gt;所以：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*} (a+c)&amp;#x26;=k_1p+n+k_3p+m\&amp;#x26;=(k_1+k_3)p+m+n\(a+c)\bmod p &amp;#x26;= m+n\end{align*}
$$&lt;/p&gt;
&lt;p&gt;同理：&lt;/p&gt;
&lt;p&gt;$$
(b+d)\bmod p=m+n
$$&lt;/p&gt;
&lt;p&gt;所以：$(a+c) \equiv (b+d)\pmod p$&lt;/p&gt;
&lt;h3&gt;加法性质的引申性质&lt;/h3&gt;
&lt;p&gt;对于整数 $a,b,c,d,p$，如果有：&lt;/p&gt;
&lt;p&gt;$$
\left{ \begin{aligned} &amp;#x26; a\equiv b\pmod p \ &amp;#x26; c\equiv d\pmod p\end{aligned} \right.
$$&lt;/p&gt;
&lt;p&gt;则有：&lt;/p&gt;
&lt;p&gt;$$
a-c \equiv b-d\pmod p
$$&lt;/p&gt;
&lt;p&gt;$$
a\times c \equiv b\times d\pmod p
$$&lt;/p&gt;
&lt;p&gt;$$
a^c \equiv b^d\pmod p
$$&lt;/p&gt;
&lt;h2&gt;移项和约分&lt;/h2&gt;
&lt;p&gt;同余方程允许&lt;strong&gt;移项&lt;/strong&gt;．&lt;/p&gt;
&lt;p&gt;对于整数 $a,b,c,p$，如果有：&lt;/p&gt;
&lt;p&gt;$$
a\equiv b+c\pmod p
$$&lt;/p&gt;
&lt;p&gt;则有：&lt;/p&gt;
&lt;p&gt;$$
a-c \equiv b\pmod p
$$&lt;/p&gt;
&lt;p&gt;证明：&lt;/p&gt;
&lt;p&gt;$$
\begin{align*}
a&amp;#x26;\equiv b+c\pmod p\
a-c&amp;#x26;\equiv b+c-c\pmod p\
a-c&amp;#x26;\equiv b\pmod p
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;同余方程允许&lt;strong&gt;约分&lt;/strong&gt;．&lt;/p&gt;
&lt;p&gt;对于整数 $a,b,p$，如果有：&lt;/p&gt;
&lt;p&gt;$$
a\equiv b\pmod p
$$&lt;/p&gt;
&lt;p&gt;且 $c$ 是 $a, b, p$ 的公因数，则有：&lt;/p&gt;
&lt;p&gt;$$
\frac{a}{c} \equiv \frac{b}{c}\pmod {\frac{p}{c}}
$$&lt;/p&gt;
&lt;p&gt;证明：&lt;/p&gt;
&lt;p&gt;由已知条件可得：&lt;/p&gt;
&lt;p&gt;$$
\left{ \begin{aligned} &amp;#x26; k_1p+n=a \ &amp;#x26; k_2p+n=b \end{aligned} \right.
$$&lt;/p&gt;
&lt;p&gt;其中，&lt;/p&gt;
&lt;p&gt;$$
n,k_1,k_2\in\mathbb{Z}
$$&lt;/p&gt;
&lt;p&gt;所以：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\frac{a}{c}\bmod \frac{p}{c}&amp;#x26;=\frac{k_1p+n}{c}\bmod \frac{p}{c}\
&amp;#x26;=(k_1\frac{p}{c}+\frac{n}{c})\bmod \frac{p}{c}\
&amp;#x26;=\frac{n}{c}
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;同理可证，&lt;/p&gt;
&lt;p&gt;$$
\frac{b}{c}\bmod \frac{p}{c}=\frac{n}{c}
$$&lt;/p&gt;
&lt;p&gt;所以：&lt;/p&gt;
&lt;p&gt;$$
\frac{a}{c} \equiv \frac{b}{c}\pmod {\frac{p}{c}}
$$&lt;/p&gt;
&lt;h2&gt;逆元&lt;/h2&gt;
&lt;p&gt;若 $ab\equiv 1\pmod p$，且 $a,b,p \in \mathbb{Z}$ ，则称 $a$ 和 $b$ 互为模 $p$ 下的逆元，$a$ 的逆元可以记作 $a^{-1}$．&lt;/p&gt;
&lt;p&gt;特别的，1 的逆元总是 1&lt;/p&gt;
&lt;h3&gt;除法性质&lt;/h3&gt;
&lt;p&gt;对于整数 $a,b,c$，如果有：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned} &amp;#x26; ac\equiv bc\pmod p\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;则有：&lt;/p&gt;
&lt;p&gt;$$
a \equiv b\pmod p
$$&lt;/p&gt;
&lt;p&gt;证明：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
ac&amp;#x26;\equiv bc\pmod p\
ac\cdot c^{-1}&amp;#x26;\equiv bc\cdot c^{-1} \pmod p\
a &amp;#x26;\equiv b\pmod p
\end{aligned}
$$&lt;/p&gt;
&lt;h2&gt;剩余类和完全剩余系&lt;/h2&gt;
&lt;p&gt;对于正整数 $p$，全部整数可以分为 $p$ 个集合，令其为 $K_0,K_1,K_2,\cdots,K_{p-1}$，其中，整数 $n$ 和其所在的集合 $K_i$ 所满足的条件为：&lt;/p&gt;
&lt;p&gt;$$
n\equiv i\pmod p
$$&lt;/p&gt;
&lt;p&gt;则称 $K_0,K_1,K_2,\cdots,K_{p-1}$ 为模 $p$ 的&lt;strong&gt;剩余类&lt;/strong&gt;．&lt;/p&gt;
&lt;p&gt;对于一个集合 $s$，如果集合中有 $p$ 个元素，且每一个元素和其他元素都不在同一个模 $p$ 的剩余类中，则称这个集合 $s$ 是模 $p$ 的&lt;strong&gt;完全剩余系&lt;/strong&gt;．模 $p$ 的完全剩余系有无数个．&lt;/p&gt;
&lt;h2&gt;费马小定理&lt;/h2&gt;
&lt;p&gt;如果 $p$ 是一个质数，而整数 $a$ 和 $p$ 互质，则有：&lt;/p&gt;
&lt;p&gt;$$
a^{p-1}\equiv 1\pmod p
$$&lt;/p&gt;
&lt;p&gt;证明：&lt;/p&gt;
&lt;p&gt;假定模 $p$ 的完全剩余系 $s={0, 1, 2, \cdots, p-1}$，不妨将 $s$ 中的每一项都乘 $a\in\mathbb{Z}$，$a\neq 0$，得到一个新的集合 $s&apos;={0,a,2a,\cdots,(p-1)a}$．&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note] 引理：$s&apos;$ 是一个新的关于模 $p$ 的完全剩余系．&lt;/p&gt;
&lt;p&gt;假设 $s&apos;$ 中存在两个元素 $ai$ 和 $aj$，他们符合：$ai\equiv aj\pmod p,i\neq j$&lt;/p&gt;
&lt;p&gt;所以，&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
ai\equiv aj\pmod p
\Rightarrow p \mid (ai-aj)
\Rightarrow p \mid a(i-j)
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;由于 $p$ 与 $a$ 互质，所以 $p \mid i-j$，又因为 $0\leq i,j\lt p$&lt;/p&gt;
&lt;p&gt;所以不存在这样的 $i,j$ 使得 $p \mid i-j$，这与假设矛盾．&lt;/p&gt;
&lt;p&gt;得证．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们现在将模 $p$ 为 0 的一项删去，即 $s$ 和 $s&apos;$ 中的 $0$．&lt;/p&gt;
&lt;p&gt;我们可以知道，在删去 $0$ 后，$s$ 的各项之积 $T$ 为：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
T
&amp;#x26;=1\times 2\cdots \times(p-1)\
&amp;#x26;=(p-1)!
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;$s&apos;$ 的各项之积 $T&apos;$ 为：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
T&apos;&amp;#x26;=a\cdot 2a\cdots ({p-1})a\
&amp;#x26;=a^{p-1}(p-1)!
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;可推得：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
T \bmod p&amp;#x26;=(p-1)!\bmod p\
T&apos;\bmod p&amp;#x26;=(a\bmod p)(2a\bmod p)\cdots[(p-1)a\bmod p]\bmod p\
&amp;#x26;=k_1k_2k_3\cdots k_{p-1}\bmod p\
k_i\in\mathbb{Z},&amp;#x26;0\lt k_i\lt p
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;又由于 $s&apos;$ 是模 $p$ 的完全剩余系，所以每一个 $k_i$ 都是一个 $[1,p-1]$ 之间的值（注意，$k_i$ 的值不一定是 $i$），且两两互不相同．&lt;/p&gt;
&lt;p&gt;所以，&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
T&apos;\bmod p&amp;#x26;=k_1k_2k_3\cdots k_{p-1}\bmod p\
&amp;#x26;=1\times 2\times\cdots\times (p-1)\bmod p\
&amp;#x26;=(p-1)!\bmod p\
&amp;#x26;=T\bmod p
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;由此可得，$T&apos;\equiv T\pmod p$&lt;/p&gt;
&lt;p&gt;所以：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
T&apos;&amp;#x26;\equiv T\pmod p\
a^{p-1}(p-1)!&amp;#x26;\equiv (p-1)!\pmod p
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;令 $(p-1)!$ 的逆元为 $n$，所以：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
a^{p-1}(p-1)!&amp;#x26;\equiv (p-1)!\pmod p\
a^{p-1}(p-1)!n&amp;#x26;\equiv (p-1)!n\pmod p\
a^{p-1}&amp;#x26;\equiv 1\pmod p\
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;得证．&lt;/p&gt;
&lt;h3&gt;利用费马小定理求逆元&lt;/h3&gt;
&lt;p&gt;$$
\begin{aligned}
a^{p-1}&amp;#x26;\equiv 1\pmod p\
a\cdot a^{p-2}&amp;#x26;\equiv 1\pmod p\
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;所以，$a$ 的逆元是 $a^{p-2}$．在实战中，可以使用快速幂在 $O(\log n)$ 的时间复杂度之内解决．&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>动态规划</title><link>https://blog.jerrylab.top/blog/count/dp</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/count/dp</guid><description>最基础的线性动态规划及一些例题</description><pubDate>Wed, 04 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;把一个大问题分解成很多小问题，然后把小问题的答案保存起来，避免重复计算，最后根据小问题的答案推出大问题的答案，这样的算法叫动态规划（DP）．&lt;/p&gt;
&lt;h1&gt;作用与核心思想&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;作用：高效地解决一些复杂的问题，例如最短路径，背包问题等．&lt;/li&gt;
&lt;li&gt;核心思想：把一个大问题分解成很多小问题，然后把小问题的答案保存起来，避免重复计算，最后根据小问题的答案推出大问题的答案．&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;概念&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;状态：描述问题的某个子问题的情况&lt;/li&gt;
&lt;li&gt;初始状态：最简单的情况，边界条件&lt;/li&gt;
&lt;li&gt;状态转移方程：状态之间转换的规律，即如果得知前面的状态，怎么退出后面的状态（一种状态一般会由多种状态堆叠而成）&lt;/li&gt;
&lt;li&gt;最终状态：最终的答案&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;例题：求斐波那契数列的第 n 项&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;设定一个数组为 &lt;code&gt;dp[i]&lt;/code&gt;，表示斐波那契数列的第 i 项&lt;/p&gt;
&lt;p&gt;状态：&lt;code&gt;dp[i]&lt;/code&gt; 表示前两个数之和&lt;/p&gt;
&lt;p&gt;初始状态：&lt;code&gt;dp[1]=1&lt;/code&gt;，&lt;code&gt;dp[2]=1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;状态转移方程：$dp[i]=dp[i-1]+dp[i-2]$`&lt;/p&gt;
&lt;p&gt;最终状态：&lt;code&gt;dp[n]&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;动态规划基本步骤&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;定义状态&lt;/li&gt;
&lt;li&gt;确定初始状态&lt;/li&gt;
&lt;li&gt;找出状态转换方程（可以倒过来思考，先想好每一个状态可以迁移到哪些状态）&lt;/li&gt;
&lt;li&gt;返回最终状态&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;性质&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;最优子结构：一个问题的最优解可以由它的子问题来得出&lt;/li&gt;
&lt;li&gt;无后效性：如果知道了一个状态的值，那么就不需要再考虑它是怎么得到的，也不需要考虑它之前的状态是什么．因为状态的值只取决于当前问题的情况，并不受之前具体选择的影响，简化状态转移方程，并且避免一些不必要的计算&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;常见优化方案&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;预处理&lt;/li&gt;
&lt;li&gt;影响参数化，参数结果化
&lt;ul&gt;
&lt;li&gt;如果一个问题有后效性，考虑将影响参数化，增加一维参数&lt;/li&gt;
&lt;li&gt;尽力将参数消去，这样可以减少时间复杂度&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;典型案例&lt;/h1&gt;
&lt;h2&gt;最长上升子序列问题&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;给出一个由 $n(n\le 5000)$ 个不超过 $10^6$ 的正整数组成的序列．请输出这个序列的&lt;strong&gt;最长上升子序列&lt;/strong&gt;的长度．最长上升子序列是指，从原序列中&lt;strong&gt;按顺序&lt;/strong&gt;取出一些数字排在一起，这些数字是&lt;strong&gt;逐渐增大&lt;/strong&gt;的．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;带入基本步骤：&lt;/p&gt;
&lt;p&gt;设立原数组为 a 数组，长度为 n&lt;/p&gt;
&lt;p&gt;状态：&lt;code&gt;dp[i]&lt;/code&gt; 表示 &lt;code&gt;a[1]~a[i]&lt;/code&gt; 中最长子序列的长度&lt;/p&gt;
&lt;p&gt;初始状态：&lt;code&gt;dp[1] = 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;状态转移方程：$dp[i]=max(dp[j]+1),j&amp;#x3C;i \land a[j]&amp;#x3C;a[i]$，即枚举在 &lt;code&gt;a[i]&lt;/code&gt; 之前且小于 &lt;code&gt;a[i]&lt;/code&gt; 的元素 &lt;code&gt;a[j]&lt;/code&gt;，并更新 &lt;code&gt;dp[i]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;最终状态：&lt;code&gt;max(dp[i])&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度&lt;/strong&gt;：由于上述方案，对于每一个元素，都要枚举其前面的元素，时间复杂度为 $O(n^2)$&lt;/p&gt;
&lt;h3&gt;优化&lt;/h3&gt;
&lt;p&gt;定义 &lt;code&gt;d&lt;/code&gt; 数组记录所有长度为 i 的最长子序列中最后一个元素的最小值，若没有长度为 i 的最长子序列则记为 $\infty$&lt;/p&gt;
&lt;p&gt;可知：&lt;code&gt;d&lt;/code&gt; 数组是单调不递减的．由此，对于每一个新的节点，都有&lt;/p&gt;
&lt;p&gt;$$
dp[i] = \arg\max_{j &amp;#x3C; i,\ a[i] &gt; d[j]} d[j]
$$&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\max f(x)$ 表示函数 $f(x)$ 的最大值（一个具体的数值）&lt;/li&gt;
&lt;li&gt;$\arg\max f(x)$ 表示使得 $f(x)$ 达到最大值时的 $x$ 值（自变量）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;再更新 $d[j]$，$d[j]=a[i]$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度&lt;/strong&gt;：如果这样，那么可以二分查找符合目标的 j ，时间复杂度 $O(logn)$&lt;/p&gt;
&lt;h2&gt;01 背包&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;有 $n$ 个物品和一个容量为 $w$ 的背包，每个物品有重量 $w_i$ 和价值 $v_i$ 两种属性，要求选若干物品放入背包使背包中物品的总价值最大且背包中物品的总重量不超过背包的容量．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在上述例题中，由于每个物体只有两种可能的状态（取与不取），对应二进制中的 0&lt;img src=&quot;data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7&quot; alt=&quot;&quot; title=&quot;0&quot;&gt; 和 1&lt;img src=&quot;data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7&quot; alt=&quot;&quot; title=&quot;1&quot;&gt;，这类问题便被称为&lt;strong&gt;0-1 背包问题&lt;/strong&gt;．&lt;/p&gt;
&lt;p&gt;定义 &lt;code&gt;dp[i][j]&lt;/code&gt; 表示考虑前 i 个元素，背包占用空间达到 j，所能够获取的最大空间．初始时 &lt;code&gt;dp[1][0]=0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;对于所有的物品，都有选和不选两种选择，因此，有如下的状态转移方程：&lt;/p&gt;
&lt;p&gt;$$
dp[i][j] = max(dp[i-1][j-w[i]] + a[i], dp[i-1][j])
$$&lt;/p&gt;
&lt;p&gt;不难发现，&lt;code&gt;dp[i][j]&lt;/code&gt; 依赖于以下项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[i-1][j]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[i-1][j-w[i]]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于第一维只与 i-1 有关，因此，可以倒着遍历 j，如果这样，第一维状态就可以省去．&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>树形DP</title><link>https://blog.jerrylab.top/blog/count/tree-dp</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/count/tree-dp</guid><description>在树上的动态规划</description><pubDate>Wed, 04 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;树形 DP，顾名思义，就是在树上的 DP．&lt;/p&gt;
&lt;p&gt;树形 DP 有以下特征：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;转移方程一般从当前点的子树转移而来&lt;/li&gt;
&lt;li&gt;转换方程的第一维 &lt;code&gt;n&lt;/code&gt; 一般表示以 &lt;code&gt;n&lt;/code&gt; 为根的子树，怎么怎么样．&lt;/li&gt;
&lt;li&gt;一般来说，树的叶子节点是初始状态，根节点是最终状态&lt;/li&gt;
&lt;li&gt;一般会综合所有子节点&lt;/li&gt;
&lt;li&gt;树形 DP 的实现类似后序遍历．&lt;/li&gt;
&lt;li&gt;树形 DP 和线性 DP 有关联，有时候可以相互转化．&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;树形 DP 一般只有两个顺序：从上往下（不常见）、从下往上，因此，&lt;strong&gt;表达式中不能同时存在子节点信息和父节点信息&lt;/strong&gt;（即无后效性）&lt;/p&gt;
&lt;h1&gt;典型案例&lt;/h1&gt;
&lt;h2&gt;树的大小&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一棵树，需要统计树中有多少子节点．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个问题很简单：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;状态定义：&lt;code&gt;dp[u]&lt;/code&gt; 表示以 &lt;code&gt;u&lt;/code&gt; 为根节点的树中有多少子节点．&lt;/li&gt;
&lt;li&gt;初始状态：&lt;code&gt;dp[l]=1&lt;/code&gt;，其中 &lt;code&gt;l&lt;/code&gt; 是叶子节点．&lt;/li&gt;
&lt;li&gt;状态转移：&lt;code&gt;dp[u]=sum(dp[v])+1&lt;/code&gt;，其中 &lt;code&gt;v&lt;/code&gt; 是 &lt;code&gt;u&lt;/code&gt; 的子节点．即 &lt;code&gt;dp[u]&lt;/code&gt; 为其所有子节点之和再加上本身．&lt;/li&gt;
&lt;li&gt;最终状态：&lt;code&gt;dp[r]&lt;/code&gt;，&lt;code&gt;r&lt;/code&gt; 是根节点．&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;最大独立集&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- 题目：P1352 没有上司的舞会
某大学有 $n$ 个职员，编号为 $1\ldots n$．&lt;/p&gt;
&lt;p&gt;他们之间有从属关系，员工 $k$ 是员工 $l$ 的直接上司，也就是说他们的关系就像一棵以校长为根的树，父结点就是子结点的直接上司．&lt;/p&gt;
&lt;p&gt;现在有个周年庆宴会，宴会每邀请来一个职员都会增加一定的快乐指数 $r_i$，但是呢，如果某个职员的直接上司来参加舞会了，那么这个职员就无论如何也不肯来参加舞会了．&lt;/p&gt;
&lt;p&gt;所以，请你编程计算，邀请哪些职员可以使快乐指数最大，求最大的快乐指数．&lt;/p&gt;
&lt;p&gt;对于 $100%$ 的数据，保证 $1\leq n \leq 6 \times 10^3$，$-128 \leq r_i\leq 127$，$1 \leq l, k \leq n$，且给出的关系一定是一棵树．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在这里，会用到影响参数化的思想．&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;状态定义：&lt;code&gt;dp[u][s]&lt;/code&gt; 表示以 &lt;code&gt;u&lt;/code&gt; 为根节点的树中员工（即员工 &lt;code&gt;u&lt;/code&gt; 的间接或直接下属）的最大快乐指数，&lt;code&gt;s&lt;/code&gt; 表示员工 &lt;code&gt;u&lt;/code&gt; 是（1）否（0）会参加舞会&lt;/li&gt;
&lt;li&gt;初始状态：&lt;code&gt;dp[l]=r[l]&lt;/code&gt;，其中 &lt;code&gt;l&lt;/code&gt; 是叶子节点．&lt;/li&gt;
&lt;li&gt;状态转移：$dp[u][0] = max(dp[v][0], dp[v][1], 0)$，$dp[u][1] += max(dp[v][0], 0)+r[u]$，其中 &lt;code&gt;v&lt;/code&gt; 是 &lt;code&gt;u&lt;/code&gt; 的子节点．即 &lt;code&gt;dp[u][0]&lt;/code&gt; 为其所有子节点去或不去所得的最大值，&lt;code&gt;dp[u][1]&lt;/code&gt; 为其所有子节点不去所得的最大值．&lt;/li&gt;
&lt;li&gt;最终状态：&lt;code&gt;max(dp[r][0],dp[r][1])&lt;/code&gt;，&lt;code&gt;r&lt;/code&gt; 是根节点．&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;标程&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

const int N = 6e3 + 100;

int n, r[N], dp[N][2], fa[N];
vector&amp;#x3C;int&gt; e[N];

void dfs(int u) {
    for (int i = 0; i &amp;#x3C; e[u].size(); i++) {
        int v = e[u][i];
        dfs(v);
        dp[u][0] += max(max(dp[v][0], dp[v][1]), 0);
        dp[u][1] += max(dp[v][0], 0);
    }
    dp[u][1] += r[u];
}

int main() {
    cin &gt;&gt; n;
    for (int i = 1; i &amp;#x3C;= n; i++) {
        cin &gt;&gt; r[i];
    }
    for (int i = 1; i &amp;#x3C;= n - 1; i++)
    {
        int l, k;
        cin &gt;&gt; l &gt;&gt; k;
        e[k].push_back(l);
        fa[l] = k;
    }
    int root = 1;
    while (1) {
        if (fa[root] == 0) break;
        root = fa[root];
    }
    dfs(root);
    cout &amp;#x3C;&amp;#x3C; max(dp[root][0], dp[root][1]);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;换根 DP&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- 题目：P3478 STA-Station
给定一个 $n$ 个点的树，请求出一个结点，使得以这个结点为根时，所有结点的深度之和最大．&lt;/p&gt;
&lt;p&gt;一个结点的深度之定义为该节点到根的简单路径上边的数量．&lt;/p&gt;
&lt;p&gt;对于全部的测试点，保证 $1 \leq n \leq 10^6$，$1 \leq u, v \leq n$，给出的是一棵树．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;想法一：暴力枚举&lt;/h3&gt;
&lt;p&gt;尝试以 $O(n)$ 枚举每一个点，然后再通过 dfs 统计一遍答案，总计 $O(n^2)$&lt;/p&gt;
&lt;h3&gt;想法二&lt;/h3&gt;
&lt;p&gt;考虑将根上移带来的贡献．&lt;/p&gt;
&lt;p&gt;定义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sz[t]&lt;/code&gt; 表示在 1 为根节点的树中，以 &lt;code&gt;t&lt;/code&gt; 为根节点的子树的节点数量．&lt;/li&gt;
&lt;li&gt;&lt;code&gt;low[t]&lt;/code&gt; 表示在 1 为根节点的树中，以 &lt;code&gt;t&lt;/code&gt; 为根的子树中所有节点到节点 &lt;code&gt;t&lt;/code&gt; 的距离之和．&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[t]&lt;/code&gt; 表示在 &lt;code&gt;t&lt;/code&gt; 为根节点的树中，书中所有节点到节点 &lt;code&gt;t&lt;/code&gt; 的距离．&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;sz[t]&lt;/code&gt; 很好求，参考&lt;strong&gt;树的大小&lt;/strong&gt;一节．&lt;/p&gt;
&lt;p&gt;第一个 dfs 求 &lt;code&gt;low&lt;/code&gt; 时，&lt;strong&gt;从下到上&lt;/strong&gt;，有：&lt;/p&gt;
&lt;p&gt;$$
low[u]=sz[u] - 1 +\sum_{u\rightarrow v} low[v]
$$&lt;/p&gt;
&lt;p&gt;在上面的式子中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$sz[u]-1$ 表示边 $u \rightarrow v$ 这条边所做的贡献．&lt;/li&gt;
&lt;li&gt;$\sum low[v]$ 表示其子节点的贡献．&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;边界条件&lt;/strong&gt;：无需边界条件，所有节点都是自洽的，即使是根节点．&lt;/p&gt;
&lt;p&gt;第二个 dfs 求 &lt;code&gt;dp&lt;/code&gt; 时，考虑若将根节点从 &lt;code&gt;u&lt;/code&gt; 改成 &lt;code&gt;v&lt;/code&gt;，&lt;code&gt;v&lt;/code&gt; 是 &lt;code&gt;u&lt;/code&gt; 的子节点，则有以下变化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不位于节点 &lt;code&gt;v&lt;/code&gt; 下面的节点（不包括 &lt;code&gt;v&lt;/code&gt;，共计 $n-sz[v]$ 个节点）每一个到根节点的距离增大 1&lt;/li&gt;
&lt;li&gt;位于节点 &lt;code&gt;v&lt;/code&gt; 下面的节点（包括 &lt;code&gt;v&lt;/code&gt;，共计 $sz[v]$ 个节点）每一个到根节点的距离减少 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;则&lt;strong&gt;从上到下&lt;/strong&gt;，有：&lt;/p&gt;
&lt;p&gt;$$
dp[v] = dp[u]+(n-sz[v])-(sz[v])
$$&lt;/p&gt;
&lt;p&gt;化简一下，就是：&lt;/p&gt;
&lt;p&gt;$$
dp[v]=dp[u]+n-2*sz[v]
$$&lt;/p&gt;
&lt;p&gt;考虑边界条件：当 &lt;code&gt;v&lt;/code&gt; 是根节点时，$dp[v] = low[v]$&lt;/p&gt;
&lt;p&gt;整个题的最终状态为：$max(dp[t]),t\in \mathbb{Z},1\leq t\leq n$&lt;/p&gt;
&lt;p&gt;时间复杂度为 $O(n)$&lt;/p&gt;
&lt;p&gt;标程：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

const int N = 1e6 + 100;

int n, f[N], sz[N], vis[N];
long long dp[N];
int low[N];
vector&amp;#x3C;int&gt; ee[N], e[N];

void build(int u, int fa = 0) {
    if (vis[u]) return;
    vis[u] = true;
    if (fa != 0) e[fa].push_back(u);
    for (auto it : ee[u]) {
        build(it, u);
    }
}

void dfs_sz(int u) {
    for (auto it : e[u]) {
        dfs_sz(it);
        sz[u] += sz[it];
    }
    sz[u]++;
}

void dfs_1(int u) {
    low[u] = sz[u] - 1;
    for (auto v : e[u]) {
        dfs_1(v);
        low[u] += low[v];
    }
}

void dfs_2(int u) {
    for (auto v : e[u]) {
        dp[v] = dp[u] + n - 2 * sz[v];
        dfs_2(v);
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin &gt;&gt; n;
    for (int i = 1; i &amp;#x3C; n; i++) {
        int u, v;
        cin &gt;&gt; u &gt;&gt; v;
        ee[u].push_back(v);
        ee[v].push_back(u);
    }
    int root = 1;
    build(root);
    dfs_sz(root);
    dfs_1(root);
    dp[root] = low[root];
    dfs_2(root);
    long long ans = -1, ans_id = -1;
    for (int i = 1; i &amp;#x3C;= n; i++) {
        if (ans &amp;#x3C; dp[i]) {
            ans = dp[i];
            ans_id = i;
        }
    }
    cout &amp;#x3C;&amp;#x3C; ans_id;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;树形背包&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- 题目：P2014 选课&lt;/p&gt;
&lt;p&gt;在大学里每个学生，为了达到一定的学分，必须从很多课程里选择一些课程来学习，在课程里有些课程必须在某些课程之前学习，如高等数学总是在其它课程之前学习．现在有 $N$ 门功课，每门课有若干学分，分别记作 $s_1,s_2,\cdots,s_N$，每门课有一门或没有直接先修课（若课程 $a$ 是课程 $b$ 的先修课即只有学完了课程 $a$，才能学习课程 $b$）．一个学生要从这些课程里选择 $M$ 门课程学习，问他能获得的最大学分是多少？&lt;/p&gt;
&lt;p&gt;题目保证课程安排无冲突．（即不会有 $a$ 是 $b$ 的先修课，$b$ 也是 $a$ 的先修课这类情况存在．）&lt;/p&gt;
&lt;p&gt;对于 $100%$ 数据，保证 $1 \leq N \leq 300$ , $1 \leq M \leq 300$ , $1 \leq {s_i} \leq 20$．&lt;/p&gt;
&lt;p&gt;数据保证至少存在一个 $k_i=0$，即至少一门课无先修课．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;和&lt;strong&gt;01 背包&lt;/strong&gt;相类似，定义 &lt;code&gt;dp[t][k]&lt;/code&gt; 表示在以 &lt;code&gt;t&lt;/code&gt; 为根的子树中，选择 &lt;code&gt;k&lt;/code&gt; 门进行学习，所能够获得的最大分数．&lt;/p&gt;
&lt;p&gt;此时，初始状态为 $dp[t][1]=s[t],t\in \mathbb{Z},1\leq t\leq n$，最终状态为 &lt;code&gt;dp[root][m]&lt;/code&gt;，&lt;code&gt;root&lt;/code&gt; 是根节点．&lt;/p&gt;
&lt;p&gt;但状态转移方程是什么呢？&lt;/p&gt;
&lt;p&gt;我们要将 &lt;code&gt;k&lt;/code&gt; 门学习学科的配额进行有效分配．对于每一门学科，又可以分配 $[0,n]$ 个可学习配额，因此，有：&lt;/p&gt;
&lt;p&gt;$$
dp[u][k] = max(\sum_{u\rightarrow v} dp[v][a_i]) + s[u],0\leq a_i\leq k,\sum_{u\rightarrow v}a_i=k-1
$$&lt;/p&gt;
&lt;p&gt;，即：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于每一种分配方案，为每一个子节点分配一定的可学习科目额度 $a_i$，所有可学习科目额度之和恰好等于 $k-1$（这是由于节点 $u$ 必修，如果不修节点 $u$，则无法学习其子节点）&lt;/li&gt;
&lt;li&gt;对于所有分配方案，取其中收益最大的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那要通过超高的时间复杂度来枚举每一种情况并计算吗？&lt;/p&gt;
&lt;p&gt;我们可以引入一个辅助数组 &lt;code&gt;g[m][k]&lt;/code&gt;，表示已经考虑 &lt;code&gt;m&lt;/code&gt; 个 &lt;code&gt;u&lt;/code&gt; 的子节点节点，且使用了 &lt;code&gt;k&lt;/code&gt; 个学习配额，所能够获得的最大收益．&lt;/p&gt;
&lt;p&gt;则有初始状态 $g[1][H]=dp[v_1][H],0\leq H\leq k$，此处 $v_i$ 表示节点 $u$ 的第 $i$ 个子节点．&lt;/p&gt;
&lt;p&gt;不难发现状态转移方程：$g[m][H] = g[m-1][A]+dp[v_m][H-A],0\leq A\leq H$，其实就是在增加一个子节点时，枚举这个子节点所得的配额，用一种类似打擂台的方法统计出结果．使用完 &lt;code&gt;g&lt;/code&gt; 数组后一定要清空！！！&lt;/p&gt;
&lt;p&gt;利用 &lt;code&gt;g&lt;/code&gt; 数组来优化 &lt;code&gt;dp&lt;/code&gt; 的状态转移方程：&lt;/p&gt;
&lt;p&gt;$$
dp[u][k] = g[v_{max}][k-1] + s[i]
$$，$v_{max}$ 为节点 $u$ 的子节点数量．必须选取节点 $u$，因为如果不选取节点 $u$，节点 $u$ 中的所有子节点都无法被选取．&lt;/p&gt;
&lt;p&gt;标程：&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>KMP算法</title><link>https://blog.jerrylab.top/blog/str/kmp</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/str/kmp</guid><description>关于字符串搜索</description><pubDate>Wed, 04 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!note]- 题目：P3375 【模板】KMP&lt;/p&gt;
&lt;p&gt;给出两个字符串 $s_1$ 和 $s_2$，若 $s_1$ 的区间 $[l, r]$ 子串与 $s_2$ 完全相同，则称 $s_2$ 在 $s_1$ 中出现了，其出现位置为 $l$．&lt;/p&gt;
&lt;p&gt;现在请你求出 $s_2$ 在 $s_1$ 中所有出现的位置．&lt;/p&gt;
&lt;p&gt;定义一个字符串 $s$ 的 border 为 $s$ 的一个&lt;strong&gt;非 $s$ 本身&lt;/strong&gt;的子串 $t$，满足 $t$ 既是 $s$ 的前缀，又是 $s$ 的后缀．&lt;/p&gt;
&lt;p&gt;对于 $s_2$，你还需要求出对于其每个前缀 $s&apos;$ 的最长 border $t&apos;$ 的长度．&lt;/p&gt;
&lt;p&gt;对于全部的测试点，保证 $1 \leq |s_1|,|s_2| \leq 10^6$，$s_1, s_2$ 中均只含大写英文字母．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;KMP 算法的作用&lt;/h1&gt;
&lt;p&gt;KMP 算法用于在主串（&lt;code&gt;s1&lt;/code&gt;）中高效查找模式串（&lt;code&gt;s2&lt;/code&gt;）出现的位置．相比朴素匹配，KMP 能避免重复回溯，大大提高效率，时间复杂度为 $O(n+m)$．&lt;/p&gt;
&lt;h1&gt;KMP 算法的原理&lt;/h1&gt;
&lt;p&gt;KMP 的核心思想是： &lt;strong&gt;当匹配失败时，利用模式串自身的信息，快速移动模式串，而不是主串回退．&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这依赖于一个“部分匹配表”（fail 数组），它记录了模式串每个前缀的最长相等前后缀长度．&lt;/p&gt;
&lt;h1&gt;实现过程详解&lt;/h1&gt;
&lt;h2&gt;1. 预处理 fail 数组&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fail[i]&lt;/code&gt; 表示：当匹配到模式串第 i 位失败时，下一步应该跳到模式串的 &lt;code&gt;fail[i]&lt;/code&gt; 位置继续尝试．&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int t = 0;
for (int i = 1; i &amp;#x3C; s2.size(); i++)
{
	while (s2[i] != s2[t] &amp;#x26;&amp;#x26; t)
	{
		t = fail[t];
	}
	if (s2[i] == s2[t])
	{
		fail[i + 1] = t + 1;
		t++;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;作用：为每个位置计算最长相等前后缀长度，便于失配时快速跳转．&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 匹配过程&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用变量 &lt;code&gt;t&lt;/code&gt; 表示当前匹配到模式串的位置．&lt;/li&gt;
&lt;li&gt;遍历主串 &lt;code&gt;s1&lt;/code&gt;，每次比较 &lt;code&gt;s1[i]&lt;/code&gt; 和 &lt;code&gt;s2[t]&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;如果不等且 $t&gt;0$，就用 &lt;code&gt;fail[t]&lt;/code&gt; 回退模式串指针 &lt;code&gt;t&lt;/code&gt;．&lt;/li&gt;
&lt;li&gt;如果相等，&lt;code&gt;t++&lt;/code&gt;，继续比较下一个字符．&lt;/li&gt;
&lt;li&gt;如果 t 达到 &lt;code&gt;s2.size()&lt;/code&gt;，说明找到一次完整匹配，输出匹配位置．&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;t = 0;
for (int i = 0; i &amp;#x3C; s1.size(); i++)
{
	while (s1[i] != s2[t] &amp;#x26;&amp;#x26; t)
	{
		t = fail[t];
	}
	if (s1[i] == s2[t])
	{
		t++;
	}
	if (t == s2.size())
	{
		cout &amp;#x3C;&amp;#x3C; i - s2.size() + 2 &amp;#x3C;&amp;#x3C; endl;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 输出 fail 数组&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fail&lt;/code&gt; 数组的每一项表示模式串每个前缀的最长相等前后缀长度 +1（因为下标偏移）．&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;KMP 的优势&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;只需预处理一次模式串，匹配时不会回退主串指针．&lt;/li&gt;
&lt;li&gt;时间复杂度 $O(n+m)$，适合大数据量字符串匹配．&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;举个例子&lt;/h1&gt;
&lt;p&gt;假定输入为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BBC ABCDAB ABCDABCDABDE
ABCDABD
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./../pic/P3375-1.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;首先，匹配第一个字符．因为 B 与 A 不匹配，所以后移一位．&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../pic/P3375-2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;发现又不匹配，搜索词再往后移．&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../pic/P3375-3.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;一直匹配失败直到 $i = 5$ 时，第一次匹配成功&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../pic/P3375-4.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;仍旧是匹配成功的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../pic/P3375-5.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;发现对应字符不相同&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../pic/P3375-6.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;此时，我们可能需要回到开始，从新逐个比较，这样虽然可行，但是效率很低．&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../pic/P3375-7.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;当空格与 D 不匹配时，你其实知道前面六个字符是 &lt;code&gt;ABCDAB&lt;/code&gt;．KMP 算法的想法是，设法利用这个已知信息，不要把 &quot; 搜索位置 &quot; 移回已经比较过的位置，继续把它向后移，这样就提高了效率．&lt;/p&gt;
&lt;p&gt;在这里，我们可以发现，匹配成功的字符串的后缀为 &lt;code&gt;AB&lt;/code&gt;，恰好字符串是以 &lt;code&gt;AB&lt;/code&gt; 开头的，所以，我们不妨使一招“偷梁换柱”，如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../pic/P3375-9.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;因为空格与 &lt;code&gt;C&lt;/code&gt; 不匹配，搜索词还要继续往后移．&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../pic/P3375-10.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;因为空格与 A 不匹配，继续后移一位．&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../pic/P3375-11.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;逐位比较，直到发现 C 与 D 不匹配．于是，将 &lt;code&gt;t&lt;/code&gt; 跳到 &lt;code&gt;fail[t]&lt;/code&gt;，即 $3$．&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../pic/P3375-12.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;直到完全匹配，搜索完成．&lt;/p&gt;
&lt;h1&gt;标程&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
string s1, s2;
// fail[i]表示如果在匹配字符串时失误了，且失误匹配字符的索引为i，则下一步应该将匹配字符串的索引重置为fail[i]并依此重新匹配.
// 或者说，fail[i]表示移动到i时出现错误的话，不能保留的最小字符索引
int fail[1000005];
int main()
{
	cin &gt;&gt; s1 &gt;&gt; s2;
	int t = 0; // t是当前
	// 预处理出s2的fail数组
	for (int i = 1; i &amp;#x3C; s2.size(); i++)
	{
		// 如果匹配出现了失误，且匹配字符串不在第一个字符（如果在的话，就直接往下匹配就可以了）
		while (s2[i] != s2[t] &amp;#x26;&amp;#x26; t)
		{
			// 就一直往前跳，直到能够匹配
			t = fail[t];
		}
		// 如果相等了
		if (s2[i] == s2[t])
		{
			// 就记录一下，如果在下一次匹配中失败了，就回到下一个位置
			// 不要回到这个位置，因为这个位置已经匹配成功了
			fail[i + 1] = t + 1;
			// 可以匹配下一个字符了
			t++;
		}
	}
	t = 0;
	for (int i = 0; i &amp;#x3C; s1.size(); i++)
	{
		// 如果匹配出现了失误，且匹配字符串不在第一个字符
		while (s1[i] != s2[t] &amp;#x26;&amp;#x26; t)
		{
			// 就一直往前跳，直到能够匹配
			t = fail[t];
		}
		// 如果相等了
		if (s1[i] == s2[t])
			// 可以匹配下一个字符了
			t++;
		if (t == s2.size())
			// 成功匹配
			cout &amp;#x3C;&amp;#x3C; i - s2.size() + 2 &amp;#x3C;&amp;#x3C; endl;
	}
	for (int i = 1; i &amp;#x3C;= s2.size(); i++)
	{
		cout &amp;#x3C;&amp;#x3C; fail[i] &amp;#x3C;&amp;#x3C; &quot; &quot;;
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>字符串哈希</title><link>https://blog.jerrylab.top/blog/str/hash</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/str/hash</guid><description>将字符串转化为数字</description><pubDate>Wed, 04 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;哈希，就是将一些内容转换为数字，一般可以用来压缩，替换等．&lt;/p&gt;
&lt;h1&gt;整数哈希&lt;/h1&gt;
&lt;p&gt;我们先要取定一个模 &lt;code&gt;MOD&lt;/code&gt;，然后将目标数字对 &lt;code&gt;MOD&lt;/code&gt; 取余，其结果就是目标数字的哈希值．&lt;/p&gt;
&lt;p&gt;两个不同的整数的哈希值可能相同，我们将其称为&lt;strong&gt;哈希冲突&lt;/strong&gt;，为了避免哈希冲突，我们要将 &lt;code&gt;MOD&lt;/code&gt; 定的大一些，且最好是一个质数．常用的 &lt;code&gt;MOD&lt;/code&gt; 值有：$10^9+7$，$10^9+9$，$10^7+9$&lt;/p&gt;
&lt;p&gt;以下代码可以生成一个整数的哈希值：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;long long Hash(long long n, int mod)
{
    return n % mod;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;字符串哈希&lt;/h1&gt;
&lt;p&gt;为了获取一个字符串的哈希值，我们需要将字符串化作一个整数，由于 ASCII 一共有 128 格字符，所以最好的方式，就是将一个字符串视作一个 128 进制数，然后将其转换为 10 进制数．&lt;/p&gt;
&lt;p&gt;我们可以通过下面的代码将一个字符串转换为一个整数对 &lt;code&gt;mod&lt;/code&gt; 取模的结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;long long ans = 0;
for (int i = 0; i &amp;#x3C; str.size(); i++)
    ans = (ans * 128 + str[i]) % mod;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以我们可以通过下面的函数来求字符串的哈希：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;long long strHash(string str, int mod)
{
    long long ans = 0;
    for (int i = 0; i &amp;#x3C; str.size(); i++)
        ans = (ans * 127 + str[i]) % mod;
    return ans % mod;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;双哈希&lt;/h1&gt;
&lt;p&gt;如果只进行一次哈希，特别容易进行哈希碰撞，所以，我们可以进行&lt;strong&gt;双哈希&lt;/strong&gt;．&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- 题目：P3370 字符串哈希&lt;/p&gt;
&lt;p&gt;如题，给定 $N$ 个字符串（第 $i$ 个字符串长度为 $M_i$，字符串内包含数字、大小写字母，大小写敏感），请求出 $N$ 个字符串中共有多少个不同的字符串．&lt;/p&gt;
&lt;p&gt;对于 $100%$ 的数据：$N\leq 10000$，$M_i≈1000$，$M_{\max}\leq 1500$．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;思路&lt;/h2&gt;
&lt;p&gt;为了方便记录，我们定义以 $M$ 为模的哈希操作为 $hash_M$&lt;/p&gt;
&lt;p&gt;对于这个题，我们可以使 &lt;code&gt;h[i]&lt;/code&gt; 表示是否已经读取过了 $hash_M=i$ 的字符串．为了减少哈希冲突，我们可以使得当 &lt;code&gt;h[i]&lt;/code&gt; 等于 $0$ 时，表示还没有 $hash_M=i$ 的字符串，否则，&lt;code&gt;h[i]&lt;/code&gt; 表示 $hash_M=i$ 的字符串经过一次 $hash_N$ 的结果．&lt;/p&gt;
&lt;p&gt;依照这个规定，我们每次记录一个字符串时，先检查 $h[hash_M]$ 是否为 $0$，如果这样，将 $h[hash_M]$ 设定为 $hash_N$，否则，先检查 $h[hash_M] = hash_N$，如果等于，那么说明这个字符串已经被记录过了，可以跳过，如果不等于，那么就表示发生了哈希冲突，我们通常将字符串储存到原位置的下一个空着的位置．&lt;/p&gt;
&lt;h3&gt;复杂度分析&lt;/h3&gt;
&lt;p&gt;求一个字符串的哈希，其复杂度为 $O(m)$．但是如果需要移位，其复杂度就为 $O(n)$，现在有 $n$ 个字符串，那么其总复杂度就为 $O(n^2m)$．但是，由于移位只有极小概率会一直移到相同的位置（即发生哈希冲突），所以，可以近似的认为，其复杂度为 $O(nm)$，且在极端条件下会退化为 $O(n^2m)$，我们可以通过增大模数来减少哈希冲突，即减小移位的时间复杂度．&lt;/p&gt;
&lt;h2&gt;标程&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define MOD1 (int)1e5 + 7
#define MOD2 (int)1e5 + 9
using namespace std;

long long Hash(string str, int mod)
{
    long long ans = 0;
    for (int i = 0; i &amp;#x3C; str.size(); i++)
        ans = (ans * 127 + str[i]) % mod;
    return ans % mod;
}

int h[MOD1 + 100];

int main()
{
    int n, ans = 0;
    cin &gt;&gt; n;
    for (int i = 1; i &amp;#x3C;= n; i++)
    {
        string str;
        cin &gt;&gt; str;
        long long hs1 = Hash(str, MOD1);
        long long hs2 = Hash(str, MOD2);
        while (h[hs1] != 0 &amp;#x26;&amp;#x26; h[hs1] != hs2)
            hs1 = (hs1 + 1) % MOD1;
        if (h[hs1] == hs2)
            continue;
        if (h[hs1] == 0)
        {
            h[hs1] = hs2;
            ans++;
        }
    }
    cout &amp;#x3C;&amp;#x3C; ans;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>割点和割边</title><link>https://blog.jerrylab.top/blog/graph/cut</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/graph/cut</guid><description>如何求图中的割点和割边呢？</description><pubDate>Wed, 04 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前置：强连通分量&lt;/p&gt;
&lt;h1&gt;定义&lt;/h1&gt;
&lt;p&gt;假定我们有一张图，其 dfs 生成树是像下面这样的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../pic/%E5%89%B2%E7%82%B9%E5%92%8C%E5%89%B2%E8%BE%B9.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;对于无向图的 dfs 生成树，不存在一条横插边连接根节点两边（所有的横插边会转换为树边，然后等数量的树边回转换为返祖边），由于没有方向，所以前向边和返祖边是一样的，暂且认为他们都是返祖边．因此，在这棵生成树中，只存在树边（图中黑边）和返祖边（图中红边）．&lt;/p&gt;
&lt;p&gt;在上图中，我们已经求出了每个结点的 &lt;code&gt;dfn&lt;/code&gt;（黑色数字，也为节点编号）和 &lt;code&gt;last&lt;/code&gt;（绿色数字）．&lt;/p&gt;
&lt;p&gt;在无向图中，双连通分量分为两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;边双连通分量（边双）&lt;/strong&gt;：如果说删除任意一条边后，点 $u$ 和点 $v$ 仍能连通，那么，点 $u$ 和点 $v$ 边双连通．（如上图中的点 1，2，3，4，5，8，9）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;点双连通分量（点双）&lt;/strong&gt;：如果说删除任意一个点及其邻边后，点 $u$ 和点 $v$ 仍能连通，那么，点 $u$ 和点 $v$ 点双连通（如上图中的 1，2，8，9）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于一个无向图中&lt;strong&gt;极大&lt;/strong&gt;的双连通子图，我们称这个子图为一个双连通分量．&lt;/p&gt;
&lt;p&gt;边双具有&lt;strong&gt;传递性&lt;/strong&gt;：即 AC 边双，BC 边双，则 AC 肯定边双&lt;/p&gt;
&lt;p&gt;而点双没有这种性质&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;桥（割边）&lt;/strong&gt;：满足一条桥被删除后，原图被分成两块（图中的黄边为桥）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;割点&lt;/strong&gt;：满足一个割点及其邻边被删除后，原图被分成两块（图中的绿点为割点）&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;边双连通分量&lt;/h1&gt;
&lt;p&gt;对于一个点 $t$，如果有子节点 $son$ ，满足 $son$ 只能通过树边到达 $t$，那就代表 $son$ 和 $t$ 之间只存在一条树边，因此，这条边就是割边．&lt;/p&gt;
&lt;p&gt;$son$ 只能通过树边到达 $t$，也就是说 $low_{son} \gt dfn_t$，由于 $son$ 是 $t$ 的子节点，所以，在 $son$ 能够到达的所有节点都是在以 $son$ 为根的子树之中，在这些节点中，$son$ 自身的 $dfn$ 值是最小的，因此又有 $low_{son} = dfn_{son}$&lt;/p&gt;
&lt;p&gt;综上，对于一个点 $t$，如果有子节点 $son$，使得 $low_{son} = dfn_{son}$（即 $low_{son} \gt dfn_t$），那么从 $t$ 到 $son$ 的边就是一条割边．&lt;/p&gt;
&lt;p&gt;如果边双之中有割边，那么删除了割边后，边双会被分成两块，这不满足边双的定义．影刺，不难得出，边双有一个重要的性质：&lt;strong&gt;边双里一定没有割边．&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;之后，使用 DFS 扫一遍就可以了．&lt;/p&gt;
&lt;h1&gt;点双连通分量&lt;/h1&gt;
&lt;p&gt;对于一个点 $t$（不是根节点），如果有子节点 $son$，使得节点 $son$ 如果不通过 $t$ 就无法达到 $t$ 的父节点 $u$，那么点 $t$ 就是一个割点．&lt;/p&gt;
&lt;p&gt;节点 $son$ 如果不通过 $t$ 就无法达到 $t$ 的父节点，即 $low_{son} \geq dfn_t$&lt;/p&gt;
&lt;p&gt;综上，对于一个点 $t$（不是根节点），如果有子节点 $son$，使得 $low_{son} \geq dfn_t$，那么点 $t$ 就是一个割点．&lt;/p&gt;
&lt;p&gt;对于根节点，如果存在两条或以上的树边，则根节点是割点．&lt;/p&gt;
&lt;p&gt;是不是像边双一样，点双里一定没有割点？&lt;/p&gt;
&lt;p&gt;不！&lt;/p&gt;
&lt;p&gt;点双有以下两个性质&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;性质一：不同点双，相交于仅一个割点（否则这两个点可以合成一个更大的点双）&lt;/li&gt;
&lt;li&gt;性质二：一个点双，在 DFS 生成树上最上面的（即 &lt;code&gt;dfn&lt;/code&gt; 最小的）点是割点或者是根&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;性质一证明：如果又两个点双子图，他们相交于两个割点，那么这两个点双子图可以合成一个更大的点双．&lt;/p&gt;
&lt;p&gt;对于一个割点 $u$，如果其子节点 $v$ 有 &lt;code&gt;low[v]&gt;=u&lt;/code&gt;，则点 $u$，$v$ 和 $v$ 的子树中的待定节点（即不在其他点双的节点）是一组点双&lt;/p&gt;
&lt;p&gt;以下是例图中所有的点双．&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../pic/Tarjan-%E5%89%B2%E7%82%B9%E5%92%8C%E5%89%B2%E8%BE%B92.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>技巧</title><link>https://blog.jerrylab.top/blog/other/trick</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/other/trick</guid><description>一些技巧</description><pubDate>Wed, 04 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;考试技巧&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;估分——时间：30&apos;~60&apos;（即 30 到 60 分钟）&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;从头到尾看一遍所有题目，不要写代码！忌讳开始就写代码．&lt;/li&gt;
&lt;li&gt;看题意，用样例验证有没有看错，一定看样例解释！必要的时候手算样例，这很重要！&lt;/li&gt;
&lt;li&gt;初步想一想，确定哪些分是能得的&lt;/li&gt;
&lt;li&gt;然后对每一组部分分排一个顺序（从易到难，而非分值从大到小）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原则 1：&lt;strong&gt;60‘想 +30&apos; 写 +10‘调&gt;10&apos; 想 +20&apos; 写 +60&apos; 调&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;降低调试代码的时间，多想少调，调试代码会打乱比赛节奏，影响心态．&lt;/p&gt;
&lt;p&gt;原则 2：&lt;strong&gt;20’调&amp;#x3C;30&apos; 重写&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;必要的时候重新写代码．&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;严格执行，一定不能跳！——时间：&amp;#x3C;120&apos;，最多 150&apos;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;新想法暂缓：如果在写部分分做法时，想到了正确做法或其余部分分的做法（大概率是错的），先记下来，继续把部分分做完，把其他题部分分做完，再去验证&lt;/li&gt;
&lt;li&gt;错做法放弃&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;复盘——时间：30&apos;~60&apos;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;查错：对拍、检查边界情况、清零情况&lt;/li&gt;
&lt;li&gt;急救：只选择一个题（一般靠前）直接做&lt;/li&gt;
&lt;li&gt;奖励：骗分！&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;分步编程&lt;/h1&gt;
&lt;p&gt;如果有多种数据……&lt;/p&gt;
&lt;p&gt;在 main 函数下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int T;
cin &gt;&gt; T;
while(T--) solve();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 solve 函数下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;清空&lt;/li&gt;
&lt;li&gt;读取&lt;/li&gt;
&lt;li&gt;计算&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意读写分离！&lt;/p&gt;
&lt;h1&gt;随机数&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;srand(time(0)); rand();
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;大约二分&lt;/h1&gt;
&lt;p&gt;为了避免二分带来的死循环，我们可以在 &lt;code&gt;r-l+1=logn&lt;/code&gt; 的时候放弃二分，使用枚举．时间复杂度从 $O(logn)\Rightarrow O(2\times logn)$，即为 $O(logn)$&lt;/p&gt;
&lt;p&gt;大约三分也是如此&lt;/p&gt;
&lt;h1&gt;骗分策略&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;猜规律&lt;/li&gt;
&lt;li&gt;打表：使用大复杂度算法打表（挂后台），然后使用数组返回，或者将输入输出中间量输出，找规律&lt;/li&gt;
&lt;li&gt;贪心：多种贪心策略并行，将所有贪心结果汇总、取最佳&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;贪心默认&lt;/h1&gt;
&lt;p&gt;对于一组做出选择的贪心题，我们可以&lt;strong&gt;默认&lt;/strong&gt;选择第一个，然后将之后选择的收益转换为从选择一转换到当前选择的收益．最后贪心地选择收益．例题：P14361&lt;/p&gt;
&lt;h1&gt;最小生成树性质&lt;/h1&gt;
&lt;p&gt;一张图的最小生成树中，若把采用的边称作“优边”，未采用的点称作“劣边”，那么如果在图中增加一些边，新的图的最小生成树中，存在：“优边”可能有用，“劣边”一定没用&lt;/p&gt;
&lt;h1&gt;排序去 log&lt;/h1&gt;
&lt;p&gt;对于多次重复的排序，可以在排序前将所有可能出现的数进行一次预排序，然后在每一次排序的时候标记有用的数．&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>组合数学</title><link>https://blog.jerrylab.top/blog/oi/%E6%95%B0%E6%95%B0/1-%E7%BB%84%E5%90%88%E6%95%B0%E5%AD%A6</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/oi/%E6%95%B0%E6%95%B0/1-%E7%BB%84%E5%90%88%E6%95%B0%E5%AD%A6</guid><description>排列组合和小球八题</description><pubDate>Wed, 04 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;加法原理&lt;/h1&gt;
&lt;p&gt;如果解决某个问题的方案可以分为&lt;strong&gt;互不重叠的几类&lt;/strong&gt;，那么把每一类的方案数加起来，就能得到总的方案数．&lt;/p&gt;
&lt;h1&gt;乘法原理&lt;/h1&gt;
&lt;p&gt;如果把解决某个问题的方案可以分成&lt;strong&gt;几个步骤&lt;/strong&gt;，那么把每一步的方案数依次乘起来，就能得到总的方案数．&lt;/p&gt;
&lt;h1&gt;排列数和组合数&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;排列数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假定一共有 $n$ 个人，我们要从中选择 $m$ 个人，让他们排成一排，拍一张照，最多可能有多少种不同的照片呢？&lt;/p&gt;
&lt;p&gt;像这样的题目，从 $n$ 个不同的元素中任取 $m(m\leq n)$ 个元素的所有排列的个数，叫做从 $n$ 个不同的元素中取出 $m$ 个元素的&lt;strong&gt;排列数&lt;/strong&gt;，记作 $A^m_n$&lt;/p&gt;
&lt;p&gt;那么，$A^m_n$ 到底是多少呢？首先，考虑第一个位置，由于有 $n$ 个人，所以有 $n$ 种情况，第二个位置呢？由于已经被选掉了一个人，所以第二个位置就有 $n-1$ 种选择，以此类推，第 $m$ 个位置就有 $n-m+1$ 种可能，它们是解决问题的几个步骤，所以使用乘法连接．&lt;/p&gt;
&lt;p&gt;一般的：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
A^m_n
&amp;#x26;=n(n-1)(n-2)\cdots(n-m+1)\
&amp;#x26;=\frac{n!}{(n-m)!}
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;特殊的：&lt;/p&gt;
&lt;p&gt;$$
A^n_n=n!
$$&lt;/p&gt;
&lt;p&gt;其中，形如 $n!$ 的运算是阶乘，定义 $n! = n(n-1)(n-2)\cdots\times1$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;组合数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假定你到一个洞穴去冒险，洞穴中有 $n$ 种互不相同的宝物，但是你的袋子只能装下 $m$ 件，你有多少种拿取的方式呢？&lt;/p&gt;
&lt;p&gt;考虑一个有 $n$ 个元素的集合 $T$，其拥有 $m$ 个元素的子集 $S$ 满足 $S\subseteq T$，符合条件的 $S$ 的个数即为&lt;strong&gt;组合数&lt;/strong&gt;，记作 $C^m_n$&lt;/p&gt;
&lt;p&gt;同样的，为了求取 $C^m_n$ 的值，我们可以优先求取 $A^m_n$ 的值，然后，由于选出的 $m$ 个元素应当不计顺序，故需排除上述选法中的重复：对于上述的每一个集合 $S$，在排列数中都被正好地重复计算了 $A^m_m$ 次．&lt;/p&gt;
&lt;p&gt;所以，一般的：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
C^m_n
&amp;#x26;=\frac{A^m_n}{A^m_m}\
&amp;#x26;=\frac{n!}{m!(n-m)!}
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;特殊的，$C^m_m=1$&lt;/p&gt;
&lt;p&gt;以下是一个使用费马小定理（参见 取模与逆元）和组合数公式来求组合数的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
#define int long long

const int MOD = 1e9 + 7;
const int N = 1e5 + 10;

int fac[N], inv[N];

// 快速幂
int fastpow(int a, int p) {
	int now = a;
	int ans = 1;
	while (p &gt;= 1) {
		if (p % 2 == 1) {
			(ans *= now) %= MOD;
		}
		(now *= now) %= MOD;
		p /= 2;
	}
	return ans;
}

// 初始化阶乘和对应的逆元
void init()
{
	// 初始化阶乘 
	fac[0] = 1;
	for (int i = 1; i &amp;#x3C;= N - 5; i++) {
		fac[i] = fac[i - 1] * i % MOD;
	}
	// 初始化逆元
	for (int i = 1; i &amp;#x3C;= N - 5; i++) {
		inv[i] = fastpow(fac[i], MOD - 2);
	}
}

// 计算组合数（n选m）对MOD取模
int C(int n, int m) {
	return fac[n] * inv[m] % MOD * inv[n - m] % MOD;
}

signed main() {
	init();
	int a, b;
	cin &gt;&gt; a &gt;&gt; b;
	cout &amp;#x3C;&amp;#x3C; C(a, b);
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;小球八题&lt;/h1&gt;
&lt;p&gt;已知有 $n$ 个球，将其放入 $m$ 个盒子里，有几种放法呢？根据球或盒子是否相同和是否允许空盒，关于这类题有 8 个变种，我们可以列出以下表格：&lt;/p&gt;
&lt;p&gt;| 类型  | 每个球是否完全等价 | 每个盒子是否完全等价 | 是否允许空盒 |
| --- | --------- | ---------- | ------ |
| A   | 是         | 是          | 是      |
| B   | 是         | 是          | 否      |
| C   | 是         | 否          | 是      |
| D   | 是         | 否          | 否      |
| E   | 否         | 是          | 是      |
| F   | 否         | 是          | 否      |
| G   | 否         | 否          | 是      |
| H   | 否         | 否          | 否      |&lt;/p&gt;
&lt;h2&gt;类型 G&lt;/h2&gt;
&lt;p&gt;这个类型最简单，不同的小球，不同的盒子，类似下面这个问题：&lt;/p&gt;
&lt;p&gt;有 $n$ 个人，$m$ 个景点，每个人只能去 1 个景点，求不同方案数．&lt;/p&gt;
&lt;p&gt;由于每个人有 $m$ 种选择方式且人与人之间互不干扰，所以总共方案数为 $m^n$&lt;/p&gt;
&lt;h2&gt;类型 D&lt;/h2&gt;
&lt;p&gt;考虑这样一个场景，有 $m$ 个人在分蛋糕，这个蛋糕是一个长方形的，长 $n$ 个单位，宽 1 个单位，且每个人都要有蛋糕，且长宽必须是整数．&lt;/p&gt;
&lt;p&gt;我们考虑竖着切，将其切成 $m$ 份，就是要在 $n-1$ 个空隙里选择 $m-1$ 空隙，切一刀，所以，一共有 $C^{m-1}_{n-1}$ 种方案．&lt;/p&gt;
&lt;h2&gt;类型 C&lt;/h2&gt;
&lt;p&gt;相对于类型 D 来说，类型 C 允许有人没分到蛋糕，因此，我们先让每个人拿一个单位的蛋糕，所以一共就有 $n+m$ 个单位，这样就转换为类型 D 了，答案是 $C^{m-1}_{n+m-1}$&lt;/p&gt;
&lt;h2&gt;类型 E&lt;/h2&gt;
&lt;p&gt;考虑这样一个情况，有 $n$ 个不同颜色的糖，需要分成 $m$ 堆．&lt;/p&gt;
&lt;p&gt;这个问题可以用 dp 解决．&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;状态定义&lt;/strong&gt;：定义 &lt;code&gt;dp[t][k]&lt;/code&gt; 表示在前 &lt;code&gt;t&lt;/code&gt; 个糖中分成 &lt;code&gt;k&lt;/code&gt; 堆的方案数．&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;初始状态&lt;/strong&gt;：$dp[1][k]=1,1\leq k\leq m$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;状态转移方程&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假定有一颗新的糖，你有两种选择，一种是新开一堆，另一种是将其放到之前的任意一堆之中．&lt;/p&gt;
&lt;p&gt;这两种贡献中，其一是新开一堆这种方式的贡献，则之前的状态可以表示为 $dp[t-1][k-1]$，其二是放到之前的堆中这种方式的贡献，则之前的状态可以表示为 $dp[t-1][k]$，由于有 $k$ 堆，所以这种方式一共有 $k$ 种情况．&lt;/p&gt;
&lt;p&gt;综合一下，可得：$dp[t][k]=dp[t-1][k-1]+dp[t-1][k]\times k$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最终状态&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;由于允许空盒，所以答案就是 $dp[n][1]+dp[n][2]+\cdots +dp[n][m]$，也就是：&lt;/p&gt;
&lt;p&gt;$$
\sum^{m}_{i=1}dp[n][i]
$$&lt;/p&gt;
&lt;h2&gt;类型 F&lt;/h2&gt;
&lt;p&gt;类似类型 E，但是由于不允许为空，所以最终状态是 $dp[n][m]$&lt;/p&gt;
&lt;h2&gt;类型 H&lt;/h2&gt;
&lt;p&gt;类似类型 F，但是，对于 F 中的每一种情况，由于盒子是不同的，都会有组合相同但顺序不同的 $m!$ 种变体，所以答案是 $dp[n][m]\times m!$&lt;/p&gt;
&lt;h2&gt;类型 A&lt;/h2&gt;
&lt;p&gt;已知有 $n$ 个一模一样的乒乓球，将其装进 $m$ 个一样的盒子里，有几种分法呢？由于每个盒子都是一样的，我们不妨规定前面盒子里球的数量必须比后面盒子里的多．&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;状态定义&lt;/strong&gt;：定义 &lt;code&gt;dp[t][k]&lt;/code&gt; 表示对于前 $t$ 个小球，放 $k$ 个盒子的方案数．&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;初始状态&lt;/strong&gt;：$dp[1][k]=1,1\leq k\leq m$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;状态转移方程&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假定最后一个盒子是空盒，那么可以把那个空盒去掉：$dp[t][k] \Rightarrow dp[t][k-1]$&lt;/p&gt;
&lt;p&gt;假定最后一个盒子不为空，那么由于最后一个盒子的球的数量小于前面的任何一个盒子，所以，在这种情况下，没有任何一个盒子为空，所以，我们不妨将每个盒子都取出来一个：$dp[t][k]\Rightarrow dp[t-k][k]$&lt;/p&gt;
&lt;p&gt;综合一下：$dp[t][k] = dp[t][k-1]+dp[t-k][k]$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最终状态&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;由于允许空盒，所以答案就是 $dp[n][1]+dp[n][2]+\cdots +dp[n][m]$，也就是：&lt;/p&gt;
&lt;p&gt;$$
\sum^{m}_{i=1}dp[n][i]
$$&lt;/p&gt;
&lt;p&gt;不难发现，和类型 E 的最终状态是一样的．&lt;/p&gt;
&lt;h2&gt;类型 B&lt;/h2&gt;
&lt;p&gt;类似类型 A，但是由于不允许为空，所以最终状态是 $dp[n][m]$&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>强连通分量</title><link>https://blog.jerrylab.top/blog/graph/scc</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/graph/scc</guid><description>将树和图存储起来</description><pubDate>Tue, 03 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我们可以使用 tarjan 来求取一张有向图中强连通分量的个数，以及每个强连通分量包含的点的编号．&lt;/p&gt;
&lt;h1&gt;强连通&lt;/h1&gt;
&lt;p&gt;在一张&lt;strong&gt;有向图&lt;/strong&gt;中，如果存在由一些点组成的集合，满足这个集合中的点都可以两两连通，那么称这些点和边为原图的&lt;strong&gt;强连通子图&lt;/strong&gt;．&lt;/p&gt;
&lt;p&gt;如果对于一个强连通子图 $T$，不存在任意一个其他点在加入 $T$ 后，能够仍然保持强连通子图的性质，那么称 $T$ 为原图的&lt;strong&gt;强连通分量&lt;/strong&gt;．通俗来说，强连通分量就是&lt;strong&gt;极大的&lt;/strong&gt;强连通图．&lt;/p&gt;
&lt;h1&gt;DFS 生成树&lt;/h1&gt;
&lt;p&gt;在有向图上，以任意一个节点为根节点进行一次 DFS，我们可以使用&lt;strong&gt;DFS 生成树&lt;/strong&gt;记录其过程．对于一个节点 A，其子节点 B 满足没有在图上出现过、且有一条边从 A 连接到 B．&lt;/p&gt;
&lt;p&gt;DFS 生成树可以辅助将图转换为树 + 边．&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    A((A)) --&gt; B((B))
    A --&gt; D((D))
    D --&gt; A
    B --&gt; C((C))
    D --&gt; E((E))
    E --&gt; F((F))
    E --&gt; H((H))
    F --&gt; G
    B --&gt; E
    E --&gt; G((G))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于上面这条边，如果以 A 为根节点建 DFS 生成树，一种是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TD
    A((A)) --&gt; B((B))
    B --&gt; C((C))
    %% cross
    B -.-&gt; E((E))
    E --&gt; H((H))
    E --&gt; F((F))
    F --&gt; G((G))
    %% forward
    E -.-&gt; G
    A --&gt; D((D))
    D --&gt; E
    %% back
    D -.-&gt; A
    linkStyle 2 stroke:blue
    linkStyle 6 stroke:green
    linkStyle 9 stroke:red
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这张图中，有四种边：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;树边：原有的，构成生成树的边
除此以外，还有三种额外的边（假定这些边是从节点 $u$ 连接到节点 $v$ 的）：&lt;/li&gt;
&lt;li&gt;反祖边：$v$ 是 $u$ 的祖先节点．&lt;/li&gt;
&lt;li&gt;前向边：$u$ 不是 $v$ 的父节点，但是是 $v$ 的祖先节点．&lt;/li&gt;
&lt;li&gt;横叉边：不是树边、反祖边和前向边的边．&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不难发现，每一个强连通分量都&lt;strong&gt;至少有一条&lt;/strong&gt;反祖边．&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如何在 DFS 的同时判断一条边是什么边？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们需要定义一些数据：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bool in_stack[i]&lt;/code&gt; 表示点 $i$ 是否在当前 DFS 的路径上．&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bool vis[i]&lt;/code&gt; 表示点 $i$ 是否被访问过．&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;|                 | &lt;strong&gt;&lt;code&gt;in_stack&lt;/code&gt; 成立&lt;/strong&gt;             | &lt;strong&gt;&lt;code&gt;in_stack&lt;/code&gt; 不成立&lt;/strong&gt;                                            |
| --------------- | ---------------------------- | ------------------------------------------------------------ |
| &lt;strong&gt;&lt;code&gt;vis[i]&lt;/code&gt; 成立&lt;/strong&gt;  | 反祖边 | 前向边或横叉边 |
| &lt;strong&gt;&lt;code&gt;vis[i]&lt;/code&gt; 不成立&lt;/strong&gt; | 不可能                          | 树边                                                           |&lt;/p&gt;
&lt;h1&gt;Tarjan 求强连通分量&lt;/h1&gt;
&lt;p&gt;Tarjan 只需要进行一次 DFS 搜索就能办到．&lt;/p&gt;
&lt;p&gt;为了解决问题，我们定义了以下数据：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bool in_stack[i]&lt;/code&gt; 表示点 $i$ 是否在当前 DFS 的路径上．&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stack&amp;#x3C;int&gt; s&lt;/code&gt; 当前强连通分量中的节点编号．&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int dfn[i]&lt;/code&gt; 第 $i$ 个点先序遍历到达时间．&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int low[i]&lt;/code&gt; 第 $i$ 个点通过当前子树内返祖边（即不经过其父节点）回到的最小时间．&lt;/li&gt;
&lt;li&gt;无需定义 &lt;code&gt;vis[i]&lt;/code&gt;，应为 &lt;code&gt;vis[i]&lt;/code&gt; 等效于 &lt;code&gt;dfn[i] == 0&lt;/code&gt;．&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不难发现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;祖先 &lt;code&gt;dfn&lt;/code&gt; 一定小于当前 &lt;code&gt;dfn&lt;/code&gt;．&lt;/li&gt;
&lt;li&gt;如果一个点 $u$ 能够到达一个点 $v$，点 u 的 &lt;code&gt;dfn&lt;/code&gt; 大于点 v 的 &lt;code&gt;dfn&lt;/code&gt;，那么这样的一组构成了一个强连通分量&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于到达一个新的节点 $u$，我们会进行以下操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始化操作
&lt;ul&gt;
&lt;li&gt;初始化 &lt;code&gt;dfn[u]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;初始化 &lt;code&gt;low[u] = dfn[u]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;将当前节点入栈 &lt;code&gt;s&lt;/code&gt;，并且标记 &lt;code&gt;in_stack&lt;/code&gt;．&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;遍历所有起点为 $u$，终点为 $v$ 的边
&lt;ul&gt;
&lt;li&gt;如果这条边是反祖边，将 &lt;code&gt;low[u]&lt;/code&gt; 进行更新．&lt;/li&gt;
&lt;li&gt;如果这条边是树边，DFS 搜索节点 $v$，然后将 &lt;code&gt;low[u]&lt;/code&gt; 进行更新．&lt;/li&gt;
&lt;li&gt;如果这条边是横叉边或前向边，那么说明 $v$ 已搜索完毕，其所在连通分量已被处理，所以不用对其做操作．&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;检查当前节点是不是一个新的强连通分量的根节点（&lt;code&gt;low[u] == dfn[u]&lt;/code&gt;）
&lt;ul&gt;
&lt;li&gt;如果是，那么当前 &lt;code&gt;s&lt;/code&gt; 中存储的就是一个强连通分量．清空 &lt;code&gt;s&lt;/code&gt;，初始化 &lt;code&gt;in_stack&lt;/code&gt; 来处理下一个强连通分量．&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;时间复杂度分析&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;初始化操作：$O(n)$&lt;/li&gt;
&lt;li&gt;遍历边操作：$O(m)$&lt;/li&gt;
&lt;li&gt;检查节点是不是强连通分量的根节点：$O(n)$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综合一下，时间复杂度 $O(m+n)$&lt;/p&gt;
&lt;h1&gt;标程&lt;/h1&gt;
&lt;p&gt;下面这个程序求解了图中的强连通分量数量和每一个强连通分量的组成部分．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;

const int N = 1e4 + 100;

int m, n, ans;
vector&amp;#x3C;int&gt; e[N];

//bool in_stack[i]表示点i是否在当前DFS的路径上．
//stack&amp;#x3C;int&gt; s当前强连通分量中的节点编号．
//int dfn[i]第i个点先序遍历到达时间．
//int low[i]第i个点通过当前子树内返祖边回到的最小时间．
int dfn[N], low[N], cnt;
bool in_stack[N];
stack&amp;#x3C;int&gt; s;

void tarjan(int u)
{
	if (dfn[u] != 0)
		return;
	// 初始化
	dfn[u] = ++cnt;
	low[u] = dfn[u];
	in_stack[u] = true;
	s.push(u);
	// 搜索边
	for (auto v : e[u])
	{
		// 如果是返祖边
		if (in_stack[v])
		{
			// 那么刷新low[u]
			low[u] = min(low[u], low[v]);
		}
		// 如果是树边
		else if (!dfn[v])
		{
			// 递归处理
			tarjan(v);
			low[u] = min(low[u], low[v]);
		}
		// 如果是横插边或前向边
		else
		{
			// 节点v已经被处理或将被处理
			// 什么也不用做
		}
	}
	// 检查现在的节点是不是强连通分量的根节点．
	if (low[u] == dfn[u]) // 如果说这个节点最小只能到达自己
	{
		// 那么这个节点就是强连通分量的根节点．
		ans++;
		vector&amp;#x3C;int&gt; v;
		while (s.top() != u)
		{
			v.push_back(s.top());
			in_stack[s.top()] = false;
			s.pop();

		}
		v.push_back(s.top());
		in_stack[s.top()] = false;
		s.pop();
		cout &amp;#x3C;&amp;#x3C; v.size() &amp;#x3C;&amp;#x3C; &quot; &quot;;
		for (auto it : v)
		{
			cout &amp;#x3C;&amp;#x3C; it &amp;#x3C;&amp;#x3C; &quot; &quot;;
		}
		cout &amp;#x3C;&amp;#x3C; endl;
	}
}

signed main()
{
#ifndef ONLINE_JUDGE
	freopen(R&quot;(E:\code\C++\sandbox\sandbox\in.txt)&quot;, &quot;r&quot;, stdin);
#endif
	cin &gt;&gt; n &gt;&gt; m;
	for (int i = 1; i &amp;#x3C;= m; i++)
	{
		int u, v;
		cin &gt;&gt; u &gt;&gt; v;
		e[u].push_back(v);
	}
	for (int i = 1; i &amp;#x3C;= n; i++)
	{
		tarjan(i);
	}
	cout &amp;#x3C;&amp;#x3C; ans &amp;#x3C;&amp;#x3C; endl;
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>最短路</title><link>https://blog.jerrylab.top/blog/graph/min-path</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/graph/min-path</guid><description>求图中的最短路问题</description><pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;问题重现&lt;/h1&gt;
&lt;p&gt;问题来自 &lt;a href=&quot;https://www.luogu.com.cn/problem/P4779&quot;&gt;洛谷-P4779&lt;/a&gt;，选取问题时稍作了更改．&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- 题目：P4779【模板】单源最短路径&lt;/p&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;给出一个有向图，请输出从某一点出发到所有点的最短路径长度．&lt;/p&gt;
&lt;h3&gt;输入格式&lt;/h3&gt;
&lt;p&gt;第一行包含三个整数 $n,m,s$，分别表示点的个数、有向边的个数、出发点的编号．&lt;/p&gt;
&lt;p&gt;接下来 $m$ 行每行包含三个整数 $u,v,w$，表示一条 $u \to v$ 的，长度为 $w$ 的边．&lt;/p&gt;
&lt;h3&gt;输出格式&lt;/h3&gt;
&lt;p&gt;输出一行 $n$ 个整数，第 $i$ 个表示 $s$ 到第 $i$ 个点的最短路径，若不能到达则输出 $2^{31}-1$．&lt;/p&gt;
&lt;h3&gt;数据范围&lt;/h3&gt;
&lt;p&gt;对于 $100%$ 的数据：$1 \leq n \leq 10^5$；$1 \leq m \leq 2\times 10^5$；$1 \leq u_i, v_i\leq n$；$0 \leq w_i \leq 10 ^ 9$；$0 \leq \sum w_i \leq 10 ^ 9$．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;最短路算法&lt;/h1&gt;
&lt;p&gt;注：为了表示方便，我们将从 $a$ 到点 $b$ 之间的边的权重表示为 &lt;code&gt;g[a][b]&lt;/code&gt;，如果没有边则为 &lt;code&gt;INF&lt;/code&gt;．&lt;/p&gt;
&lt;p&gt;在介绍最短路之前，我们需要了解一个概念：&lt;strong&gt;松弛&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们已知有两个点 $a$ 、$b$，它们之间的最短距离为 &lt;code&gt;g[a][b]&lt;/code&gt;．我们尝试使用第三个点 $c$，如果从 $a$ 点经过 $c$ 点再到 $b$ 点比从 $a$ 点直接到 $b$ 点所需要的路程少，则将最短距离更新．这样的操作叫做使用 $c$ 对从点 $a$ 到点 $b$ 之间的边进行松弛．&lt;/p&gt;
&lt;p&gt;使用代码表示如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;g[a][b] = max(g[a][b], g[a][c] + g[c][b]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Floyd 算法&lt;/h2&gt;
&lt;p&gt;Floyd 算法很简单，它的本质是动态规划．&lt;/p&gt;
&lt;p&gt;我们首先定义一个二维数组 &lt;code&gt;dist[i][j]&lt;/code&gt;，表示从点 $i$ 到点 $j$ 的最短距离．输入数据时，类似邻接矩阵，直接将数据输入 &lt;code&gt;dist&lt;/code&gt; 数组．&lt;/p&gt;
&lt;p&gt;而后，枚举每一个 $k$, $i$, $j$，尝试使用点 $k$ 对从点 $i$ 到点 $j$ 之间的边进行松弛．&lt;/p&gt;
&lt;p&gt;枚举完成后，此时的 &lt;code&gt;dist&lt;/code&gt; 数组就是最终的结果．&lt;/p&gt;
&lt;p&gt;Floyd 算法不仅可以解决单源最短路的题目，还可以解决多源单目标最短路的问题．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define N (int)1e5 // 最多可能有的节点数
#define M (int)2e5 // 最多可能有的边数
#define INF 0xffffff // 极大值，代表无穷
using namespace std;
int n, m, dist[N][N];
void floyd()
{
	// 初始化数据
    for (int i = 1; i &amp;#x3C;= n; i++){
        for (int j = 1; j &amp;#x3C;= n; j++){
            if (i == j) dist[i][j] = 0; // 自己到自己的距离为0
            else dist[i][j] = INF;
        }
    }

    for (int k = 1; k &amp;#x3C;= n; k++)
        for(int i = 1; i &amp;#x3C;= n; i++)
            for(int j = 1; j &amp;#x3C;= n; j++)
                dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
                // 尝试使用点k对从点i到点j之间的边进行松弛
}
int main(){
    int s;
    cin &gt;&gt; n &gt;&gt; m &gt;&gt; s;

    // 输入数据
    for (int i = 1; i &amp;#x3C;= m; i++){
        int u, v, w;
        cin &gt;&gt; u &gt;&gt; v &gt;&gt; w;
        dist[u][v] = w;
    }

    floyd();

    for (int i = 1; i &amp;#x3C;= n; i++) {
        if (dist[s][i] == INF) {
            cout &amp;#x3C;&amp;#x3C; &quot;2147483647 &quot;; // 输出2^31-1
        } else {
            cout &amp;#x3C;&amp;#x3C; dist[s][i] &amp;#x3C;&amp;#x3C; &quot; &quot;;
        }
    }
    cout &amp;#x3C;&amp;#x3C; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，Floyd 算法太慢了，时间复杂度到达了 $O(n^3)$，因为基于邻接矩阵，空间复杂度也达到了 $O(n^2)$，所以只能拿到部分分．&lt;/p&gt;
&lt;h2&gt;SPFA 算法&lt;/h2&gt;
&lt;p&gt;我们可以定义一个数组 &lt;code&gt;dist[i]&lt;/code&gt;，表示从源点 $s$ 到 $i$ 的最小距离．初始时每一项都是无穷大，而后将 &lt;code&gt;dist[s]&lt;/code&gt; 设定为 &lt;code&gt;0&lt;/code&gt;，表示从源点 $s$ 到本身的最小距离为 &lt;code&gt;0&lt;/code&gt;．之后，对于每个目标点 $i$ （一开始是 $s$ ）的每一个相邻节点 $j$，都尝试使用点 $i$ 对从点 $s$ 到点 $j$ 的边进行松弛．而后，将 $j$ 选中为目标点，重复．直到没有待被选中的点位置，此时的 &lt;code&gt;dist&lt;/code&gt; 数组就是最终的结果，类似于广度优先搜索．&lt;/p&gt;
&lt;p&gt;SPFA 算法对于处理带有负权的图是最快的．&lt;/p&gt;
&lt;p&gt;以下是基于链式前向星的源代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int dist[N]; // dist[i]表示从源点到i的最小距离
bool vis[N]; // 检查是否在队列中
void spfa(int s){
// 在dist数组中记录从s出发到每个点的最短路
    for(int i = 1;i &amp;#x3C;= n;i++){ // 初始化
        dist[i] = INF; // 从s到每一个点都暂时无法到达
        vis[i] = false; // 每一个节点都没有被访问过
    }
    queue&amp;#x3C;int&gt; q; // 类似于bfs中的队列，储存待被松弛的目标点
    q.push(s); // 将s设定为目标点
    vis[s] = true; // s现在在队列中
    dist[s] = 0; // 从源点s到本身的最小距离为0
    while(!q.empty()){ // 直到q为空时才停止
        int x = q.front(); // 将x设为当前的目标点
        q.pop(); // 从待办清单中移除
        vis[x] = false; // 移除标记
        for(int i = fi[x]; i != 0; i = ne[i]){
        // 使用链式前向星寻找所有x的相邻节点
                int v = to[i], w = co[i];
                if (dist[v] &gt; dist[x] + w){
                    dist[v] = dist[x] + w; // 松弛
                    if (!vis[v]) { // 只有当v不在队列中时才将其加入队列
                            q.push(v); // 将v入队
                            vis[v] = true; // 将v标记为在队列中
                    }
                }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显然，SPFA 算法的时间复杂度在最坏的情况下为 $O(nm)$，仍旧无法拿到满分．&lt;/p&gt;
&lt;h2&gt;dijkstra 算法&lt;/h2&gt;
&lt;p&gt;我们仍旧可以像 SPFA 一样，定义一个 &lt;code&gt;dist&lt;/code&gt; 数组，不过，在松弛的时候，我们需要选取 &lt;code&gt;dist&lt;/code&gt; 数组中最小的一个节点 $i$，对于 $i$ 的每一个相邻节点 $j$，都尝试使用 $i$ 来对从节点 $s$ 到节点 $j$ 的这条边进行松弛，直到每一个点都被选中作为点 $i$ 过，此时的 &lt;code&gt;dist&lt;/code&gt; 数组就是最终的结果．&lt;/p&gt;
&lt;p&gt;dijkstra 其实就是 SPFA 的贪心版本．&lt;/p&gt;
&lt;p&gt;以下是基于链式前向星的源代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int dist[N];
bool vis[N]; // 检查是否在已经被选中过
void dijkstra(int s){
// 以s为原点进行松弛
    for(int i = 1;i &amp;#x3C;= n;i++){ // 初始化
        dist[i] = INF; // 从s到每一个点都暂时无法到达
        vis[i] = false; // 每一个节点都没有被访问过
    }
    dist[s] = 0; // s到自己的距离为0
    for (int i = 1; i &amp;#x3C;= n; i++) // 依次选择每一个节点
    {
	    // 找出dist中最小且没有被选择过的一项
        int minn = -1; // minn表示最小的一项的索引
        for(int j = 1; j &amp;#x3C;= n; j++){
            if (vis[j]) continue; // 如果被选择过，则不会再次被选择
            if (minn == -1 || dist[j] &amp;#x3C; dist[minn]) minn = j;
        }

        if (dist[minn] == INF) continue;
        // 如果被选择的这个节点无法到达，则这个节点不能被选中

		// 使用链式前向星进行遍历
        for(int j = fi[minn]; j != 0; j = ne[j])
        {
	        // 松弛
            dist[to[j]] = min(dist[to[j]], dist[minn] + co[j]);
        }
        vis[minn] = true; // 将minn这个节点标记为已被选择过
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显然，dijkstra 算法的时间复杂度在最坏的情况下为 $O(n^2)$，仍旧无法拿到满分．此外，此算法无法处理含有负权边的图．&lt;/p&gt;
&lt;h2&gt;dijkstra 算法优化&lt;/h2&gt;
&lt;p&gt;dijkstra 算法可以利用堆进行优化．&lt;/p&gt;
&lt;p&gt;宏观整个算法，最浪费时间的事情就是找出 &lt;code&gt;dist&lt;/code&gt; 中最小的一项，因此我们就想到了使用堆来优化．&lt;/p&gt;
&lt;p&gt;在赛场上直接实现堆不太现实，因此我们可以使用 C++STL 优先队列来快速查找最小的一项．&lt;/p&gt;
&lt;p&gt;以下是基于链式前向星的源代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int dist[N];
bool vis[N]; // 检查是否在队列中

struct Node {
    int dist; // 距离
    int id; // 节点编号
    friend bool operator&amp;#x3C;(Node a, Node b){
        return a.dist &gt; b.dist; // 此时如果返回true，则表示a的优先级低于b
    }  // 由于优先队列是大的元素优先，所以需要重载运算符，使得dist小的元素优先
};

void dijkstra(int s){
    // 初始化
    memset(dist, 0x3f, sizeof(dist));
    memset(vis, 0, sizeof(vis));

    priority_queue&amp;#x3C;Node&gt; q; // 优先队列，用于存储节点
    dist[s] = 0; // s点到s点的距离为0
    q.push({0, s}); // 将s点加入队列
    while(!q.empty()){ // 当队列为空时，立即结束
        Node tmp = q.top(); // tmp为队列中最小的元素，获得最小的元素
        q.pop(); // 将最小的元素弹出
        int x = tmp.id; // x为获得的最小的元素的节点编号
        if (vis[x]) continue; // 如果曾经被访问过，就跳过不访问
        vis[x] = true; // 标记为访问过
        for (int i = fi[x]; i != 0; i = ne[i]){ // 遍历所有以x为起点的边
            int v = to[i], w = co[i]; // v为终点，w为权值
            if (dist[v] &gt; dist[x] + w){ // 松弛操作：如果从s到v的距离大于从s到x的距离加上x到v的距离
                dist[v] = dist[x] + w; // 就更新从s到v的距离
                q.push({dist[v], v}); // 将v点加入队列
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的算法可以达到时间复杂度是 $O(n\log n)$，可以完成这道题目．&lt;/p&gt;
&lt;h1&gt;最短路算法比较&lt;/h1&gt;
&lt;p&gt;| 算法          | 时间复杂度        | 是否可以有负权边 | 使用场景     |
| ----------- | ------------ | -------- | -------- |
| Floyd       | $O(n^3)$     | 是        | 多源最短路    |
| SPFA        | 最差 $O(nm)$   | 是        | 在有负权时    |
| dijkstra    | $O(n^2)$     | 否        | 建议使用其优化版 |
| dijkstra 优化 | $O(n\log n)$ | 否        | 没有负权时    |&lt;/p&gt;
&lt;h1&gt;标程&lt;/h1&gt;
&lt;p&gt;以下的代码在洛谷中提交后，100% 的样例是 AC 的．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
#define N (int)1e5 + 100
#define M (int)2e5 + 100
#define INF 0xffffff
using namespace std;

int n, m;

int cnt;
int fi[N];
int ne[M];
int to[M];
int co[M];

void add(int u, int v, int w){
    cnt++;
    ne[cnt] = fi[u];
    fi[u] = cnt;
    to[cnt] = v; co[cnt] = w;
}

int dist[N];
bool vis[N];

struct Node {
    int dist;
    int id;
    friend bool operator&amp;#x3C;(Node a, Node b){
        return a.dist &gt; b.dist;
    }
};

void dijkstra(int s){
    memset(dist, 0x3f, sizeof(dist));
    memset(vis, 0, sizeof(vis));

    priority_queue&amp;#x3C;Node&gt; q;
    dist[s] = 0;
    q.push({0, s});
    while(!q.empty()){
        Node tmp = q.top();
        q.pop();
        int x = tmp.id;
        if (vis[x]) continue;
        vis[x] = true;
        for (int i = fi[x]; i != 0; i = ne[i]){
            int v = to[i], w = co[i];
            if (dist[v] &gt; dist[x] + w){
                dist[v] = dist[x] + w;
                q.push({dist[v], v});
            }
        }
    }
}

int main(){
    int s;
    cin &gt;&gt; n &gt;&gt; m &gt;&gt; s;

    for (int i = 1; i &amp;#x3C;= m; i++){
        int u, v, w;
        cin &gt;&gt; u &gt;&gt; v &gt;&gt; w;
        add(u, v, w);
    }

    dijkstra(s);

    for (int i = 1; i &amp;#x3C;= n; i++) {
        if (dist[i] == INF) {
            cout &amp;#x3C;&amp;#x3C; 2147483647 &amp;#x3C;&amp;#x3C; &quot; &quot;;
        } else {
            cout &amp;#x3C;&amp;#x3C; dist[i] &amp;#x3C;&amp;#x3C; &quot; &quot;;
        }
    }
    cout &amp;#x3C;&amp;#x3C; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>最近公共祖先</title><link>https://blog.jerrylab.top/blog/graph/lca</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/graph/lca</guid><description>倍增法求解树上LCA问题</description><pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!note]- 题目：P3379 最近公共祖先（LCA）&lt;/p&gt;
&lt;p&gt;如题，给定一棵有根多叉树，请求出指定两个点直接最近的公共祖先．&lt;/p&gt;
&lt;h3&gt;输入格式&lt;/h3&gt;
&lt;p&gt;第一行包含三个正整数 $N,M,S$，分别表示树的结点个数、询问的个数和树根结点的序号．&lt;/p&gt;
&lt;p&gt;接下来 $N-1$ 行每行包含两个正整数 $x, y$，表示 $x$ 结点和 $y$ 结点之间有一条直接连接的边（数据保证可以构成树）．&lt;/p&gt;
&lt;p&gt;接下来 $M$ 行每行包含两个正整数 $a, b$，表示询问 $a$ 结点和 $b$ 结点的最近公共祖先．&lt;/p&gt;
&lt;h3&gt;输出格式&lt;/h3&gt;
&lt;p&gt;输出包含 $M$ 行，每行包含一个正整数，依次为每一个询问的结果．&lt;/p&gt;
&lt;h3&gt;数据范围&lt;/h3&gt;
&lt;p&gt;对于 $100%$ 的数据，$1 \leq N,M\leq 5\times10^5$，$1 \leq x, y,a ,b \leq N$，&lt;strong&gt;不保证&lt;/strong&gt; $a \neq b$．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;分析&lt;/h1&gt;
&lt;p&gt;LCA（Lowest Common Ancestor）即树上两个节点 $u,v$ 路径上离根最近的公共节点．&lt;/p&gt;
&lt;p&gt;常用方法有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;倍增法（预处理 $O(n\log n)$，单次查询 $O(\log n)$）&lt;/li&gt;
&lt;li&gt;树链剖分（重链剖分）&lt;/li&gt;
&lt;li&gt;Tarjan 离线算法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最常用、易实现的是&lt;strong&gt;倍增法&lt;/strong&gt;．&lt;/p&gt;
&lt;p&gt;倍增法需要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;预处理每个节点的 $2^k$ 级祖先（即 $fa[u][k]$ 表示 $u$ 的第 $2^k$ 级祖先）．&lt;/li&gt;
&lt;li&gt;查询时先将 $u,v$ 跳到同一深度，然后一起向上跳，距离从 $2^{\log n}$ 到 $2^0$，如果跳到相同的节点就调回来，下一次少跳一些，直到找到最近公共祖先．&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体实现步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;建树与初始化&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;用邻接表存树．&lt;/li&gt;
&lt;li&gt;记录每个节点的深度 $deep[u]$．&lt;/li&gt;
&lt;li&gt;记录每个节点的父节点 $fa[u][0]$．&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;预处理倍增表&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;对每个节点 $u$，$fa[u][k] = fa[fa[u][k-1]][k-1]$．&lt;/li&gt;
&lt;li&gt;预处理 $k$ 从 $1$ 到 $\log n$．&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;查询 LCA&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;若 $u,v$ 深度不同，先将较深的节点向上跳到同一深度．&lt;/li&gt;
&lt;li&gt;然后从大到小枚举 $k$，若 $fa[u][k] \neq fa[v][k]$，则 $u,v$ 同时跳到各自的 $2^k$ 级祖先．&lt;/li&gt;
&lt;li&gt;最后 $u$ 和 $v$ 的父节点就是 LCA．&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;复杂度分析&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;预处理：$O(n\log n)$&lt;/li&gt;
&lt;li&gt;单次查询：$O(\log n)$&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;标程&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;

using namespace std;

const int N = 5e5 + 100;

int n, m, s;
vector&amp;#x3C;int&gt; edge[N];
int deep[N];
int fa[N][23];

// 预处理
void getInfo(int t) {
	// 预处理父节点
	deep[t] = deep[fa[t][0]] + 1;
	// 预处理祖宗节点
	for (int i = 1; i &amp;#x3C; 23; ++i) 
		fa[t][i] = fa[fa[t][i - 1]][i - 1];

	// 递归处理所有的边
	for (int i = 0; i &amp;#x3C; edge[t].size(); ++i) {
		int to = edge[t][i];
		if (to == fa[t][0]) continue;
		fa[to][0] = t;
		getInfo(to);
	}
}

// 获得t节点向上跳v格跳到的节点编号
int jump(int t, int v) {
	for (int i = 22; i &gt;= 0; --i) {
		if (v &gt;= (1 &amp;#x3C;&amp;#x3C; i)) {
			t = fa[t][i];
			v -= (1 &amp;#x3C;&amp;#x3C; i);
		}
	}
	return t;
}

// 获取节点x和节点y的lca
int lca(int x, int y) {
	// 统一深度
	if (deep[y] &gt; deep[x]) y = jump(y, deep[y] - deep[x]);
	else x = jump(x, deep[x] - deep[y]);
	// 处理x和y是同一支上的，有血缘关系的情况
	if (x == y) return x;

	// 向上跳
	for (int i = 22; i &gt;= 0; --i) {
		if (fa[x][i] == fa[y][i]) continue;
		x = fa[x][i], y = fa[y][i];
	}
	return fa[x][0];
}

int main() {
	cin &gt;&gt; n &gt;&gt; m &gt;&gt; s;
	for (int i = 1; i &amp;#x3C;= n - 1; ++i) {
		int x, y;
		cin &gt;&gt; x &gt;&gt; y;
		edge[x].push_back(y);
		edge[y].push_back(x);
	}
	getInfo(s);
	for (int i = 1; i &amp;#x3C;= m; ++i) {
		int x, y;
		cin &gt;&gt; x &gt;&gt; y;
		cout &amp;#x3C;&amp;#x3C; lca(x, y) &amp;#x3C;&amp;#x3C; endl;
	}
	return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>最小生成树</title><link>https://blog.jerrylab.top/blog/graph/mst</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/graph/mst</guid><description>图到树的蜕变</description><pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;定义&lt;/h1&gt;
&lt;p&gt;最小生成树是指在一张&lt;strong&gt;无向图&lt;/strong&gt;中，选取一些边，使其满足以下条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;选择的边能够使图中的每一个节点直接或间接相连&lt;/li&gt;
&lt;li&gt;在满足条件 1 的前提下，使得所有边的和尽可能小．（即获得连通所有点的最小代价）&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;[!node]- 题目：P3366 最小生成树&lt;/p&gt;
&lt;p&gt;如题，给出一个无向图，求出最小生成树，如果该图不连通，则输出 &lt;code&gt;orz&lt;/code&gt;．&lt;/p&gt;
&lt;h3&gt;输入格式&lt;/h3&gt;
&lt;p&gt;第一行包含两个整数 $N,M$，表示该图共有 $N$ 个结点和 $M$ 条无向边．&lt;/p&gt;
&lt;p&gt;接下来 $M$ 行每行包含三个整数 $X_i,Y_i,Z_i$，表示有一条长度为 $Z_i$ 的无向边连接结点 $X_i,Y_i$．&lt;/p&gt;
&lt;h3&gt;输出格式&lt;/h3&gt;
&lt;p&gt;如果该图连通，则输出一个整数表示最小生成树的各边的长度之和．如果该图不连通则输出 &lt;code&gt;orz&lt;/code&gt;．&lt;/p&gt;
&lt;h3&gt;数据范围&lt;/h3&gt;
&lt;p&gt;对于 $100%$ 的数据：$1\le N\le 5000$，$1\le M\le 2\times 10^5$，$1\le Z_i \le 10^4$．&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;kruskal 算法&lt;/h1&gt;
&lt;p&gt;kruskal 算法的本质是贪心，主要分为以下几步&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先将所有边按权值从小到大排序．&lt;/li&gt;
&lt;li&gt;依次选择权值最小的边，如果这条边连接的两个端点不在同一个连通块（即不形成环），就将这条边加入生成树．&lt;/li&gt;
&lt;li&gt;重复上述过程，直到选出 n-1 条边（n 为节点数），或者所有边都被处理完．&lt;/li&gt;
&lt;li&gt;如果仍旧有点没有被连通，那么原图就不是连通图&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;判断两个边是否连通，常使用并查集．&lt;/p&gt;
&lt;p&gt;时间复杂度：$O(m)$&lt;/p&gt;
&lt;h2&gt;标程——kruskal&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
const int N = 5e3 + 100;
const int M = 2e5 + 100;
int n, m;

int f[N];

// 定义一条边
struct Edge
{
	// u表示边的起点
	// v表示边的终点
	// w表示边的权值
	int u, v, w;
	friend bool operator&amp;#x3C;(Edge a, Edge b)
	{
		return a.w &amp;#x3C; b.w;
	}
} e[M];

// 并查集模板
class DSU
{
public:
	// 初始化并查集
	void build(int size)
	{
		for (int i = 1; i &amp;#x3C;= size; i++)
		{
			f[i] = i;
		}
	}
	// 找到节点t的最顶端的父节点
	int find(int t)
	{
		if (f[t] != t)
			f[t] = find(f[t]);
		return f[t];
	}
	// 将节点a和节点b所在集合合并
	void merge(int a, int b)
	{
		int x = find(a), y = find(b);
		if (x != y)
			f[x] = y;
	}
	// 查询a和b是否处于同一集合
	bool query(int a, int b)
	{
		return find(a) == find(b);
	}
} dsu;

void kruskal()
{
	// 进行排序
	sort(e + 1, e + m + 1);
	int ans = 0;
	// 依次选取每一条边，尝试添加
	for (int i = 1; i &amp;#x3C;= m; i++)
	{
		int u = e[i].u, v = e[i].v;
		if (!dsu.query(u, v))
		{
			ans += e[i].w;
			dsu.merge(u, v);
		}
	}
	// 判断是否连通
	for (int i = 1; i &amp;#x3C;= n; i++)
	{
		if (!dsu.query(1, i))
		{
			cout &amp;#x3C;&amp;#x3C; &quot;orz&quot; &amp;#x3C;&amp;#x3C; endl;
			return;
		}
	}
	cout &amp;#x3C;&amp;#x3C; ans &amp;#x3C;&amp;#x3C; endl;
	return;
}

int main()
{
	cin &gt;&gt; n &gt;&gt; m;
	for (int i = 1; i &amp;#x3C;= m; i++)
	{
		cin &gt;&gt; e[i].u &gt;&gt; e[i].v &gt;&gt; e[i].w;
	}
	dsu.build(n);
	kruskal();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;prim 算法&lt;/h1&gt;
&lt;p&gt;Prim 算法也是一种贪心．&lt;/p&gt;
&lt;p&gt;核心思想：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从任意一个点出发，每次选择一条连接已选点集和未选点集、权值最小的边，将新点加入生成树．&lt;/li&gt;
&lt;li&gt;重复 n-1 次，直到所有点都被包含．&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用优先队列（堆）优化，时间复杂度 $O(m\log n)$．&lt;/p&gt;
&lt;p&gt;适合稠密图，代码实现类似 Dijkstra，但 Prim 关注的是“连通所有点的最小代价”，不是最短路．&lt;/p&gt;
&lt;h2&gt;标程——prim&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
const int N = 5e3 + 100;
const int M = 2e5 + 100;
int n, m;

struct Edge
{
	int to, w;
	friend bool operator&amp;#x3C;(Edge a, Edge b)
	{
		return a.w &gt; b.w;
	}
};

vector&amp;#x3C;Edge&gt; e[N];
bool mark[N];

void prim()
{
	int ans = 0;
	priority_queue&amp;#x3C;Edge&gt; q;
	q.push({1, 0});
	while (!q.empty())
	{
		int v = q.top().to, w = q.top().w;
		q.pop();
		if (mark[v])
			continue;
		mark[v] = true;
		ans += w;
		for (int i = 0; i &amp;#x3C; e[v].size(); i++)
		{
			q.push(e[v][i]);
		}
	}
	for (int i = 1; i &amp;#x3C;= n; i++)
	{
		if (!mark[i])
		{
			cout &amp;#x3C;&amp;#x3C; &quot;orz&quot;;
			return;
		}
	}
	cout &amp;#x3C;&amp;#x3C; ans;
	return;
}

int main()
{
	cin &gt;&gt; n &gt;&gt; m;
	for (int i = 1; i &amp;#x3C;= m; i++)
	{
		int u, v, w;
		cin &gt;&gt; u &gt;&gt; v &gt;&gt; w;
		e[u].push_back({v, w});
		e[v].push_back({u, w});
	}
	prim();
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>图的储存</title><link>https://blog.jerrylab.top/blog/graph/store</link><guid isPermaLink="true">https://blog.jerrylab.top/blog/graph/store</guid><description>将树和图存储起来</description><pubDate>Sun, 01 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;为了储存一张图，发明算法的人们费劲了心思，极其主要的，有以下几种．&lt;/p&gt;
&lt;h1&gt;邻接矩阵&lt;/h1&gt;
&lt;p&gt;我们定义一个二维数组 &lt;code&gt;g[i][j]&lt;/code&gt;，表示从点 &lt;code&gt;i&lt;/code&gt; 到点 &lt;code&gt;j&lt;/code&gt; 的权重．千万要记得初始化．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#define N (int)1e5 // 最多可能有的节点数
#define INF 0xffffff // 极大值，代表无穷

int g[N][N]

void addEdge(int u, int v, int w)
{
    g[u][v] = w;
    // g[v][u] = w; // 如果是无向图的话
}

int getWeight(int u, int v)
{
    return g[u][v];
}

int main()
{
    for(int i = 1; i &amp;#x3C;= n; i++){
        for(int j = 1; j &amp;#x3C;= n; j++){
            g[i][j] = INF; // 初始化
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;邻接矩阵很简单，也很容易写，但很低效，很占用空间，在上面的题目中，$n$ 达到了 $10^5$，明显会 MLE．&lt;/p&gt;
&lt;h1&gt;邻接表&lt;/h1&gt;
&lt;p&gt;为了解决邻接矩阵占用空间的问题，我们可以定义 &lt;code&gt;vector&amp;#x3C;Node&gt; v[N]&lt;/code&gt; 来创建一个邻接链表．&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;v[i]&lt;/code&gt;，会存储所有以 &lt;code&gt;i&lt;/code&gt; 为起点的边的终点．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#define N (int)1e5 // 最多可能有的节点数
#define INF 0xffffff // 极大值，代表无穷

// Node储存一个节点
struct Node
{
    int id; // 编号
    int w; // 权重
}

vector&amp;#x3C;Node&gt; v[N];

// 添加一条边
void addEdge(int u, int v, int w)
{
    v[u].push_back({v, w});
    // v[v].push_back({u, w}); // 如果是无向图的话
}

// 获取权值
int getWeight(int u, int v)
{
    for(int i = 0; i &amp;#x3C; v[u].size(); i++){ // 遍历以u为起点的边
        if (v[u][i].id == v) return v[u][i].w;
    }
    return INF;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显然，这种方法解决了邻接矩阵可能出现的问题，例题可以使用．&lt;/p&gt;
&lt;h1&gt;链式前向星&lt;/h1&gt;
&lt;p&gt;当然，还有更加优雅的方式．&lt;/p&gt;
&lt;p&gt;我们为每一条边编号，储存每一条边．&lt;/p&gt;
&lt;p&gt;我们尝试将最新的边，定义为编号最大的边．&lt;/p&gt;
&lt;p&gt;我们定义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cnt&lt;/code&gt;，表示当前所有边中，编号最大的一条边．储存的是边的编号．&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fi[i]&lt;/code&gt;，表示最新的、且指向编号为 &lt;code&gt;i&lt;/code&gt; 的点的边的编号．注意：&lt;code&gt;i&lt;/code&gt; 是点的编号，储存的值是边的编号．&lt;/li&gt;
&lt;li&gt;&lt;code&gt;to[i]&lt;/code&gt;，表示编号为 &lt;code&gt;i&lt;/code&gt; 的边的终点．注意：&lt;code&gt;i&lt;/code&gt; 是边的编号，而储存的值是点的编号．&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ne[i]&lt;/code&gt;，表示编号比边 &lt;code&gt;i&lt;/code&gt; 较小的一条，同样指向 &lt;code&gt;to[i]&lt;/code&gt; 的边．注意：&lt;code&gt;i&lt;/code&gt; 和储存的值都是边的编号．&lt;/li&gt;
&lt;li&gt;&lt;code&gt;co[i]&lt;/code&gt;，表示编号为 &lt;code&gt;i&lt;/code&gt; 的边的权值．注意：&lt;code&gt;i&lt;/code&gt; 是边的编号，储存的值是权值．&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#define N (int)1e5 // 最多可能有的节点数
#define M (int)2e5 // 最多可能有的边数
#define INF 0xffffff // 极大值，代表无穷

int cnt; // 目前边的总数
int fi[N]; // fi[i]（即first）表示最新的、指向编号为i的点的边编号
int ne[M]; // ne[i]（即next）表示除去编号为i的这条边外，最新的，指向to[i]的边的编号
int to[M]; // to[i]表示编号为i的边指向的节点编号
int co[M]; // co[i]（即cost）表示编号为i的边的权值

// 增加一条以u为起点，v为终点，权值为w的边
void add(int u, int v, int w){
    cnt++; // 将总数加1，加1后的cnt就是现在要加的这条边的编号
    ne[cnt] = fi[u]; // 将这条边的下一个同源边设定为未更新过的fi[u]
    fi[u] = cnt; // 更新fi[u]，将最新的、从u发出的边（fi[u]）设为当前边
    to[cnt] = v; co[cnt] = w; // 将当前的边的终点设为v，花费设为w
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如何遍历一个链式前向星呢？&lt;/p&gt;
&lt;p&gt;其实很像 邻接表，采用“顺藤摸瓜”的方式可以遍历邻接链表．&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(int i = 1; i &amp;#x3C;= n; i++){
    int now; // 定义now表示这条以i为起点的边的终点
    now = fi[i]; // 初始化now为最新的、指向编号为i的点的边的编号
    while(now != 0){ // 检查是否遍历完成
        // 你可以在这里，获取到一条从i到to[now]，权重为co[now]的边
        // Do something...
        now = ne[now];
        // 将j设定为除去编号为j的这条边外，最新的，指向to[j]这个点的边的编号
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;稍微整合一下，将 &lt;code&gt;now&lt;/code&gt; 变成 &lt;code&gt;j&lt;/code&gt;，就可以有更加优雅的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;for(int i = 1; i &amp;#x3C;= n; i++){ // 遍历每一个点i
    for(
        int j = fi[i]; // 初始化j为最新的、指向编号为i的点的边的编号
        j != 0; // 检查是否遍历完成
        j = ne[j] // 将j设定为除去编号为j的这条边外，最新的，指向to[j]的边的编号
    ){
        // 你可以在这里，获取到一条从i到to[j]，权重为co[j]的边
        // Do something...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特别要当心的是，此处的 &lt;code&gt;now&lt;/code&gt; 和 &lt;code&gt;j&lt;/code&gt; 并非表示一条边的终点，而是&lt;strong&gt;一条边的编号&lt;/strong&gt;．所以获得这条边的终点需要使用 &lt;code&gt;to[j]&lt;/code&gt;．&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item></channel></rss>