Loading .travis.yml +0 −1 Original line number Diff line number Diff line Loading @@ -20,7 +20,6 @@ script: - mkdocs build -v - find ./site -type f -name '*.html' -exec node --max_old_space_size=512 ./scripts/render_math.js {} \; - npx gulp minify - docker build -t oi-wiki:latest . - set +e deploy: provider: pages Loading docs/basic/binary.md +16 −18 Original line number Diff line number Diff line Loading @@ -10,7 +10,7 @@ 在二分搜索过程中,每次都把查询的区间减半,因此对于一个长度为 $n$ 的数组,至多会进行 $O(\log n)$ 次查找。 ```c++ ```cpp int binary_search(int start, int end, int key) { int ret = -1; // 未搜索到数据返回-1下标 int mid; Loading Loading @@ -59,15 +59,14 @@ int binary_search(int start, int end, int key) { 解题的时候往往会考虑枚举答案然后检验枚举的值是否正确。如果我们把这里的枚举换成二分,就变成了“二分答案”。 来看一看一道例题[Luogu P1873 砍树](https://www.luogu.org/problemnew/show/P1873),我们可以从 1 到 1000000000(10 亿)来枚举答案,但是这种朴素写法肯定拿不到满分,因为从 1 跑到 10 亿太耗时间。我们可以对答案进行 1 到 10 亿的二分,其中,每次都对其进行检查可行性(一般都是使用贪心法)。 **这就是二分答案。** 来看一看一道例题 [Luogu P1873 砍树](https://www.luogu.org/problemnew/show/P1873),我们可以在 1 到 1000000000(10 亿)中枚举答案,但是这种朴素写法肯定拿不到满分,因为从 1 跑到 10 亿太耗时间。我们可以对答案进行 1 到 10 亿的二分,其中,每次都对其进行检查可行性(一般都是使用贪心法)。 **这就是二分答案。** 下面就是例题的参考答案。 ```c++ ```cpp int a[1000005]; int n, m; bool check(int k) //检查可行性,k为锯片高度 { bool check(int k) { //检查可行性,k为锯片高度 long long sum = 0; for (int i = 1; i <= n; i++) //检查每一棵树 if (a[i] > k) //如果树高于锯片高度 Loading @@ -76,8 +75,7 @@ bool check(int k) //检查可行性,k为锯片高度 } int find(int x) { int l = 1, r = 1000000001; //因为是左闭右开的,所以10亿要加1 while (l + 1 < r) //如果两点不相邻 { while (l + 1 < r) { //如果两点不相邻 int mid = (l + r) / 2; //取中间值 if (check(mid)) //如果可行 l = mid; //升高锯片高度 Loading Loading @@ -114,29 +112,29 @@ int main() { ## 三分法 ```c++ mid = left + (right - left >> 1); midmid = mid + (right - mid >> 1); // 对右侧区间取半 if (cal(mid) > cal(midmid)) right = midmid; ```cpp lmid = left + (right - left >> 1); rmid = lmid + (right - lmid >> 1); // 对右侧区间取半 if (cal(lmid) > cal(rmid)) right = rmid; else left = mid; left = lmid; ``` 三分法可以用来查找凸函数的最大(小)值。 画一下图好理解一些(图待补) - 如果 `mid` 和 `midmid` 在最大(小)值的同一侧: - 如果 `lmid` 和 `rmid` 在最大(小)值的同一侧: 那么由于单调性,一定是二者中较大(小)的那个离最值近一些,较远的那个点对应的区间不可能包含最值,所以可以舍弃。 - 如果在两侧: 由于最值在二者中间,我们舍弃两侧的一个区间后,也不会影响最值,所以可以舍弃。 ## 分数规划 分数规划是这样一类问题,每个物品有两个代价 $c_i$ , $d_i$ ,要求通过某种方式选出若干个,使得 $\frac{\sum{c_i}}{\sum{d_i}}$ 最大或最小。 分数规划是这样一类问题,每个物品有两个属性 $c_i$ , $d_i$ ,要求通过某种方式选出若干个,使得 $\frac{\sum{c_i}}{\sum{d_i}}$ 最大或最小。 经典的例子是 最优比率环、最优比率生成树 等等。 经典的例子有 最优比率环、最优比率生成树 等等。 ### 二分法 Loading Loading @@ -170,4 +168,4 @@ $$ ### Dinkelbach 算法 Dinkelbach 算法是每次用上一轮的答案当做新的 $L$ 来输入,不断地迭代,直至答案收敛。 Dinkelbach 算法的大概思想是每次用上一轮的答案当做新的 $L$ 来输入,不断地迭代,直至答案收敛。 docs/basic/bubble-sort.md 0 → 100644 +23 −0 Original line number Diff line number Diff line 冒泡排序是一种稳定的排序方法。 以升序为例,冒泡排序每次检查相邻两个元素,如果前面的元素大于后面的元素,就将相邻两个元素交换。当没有相邻的元素需要交换时,排序就完成了。 不难发现,我们最多需要扫描 $n$ 遍数组才能完成排序。 ```cpp void bubble_sort() { for (int i = 1; i <= n; i++) { bool flag = false; for (int j = 1; j < n; j++) if (a[j] > a[j + 1]) { flag = true; int t = a[j]; a[j] = a[j + 1]; a[j + 1] = t; } if (!flag) break; //如果没有执行交换操作,说明数列已经有序 } } ``` 在序列完全有序时,该算法只需遍历一遍数组,不用执行任何交换操作,时间复杂度为 $O(n)$ 。在最坏情况下,冒泡排序要执行 $\frac{(n-1)n}{2}$ 次交换操作,时间复杂度为 $O(n^2)$ 。在平均情况下,冒泡排序的时间复杂度也是 $O(n^2)$ 。 No newline at end of file docs/basic/bucket-sort.md 0 → 100644 +11 −0 Original line number Diff line number Diff line 计数排序也称桶排序,可以在 $O(n)$ 的时间复杂度内对数组进行排序,但是它的空间复杂度与需要排序的数组的值域相关。 但实际操作时,由于时空同阶,时间复杂度应为 $O(\max\left(n,U\right))$,其中 $U$ 代表数组元素的值域大小。 !!! warning "注" 注意区分 **基数排序** 与 **桶排序** 算法流程就是记录每一个数出现了多少次,然后从小到大依次输出。 一般考虑的是某一范围内的整数,但是计数排序也可以和[离散化](/misc/discrete)一起使用,来对浮点数、大整数进行排序。 docs/basic/construction.md +46 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,10 @@ 这要求我们在解题时,要思考问题规模增长对答案的影响,这种影响是否可以推广。(比如在设计动态规划方法的时候,要考虑从一个状态到后继状态的转移会造成什么影响)。 构造题一个很显著的特点就是高自由度,也就是说一道题的构造方式可能有很多种,但是会有一种较为简单的构造方式满足题意。看起来是放宽了要求,让题目变的简单了,但很多时候,正是这种高自由度导致题目没有明确思路而无从下手。 构造题另一个特点就是形式灵活,变化多样。并不存在一个通用解法或套路可以解决所有构造题,甚至很难找出解题思路的共性。因此,下面将列举一些例题帮助读者体会构造题的一些思想内涵,给予思路上的启发。建议大家深入思考后再查看题解,也欢迎大家参与分享有趣的构造题。 ## 例题: ### Example 1 Loading Loading @@ -65,3 +69,45 @@ $$ 和 task1 同样的思路,我们发现 $1$ 必定出现在数列的第一位,否则 $1$ 出现前后的两个前缀积必然相等;而 $n$ 必定出现在数列的最后一位,因为 $n$ 出现位置后的所有前缀积在模意义下都为 $0$。手玩几组样例以后发现,所有样例中均有一组合法解满足前缀积在模意义下为 $1,2,3,\cdots,n$,因此我们可以构造出上文所述的数列来满足这个条件。那么我们只需证明这 $n$ 个数互不相同即可。 我们发现这些数均为 $1 \cdots n-2$ 的逆元 $+1$,因此各不相同,此题得解。 ### Example 3 #### Problem [AtCoder Grand Contest 032 B](<https://atcoder.jp/contests/agc032/tasks/agc032_b>) #### Solution 手玩一下 $n=3,4,5$ 的情况,我们可以找到一个构造思路。 构造一个完全 $k$ 分图,保证这 $k$ 部分和相等。则每个点的 $S$ 均相等,为$\dfrac{(k-1)\sum_{i=1}^{n}i}{k}$。 如果 $n$ 为偶数,那么我们可以前后两两配对,即 $\{1,n\},\{2,n-1\}\cdots$ 如果 $n$ 为奇数,那么我们可以把 $n$ 单拿出来作为一组,剩余的 $n-1$ 个两两配对,即$\{n\},\{1,n-1\},\{2,n-2\}\cdots$ 这样构造出的图在 $n\ge 3$ 时连通性易证,在此不加赘述。 此题得解 ### Example 4 #### Problem [BZOJ4971:[Lydsy1708月赛]记忆中的背包](<https://www.lydsy.com/JudgeOnline/problem.php?id=4971>) #### Solution 这道题是自由度最高的构造题之一了。这就导致了没有头绪,难以入手。 首先,不难发现模数是假的。由于我们自由构造数据,我们一定可以让方案数不超过模数。 通过奇怪的方式,我们想到可以通过构造 $n$ 个 代价为 $1$ 的小物品和几个代价大于 $\dfrac{w}{2}$ 的大物品。 由于大物品只能取一件,所以每个代价为 $x$ 的大物品对方案数的贡献为 $C_{n}^{w-x}$。 令 $f_{i,j}$ 表示有 $i$ 个 $1$,方案数为 $j$ 的最小大物品数。 用 dp 预处理出 $f$ ,通过计算可知只需预处理 $i\le 20$ 的所有值即可。 此题得解 Loading
.travis.yml +0 −1 Original line number Diff line number Diff line Loading @@ -20,7 +20,6 @@ script: - mkdocs build -v - find ./site -type f -name '*.html' -exec node --max_old_space_size=512 ./scripts/render_math.js {} \; - npx gulp minify - docker build -t oi-wiki:latest . - set +e deploy: provider: pages Loading
docs/basic/binary.md +16 −18 Original line number Diff line number Diff line Loading @@ -10,7 +10,7 @@ 在二分搜索过程中,每次都把查询的区间减半,因此对于一个长度为 $n$ 的数组,至多会进行 $O(\log n)$ 次查找。 ```c++ ```cpp int binary_search(int start, int end, int key) { int ret = -1; // 未搜索到数据返回-1下标 int mid; Loading Loading @@ -59,15 +59,14 @@ int binary_search(int start, int end, int key) { 解题的时候往往会考虑枚举答案然后检验枚举的值是否正确。如果我们把这里的枚举换成二分,就变成了“二分答案”。 来看一看一道例题[Luogu P1873 砍树](https://www.luogu.org/problemnew/show/P1873),我们可以从 1 到 1000000000(10 亿)来枚举答案,但是这种朴素写法肯定拿不到满分,因为从 1 跑到 10 亿太耗时间。我们可以对答案进行 1 到 10 亿的二分,其中,每次都对其进行检查可行性(一般都是使用贪心法)。 **这就是二分答案。** 来看一看一道例题 [Luogu P1873 砍树](https://www.luogu.org/problemnew/show/P1873),我们可以在 1 到 1000000000(10 亿)中枚举答案,但是这种朴素写法肯定拿不到满分,因为从 1 跑到 10 亿太耗时间。我们可以对答案进行 1 到 10 亿的二分,其中,每次都对其进行检查可行性(一般都是使用贪心法)。 **这就是二分答案。** 下面就是例题的参考答案。 ```c++ ```cpp int a[1000005]; int n, m; bool check(int k) //检查可行性,k为锯片高度 { bool check(int k) { //检查可行性,k为锯片高度 long long sum = 0; for (int i = 1; i <= n; i++) //检查每一棵树 if (a[i] > k) //如果树高于锯片高度 Loading @@ -76,8 +75,7 @@ bool check(int k) //检查可行性,k为锯片高度 } int find(int x) { int l = 1, r = 1000000001; //因为是左闭右开的,所以10亿要加1 while (l + 1 < r) //如果两点不相邻 { while (l + 1 < r) { //如果两点不相邻 int mid = (l + r) / 2; //取中间值 if (check(mid)) //如果可行 l = mid; //升高锯片高度 Loading Loading @@ -114,29 +112,29 @@ int main() { ## 三分法 ```c++ mid = left + (right - left >> 1); midmid = mid + (right - mid >> 1); // 对右侧区间取半 if (cal(mid) > cal(midmid)) right = midmid; ```cpp lmid = left + (right - left >> 1); rmid = lmid + (right - lmid >> 1); // 对右侧区间取半 if (cal(lmid) > cal(rmid)) right = rmid; else left = mid; left = lmid; ``` 三分法可以用来查找凸函数的最大(小)值。 画一下图好理解一些(图待补) - 如果 `mid` 和 `midmid` 在最大(小)值的同一侧: - 如果 `lmid` 和 `rmid` 在最大(小)值的同一侧: 那么由于单调性,一定是二者中较大(小)的那个离最值近一些,较远的那个点对应的区间不可能包含最值,所以可以舍弃。 - 如果在两侧: 由于最值在二者中间,我们舍弃两侧的一个区间后,也不会影响最值,所以可以舍弃。 ## 分数规划 分数规划是这样一类问题,每个物品有两个代价 $c_i$ , $d_i$ ,要求通过某种方式选出若干个,使得 $\frac{\sum{c_i}}{\sum{d_i}}$ 最大或最小。 分数规划是这样一类问题,每个物品有两个属性 $c_i$ , $d_i$ ,要求通过某种方式选出若干个,使得 $\frac{\sum{c_i}}{\sum{d_i}}$ 最大或最小。 经典的例子是 最优比率环、最优比率生成树 等等。 经典的例子有 最优比率环、最优比率生成树 等等。 ### 二分法 Loading Loading @@ -170,4 +168,4 @@ $$ ### Dinkelbach 算法 Dinkelbach 算法是每次用上一轮的答案当做新的 $L$ 来输入,不断地迭代,直至答案收敛。 Dinkelbach 算法的大概思想是每次用上一轮的答案当做新的 $L$ 来输入,不断地迭代,直至答案收敛。
docs/basic/bubble-sort.md 0 → 100644 +23 −0 Original line number Diff line number Diff line 冒泡排序是一种稳定的排序方法。 以升序为例,冒泡排序每次检查相邻两个元素,如果前面的元素大于后面的元素,就将相邻两个元素交换。当没有相邻的元素需要交换时,排序就完成了。 不难发现,我们最多需要扫描 $n$ 遍数组才能完成排序。 ```cpp void bubble_sort() { for (int i = 1; i <= n; i++) { bool flag = false; for (int j = 1; j < n; j++) if (a[j] > a[j + 1]) { flag = true; int t = a[j]; a[j] = a[j + 1]; a[j + 1] = t; } if (!flag) break; //如果没有执行交换操作,说明数列已经有序 } } ``` 在序列完全有序时,该算法只需遍历一遍数组,不用执行任何交换操作,时间复杂度为 $O(n)$ 。在最坏情况下,冒泡排序要执行 $\frac{(n-1)n}{2}$ 次交换操作,时间复杂度为 $O(n^2)$ 。在平均情况下,冒泡排序的时间复杂度也是 $O(n^2)$ 。 No newline at end of file
docs/basic/bucket-sort.md 0 → 100644 +11 −0 Original line number Diff line number Diff line 计数排序也称桶排序,可以在 $O(n)$ 的时间复杂度内对数组进行排序,但是它的空间复杂度与需要排序的数组的值域相关。 但实际操作时,由于时空同阶,时间复杂度应为 $O(\max\left(n,U\right))$,其中 $U$ 代表数组元素的值域大小。 !!! warning "注" 注意区分 **基数排序** 与 **桶排序** 算法流程就是记录每一个数出现了多少次,然后从小到大依次输出。 一般考虑的是某一范围内的整数,但是计数排序也可以和[离散化](/misc/discrete)一起使用,来对浮点数、大整数进行排序。
docs/basic/construction.md +46 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,10 @@ 这要求我们在解题时,要思考问题规模增长对答案的影响,这种影响是否可以推广。(比如在设计动态规划方法的时候,要考虑从一个状态到后继状态的转移会造成什么影响)。 构造题一个很显著的特点就是高自由度,也就是说一道题的构造方式可能有很多种,但是会有一种较为简单的构造方式满足题意。看起来是放宽了要求,让题目变的简单了,但很多时候,正是这种高自由度导致题目没有明确思路而无从下手。 构造题另一个特点就是形式灵活,变化多样。并不存在一个通用解法或套路可以解决所有构造题,甚至很难找出解题思路的共性。因此,下面将列举一些例题帮助读者体会构造题的一些思想内涵,给予思路上的启发。建议大家深入思考后再查看题解,也欢迎大家参与分享有趣的构造题。 ## 例题: ### Example 1 Loading Loading @@ -65,3 +69,45 @@ $$ 和 task1 同样的思路,我们发现 $1$ 必定出现在数列的第一位,否则 $1$ 出现前后的两个前缀积必然相等;而 $n$ 必定出现在数列的最后一位,因为 $n$ 出现位置后的所有前缀积在模意义下都为 $0$。手玩几组样例以后发现,所有样例中均有一组合法解满足前缀积在模意义下为 $1,2,3,\cdots,n$,因此我们可以构造出上文所述的数列来满足这个条件。那么我们只需证明这 $n$ 个数互不相同即可。 我们发现这些数均为 $1 \cdots n-2$ 的逆元 $+1$,因此各不相同,此题得解。 ### Example 3 #### Problem [AtCoder Grand Contest 032 B](<https://atcoder.jp/contests/agc032/tasks/agc032_b>) #### Solution 手玩一下 $n=3,4,5$ 的情况,我们可以找到一个构造思路。 构造一个完全 $k$ 分图,保证这 $k$ 部分和相等。则每个点的 $S$ 均相等,为$\dfrac{(k-1)\sum_{i=1}^{n}i}{k}$。 如果 $n$ 为偶数,那么我们可以前后两两配对,即 $\{1,n\},\{2,n-1\}\cdots$ 如果 $n$ 为奇数,那么我们可以把 $n$ 单拿出来作为一组,剩余的 $n-1$ 个两两配对,即$\{n\},\{1,n-1\},\{2,n-2\}\cdots$ 这样构造出的图在 $n\ge 3$ 时连通性易证,在此不加赘述。 此题得解 ### Example 4 #### Problem [BZOJ4971:[Lydsy1708月赛]记忆中的背包](<https://www.lydsy.com/JudgeOnline/problem.php?id=4971>) #### Solution 这道题是自由度最高的构造题之一了。这就导致了没有头绪,难以入手。 首先,不难发现模数是假的。由于我们自由构造数据,我们一定可以让方案数不超过模数。 通过奇怪的方式,我们想到可以通过构造 $n$ 个 代价为 $1$ 的小物品和几个代价大于 $\dfrac{w}{2}$ 的大物品。 由于大物品只能取一件,所以每个代价为 $x$ 的大物品对方案数的贡献为 $C_{n}^{w-x}$。 令 $f_{i,j}$ 表示有 $i$ 个 $1$,方案数为 $j$ 的最小大物品数。 用 dp 预处理出 $f$ ,通过计算可知只需预处理 $i\le 20$ 的所有值即可。 此题得解