数据范围: $1\leq n\leq 10^6$ , $1\leq m\leq 10^{18}$ , $1\leq k\leq n$ , $0\le a_i\le 10^9$ 。
??? note "解题思路"
-
这里显然不能暴力模拟跳 $m$ 次。因为 $m$ 最大可到 $10^{18}$ 级别,如果暴力模拟的话,时间承受不住。
所以就需要进行一些预处理,提前整合一些信息,以便于在查询的时候更快得出结果。如果记录下来每一个可能的跳跃次数的结果的话,不论是时间还是空间都难以承受。
???+note "[Luogu P1873 砍树](https://www.luogu.com.cn/problem/P1873)"
伐木工人米尔科需要砍倒 M 米长的木材。这是一个对米尔科来说很容易的工作,因为他有一个漂亮的新伐木机,可以像野火一样砍倒森林。不过,米尔科只被允许砍倒单行树木。
- 米尔科的伐木机工作过程如下:米尔科设置一个高度参数H(米),伐木机升起一个巨大的锯片到高度H,并锯掉所有的树比H高的部分(当然,树木不高于H米的部分保持不变)。米尔科就行到树木被锯下的部分。
+ 米尔科的伐木机工作过程如下:米尔科设置一个高度参数 H(米),伐木机升起一个巨大的锯片到高度 H,并锯掉所有的树比 H 高的部分(当然,树木不高于 H 米的部分保持不变)。米尔科就行到树木被锯下的部分。
- 例如,如果一行树的高度分别为20,15,10和17,米尔科把锯片升到15米的高度,切割后树木剩下的高度将是15,15,10和15,而米尔科将从第1棵树得到5米,从第4棵树得到2米,共得到7米木材。
+ 例如,如果一行树的高度分别为 20,15,10 和 17,米尔科把锯片升到 15 米的高度,切割后树木剩下的高度将是 15,15,10 和 15,而米尔科将从第 1 棵树得到 5 米,从第 4 棵树得到 2 米,共得到 7 米木材。
- 米尔科非常关注生态保护,所以他不会砍掉过多的木材。这正是他为什么尽可能高地设定伐木机锯片的原因。帮助米尔科找到伐木机锯片的最大的整数高度H,使得他能得到木材至少为M米。换句话说,如果再升高1米,则他将得不到M米木材。
+ 米尔科非常关注生态保护,所以他不会砍掉过多的木材。这正是他为什么尽可能高地设定伐木机锯片的原因。帮助米尔科找到伐木机锯片的最大的整数高度 H,使得他能得到木材至少为 M 米。换句话说,如果再升高 1 米,则他将得不到 M 米木材。
??? note "解题思路"
我们可以在 1 到 1,000,000,000(10 亿)中枚举答案,但是这种朴素写法肯定拿不到满分,因为从 1 跑到 10 亿太耗时间。我们可以对答案进行 1 到 10 亿的二分,然后,每次都对其进行检查可行性(一般都是使用贪心法)。 **这就是二分答案。**
看完了上面的代码,你肯定会有两个疑问:
- 1. 为何搜索区间是左闭右开的?
+ 1. 为何搜索区间是左闭右开的?
因为搜到最后,会这样(以合法的最大值为例):
## 例题详解
???+note "[437. 路径总和 III](https://leetcode-cn.com/problems/path-sum-iii/)"
-
给定一个二叉树,它的每个结点都存放着一个整数值。
找出路径和等于给定数值的路径总数。
路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
- 二叉树不超过1000个节点,且节点数值范围是 [-1000000,1000000] 的整数。
+ 二叉树不超过 1000 个节点,且节点数值范围是[-1000000,1000000]的整数。
示例:
2. 5 -> 2 -> 1
3. -3 -> 11
```
+
```cpp
/**
- * 二叉树结点的定义
- * struct TreeNode {
- * int val;
- * TreeNode *left;
- * TreeNode *right;
- * TreeNode(int x) : val(x), left(NULL), right(NULL) {}
- * };
- */
+ * 二叉树结点的定义
+ * struct TreeNode {
+ * int val;
+ * TreeNode *left;
+ * TreeNode *right;
+ * TreeNode(int x) : val(x), left(NULL), right(NULL) {}
+ * };
+ */
```
??? note "参考代码"
pathSum(root->right, sum); // 右边路径总数(相信它能算出来)
return leftPathSum + rightPathSum + pathImLeading;
}
+ ```
int count(TreeNode *node, int sum) {
if (node == nullptr) return 0;
```
??? note "解题思路"
-
对于这道题,我们有两种做法:
- 把对数组 A 的累加依次放入数组 B 中。
多维前缀和的普通求解方法几乎都是基于容斥原理。
???+note "示例:一维前缀和扩展到二维前缀和"
-
比如我们有这样一个矩阵 $a$ ,可以视为二维数组:
```text
#### 例题
???+note "[洛谷 P1387 最大正方形](https://www.luogu.com.cn/problem/P1387)"
-
- 在一个n*m的只包含0和1的矩阵里找出一个不包含0的最大正方形,输出边长。
+ 在一个 n\*m 的只包含 0 和 1 的矩阵里找出一个不包含 0 的最大正方形,输出边长。
??? note "参考代码"
```cpp
它可以维护多次对序列的一个区间加上一个数,并在最后询问某一位的数或是多次询问某一位的数。注意修改操作一定要在查询操作之前。
???+note "示例"
-
譬如使 $[l,r]$ 中的每个数加上一个 $k$ ,就是
$$
b_l \leftarrow b_l + k,b_{r + 1} \leftarrow b_{r + 1} - k
$$
- 其中 $b_l+k=a_l+k-a_{l-1}$ , $b_{r+1}-k=a_{r+1}-(a_r+k)$
+ 其中 $b_l+k=a_l+k-a_{l-1}$ , $b_{r+1}-k=a_{r+1}-(a_r+k)$
最后做一遍前缀和就好了。
???+note "[洛谷 3128 最大流](https://www.luogu.com.cn/problem/P3128)"
FJ 给他的牛棚的 $N(2 \le N \le 50,000)$ 个隔间之间安装了 $N-1$ 根管道,隔间编号从 $1$ 到 $N$ 。所有隔间都被管道连通了。
- FJ有 $K(1 \le K \le 100,000)$ 条运输牛奶的路线,第 $i$ 条路线从隔间 $s_i$ 运输到隔间 $t_i$ 。一条运输路线会给它的两个端点处的隔间以及中间途径的所有隔间带来一个单位的运输压力,你需要计算压力最大的隔间的压力是多少。
+ FJ 有 $K(1 \le K \le 100,000)$ 条运输牛奶的路线,第 $i$ 条路线从隔间 $s_i$ 运输到隔间 $t_i$ 。一条运输路线会给它的两个端点处的隔间以及中间途径的所有隔间带来一个单位的运输压力,你需要计算压力最大的隔间的压力是多少。
??? note "解题思路"
需要统计每个点经过了多少次,那么就用树上差分将每一次的路径上的点加一,可以很快得到每个点经过的次数。这里采用倍增法进行 lca 的计算。最后对 DFS 遍历整棵树,在回溯时对差分数组求和就能求得答案了。
## 例题详解
???+note " [Climbing Worm - HDU](http://acm.hdu.edu.cn/showproblem.php?pid=1049)"
-
一只一英寸的蠕虫位于 n 英寸深的井的底部。它每分钟向上爬 u 英寸,但是必须休息一分钟才能再次向上爬。在休息的时候,它滑落了 d 英寸。之后它将重复向上爬和休息的过程。蠕虫爬出井口花费了多长时间?我们将不足一分钟的部分算作一整分钟。如果蠕虫爬完后刚好到达井的顶部,我们也设作蠕虫已经爬出井口。
??? note "解题思路"
-
直接使用程序模拟蠕虫爬井的过程就可以了。用一个循环重复蠕虫的爬井过程,当攀爬的长度超过或者等于井的深度时跳出。注意上爬和下滑时都要递增时间。
??? note "参考代码"
### 例题 1
???+note "[Codeforces Round #384 (Div. 2) C.Vladik and fractions](http://codeforces.com/problemset/problem/743/C)"
-
构造一组 $x,y,z$ ,使得对于给定的 $n$ ,满足 $\dfrac{1}{x}+\dfrac{1}{y}+\dfrac{1}{z}=\dfrac{2}{n}$
??? note "解题思路"
### 例题 2
???+note "[Luogu P3599 Koishi Loves Construction](https://www.luogu.com.cn/problem/P3599)"
+ Task1:试判断能否构造并构造一个长度为 $n$ 的 $1\dots n$ 的排列,满足其 $n$ 个前缀和在模 $n$ 的意义下互不相同
- Task1:试判断能否构造并构造一个长度为$n$的$1\dots n$的排列,满足其$n$个前缀和在模$n$的意义下互不相同
-
- Taks2:试判断能否构造并构造一个长度为$n$的$1\dots n$的排列,满足其$n$个前缀积在模$n$的意义下互不相同
+ Taks2:试判断能否构造并构造一个长度为 $n$ 的 $1\dots n$ 的排列,满足其 $n$ 个前缀积在模 $n$ 的意义下互不相同
??? note "解题思路"
对于 task1:
### 例题 3
???+note "[AtCoder Grand Contest 032 B](https://atcoder.jp/contests/agc032/tasks/agc032_b)"
-
- You are given an integer $N$. Build an undirected graph with $N$ vertices with indices $1$ to $N$ that satisfies the following two conditions:
+ You are given an integer $N$ . Build an undirected graph with $N$ vertices with indices $1$ to $N$ that satisfies the following two conditions:
- The graph is simple and connected.
- - There exists an integer $S$ such that, for every vertex, the sum of the indices of the vertices adjacent to that vertex is $S$.
+ - There exists an integer $S$ such that, for every vertex, the sum of the indices of the vertices adjacent to that vertex is $S$ .
It can be proved that at least one such graph exists under the constraints of this problem.
#### 对顶堆
??? note "[SP16254 RMID2 - Running Median Again](https://www.luogu.com.cn/problem/SP16254)"
- 维护一个序列,支持两种操作:
-
- 1. 向序列中插入一个元素
-
- 2. 输出并删除当前序列的中位数(若序列长度为偶数,则输出较小的中位数)
-这个问题可以被进一步抽象成:动态维护一个序列上第 $k$ 大的数,$k$ 值可能会发生变化。
+ 维护一个序列,支持两种操作:
+
+ 1. 向序列中插入一个元素
+
+ 2. 输出并删除当前序列的中位数(若序列长度为偶数,则输出较小的中位数)
+
+这个问题可以被进一步抽象成:动态维护一个序列上第 $k$ 大的数, $k$ 值可能会发生变化。
对于此类问题,我们可以使用 **对顶堆** 这一技巧予以解决(可以避免写权值线段树或 BST 带来的繁琐)。
这两个堆构成的数据结构支持以下操作:
-- 维护:当大根堆的大小小于 $k$ 时,不断将小根堆堆顶元素取出并插入大根堆,直到大根堆的大小等于 $k$;当大根堆的大小大于 $k$ 时,不断将大根堆堆顶元素取出并插入小根堆,直到大根堆的大小等于 $k$;
+- 维护:当大根堆的大小小于 $k$ 时,不断将小根堆堆顶元素取出并插入大根堆,直到大根堆的大小等于 $k$ ;当大根堆的大小大于 $k$ 时,不断将大根堆堆顶元素取出并插入小根堆,直到大根堆的大小等于 $k$ ;
- 插入元素:若插入的元素小于等于大根堆堆顶元素,则将其插入大根堆,否则将其插入小根堆,然后维护对顶堆;
- 查询第 $k$ 大元素:大根堆堆顶元素即为所求;
- 删除第 $k$ 大元素:删除大根堆堆顶元素,然后维护对顶堆;
-- $k$ 值 $+1/-1$ :根据新的 $k$ 值直接维护对顶堆。
+- $k$ 值 $+1/-1$ :根据新的 $k$ 值直接维护对顶堆。
-显然,查询第 $k$ 大元素的时间复杂度是 $O(1)$ 的。由于插入、删除或调整 $k$ 值后,大根堆的大小与期望的 $k$ 值最多相差 $1$,故每次维护最多只需对大根堆与小根堆中的元素进行一次调整,因此,这些操作的时间复杂度都是 $O(\log n)$ 的。
+显然,查询第 $k$ 大元素的时间复杂度是 $O(1)$ 的。由于插入、删除或调整 $k$ 值后,大根堆的大小与期望的 $k$ 值最多相差 $1$ ,故每次维护最多只需对大根堆与小根堆中的元素进行一次调整,因此,这些操作的时间复杂度都是 $O(\log n)$ 的。
??? "参考代码"
- ```cpp
- #include <iostream>
- #include <cstdio>
- #include <queue>
- using namespace std;
- int t, x;
- int main()
- {
- scanf("%d", &t);
- while (t--)
- {
- // 大根堆,维护前一半元素
- priority_queue<int, vector<int>, less<int> > a;
- // 小根堆,维护后一半元素
- priority_queue<int, vector<int>, greater<int> > b;
- while (scanf("%d", &x) && x)
- {
- // 若为查询并删除操作,输出并删除大根堆堆顶元素
- if (x == -1)
- {
- printf("%d\n", a.top());
- a.pop();
- }
- // 若为插入操作,根据大根堆堆顶的元素值,选择合适的堆进行插入
- else
- {
- if (a.empty() || x <= a.top())
- a.push(x);
- else
- b.push(x);
- }
- // 对堆顶堆进行调整
- if (a.size() > (a.size() + b.size() + 1) / 2)
- {
- b.push(a.top());
- a.pop();
- }
- else if (a.size() < (a.size() + b.size() + 1) / 2)
- {
- a.push(b.top());
- b.pop();
- }
- }
- }
- return 0;
- }
- ```
-
-- 双倍经验:[SP15376 RMID - Running Median](https://www.luogu.com.cn/problem/SP15376)
-- 典型习题:[P1801 黑匣子](https://www.luogu.com.cn/problem/P1801)
+
+ ```cpp
+ #include <iostream>
+ #include <cstdio>
+ #include <queue>
+ using namespace std;
+ int t, x;
+ int main()
+ {
+ scanf("%d", &t);
+ while (t--)
+ {
+ // 大根堆,维护前一半元素
+ priority_queue<int, vector<int>, less<int> > a;
+ // 小根堆,维护后一半元素
+ priority_queue<int, vector<int>, greater<int> > b;
+ while (scanf("%d", &x) && x)
+ {
+ // 若为查询并删除操作,输出并删除大根堆堆顶元素
+ if (x == -1)
+ {
+ printf("%d\n", a.top());
+ a.pop();
+ }
+ // 若为插入操作,根据大根堆堆顶的元素值,选择合适的堆进行插入
+ else
+ {
+ if (a.empty() || x <= a.top())
+ a.push(x);
+ else
+ b.push(x);
+ }
+ // 对堆顶堆进行调整
+ if (a.size() > (a.size() + b.size() + 1) / 2)
+ {
+ b.push(a.top());
+ a.pop();
+ }
+ else if (a.size() < (a.size() + b.size() + 1) / 2)
+ {
+ a.push(b.top());
+ b.pop();
+ }
+ }
+ }
+ return 0;
+ }
+ ```
+
+- 双倍经验: [SP15376 RMID - Running Median](https://www.luogu.com.cn/problem/SP15376)
+- 典型习题: [P1801 黑匣子](https://www.luogu.com.cn/problem/P1801)
???+ note "矩形区域查询"
给出 $n$ 个二维平面中的点 $(x_i, y_i)$ ,其中 $1 \le i \le n, 1 \le x_i, y_i \le n, 1 \le n \le 10^5$ , 要求实现以下中操作:
- 1. 给出$a, b, c, d$,询问以$(a, b)$为左上角, $c, d$为右下角的矩形区域内点的个数。
- 2. 给出$x, y$,将横坐标为$x$的点的纵坐标改为$y$。
+ 1. 给出 $a, b, c, d$ ,询问以 $(a, b)$ 为左上角, $c, d$ 为右下角的矩形区域内点的个数。
+ 2. 给出 $x, y$ ,将横坐标为 $x$ 的点的纵坐标改为 $y$ 。
- 题目**强制在线**,保证$x_i \ne x_j(1 \le i, j \le n, i \ne j)$。
+ 题目 **强制在线** ,保证 $x_i \ne x_j(1 \le i, j \le n, i \ne j)$ 。
对于操作 1,可以通过矩形容斥将其转化为 4 个二维偏序的查询去解决,然后因为强制在线,CDQ 分治之类的离线算法就解决不了,于是想到了树套树,比如树状数组套 Treap。这确实可以解决这个问题,但是代码太长了,也不是特别好实现。
???+ note " [Intersection of Permutations](https://codeforces.com/problemset/problem/1093/E) "
给出两个排列 $a$ 和 $b$ ,要求实现以下两种操作:
- 1. 给出$l_a, r_a, l_b, r_b$,要求查询既出现在$a[l_a ... r_a]$又出现在$b[l_b ... r_b]$中的元素的个数。
- 2. 给出$x, y$,$swap(b_x, b_y)$。
+ 1. 给出 $l_a, r_a, l_b, r_b$ ,要求查询既出现在 $a[l_a ... r_a]$ 又出现在 $b[l_b ... r_b]$ 中的元素的个数。
+ 2. 给出 $x, y$ , $swap(b_x, b_y)$ 。
- 序列长度$n$满足$2 \le n \le 2 \cdot 10^5$,操作个数$q$满足$1 \le q \le 2 \cdot 10^5$。
+ 序列长度 $n$ 满足 $2 \le n \le 2 \cdot 10^5$ ,操作个数 $q$ 满足 $1 \le q \le 2 \cdot 10^5$ 。
对于每个值 $i$ ,记 $x_i$ 是它在排列 $b$ 中的下标, $y_i$ 是它在排列 $a$ 中的下标。这样,操作一就变成了一个矩形区域内点的个数的询问,操作 2 可以看成两个修改操作。而且因为是排列,所以满足一个 $x$ 对应一个 $y$ ,所以这题可以用分块套树状数组来写。
???+ note " [Complicated Computations](https://codeforces.com/contest/1436/problem/E) "
给出一个序列 $a$ ,将 $a$ 所有连续子序列的 MEX 构成的数组作为 $b$ ,问 $b$ 的 MEX。一个序列的 MEX 是序列中最小的没出现过的 **正整数** 。
- 序列的长度$n$满足$1 \le n \le 10^5$。
+ 序列的长度 $n$ 满足 $1 \le n \le 10^5$ 。
**观察** :一个序列的 MEX 为 $mex$ ,当且仅当这个序列包含 $1$ 至 $mex-1$ ,但不包含 $mex$ 。
???+note "洛谷 4097 [HEOI2013]Segment"
要求在平面直角坐标系下维护两个操作(强制在线):
- 1. 在平面上加入一条线段。记第 $i$ 条被插入的线段的标号为 $i$,该线段的两个端点分别为 $(x_0,y_0)$,$(x_1,y_1)$。
- 2. 给定一个数 $k$,询问与直线 $x = k$ 相交的线段中,交点纵坐标最大的线段的编号(若有多条线段与查询直线的交点纵坐标都是最大的,则输出编号最小的线段)。特别地,若不存在线段与给定直线相交,输出 $0$。
+ 1. 在平面上加入一条线段。记第 $i$ 条被插入的线段的标号为 $i$ ,该线段的两个端点分别为 $(x_0,y_0)$ , $(x_1,y_1)$ 。
+ 2. 给定一个数 $k$ ,询问与直线 $x = k$ 相交的线段中,交点纵坐标最大的线段的编号(若有多条线段与查询直线的交点纵坐标都是最大的,则输出编号最小的线段)。特别地,若不存在线段与给定直线相交,输出 $0$ 。
- 数据满足:操作总数 $1 \leq n \leq 10^5$,$1 \leq k, x_0, x_1 \leq 39989$,$1 \leq y_0, y_1 \leq 10^9$。
+ 数据满足:操作总数 $1 \leq n \leq 10^5$ , $1 \leq k, x_0, x_1 \leq 39989$ , $1 \leq y_0, y_1 \leq 10^9$ 。
我们发现,传统的线段树无法很好地维护这样的信息。这种情况下, **李超线段树** 便应运而生。
## 例题
???+note "[LOJ6515「雅礼集训 2018 Day10」贪玩蓝月](https://loj.ac/problem/6515)"
-
一个双端队列(deque),m 个事件:
1. 在前端插入 (w,v)
2. 在后端插入 (w,v)
3. 删除前端的二元组
4. 删除后端的二元组
- 5. 给定 l,r,在当前 deque 中选择一个子集 S 使得 $\sum_{(w,v)\in S}w\bmod p\in[l,r]$ ,且最大化 $\sum_{(w,v)\in S}v$ .
+ 5. 给定 l,r,在当前 deque 中选择一个子集 S 使得 $\sum_{(w,v)\in S}w\bmod p\in[l,r]$ ,且最大化 $\sum_{(w,v)\in S}v$ .
- $m\leq 5\times 10^4,p\leq 500$ .
+ $m\leq 5\times 10^4,p\leq 500$ .
??? note "解题思路"
-
每个二元组是有一段存活时间的,因此对时间建立线段树,每个二元组做 log 个存活标记。因此我们要做的就是对每个询问,求其到根节点的路径上的标记的一个最优子集。显然这个可以 DP 做。 $f[S,j]$ 表示选择集合 S 中的物品余数为 j 的最大价值。(其实实现的时侯是有序的,直接 f[i,j]做)
一共有 $O(m\log m)$ 个标记,因此这么做的话复杂度是 $O(mp\log m)$ 的。
- ---
+ * * *
这是一个在线算法比离线算法快的神奇题目。而且还比离线的好写。
上述离线算法其实是略微小题大做的,因为如果把题目的 deque 改成直接维护一个集合的话(即随机删除集合内元素),那么离线算法同样适用。既然是 deque,不妨在数据结构上做点文章。
- ---
+ * * *
如果题目中维护的数据结构是一个栈呢?
删除的时侯直接指针前移即可。这样做的复杂度是 $O(mp)$ 的。
- ---
+ * * *
如果题目中维护的数据结构是队列?
有一种操作叫双栈模拟队列。这就是这个东西的用武之地。因为用栈是可以轻松维护 DP 过程的,而双栈模拟队列的复杂度是均摊 $O(1)$ 的,因此,复杂度仍是 $O(mp)$ 。
- ---
+ * * *
回到原题,那么 Deque 怎么做?
这样的复杂度其实均摊下来仍是常数级别。具体地说,丢一半指的是把一个栈靠近栈底的一半倒过来丢到另一个栈中。也就是说要手写栈以支持这样的操作。
- ---
+ * * *
似乎可以用 [势能分析法](https://yhx-12243.github.io/OI-transit/records/cf601E.html) 证明。其实本蒟蒻有一个很仙的想法。我们考虑这个双栈结构的整体复杂度。m 个事件,我们希望尽可能增加这个结构的复杂度。
于是,总复杂度仍是 $O(mp)$ 。
- ---
+ * * *
在询问的时侯,我们要处理的应该是“在两个栈中选若干个元素的最大价值”的问题。因此要对栈顶的 DP 值做查询,即两个 $f,g$ 对于询问[l,r]的最大价值:
- [SPOJ #8725 CLOPPAIR "Closest Point Pair"\[难度:低\]](https://www.spoj.com/problems/CLOPPAIR/)
- [CODEFORCES Team Olympiad Saratov - 2011 "Minimum amount"\[难度:中\]](http://codeforces.com/contest/120/problem/J)
- [SPOJ #7029 CLOSEST "Closest Triple"\[难度:中\]](https://www.spoj.com/problems/CLOSEST/)
-- [Google Code Jam 2009 Final "Min Perimeter"\[难度:中\]](https://codingcompetitions.withgoogle.com/codejam/round/0000000000432ad5/0000000000433195)
+- [Google Code Jam 2009 Final "Min Perimeter"\[难度:中\]](https://codingcompetitions.withgoogle.com/codejam/round/0000000000432ad5/0000000000433195)
* * *
## 例题
???+note "[洛谷 P2731 骑马修栅栏](https://www.luogu.com.cn/problem/P2731)"
-
给定一张有 500 个顶点的无向图,求这张图的一条欧拉路或欧拉回路。如果有多组解,输出最小的那一组。
在本题中,欧拉路或欧拉回路不需要经过所有顶点。
边的数量 m 满足 $1\leq m \leq 1024$ 。
??? note "解题思路"
-
用 Fleury 算法解决本题的时候只需要再贪心就好,不过由于复杂度不对,还是换 Hierholzer 算法吧。
保存答案可以使用 `stack<int>` ,因为如果找的不是回路的话必须将那一部分放在最后。
};
struct EK {
- int n, m; // n:点数,m:边数
- vector<Edge> edges; // edges:所有边的集合
- vector<int> G[maxn]; // G:点 x -> x 的所有边在 edges 中的下标
- int a[maxn], p[maxn]; // a:点 x -> BFS 过程中最近接近点 x 的边给它的最大流
- // p:点 x -> BFS 过程中最近接近点 x 的边
+ int n, m; // n:点数,m:边数
+ vector<Edge> edges; // edges:所有边的集合
+ vector<int> G[maxn]; // G:点 x -> x 的所有边在 edges 中的下标
+ int a[maxn], p[maxn]; // a:点 x -> BFS 过程中最近接近点 x 的边给它的最大流
+ // p:点 x -> BFS 过程中最近接近点 x 的边
void init(int n) {
for (int i = 0; i < n; i++) G[i].clear();
while (!Q.empty()) {
int x = Q.front();
Q.pop();
- for (int i = 0; i < G[x].size(); i++) { // 遍历以 x 作为起点的边
+ for (int i = 0; i < G[x].size(); i++) { // 遍历以 x 作为起点的边
Edge& e = edges[G[x][i]];
if (!a[e.to] && e.cap > e.flow) {
- p[e.to] = G[x][i]; // G[x][i] 是最近接近点 e.to 的边
- a[e.to] = min(a[x], e.cap - e.flow); // 最近接近点 e.to 的边赋给它的流
+ p[e.to] = G[x][i]; // G[x][i] 是最近接近点 e.to 的边
+ a[e.to] =
+ min(a[x], e.cap - e.flow); // 最近接近点 e.to 的边赋给它的流
Q.push(e.to);
}
}
- if (a[t]) break; // 如果汇点接受到了流,就退出 BFS
+ if (a[t]) break; // 如果汇点接受到了流,就退出 BFS
}
- if (!a[t]) break; // 如果汇点没有接受到流,说明源点和汇点不在同一个连通分量上
- for (int u = t; u != s; u = edges[p[u]].from) { // 通过 u 追寻 BFS 过程中 s -> t 的路径
- edges[p[u]].flow += a[t]; // 增加路径上边的 flow 值
- edges[p[u] ^ 1].flow -= a[t]; // 减小反向路径的 flow 值
+ if (!a[t])
+ break; // 如果汇点没有接受到流,说明源点和汇点不在同一个连通分量上
+ for (int u = t; u != s;
+ u = edges[p[u]].from) { // 通过 u 追寻 BFS 过程中 s -> t 的路径
+ edges[p[u]].flow += a[t]; // 增加路径上边的 flow 值
+ edges[p[u] ^ 1].flow -= a[t]; // 减小反向路径的 flow 值
}
flow += a[t];
}
4. 图的绝对中心可能在某条边上,枚举所有的边。对于一条边 $w(u,j)$ 从距离 $u$ 最远的结点开始更新。当出现 $d(v,\textit{rk}(u,i)) > d(v,\textit{rk}(u,i-1))$ 的情况时,用 $\textit{ans}\leftarrow \min(\textit{ans}, d(v,\textit{rk}(u,i))+d(v,\textit{rk}(u,i-1))+w(i,j))$ 来更新。因为这种情况会使图的绝对中心改变。
??? note "参考实现"
-
```cpp
- bool cmp(int a, int b)
- {
- return val[a] < val[b];
- }
+ bool cmp(int a, int b) { return val[a] < val[b]; }
- void Floyd()
- {
- for (int k = 1; k <= n; k ++)
- for (int i = 1; i <= n; i ++)
- for (int j = 1; j <= n; j ++)
- d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
+ void Floyd() {
+ for (int k = 1; k <= n; k++)
+ for (int i = 1; i <= n; i++)
+ for (int j = 1; j <= n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
- void solve()
- {
- Floyd();
- for (int i = 1; i <= n; i ++)
- {
- for (int j = 1; j <= n; j ++)
- {
- rk[i][j] = j;
- val[j] = d[i][j];
- }
- sort(rk[i] + 1, rk[i] + 1 + n, cmp);
+ void solve() {
+ Floyd();
+ for (int i = 1; i <= n; i++) {
+ for (int j = 1; j <= n; j++) {
+ rk[i][j] = j;
+ val[j] = d[i][j];
}
- int ans = INF;
- // 图的绝对中心可能在结点上
- for (int i = 1; i <= n; i ++) ans = min(ans, d[i][rk[i][n]] * 2);
- // 图的绝对中心可能在边上
- for (int i = 1; i <= m; i ++)
- {
- int u = a[i].u, v = a[i].v, w = a[i].w;
- for (int p = n, i = n - 1; i >= 1; i --)
- {
- if (d[v][rk[u][i]] > d[v][rk[u][p]])
- {
- ans = min(ans, d[u][rk[u][i]] + d[v][rk[u][p]] + w);
- p = i;
- }
- }
+ sort(rk[i] + 1, rk[i] + 1 + n, cmp);
+ }
+ int ans = INF;
+ // 图的绝对中心可能在结点上
+ for (int i = 1; i <= n; i++) ans = min(ans, d[i][rk[i][n]] * 2);
+ // 图的绝对中心可能在边上
+ for (int i = 1; i <= m; i++) {
+ int u = a[i].u, v = a[i].v, w = a[i].w;
+ for (int p = n, i = n - 1; i >= 1; i--) {
+ if (d[v][rk[u][i]] > d[v][rk[u][p]]) {
+ ans = min(ans, d[u][rk[u][i]] + d[v][rk[u][p]] + w);
+ p = i;
+ }
}
+ }
}
```
### 实现
伪代码:
+
<!--
```pseudo
\begin{algorithm}
## 不同方法的比较
-| Floyd | Bellman-Ford | Dijkstra | Johnson | D´Esopo-Pape |
-| ---------- | --------------- | -------------- | --------------- | --------------- |
-| 每对结点之间的最短路 | 单源最短路 | 单源最短路 | 每对结点之间的最短路 | 单源最短路 |
-| 没有负环的图 | 任意图(可以判定负环是否存在) | 非负权图 | 没有负环的图 | 没有负环的图 |
+| Floyd | Bellman-Ford | Dijkstra | Johnson | D´Esopo-Pape |
+| ---------- | --------------- | -------------- | --------------- | ---------------- |
+| 每对结点之间的最短路 | 单源最短路 | 单源最短路 | 每对结点之间的最短路 | 单源最短路 |
+| 没有负环的图 | 任意图(可以判定负环是否存在) | 非负权图 | 没有负环的图 | 没有负环的图 |
| $O(N^3)$ | $O(NM)$ | $O(M\log M)$ | $O(NM\log M)$ | $O(N\cdot2^N)$ |
注:表中的 Dijkstra 算法在计算复杂度时均用 `priority_queue` 实现。
根据定理 2,可以求出方程的所有解。但在实际问题中,我们往往被要求求出一个最小整数解,也就是一个特解 $x=(x \bmod t+t) \bmod t$ ,其中 $t=\dfrac{b}{\gcd(a,b)}$ 。
???+note "代码实现"
-
```cpp
int ex_gcd(int a, int b, int& x, int& y) {
if (b == 0) {
for (int i = 0; i < sz; ++i)
for (int k = 0; k < sz; ++k) {
r = a[i][k];
- for (int j = 0; j < sz; ++j) res.a[i][j] += T.a[k][j] * r, res.a[i][j] %= MOD;
+ for (int j = 0; j < sz; ++j)
+ res.a[i][j] += T.a[k][j] * r, res.a[i][j] %= MOD;
}
return res;
}
2. $\operatorname{Mul}( x , d )$ 操作:将 $x$ 到根的路径上所有点的 $t_i\leftarrow t_i + d \times k_i$
3. $\operatorname{Query}( x )$ 操作:询问点 $x$ 的权值 $t_x$
- $n,~m \leq 100000, ~-10 \leq d \leq 10$
+ $n,~m \leq 100000, ~-10 \leq d \leq 10$
若直接思考,下放操作和维护信息并不是很好想。但是矩阵可以轻松地表达。
- 莫比乌斯函数: $\mu(n) = \begin{cases}1 & n=1 \\ 0 & \exists d>1:d^{2} \mid n \\ (-1)^{\omega(n)} & otherwise\end{cases}$ ,其中 $\omega(n)$ 表示 $n$ 的本质不同质因子个数,它是一个加性函数。
???+note "加性函数"
- 此处加性函数指数论上的加性函数 (Additive function) 。对于加性函数$\operatorname{f}$,当整数 $a,b$ 互质时,均有 $\operatorname{f}(ab)=\operatorname{f}(a)+\operatorname{f}(b)$ 。
+ 此处加性函数指数论上的加性函数 (Additive function)。对于加性函数 $\operatorname{f}$ ,当整数 $a,b$ 互质时,均有 $\operatorname{f}(ab)=\operatorname{f}(a)+\operatorname{f}(b)$ 。
应与代数中的加性函数 (Additive map) 区分。
* * *
???+note "证明 `result` 中均为 $N$ 的素因数"
首先证明元素均为 $N$ 的素因数:因为当且仅当 `N % i == 0` 满足时, `result` 发生变化:储存 $i$ ,说明此时 $i$ 能整除 $\frac{N}{A}$ ,说明了存在一个数 $p$ 使得 $pi=\frac{N}{A}$ ,即 $piA = N$ (其中, $A$ 为 $N$ 自身发生变化后遇到 $i$ 时所除的数。我们注意到 `result` 若在 push $i$ 之前就已经有数了,为 $R_1,\,R_2,\,\ldots,\,R_n$ ,那么有 `N` $=\frac{N}{R_1^{q_1}\cdot R_2^{q_2}\cdot \cdots \cdot R_n^{q_n}}$ ,被除的乘积即为 $A$ )。所以 $i$ 为 $N$ 的因子。
- 其次证明 `result` 中均为素数。我们假设存在一个在 `result` 中的合数 $K$,并根据整数基本定理,分解为一个素数序列 $K = K_1^{e_1}\cdot K_2^{e_2}\cdot\cdots\cdot K_3^{e_3}$,而因为 $K_1 < K$ ,所以它一定会在 $K$ 之前被遍历到,并令 `while(N % k1 == 0) N /= k1`,即让 `N` 没有了素因子 $K_1$,故遍历到 $K$ 时,`N` 和 $K$ 已经没有了整除关系了。
+ 其次证明 `result` 中均为素数。我们假设存在一个在 `result` 中的合数 $K$ ,并根据整数基本定理,分解为一个素数序列 $K = K_1^{e_1}\cdot K_2^{e_2}\cdot\cdots\cdot K_3^{e_3}$ ,而因为 $K_1 < K$ ,所以它一定会在 $K$ 之前被遍历到,并令 `while(N % k1 == 0) N /= k1` ,即让 `N` 没有了素因子 $K_1$ ,故遍历到 $K$ 时, `N` 和 $K$ 已经没有了整除关系了。
值得指出的是,如果开始已经打了一个素数表的话,时间复杂度将从 $O(\sqrt N)$ 下降到 $O(\sqrt{\frac N {\ln N}})$ 。去 [筛法](./sieve.md) 处查阅更多打表的信息。
ll n, k, x[maxn], y[maxn], ans, s1, s2;
ll powmod(ll x, ll n) {
- ll ret = 1ll;
- while (n) {
- if (n & 1) ret = ret * x % mod;
- x = x * x % mod;
- n >>= 1;
- }
- return ret;
+ ll ret = 1ll;
+ while (n) {
+ if (n & 1) ret = ret * x % mod;
+ x = x * x % mod;
+ n >>= 1;
+ }
+ return ret;
}
ll inv(ll x) { return powmod(x, mod - 2); }
int main() {
- scanf("%lld%lld", &n, &k);
- for (int i = 1; i <= n; i++) scanf("%lld%lld", x + i, y + i);
- for (int i = 1; i <= n; i++) {
- s1 = y[i] % mod;
- s2 = 1ll;
- for (int j = 1; j <= n; j++)
- if (i != j)
- s1 = s1 * (k - x[j]) % mod, s2 = s2 * (x[i] - x[j]) % mod;
- ans += s1 * inv(s2) % mod;
- }
- printf("%lld\n", (ans % mod + mod) % mod);
- return 0;
+ scanf("%lld%lld", &n, &k);
+ for (int i = 1; i <= n; i++) scanf("%lld%lld", x + i, y + i);
+ for (int i = 1; i <= n; i++) {
+ s1 = y[i] % mod;
+ s2 = 1ll;
+ for (int j = 1; j <= n; j++)
+ if (i != j) s1 = s1 * (k - x[j]) % mod, s2 = s2 * (x[i] - x[j]) % mod;
+ ans += s1 * inv(s2) % mod;
+ }
+ printf("%lld\n", (ans % mod + mod) % mod);
+ return 0;
}
```
#### 二次探测定理
-如果 $p$ 是奇素数,则 $x^2 \equiv 1 \pmod p$ 的解为 $x \equiv 1 \pmod p$ 或者 $x \equiv p - 1 \pmod p$。
+如果 $p$ 是奇素数,则 $x^2 \equiv 1 \pmod p$ 的解为 $x \equiv 1 \pmod p$ 或者 $x \equiv p - 1 \pmod p$ 。
-要证明该定理,只需将上面的方程移项,再使用平方差公式,得到 $(x+1)(x-1) \equiv 0 \bmod p$,即可得出上面的结论。
+要证明该定理,只需将上面的方程移项,再使用平方差公式,得到 $(x+1)(x-1) \equiv 0 \bmod p$ ,即可得出上面的结论。
### 实现
??? note "关于四个循环位置的讨论"
莫队区间的移动过程,就相当于加入了 $[1,r]$ 的元素,并删除了 $[1,l-1]$ 的元素。因此,
- - 对于 $l\le r$ 的情况,$[1,l-1]$ 的元素相当于被加入了一次又被删除了一次,$[l,r]$ 的元素被加入一次,$[r+1,+\infty)$ 的元素没有被加入。这个区间是合法区间。
- - 对于 $l=r+1$ 的情况,$[1,r]$ 的元素相当于被加入了一次又被删除了一次,$[r+1,+\infty)$ 的元素没有被加入。这时这个区间表示空区间。
- - 对于 $l>r+1$ 的情况,那么 $[r+1,l-1]$(这个区间非空)的元素被删除了一次但没有被加入,因此这个元素被加入的次数是负数。
+ - 对于 $l\le r$ 的情况, $[1,l-1]$ 的元素相当于被加入了一次又被删除了一次, $[l,r]$ 的元素被加入一次, $[r+1,+\infty)$ 的元素没有被加入。这个区间是合法区间。
+ - 对于 $l=r+1$ 的情况, $[1,r]$ 的元素相当于被加入了一次又被删除了一次, $[r+1,+\infty)$ 的元素没有被加入。这时这个区间表示空区间。
+ - 对于 $l>r+1$ 的情况,那么 $[r+1,l-1]$ (这个区间非空)的元素被删除了一次但没有被加入,因此这个元素被加入的次数是负数。
因此,如果某时刻出现 $l>r+1$ 的情况,那么会存在一个元素,它的加入次数是负数。这在某些题目会出现问题,例如我们如果用一个 `set` 维护区间中的所有数,就会出现“需要删除 `set` 中不存在的元素”的问题。
代码中的四个 while 循环一共有 $4!=24$ 种排列顺序。不妨设第一个循环用于操作左端点,就有以下 $12$ 种排列(另外 $12$ 种是对称的)。下表列出了这 12 种写法的正确性,还给出了错误写法的反例。
- | 循环顺序 | 正确性 | 反例或注释 |
- | ----------------- | ------ | ----------- |
- | `l--,l++,r--,r++` | 错误 | $l<r<l'<r'$ |
- | `l--,l++,r++,r--` | 错误 | $l<r<l'<r'$ |
- | `l--,r--,l++,r++` | 错误 | $l<r<l'<r'$ |
- | `l--,r--,r++,l++` | 正确 | 证明较繁琐 |
- | `l--,r++,l++,r--` | 正确 | |
- | `l--,r++,r--,l++` | 正确 | |
- | `l++,l--,r--,r++` | 错误 | $l<r<l'<r'$ |
- | `l++,l--,r++,r--` | 错误 | $l<r<l'<r'$ |
- | `l++,r++,l--,r--` | 错误 | $l<r<l'<r'$ |
- | `l++,r++,r--,l--` | 错误 | $l<r<l'<r'$ |
- | `l++,r--,l--,r++` | 错误 | $l<r<l'<r'$ |
- | `l++,r--,r++,l--` | 错误 | $l<r<l'<r'$ |
+ | 循环顺序 | 正确性 | 反例或注释 |
+ | ------------------- | --- | ------------- |
+ | `l--,l++,r--,r++` | 错误 | $l<r<l'<r'$ |
+ | `l--,l++,r++,r--` | 错误 | $l<r<l'<r'$ |
+ | `l--,r--,l++,r++` | 错误 | $l<r<l'<r'$ |
+ | `l--,r--,r++,l++` | 正确 | 证明较繁琐 |
+ | `l--,r++,l++,r--` | 正确 | |
+ | `l--,r++,r--,l++` | 正确 | |
+ | `l++,l--,r--,r++` | 错误 | $l<r<l'<r'$ |
+ | `l++,l--,r++,r--` | 错误 | $l<r<l'<r'$ |
+ | `l++,r++,l--,r--` | 错误 | $l<r<l'<r'$ |
+ | `l++,r++,r--,l--` | 错误 | $l<r<l'<r'$ |
+ | `l++,r--,l--,r++` | 错误 | $l<r<l'<r'$ |
+ | `l++,r--,r++,l--` | 错误 | $l<r<l'<r'$ |
全部 24 种排列中只有 6 种是正确的,其中有 2 种的证明较繁琐,这里只给出其中 4 种的证明。
- 这 4 种正确写法的共同特点是,前两步先扩大区间(`l--` 或 `r++`),后两步再缩小区间(`l++` 或 `r--`)。这样写,前两步是扩大区间,可以保持 $l\le r+1$;执行完前两步后,$l\le l'\le r'\le r$ 一定成立,再执行后两步只会把区间缩小到 $[l',r']$,依然有 $l\le r+1$,因此这样写是正确的。
+ 这 4 种正确写法的共同特点是,前两步先扩大区间( `l--` 或 `r++` ),后两步再缩小区间( `l++` 或 `r--` )。这样写,前两步是扩大区间,可以保持 $l\le r+1$ ;执行完前两步后, $l\le l'\le r'\le r$ 一定成立,再执行后两步只会把区间缩小到 $[l',r']$ ,依然有 $l\le r+1$ ,因此这样写是正确的。
??? 参考代码
```cpp
这样做,单次的正确率是 $\big(\frac 23\big)^n$ 。将算法重复运行 $-\big(\frac 32\big)^n\log \epsilon$ 次,只要有一次得到解就输出,这样即可保证 $1-\epsilon$ 的正确率。(详见后文中“自然常数的使用”和“抽奖问题”)
----
+* * *
**回顾** :本题中“解空间”就是集合 $\{R,G,B\}^n$ ,我们每次通过随机施加限制来在一个缩小的范围内搜寻“目标解”——即合法的染色方案。
复杂度 $O\big((|V|+|E|) \log |V|\big)$ 。
----
+* * *
**回顾** :我们需要确定任意一对能够实现目标 I 的二元组 $(s,t)$ ,为此我们随机选择 $(s,t)$ 。
???+ note "简要题意"
给定一张 $n$ 个点、带点权的无向图,在其中所有大小不小于 $\dfrac {2n}3$ 的团中,找到点权和最大的那个。
- $n\leq 50$
+ $n\leq 50$
不难想到折半搜索。把点集均匀分成左右两半 $V_L,V_R$ (大小都为 $\dfrac n2$ ),计算数组 $f_{L,k}$ 表示点集 $L\subseteq V_L$ 中的所有 $\geq k$ 元团的最大权值和。接着我们枚举右半边的每个团 $C_R$ ,算出左半边有哪些点与 $C_R$ 中的所有点相连(这个点集记作 $N_L$ ),并用 $f_{N_L,\frac 23 n-|C_R|}+\textit{value}(C_R)$ 更新答案。
- 因为只需考虑大小 $\geq \dfrac n3$ 的团,所以需要考虑的左侧团 $L$ 和 右侧团 $C_R$ 的数量也大大减少至约 $1.8\cdot 10^6$ 。
- 现在的瓶颈变成了求单侧的某一子集的权值和,因为这需要 $O\big(2^{|V_L|}+2^{|V_R|}\big)$ 的预处理。
- 解决方案:在 $V_L,V_R$ 内部再次折半;当查询一个子集的权值和时,将这个子集分成左右两半查询,再把答案相加。
-- 这样即可通过本题。
+- 这样即可通过本题。
----
+* * *
**回顾** :一个随机的集合有着“在划分出的两半的数量差距不会太悬殊”这一性质,而我们通过随机划分获取了这个性质。
分析后者发生的概率:
-- 在 Schwartz-Zippel 引理中:
+- 在 Schwartz-Zippel 引理中:
- 取域 $F$ 为模 $Q$ 的剩余系对应的域
- 取 $f(x_{*,*})=P_0(x_{*,*})-P_1(x_{*,*})$ 为 $L$ 次非零多项式
- 取 $S=F$
允许 $\epsilon$ 的相对误差和 $\delta$ 的错误率,换句话说,你要对至少 $(1-\delta)q$ 个询问给出离正确答案相对误差不超过 $\epsilon$ 的回答。
- $n\cdot m\leq 2\cdot10^5;q\leq 10^6;\epsilon=0.5,\delta=0.2$
+ $n\cdot m\leq 2\cdot10^5;q\leq 10^6;\epsilon=0.5,\delta=0.2$
引理:令 $X_{1\cdots k}$ 为互相独立的随机变量,且取值在 $[0,1]$ 中均匀分布,则 $\mathrm{E}\big[\min\limits_i X_i\big]=\dfrac 1{k+1}$ 。
- 坏事件中至少一者发生的概率, **不小于** 每一个的发生概率之和,减掉每两个同时发生的概率之和。
- 坏事件中至少一者发生的概率, **不超过** 每一个的发生概率之和,减掉每两个同时发生的概率之和,加上每三个同时发生的概率之和。
- ……
- - 随着层数越来越多,交替出现的上界和下界也越来越紧。这一系列结论形式上类似容斥原理,证明过程也和容斥类似,这里略去。
+ - 随着层数越来越多,交替出现的上界和下界也越来越紧。这一系列结论形式上类似容斥原理,证明过程也和容斥类似,这里略去。
----
+* * *
**自然常数的使用** : $\Big(1-\dfrac 1n\Big)^n\leq \dfrac 1e,\forall n\geq1$
- 左式关于 $n\geq 1$ 单调递增且在 $+\infty$ 处的极限是 $\dfrac 1e$ ,因此有这个结论。
-- 这告诉我们,如果 $n$ 个互相独立的坏事件,每个的发生概率为 $1-\dfrac 1n$ ,则它们全部发生的概率至多为 $\dfrac 1e$ 。
+- 这告诉我们,如果 $n$ 个互相独立的坏事件,每个的发生概率为 $1-\dfrac 1n$ ,则它们全部发生的概率至多为 $\dfrac 1e$ 。
----
+* * *
**(\*) Hoeffding** 不等式:若 $X_{1\cdots n}$ 为互相独立的实随机变量且 $X_i\in [a_i,b_i]$ ,记随机变量 $X:=\sum\limits_{i=1}^n X_i$ ,则
现在有 $k>1$ 个奖球,那么根据 Union Bound,我们只需保证每个奖球被漏掉的概率都不超过 $\dfrac \epsilon k$ 即可。于是答案是 $-n\log\dfrac \epsilon k$ 。
----
+* * *
???+ note "例:(\*)随机选取一半元素"
给出一个算法,从 $n$ 个元素中等概率随机选取一个大小为 $\dfrac n2$ 的子集,保证 $n$ 是偶数。你能使用的唯一的随机源是一枚均匀硬币,同时请你尽量减少抛硬币的次数(不要求最少)。
首先可以想到这样的算法:
- 通过抛 $n$ 次硬币,可以从所有子集中等概率随机选一个。
- - 不断重复这一过程,直到选出的子集大小恰好为 $\dfrac n2$ 。
+ - 不断重复这一过程,直到选出的子集大小恰好为 $\dfrac n2$ 。
- 注意到大小为 $\dfrac n2$ 的子集至少占所有子集的 $\dfrac 1n$ ,因此重复次数的期望值 $\leq n$ 。
这一算法期望需要抛 $n^2$ 次硬币。
另一个算法:
- - 我们可以通过抛期望 $2\lceil\log_2 n\rceil$ 次硬币来实现随机 $n$ 选 1 。
+ - 我们可以通过抛期望 $2\lceil\log_2 n\rceil$ 次硬币来实现随机 $n$ 选 1。
- 具体方法:随机生成 $\lceil\log_2 n\rceil$ 位的二进制数,如果大于等于 $n$ 则重新随机,否则选择对应编号(编号从 0 开始)的元素并结束过程。
- 然后我们从所有元素中选一个,再从剩下的元素中再选一个,以此类推,直到选出 $\dfrac n2$ 个元素为止。
$$
\mathrm{E}\Big[\big|X-\mathrm{E}[X]\big|\Big]=\int\limits_0^\infty \mathrm{Pr}\Big[\big|X-\mathrm{E}[X]\big|\geq t\Big]\mathrm{d}t\leq2\int\limits_0^\infty \exp {-\dfrac {t^2}n}\mathrm{d}t=\sqrt{\pi n}
$$
-
+
从而第二、第三步所需抛硬币次数的期望值是 $\sqrt{\pi n}\cdot2\lceil\log_2 n\rceil$ 。
综上,该算法期望需要抛 $n+2\sqrt{\pi n}\lceil\log_2 n\rceil$ 次硬币。
???+ note "证明思路"
我们假想这两张图分别使用了一个 01 随机数生成器来获知每条边存在与否,其中 $G_1$ 的生成器 $T_1$ 每次以 $p$ 的概率输出 1, $G_2$ 的生成器 $T_2$ 每次以 $q$ 的概率输出 1。这样,要构造一张图,就只需把对应的生成器运行 $\dfrac {n(n-1)}2$ 遍即可。
- 现在我们把两个生成器合二为一。考虑随机数生成器 $T$ ,每次以 $q$ 的概率输出 0 ,以 $p-q$ 的概率输出 1 ,以 $1-p$ 的概率输出 2 。如果我们将这个 $T$ 运行 $\dfrac {n(n-1)}2$ 遍,就能同时构造出 $G_1$ 和 $G_2$ 。具体地说,如果输出是 0 ,则认为 $G_1$ 和 $G_2$ 中都没有当前考虑的边;如果输出是 1 ,则认为只有 $G_1$ 中有当前考虑的边;如果输出是 2 ,则认为 $G_1$ 和 $G_2$ 中都有当前考虑的边。
+ 现在我们把两个生成器合二为一。考虑随机数生成器 $T$ ,每次以 $q$ 的概率输出 0,以 $p-q$ 的概率输出 1,以 $1-p$ 的概率输出 2。如果我们将这个 $T$ 运行 $\dfrac {n(n-1)}2$ 遍,就能同时构造出 $G_1$ 和 $G_2$ 。具体地说,如果输出是 0,则认为 $G_1$ 和 $G_2$ 中都没有当前考虑的边;如果输出是 1,则认为只有 $G_1$ 中有当前考虑的边;如果输出是 2,则认为 $G_1$ 和 $G_2$ 中都有当前考虑的边。
- 容易验证,这样生成的 $G_1$ 和 $G_2$ 符合其定义,而且在每个实例中,$G_2$ 的边集都是 $G_1$ 边集的子集。因此在每个实例中,$G_2$ 的连通分量个数都不小于 $G_1$ 的连通分量个数;那么期望值自然也满足同样的大小关系。
+ 容易验证,这样生成的 $G_1$ 和 $G_2$ 符合其定义,而且在每个实例中, $G_2$ 的边集都是 $G_1$ 边集的子集。因此在每个实例中, $G_2$ 的连通分量个数都不小于 $G_1$ 的连通分量个数;那么期望值自然也满足同样的大小关系。
这一段证明中用到的思想被称为“耦合”,可以从字面意思来理解这种思想。本例中它体现为把两个本来独立的随机过程合二为一。
引理:如果一枚硬币有 $p$ 的概率掷出正面,则首次掷出正面所需的期望次数为 $\dfrac 1p$ 。
- - 感性理解: $\dfrac 1p \cdot p = 1$ ,所以扔这么多次期望得到 1 次正面,看起来就比较对。
- - 这种感性理解可以通过 [大数定律](https://en.wikipedia.org/wiki/Law_of_large_numbers) 严谨化,即考虑 $n\to \infty$ 次“不断抛硬币直到得到正面”的实验。推导细节略。
- - 另一种可行的证法是,直接把期望的定义带进去暴算。推导细节略。
+ - 感性理解: $\dfrac 1p \cdot p = 1$ ,所以扔这么多次期望得到 1 次正面,看起来就比较对。
+ - 这种感性理解可以通过 [大数定律](https://en.wikipedia.org/wiki/Law_of_large_numbers) 严谨化,即考虑 $n\to \infty$ 次“不断抛硬币直到得到正面”的实验。推导细节略。
+ - 另一种可行的证法是,直接把期望的定义带进去暴算。推导细节略。
显然抽一次得到新物品的概率是 $\dfrac {n-k}n$ ,那么 $R=\dfrac n{n-k}$ 。
???+ note "证明"
先考虑证明一个特殊情况。将证:
- - 随机过程 $A$ :先买物品 $x$ ,然后不断抽直到得到所有物品
- - ……一定不优于……
- - 随机过程 $B$ :不断抽直到得到 $x$ 以外的所有物品,然后如果还没有 $x$ 则买下来
+ - 随机过程 $A$ :先买物品 $x$ ,然后不断抽直到得到所有物品
+ - ……一定不优于……
+ - 随机过程 $B$ :不断抽直到得到 $x$ 以外的所有物品,然后如果还没有 $x$ 则买下来
考虑让随机过程 $A$ 和随机过程 $B$ 使用同一个随机数生成器。即, $A$ 的第一次抽取和 $B$ 的第一次抽取会抽到同一个元素,第二次、第三次……也是一样。
最后,我们枚举所有可能的局面(即已经拥有的元素集合),算出这种局面出现的概率(已有元素的排列方案数除以总方案数),乘上当前局面最优决策的代价(由拥有元素个数和剩余物品总价确定),再加起来即可。这个过程可以用背包式的 DP 优化,即可通过本题。
----
+* * *
**回顾** :可以看到,耦合的技巧在本题中使用了两次。第一次是在证明过程中,令两个随机过程使用同一个随机源;第二次是把购买转化成随机购买(即引入随机源),从而使得购买和抽取这两种操作实质上“耦合”为同一种操作(即令抽取和购买操作共享一个随机源)。
## 例题
???+note "[八数码](https://www.luogu.com.cn/problem/P1379)"
-
题目大意:在 $3\times 3$ 的棋盘上,摆有八个棋子,每个棋子上标有 $1$ 至 $8$ 的某一数字。棋盘中留有一个空格,空格用 $0$ 来表示。空格周围的棋子可以移到空格中,这样原来的位置就会变成空格。给出一种初始布局和目标布局(为了使题目简单,设目标状态如下),找到一种从初始布局到目标布局最少步骤的移动方法。
```plain
* * *
???+note "[k 短路](https://www.luogu.com.cn/problem/P2483)"
-
按顺序求一个有向图上从结点 $s$ 到结点 $t$ 的所有路径最小的前任意多(不妨设为 $k$ )个。
??? note "解题思路"
我们可以在此基础上加一点小优化:由于只需要求出第 $k$ 短路,所以当我们第 $k+1$ 次或以上走到该结点时,直接跳过该状态。因为前面的 $k$ 次走到这个点的时候肯定能因此构造出 $k$ 条路径,所以之后再加边更无必要。
??? note "参考代码"
-
```cpp
#include <algorithm>
#include <cstdio>
$1\le n\le 35$ 。
??? note "解题思路"
-
- 如果这道题暴力 DFS 找开关灯的状态,时间复杂度就是 $O(2^{n})$ , 显然超时。不过,如果我们用 meet in middle 的话,时间复杂度可以优化至 $O(n2^{n/2})$ 。 meet in middle 就是让我们先找一半的状态,也就是找出只使用编号为 $1$ 到 $\mathrm{mid}$ 的开关能够到达的状态,再找出只使用另一半开关能到达的状态。如果前半段和后半段开启的灯互补,将这两段合并起来就得到了一种将所有灯打开的方案。具体实现时,可以把前半段的状态以及达到每种状态的最少按开关次数存储在 map 里面,搜索后半段时,每搜出一种方案,就把它与互补的第一段方案合并来更新答案。
+ 如果这道题暴力 DFS 找开关灯的状态,时间复杂度就是 $O(2^{n})$ , 显然超时。不过,如果我们用 meet in middle 的话,时间复杂度可以优化至 $O(n2^{n/2})$ 。meet in middle 就是让我们先找一半的状态,也就是找出只使用编号为 $1$ 到 $\mathrm{mid}$ 的开关能够到达的状态,再找出只使用另一半开关能到达的状态。如果前半段和后半段开启的灯互补,将这两段合并起来就得到了一种将所有灯打开的方案。具体实现时,可以把前半段的状态以及达到每种状态的最少按开关次数存储在 map 里面,搜索后半段时,每搜出一种方案,就把它与互补的第一段方案合并来更新答案。
??? note "参考代码"
```cpp
题目大意:有 $N$ 种物品和一个容量为 $W$ 的背包,每种物品有重量 $w_i$ 和价值 $v_i$ 两种属性,要求选若干个物品(每种物品只能选一次)放入背包,使背包中物品的总价值最大,且背包中物品的总重量不超过背包的容量。
??? note "解题思路"
-
我们写一个估价函数 $f$ ,可以剪掉所有无效的 $0$ 枝条(就是剪去大量无用不选枝条)。
估价函数 $f$ 的运行过程如下:
显然在最坏情况下可能有 $O(n^2)$ 个回文串,因此似乎一眼看过去该问题并没有线性算法。
-但是关于回文串的信息可用 **一种更紧凑的方式** 表达:对于每个位置 $i = 0 \dots n - 1$ ,我们找出值 $d_1[i]$ 和 $d_2[i]$ 。二者分别表示以位置 $i$ 为中心的长度为奇数和长度为偶数的回文串个数。换个角度,二者也表示了以位置 $i$ 为中心的最长回文串的半径长度(半径长度 $d_1[i]$, $d_2[i]$ 均为从位置 $i$ 到回文串最右端位置包含的字符个数)。
+但是关于回文串的信息可用 **一种更紧凑的方式** 表达:对于每个位置 $i = 0 \dots n - 1$ ,我们找出值 $d_1[i]$ 和 $d_2[i]$ 。二者分别表示以位置 $i$ 为中心的长度为奇数和长度为偶数的回文串个数。换个角度,二者也表示了以位置 $i$ 为中心的最长回文串的半径长度(半径长度 $d_1[i]$ , $d_2[i]$ 均为从位置 $i$ 到回文串最右端位置包含的字符个数)。
-举例来说,字符串 $s = \mathtt{abababc}$ 以 $s[3] = b$ 为中心有三个奇数长度的回文串,最长回文串半径为 $3$,也即 $d_1[3] = 3$ :
+举例来说,字符串 $s = \mathtt{abababc}$ 以 $s[3] = b$ 为中心有三个奇数长度的回文串,最长回文串半径为 $3$ ,也即 $d_1[3] = 3$ :
$$
a\ \overbrace{b\ a\ \underset{s_3}{b}\ a\ b}^{d_1[3]=3}\ c
$$
-字符串 $s = \mathtt{cbaabd}$ 以 $s[3] = a$ 为中心有两个偶数长度的回文串,最长回文串半径为 $2$,也即 $d_2[3] = 2$ :
+字符串 $s = \mathtt{cbaabd}$ 以 $s[3] = a$ 为中心有两个偶数长度的回文串,最长回文串半径为 $2$ ,也即 $d_2[3] = 2$ :
$$
c\ \overbrace{b\ a\ \underset{s_3}{a}\ b}^{d_2[3]=2}\ d
这里我们将只描述算法中寻找所有奇数长度子回文串的情况,即只计算 $d_1[]$ ;寻找所有偶数长度子回文串的算法(即计算数组 $d_2[]$ )将只需对奇数情况下的算法进行一些小修改。
-为了快速计算,我们维护已找到的最靠右的子回文串的 **边界 $(l, r)$ ** (即具有最大 $r$ 值的回文串,其中 $l$ 和 $r$ 分别为该回文串左右边界的位置)。初始时,我们置 $l = 0$ 和 $r = -1$ ( *-1* 需区别于倒序索引位置,这里可为任意负数,仅为了循环初始时方便) 。
+为了快速计算,我们维护已找到的最靠右的子回文串的 **边界 $(l, r)$ ** (即具有最大 $r$ 值的回文串,其中 $l$ 和 $r$ 分别为该回文串左右边界的位置)。初始时,我们置 $l = 0$ 和 $r = -1$ (*-1*需区别于倒序索引位置,这里可为任意负数,仅为了循环初始时方便)。
现在假设我们要对下一个 $i$ 计算 $d_1[i]$ ,而之前所有 $d_1[]$ 中的值已计算完毕。我们将通过下列方式计算:
- 如果 $i$ 位于当前子回文串之外,即 $i > r$ ,那么我们调用朴素算法。
- 因此我们将连续地增加 $d_1[i]$ ,同时在每一步中检查当前的子串 $[i - d_1[i] \dots i + d_1[i]]$ ($d_1[i]$ 表示半径长度,下同)是否为一个回文串。如果我们找到了第一处对应字符不同,又或者碰到了 $s$ 的边界,则算法停止。在两种情况下我们均已计算完 $d_1[i]$ 。此后,仍需记得更新 $(l, r)$ 。
+ 因此我们将连续地增加 $d_1[i]$ ,同时在每一步中检查当前的子串 $[i - d_1[i] \dots i + d_1[i]]$ ( $d_1[i]$ 表示半径长度,下同)是否为一个回文串。如果我们找到了第一处对应字符不同,又或者碰到了 $s$ 的边界,则算法停止。在两种情况下我们均已计算完 $d_1[i]$ 。此后,仍需记得更新 $(l, r)$ 。
- 现在考虑 $i \le r$ 的情况。我们将尝试从已计算过的 $d_1[]$ 的值中获取一些信息。首先在子回文串 $(l, r)$ 中反转位置 $i$ ,即我们得到 $j = l + (r - i)$ 。现在来考察值 $d_1[j]$ 。因为位置 $j$ 同位置 $i$ 对称,我们 **几乎总是** 可以置 $d_1[i] = d_1[j]$ 。该想法的图示如下(可认为以 $j$ 为中心的回文串被“拷贝”至以 $i$ 为中心的位置上):
Visual Studio Code(以下简称 VS Code) 是一个由微软开发,同时支持 Windows、Linux 和 macOS 等操作系统且开放源代码的代码编辑器。它是用 TypeScript 编写的,并且采用 Electron 架构。它带有对 JavaScript、TypeScript 和 Node.js 的内置支持,并为其他语言(如 C、C++、Java、Python、PHP、Go)提供了丰富的扩展生态系统。
-官网: [Visual Studio Code - Code Editing. Redefined](https://code.visualstudio.com/)
+官网: [Visual Studio Code - Code Editing. Redefined](https://code.visualstudio.com/)
## 使用 Code Runner 插件运行代码