Loading README.md +1 −1 Original line number Diff line number Diff line [](https://oi-wiki.org/) [](https://oi-wiki.org/) # 欢迎来到 **OI Wiki**! Loading docs/dp/knapsack.md +19 −12 Original line number Diff line number Diff line Loading @@ -82,15 +82,13 @@ for (int i = 1; i <= n; i++) 可以考虑一个朴素的做法:对于第 $i$ 件物品,枚举其选了多少个来转移。这样做的时间复杂度是 $O(n^3)$ 的。 尽管这样看起来很蠢,我们还是写一下 dp 方程: 状态转移方程如下: $$ f_{i,j}=\max_{k=0}^{+\infty}(f_{i-1,j-k\times w_i}+v_i\times k) $$ 然而这样显然不够优秀,我们要对它进行优化。 可以发现,对于 $f_{i,j}$ ,只要通过 $f_{i,j-w_i}$ 转移就可以了。dp 方程为: 考虑做一个简单的优化。可以发现,对于 $f_{i,j}$ ,只要通过 $f_{i,j-w_i}$ 转移就可以了。因此状态转移方程为: $$ f_{i,j}=\max(f_{i-1,j},f_{i,j-w_i}+v_i) Loading Loading @@ -121,15 +119,21 @@ $$ ## 多重背包 多重背包也是 0-1 背包的一个变式。与 0-1 背包的区别在于每种物品可以选 $k_i$ 次,而非 $1$ 次。 多重背包也是 0-1 背包的一个变式。与 0-1 背包的区别在于每种物品 y 有 $k_i$ 个,而非 $1$ 个。 一个很朴素的想法就是:把“每种物品选 $k_i$ 次”等价转换为“有 $k_i$ 个相同的物品,每个物品选一次”。这样就转换成了一个 0-1 背包模型,套用上文所述的方法就可已解决。状态转移方程如下: 一个很朴素的想法就是:把“每种物品选 $k_i$ 次”等价转换为“有 $k_i$ 个相同的物品,每个物品选一次”。这样就转换成了一个 0-1 背包模型,套用上文所述的方法就可已解决。时间复杂度: $O(nW\sum k_i)$ ,可以轻松 TLE。 $$ f_{i,j}=\max_{k=0}^{k_i}(f_{i-1,j-k\times w_i}+v_i\times k) $$ 虽然我们失败了,但是这个朴素的想法可以给我们的思路提供一些借鉴——我们可以把多重背包转化成 0-1 背包模型来求解。 时间复杂度 $O(nW\sum k_i)$ 。 显然,复杂度中的 $O(nW)$ 部分无法再优化了,我们只能从 $O(\sum k_i)$ 处入手。 ### 二进制分组优化 为了表述方便,我们用 $A_{i,j}$ 代表第 $i$ 种物品拆分出的第 $j$ 个物品。 考虑优化。我们仍考虑把多重背包转化成 0-1 背包模型来求解。 显然,复杂度中的 $O(nW)$ 部分无法再优化了,我们只能从 $O(\sum k_i)$ 处入手。为了表述方便,我们用 $A_{i,j}$ 代表第 $i$ 种物品拆分出的第 $j$ 个物品。 在朴素的做法中, $\forall j\le k_i$ , $A_{i,j}$ 均表示相同物品。那么我们效率低的原因主要在于我们进行了大量重复性的工作。举例来说,我们考虑了“同时选 $A_{i,1},A_{i,2}$ ”与“同时选 $A_{i,2},A_{i,3}$ ”这两个完全等效的情况。这样的重复性工作我们进行了许多次。那么优化拆分方式就成为了解决问题的突破口。 Loading @@ -146,7 +150,7 @@ $$ 显然,通过上述拆分方式,可以表示任意 $\le k_i$ 个物品的等效选择方式。将每种物品按照上述方式拆分后,使用 0-1 背包的方法解决即可。 时间复杂度 $O(nW\sum\log k_i)$ 时间复杂度 $O(W\sum_{i=1}^n\log_2k_i)$ ??? 二进制分组代码 ```cpp Loading @@ -165,8 +169,11 @@ $$ } ``` ??? note "[「Luogu P1776」宝物筛选\_NOI 导刊 2010 提高(02)](https://www.luogu.org/problemnew/show/P1776)" 题意概要:有 $n$ 种物品和一个容量为 $W$ 的背包,每种物品有 $m_v{i}$ 个,同时每个物品有两种属性:重量 $w_{i}$ 和价值 $v_{i}$ 。要求选若干个物品放入背包使背包中物品的总价值最大且背包中物品的总重量不超过背包的容量。有一点需要注意的是本题数据范围较大,情况较多。 ### 单调队列优化 见 [单调队列/单调栈优化](opt/monotonous-queue-stack.md) 。 习题: [「Luogu P1776」宝物筛选\_NOI 导刊 2010 提高(02)](https://www.luogu.org/problemnew/show/P1776) ## 混合背包 Loading docs/dp/opt/binary-knapsack.mddeleted 100644 → 0 +0 −35 Original line number Diff line number Diff line author: TrisolarisHD, hsfzLZH1, greyqz, Ir1d, abc1763613206, tigerruanyifan ## 介绍 ??? note " 例题[经典问题 - 多重背包](../knapsack.md#_2)" 题目大意:有 $n$ 种物品,每种物品有 $a_i$ 件,购买一件这种物品的费用为 $c_i$ ,价值为 $v_i$ 。有一个容量为 $t$ 的背包,现在让你找到最优的一种方案,使得装入背包的物品的总价值最大。 考虑常规的动规方式,定义 $f_{i,j}$ 为当前考虑到第 $i$ 个物品,背包容量为 $j$ 所能获得的最大价值。 状态转移方程为, $f_{i,j}=\max\{f_{i-1,j},f_{i-1,j-c_i}+v_i\}$ 。 对于 **每件** 物品,都要这样循环一次,时间复杂度为 $t\times \sum_{i=1}^n a_i$ ,某些时候可能不可接受,需要优化。 考虑这样一种情况,如果我们有 $17$ 个硬币,要去买 $1$ 到 $17$ 元钱的物品,只需将这些硬币打包成 $1,2,4,8$ 和 $2$ 这样的几包。前面的 $4$ 包能保证覆盖 $1$ 到 $15$ 所有的情况,最后一包在之前的基础上再加上一个值,能保证实现支付的时候取整包,肯定能保证支付。这就是二进制优化的原理和基本思想。 用上述的方法,就可以把 $k$ 件相同的物品看作是 $O(\log k)$ 件物品了。优化后 代码实现: ```cpp for (int i = 1; i <= n; i++) { scanf("%d", a + i); tot += c[i] * a[i]; for (int j = 1; j <= a[i]; j *= 2) if (a[i] >= j) a[i] -= j, v[++cur] = c[i] * j; if (a[i]) v[++cur] = c[i] * a[i]; } for (int i = 1; i <= cur; i++) for (int j = m; j >= v[i]; j--) if (f[j - v[i]]) f[j] = true; ``` ## 习题 [HDU 2844 Coins](http://acm.hdu.edu.cn/showproblem.php?pid=2844) docs/dp/opt/monotonous-queue-stack.md +27 −0 Original line number Diff line number Diff line Loading @@ -31,6 +31,33 @@ author: TrisolarisHD, hsfzLZH1, Ir1d, greyqz, Anguei, billchenchina, Chrogeek, C 讲完了,让我们归纳一下单调队列优化动态规划问题的基本形态:当前状态的所有值可以从上一个状态的某个连续的段的值得到,要对这个连续的段进行 RMQ 操作,相邻状态的段的左右区间满足非降的关系。 ## 单调队列优化多重背包 ???+note "问题描述" 你有 $n$ 个物品,每个物品重量为 $w_i$ ,价值为 $v_i$ ,数量为 $k_i$ 。你有一个承重上限为 $m$ 的背包,现在要求你在不超过重量上限的情况下选取价值和尽可能大的物品放入背包。求最大价值。 不了解背包 DP 的请先阅读 [背包 DP](../knapsack.md) 。设 $f_{i,j}$ 表示前 $i$ 个物品装入承重为 $j$ 的背包的最大价值,朴素的转移方程为 $$ f_{i,j}=\max_{k=0}^{k_i}(f_{i-1,j-k\times w_i}+v_i\times k) $$ 时间复杂度 $O(nW\sum k_i)$ 。 考虑优化 $f_i$ 的转移。为方便表述,设 $g_{x,y}=f_{i,x\times w_i+y},g'_{x,y}=f_{i-1,x\times w_i+y}$ ,则转移方程可以表示为: $$ g_{x,y}=\max_{k=0}^{k_i}(g'_{x-k,y}+v_i\times k) $$ 设 $G_{x,y}=g'_{x,y}-v_i\times x$ 。则方程可以表示为: $$ g_{x,y}=\max_{k=0}^{k_i}(G_{x-k,y})+v_i\times x $$ 这样就转化为一个经典的单调队列优化形式了。 $G_{x,y}$ 可以 $O(1)$ 计算,因此对于固定的 $y$ ,我们可以在 $O\left( \left\lfloor \dfrac{W}{w_i} \right\rfloor \right)$ 的时间内计算出 $g_{x,y}$ 。因此求出所有 $g_{x,y}$ 的复杂度为 $O\left( \left\lfloor \dfrac{W}{w_i} \right\rfloor \right)\times O(w_i)=O(W)$ 。这样转移的总复杂度就降为 $O(nW)$ 。 ## 习题 [「Luogu P1886」滑动窗口](https://loj.ac/problem/10175) Loading docs/graph/cut.md +45 −1 Original line number Diff line number Diff line Loading @@ -117,7 +117,13 @@ low[u] = min(low[u], num[v]); 和割点差不多,叫做桥。 > 对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。 > 对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。严谨来说,就是:假设有连通图 $G=\{V,E\}$ , $e$ 是其中一条边(即 $e \in E$ ),如果 $G-e$ 是不连通的,则边 $e$ 是图 $G$ 的一条割边(桥)。 比如说,下图中,  红色箭头指向的就是割边。 ### 实现 Loading @@ -125,4 +131,42 @@ low[u] = min(low[u], num[v]); 割边是和是不是根节点没关系的,原来我们求割点的时候是指点 $v$ 是不可能不经过父节点 $u$ 为回到祖先节点(包括父节点),所以顶点 $u$ 是割点。如果 $low_v=num_u$ 表示还可以回到父节点,如果顶点 $v$ 不能回到祖先也没有另外一条回到父亲的路,那么 $u-v$ 这条边就是割边。 ### 代码实现 下面代码实现了求割边,其中,当 `isbridge[x]` 为真时, `(father[x],x)` 为一条割边。 ```cpp int low[MAXN], dfn[MAXN], iscut[MAXN], dfs_clock; bool isbridge[MAXN]; vector<int> G[MAXN]; int cnt_bridge; int father[MAXN]; void tarjan(int u, int fa) { father[u] = fa; low[u] = dfn[u] = ++dfs_clock; for (int i = 0; i < G[u].size(); i++) { int v = G[u][i]; if (!dfn[v]) { tarjan(v, u); low[u] = min(low[u], low[v]); if (low[v] > dfn[u]) { isbridge[v] = true; ++cnt_bridge; } } else if (dfn[v] < dfn[u] && v != fa) { low[u] = min(low[u], dfn[v]); } } } ``` ## 练习 - [P3388【模板】割点(割顶)](https://www.luogu.org/problem/P3388) - [POJ2117 Electricity](https://vjudge.net/problem/POJ-2117) - [HDU4738 Caocao's Bridges](https://vjudge.net/problem/HDU-4738) - [HDU2460 Network](https://vjudge.net/problem/HDU-2460) - [POJ1523 SPF](https://vjudge.net/problem/POJ-1523) Tarjan 算法还有许多用途,常用的例如求强连通分量,缩点,还有求 2-SAT 的用途等。 Loading
README.md +1 −1 Original line number Diff line number Diff line [](https://oi-wiki.org/) [](https://oi-wiki.org/) # 欢迎来到 **OI Wiki**! Loading
docs/dp/knapsack.md +19 −12 Original line number Diff line number Diff line Loading @@ -82,15 +82,13 @@ for (int i = 1; i <= n; i++) 可以考虑一个朴素的做法:对于第 $i$ 件物品,枚举其选了多少个来转移。这样做的时间复杂度是 $O(n^3)$ 的。 尽管这样看起来很蠢,我们还是写一下 dp 方程: 状态转移方程如下: $$ f_{i,j}=\max_{k=0}^{+\infty}(f_{i-1,j-k\times w_i}+v_i\times k) $$ 然而这样显然不够优秀,我们要对它进行优化。 可以发现,对于 $f_{i,j}$ ,只要通过 $f_{i,j-w_i}$ 转移就可以了。dp 方程为: 考虑做一个简单的优化。可以发现,对于 $f_{i,j}$ ,只要通过 $f_{i,j-w_i}$ 转移就可以了。因此状态转移方程为: $$ f_{i,j}=\max(f_{i-1,j},f_{i,j-w_i}+v_i) Loading Loading @@ -121,15 +119,21 @@ $$ ## 多重背包 多重背包也是 0-1 背包的一个变式。与 0-1 背包的区别在于每种物品可以选 $k_i$ 次,而非 $1$ 次。 多重背包也是 0-1 背包的一个变式。与 0-1 背包的区别在于每种物品 y 有 $k_i$ 个,而非 $1$ 个。 一个很朴素的想法就是:把“每种物品选 $k_i$ 次”等价转换为“有 $k_i$ 个相同的物品,每个物品选一次”。这样就转换成了一个 0-1 背包模型,套用上文所述的方法就可已解决。状态转移方程如下: 一个很朴素的想法就是:把“每种物品选 $k_i$ 次”等价转换为“有 $k_i$ 个相同的物品,每个物品选一次”。这样就转换成了一个 0-1 背包模型,套用上文所述的方法就可已解决。时间复杂度: $O(nW\sum k_i)$ ,可以轻松 TLE。 $$ f_{i,j}=\max_{k=0}^{k_i}(f_{i-1,j-k\times w_i}+v_i\times k) $$ 虽然我们失败了,但是这个朴素的想法可以给我们的思路提供一些借鉴——我们可以把多重背包转化成 0-1 背包模型来求解。 时间复杂度 $O(nW\sum k_i)$ 。 显然,复杂度中的 $O(nW)$ 部分无法再优化了,我们只能从 $O(\sum k_i)$ 处入手。 ### 二进制分组优化 为了表述方便,我们用 $A_{i,j}$ 代表第 $i$ 种物品拆分出的第 $j$ 个物品。 考虑优化。我们仍考虑把多重背包转化成 0-1 背包模型来求解。 显然,复杂度中的 $O(nW)$ 部分无法再优化了,我们只能从 $O(\sum k_i)$ 处入手。为了表述方便,我们用 $A_{i,j}$ 代表第 $i$ 种物品拆分出的第 $j$ 个物品。 在朴素的做法中, $\forall j\le k_i$ , $A_{i,j}$ 均表示相同物品。那么我们效率低的原因主要在于我们进行了大量重复性的工作。举例来说,我们考虑了“同时选 $A_{i,1},A_{i,2}$ ”与“同时选 $A_{i,2},A_{i,3}$ ”这两个完全等效的情况。这样的重复性工作我们进行了许多次。那么优化拆分方式就成为了解决问题的突破口。 Loading @@ -146,7 +150,7 @@ $$ 显然,通过上述拆分方式,可以表示任意 $\le k_i$ 个物品的等效选择方式。将每种物品按照上述方式拆分后,使用 0-1 背包的方法解决即可。 时间复杂度 $O(nW\sum\log k_i)$ 时间复杂度 $O(W\sum_{i=1}^n\log_2k_i)$ ??? 二进制分组代码 ```cpp Loading @@ -165,8 +169,11 @@ $$ } ``` ??? note "[「Luogu P1776」宝物筛选\_NOI 导刊 2010 提高(02)](https://www.luogu.org/problemnew/show/P1776)" 题意概要:有 $n$ 种物品和一个容量为 $W$ 的背包,每种物品有 $m_v{i}$ 个,同时每个物品有两种属性:重量 $w_{i}$ 和价值 $v_{i}$ 。要求选若干个物品放入背包使背包中物品的总价值最大且背包中物品的总重量不超过背包的容量。有一点需要注意的是本题数据范围较大,情况较多。 ### 单调队列优化 见 [单调队列/单调栈优化](opt/monotonous-queue-stack.md) 。 习题: [「Luogu P1776」宝物筛选\_NOI 导刊 2010 提高(02)](https://www.luogu.org/problemnew/show/P1776) ## 混合背包 Loading
docs/dp/opt/binary-knapsack.mddeleted 100644 → 0 +0 −35 Original line number Diff line number Diff line author: TrisolarisHD, hsfzLZH1, greyqz, Ir1d, abc1763613206, tigerruanyifan ## 介绍 ??? note " 例题[经典问题 - 多重背包](../knapsack.md#_2)" 题目大意:有 $n$ 种物品,每种物品有 $a_i$ 件,购买一件这种物品的费用为 $c_i$ ,价值为 $v_i$ 。有一个容量为 $t$ 的背包,现在让你找到最优的一种方案,使得装入背包的物品的总价值最大。 考虑常规的动规方式,定义 $f_{i,j}$ 为当前考虑到第 $i$ 个物品,背包容量为 $j$ 所能获得的最大价值。 状态转移方程为, $f_{i,j}=\max\{f_{i-1,j},f_{i-1,j-c_i}+v_i\}$ 。 对于 **每件** 物品,都要这样循环一次,时间复杂度为 $t\times \sum_{i=1}^n a_i$ ,某些时候可能不可接受,需要优化。 考虑这样一种情况,如果我们有 $17$ 个硬币,要去买 $1$ 到 $17$ 元钱的物品,只需将这些硬币打包成 $1,2,4,8$ 和 $2$ 这样的几包。前面的 $4$ 包能保证覆盖 $1$ 到 $15$ 所有的情况,最后一包在之前的基础上再加上一个值,能保证实现支付的时候取整包,肯定能保证支付。这就是二进制优化的原理和基本思想。 用上述的方法,就可以把 $k$ 件相同的物品看作是 $O(\log k)$ 件物品了。优化后 代码实现: ```cpp for (int i = 1; i <= n; i++) { scanf("%d", a + i); tot += c[i] * a[i]; for (int j = 1; j <= a[i]; j *= 2) if (a[i] >= j) a[i] -= j, v[++cur] = c[i] * j; if (a[i]) v[++cur] = c[i] * a[i]; } for (int i = 1; i <= cur; i++) for (int j = m; j >= v[i]; j--) if (f[j - v[i]]) f[j] = true; ``` ## 习题 [HDU 2844 Coins](http://acm.hdu.edu.cn/showproblem.php?pid=2844)
docs/dp/opt/monotonous-queue-stack.md +27 −0 Original line number Diff line number Diff line Loading @@ -31,6 +31,33 @@ author: TrisolarisHD, hsfzLZH1, Ir1d, greyqz, Anguei, billchenchina, Chrogeek, C 讲完了,让我们归纳一下单调队列优化动态规划问题的基本形态:当前状态的所有值可以从上一个状态的某个连续的段的值得到,要对这个连续的段进行 RMQ 操作,相邻状态的段的左右区间满足非降的关系。 ## 单调队列优化多重背包 ???+note "问题描述" 你有 $n$ 个物品,每个物品重量为 $w_i$ ,价值为 $v_i$ ,数量为 $k_i$ 。你有一个承重上限为 $m$ 的背包,现在要求你在不超过重量上限的情况下选取价值和尽可能大的物品放入背包。求最大价值。 不了解背包 DP 的请先阅读 [背包 DP](../knapsack.md) 。设 $f_{i,j}$ 表示前 $i$ 个物品装入承重为 $j$ 的背包的最大价值,朴素的转移方程为 $$ f_{i,j}=\max_{k=0}^{k_i}(f_{i-1,j-k\times w_i}+v_i\times k) $$ 时间复杂度 $O(nW\sum k_i)$ 。 考虑优化 $f_i$ 的转移。为方便表述,设 $g_{x,y}=f_{i,x\times w_i+y},g'_{x,y}=f_{i-1,x\times w_i+y}$ ,则转移方程可以表示为: $$ g_{x,y}=\max_{k=0}^{k_i}(g'_{x-k,y}+v_i\times k) $$ 设 $G_{x,y}=g'_{x,y}-v_i\times x$ 。则方程可以表示为: $$ g_{x,y}=\max_{k=0}^{k_i}(G_{x-k,y})+v_i\times x $$ 这样就转化为一个经典的单调队列优化形式了。 $G_{x,y}$ 可以 $O(1)$ 计算,因此对于固定的 $y$ ,我们可以在 $O\left( \left\lfloor \dfrac{W}{w_i} \right\rfloor \right)$ 的时间内计算出 $g_{x,y}$ 。因此求出所有 $g_{x,y}$ 的复杂度为 $O\left( \left\lfloor \dfrac{W}{w_i} \right\rfloor \right)\times O(w_i)=O(W)$ 。这样转移的总复杂度就降为 $O(nW)$ 。 ## 习题 [「Luogu P1886」滑动窗口](https://loj.ac/problem/10175) Loading
docs/graph/cut.md +45 −1 Original line number Diff line number Diff line Loading @@ -117,7 +117,13 @@ low[u] = min(low[u], num[v]); 和割点差不多,叫做桥。 > 对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。 > 对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。严谨来说,就是:假设有连通图 $G=\{V,E\}$ , $e$ 是其中一条边(即 $e \in E$ ),如果 $G-e$ 是不连通的,则边 $e$ 是图 $G$ 的一条割边(桥)。 比如说,下图中,  红色箭头指向的就是割边。 ### 实现 Loading @@ -125,4 +131,42 @@ low[u] = min(low[u], num[v]); 割边是和是不是根节点没关系的,原来我们求割点的时候是指点 $v$ 是不可能不经过父节点 $u$ 为回到祖先节点(包括父节点),所以顶点 $u$ 是割点。如果 $low_v=num_u$ 表示还可以回到父节点,如果顶点 $v$ 不能回到祖先也没有另外一条回到父亲的路,那么 $u-v$ 这条边就是割边。 ### 代码实现 下面代码实现了求割边,其中,当 `isbridge[x]` 为真时, `(father[x],x)` 为一条割边。 ```cpp int low[MAXN], dfn[MAXN], iscut[MAXN], dfs_clock; bool isbridge[MAXN]; vector<int> G[MAXN]; int cnt_bridge; int father[MAXN]; void tarjan(int u, int fa) { father[u] = fa; low[u] = dfn[u] = ++dfs_clock; for (int i = 0; i < G[u].size(); i++) { int v = G[u][i]; if (!dfn[v]) { tarjan(v, u); low[u] = min(low[u], low[v]); if (low[v] > dfn[u]) { isbridge[v] = true; ++cnt_bridge; } } else if (dfn[v] < dfn[u] && v != fa) { low[u] = min(low[u], dfn[v]); } } } ``` ## 练习 - [P3388【模板】割点(割顶)](https://www.luogu.org/problem/P3388) - [POJ2117 Electricity](https://vjudge.net/problem/POJ-2117) - [HDU4738 Caocao's Bridges](https://vjudge.net/problem/HDU-4738) - [HDU2460 Network](https://vjudge.net/problem/HDU-2460) - [POJ1523 SPF](https://vjudge.net/problem/POJ-1523) Tarjan 算法还有许多用途,常用的例如求强连通分量,缩点,还有求 2-SAT 的用途等。