动态规划 Dynamic Programming 学习笔记
文章以 CC-BY-SA 方式共享,此说明高于本站内其他说明。
本文尚未完工,但内容足够丰富,故提前发布。
内容包含大量 \(\LaTeX\) 公式,渲染可能需要一些时间,请耐心等待渲染(约 5s)。
0x00 前言
题单将介绍介绍动态规划(Dynamic Programming, DP)及其解决的问题、根据其设计的算法及优化。
动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
由于动态规划并不是某种具体的算法,而是一种解决特定问题的方法,因此它会出现在各式各样的数据结构中,与之相关的题目种类也更为繁杂。
在 OI 中,计数等非最优化问题的递推解法也常被不规范地称作 DP,因此本章将它们一并列出。事实上,动态规划与其它类型的递推的确有很多相似之处,学习时可以注意它们之间的异同。
0x01 简介
动态规划应用于子问题重叠的情况:
- 要去刻画最优解的结构特征;
- 尝试递归地定义最优解的值(就是我们常说的考虑从 \(i - 1\) 转移到 \(i\));
- 计算最优解;
- 利用计算出的信息构造一个最优解。
0x10 线性 DP
顾名思义,即一般是在数列上/字符串上进行的 DP。
0x11 基本概念
1. 子序列:从一个序列中删去一些数字,剩下的序列称为原序列的子序列。特别的,可以不删。
2. 公共子序列:若 \(c\) 同时为 \(a, b\) 的子序列,则称 \(c\) 为 \(a, b\) 的公共子序列。
3. 最长公共子序列:若 \(c\) 为 \(a, b\) 的公共子序列中长度最大的,则称 \(c\) 为 \(a, b\) 的公共子序列。
0x12 最长上升子序列 LIS 问题
注意,以下的“上升”表示不严格上升,即不下降。
求一个序列 \(a\) 的最长上升子序列,即求一个 \(a\) 的子序列 \(b\) 的长度,其中 \(\forall 1\le i<n, a_i\le a_{i+1}\)。
设 \(f_i\) 表示以 \(a_i\) 为结尾的、最长上升子序列的长度,则很自然的可以想到转移方程:\(f_i=\max(f_{j}+1)\;|\;j<i, a_j\le a_i\)。
这个方程的意思是,在 \(\forall 1\le j<i\) 中寻找一个最大的 \(f_j\),并满足 \(a_j\le a_i\)。
相当于是把“寻找以 \(a_i\) 为结尾的,最长上升子序列的长度”这个大问题分成了两个小问题:
- 寻找一个在 \(a_i\) 前面的、比他小的数,并且满足以他为结尾的上升子序列最长。
- 把 \(a_i\) 接在后边,长度加一。
特别的,\(f_1=1\)。
可以感性理解一下:
f=\{1, 2, 3, 3, 4\}
\]
for (int i = 1; i <= n; ++i)
{
int p = 0;
for (int j = 1; j < i; ++j)
if (a[i] >= a[j])
p = max(p, f[j] + 1);
f[i] = p;
}
0x13 扩展:最长上升子序列 LIS 问题 O(n log n) 解法
显然上面的解法是 \(O(n^2)\) 的。但是有更优的、\(O(n \log n)\) 的解法。
设 \(f\) 表示这个最长上升子序列,目前求到的长度为 \(l\)。对于一个元素 \(a_i\),若 \(a_i\ge f_l\),则直接将 \(a_i\) 加入到 \(f\) 的末尾。
那如果 \(a_i<f_l\) 呢?这就是保证这个算法正确性的关键了。
给出解决办法:在 \(f\) 数组中找到一个 \(p\),使得 \(a_i<f_p\) 但 \(a_i\ge f_{p-1}\),然后用 \(a_i\) 替换掉 \(f_p\)。
分析一下该算法为什么正确。
若 \(p=l\),那么 \(f_p\) 不如让位给 \(a_i\),因为由于 \(a_i<f_p\),显然 \(a_i\) 后面能接的数量比 \(f_p\) 多(比如就恰好有这么一个数 \(x\),使得 \(a_i\le x<f_p\),此时若 \(a_i\) 不替换 \(f_p\),那么 \(x\) 就接不上了,不然 \(x\) 仍然能接上)。
若 \(p\ne l\),则 \(a_i\) 替换掉 \(f_p\) 完全没问题,因为 \(f_p\) 有生之年不会再被用到了。
这时候就有同学要问了:这样 \(f\) 数组就不保证连续了!
所以 \(f_i\) 其实表示的是:长度为 \(i\) 的最优结尾数字,而不是最长上升子序列。
0x14 最长公共子序列 LCS 问题
给定两个长度为 \(n, m\) 的序列 \(a, b\),求 \(a, b\) 的最长公共子序列。
设 \(f_{i, j}\) 表示 \(a\) 的前 \(i\) 位与 \(b\) 的前 \(j\) 位所能组成的最长公共子序列。
考虑两种情况:
- \(a_i=b_j\),则此时的 \(f_{i,j}=\max(f_{i-1,j},f_{i,f-1},f_{i-1,j-1}+1)\)。
- \(a_i\ne b_j\),则 \(f_{i,j}=\max(f_{i-1,j},f_{i,j-1})\)。
对于第一种情况,其实是以下几种情况的最大值:
- \(a\) 的前 \(i\) 位与 \(b\) 的前 \(j-1\) 位的 LCS 最大值。
- \(a\) 的前 \(i-1\) 位与 \(b\) 的前 \(j\) 位的 LCS 最大值。
- \(a\) 的前 \(i-1\) 位与 \(b\) 的前 \(j-1\) 位的 LCS 的最大值,加上当前这一位(也就是选当前这一位)。
第二种情况同理。
时间复杂度 \(O(nm)\)。
int a[105], b[105], n, m, f[105][105];
int main(void)
{
cin >> n >> m;
for (int i = 1; i <= n; ++i)
cin >> a[i];
for (int i = 1; i <= m; ++i)
cin >> b[i];
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j <= m; ++j)
{
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
if (a[i] == b[j])
f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
}
}
printf("%d", f[n][m]);
return 0;
}
0x15 扩展:最长公共子序列 LCS 问题 O(n log n) 解法
这种解法的本质是将 LCS 转换为 LIS 来求解,求解 LIS 的时间复杂度为 \(O(n\log n)\),则 LCS 也可以在 \(O(n\log n)\) 的时间复杂度内求出。
假设 \(a=\{1, 3, 2, 4, 5\}, b=\{3, 1, 4, 5, 2, 3, 3\}\),记录 \(a\) 中的数在 \(b\) 中出现的位置 \(loc=\{2, \{1, 6, 7\}, 5, 3, 4\}\)。
接下来这一步很关键,将这个 \(loc\) 数组重新映射回 \(a\) 数组中,得到一个新数组 \(c=\{(1)2, (3)1, 6, 7, (2)5, (4)3, (5)4\}\)。求出 \(c\) 数组的 LIS,即为答案。
但这种算法有缺陷:不可以处理有重复元素的序列。
int main()
{
int i;
while (scanf("%d", &n) != EOF)
{
for (i = 1; i <= n; i++)
scanf("%d", &a[i]);
for (i = 1; i <= n; i++)
scanf("%d", &b[i]);
calLoc();
for (i = 1; i <= n; i++)
b[i] = loc[a[i]];
printf("%d\n", LIS());
}
return 0;
}
0x16 最长公共上升子序列 LCIS 问题
设 \(f_{i, j}\) 表示以 \(a\) 的前 \(i\) 位、\(b\) 的前 \(j\) 位,且以 \(b_j\) 结尾的公共上升子序列的长度。
引理 1:若 \(a_i=b_j\),则 \(a_i\) 必定与 \(b_j\) 配对。
证明:若 \(b_j\) 不与 \(a_i\) 配对,则 \(b_j\) 必定与 \(a\) 的前 \(i-1\) 项配对(假设 \(f_{i-1, j}>0\)),因为 \(b_j\) 不与 \(a_{i}\) 配对,之后的转移必定没有配对优。
当 \(a_i\ne b_j\) 的时候,由于 \(b_j\) 无法与 \(a_i\) 配对,我们又想让这个公共上升子序列以 \(b_j\) 结尾,那么只能在 \(a\) 序列的前 \(i-1\) 项中找一个数与 \(b_j\) 配对了,所以 \(f_{i, j}=f_{i-1, j}\)。
当 \(a_i=b_j\) 的时候,我们只需要在前面找到一个能将 \(b_j\) 接到后面的最长的公共子序列即可,所以 \(f_{i, j}=\forall 1\le k<j, b_j\ge b_k, \max(f_{i-1, k}+1)\)。
0x17 习题
Game Rooms - UVA 12991 - Virtual Judge (vjudge.net)。
有一个有 \(n\) 层楼的大楼,每一层楼可以建游泳馆 P 或乒乓球房 T,第 \(i\) 层楼有 \(p_i\) 个人打乒乓球,\(t_i\) 个人打羽毛球,请帮忙建造 P 与 T,求每个人到各自游戏室的最小距离之和。
#2593. 「NOIP2010」乌龟棋 - 题目 - LibreOJ (loj.ac)。
有 \(n\) 个格子,\(1\) 为起点,\(n\) 为终点,每个格子上写着一个数字,你的分数就是所有经过的格子的数字之和。你有 \(m\) 张移动卡牌,每张卡牌上标有 \(1, 2, 3, 4\) 中的其中一个数字,使用一张卡牌,就能往前走这张卡牌上写着的数字 格。请求出最多可以获得多少分。
0x20 背包 DP
0x21 0/1 背包
有 \(n\) 个物品和一个容量为 \(v\) 的背包,每个物品有重量 \(w_i\) 和价值 \(v_i\) 两种属性,要求选若干物品放入背包使背包中物品的总价值最大且背包中物品的总重量不超过背包的容量。
0/1 背包,也称单物品背包,是最基础的一类背包 DP 问题。
在上述问题中,每种物品能且仅能被取一次,恰好对应了二进制中的 0/1 两种情况,所以也称 0/1 背包问题。
考虑设 「考虑前 \(i\) 个物品,在背包容量为 \(j\) 的情况下,最大收益」 为 \(f_{i, j}\)。此时让我们考虑下如何转移。每个 \(f_{i, j}\) 只可能有两种情况:选第 \(i\) 种物品、不选第 \(i\) 种物品。
先考虑第一种情况,选第 \(i\) 种物品,那么所获得的收益就是 \(f_{i-1, j-w_i}+v_i\)。即为 「考虑前 \(i-1\) 个物品,背包容量为 \(j-w_i\) 的最大收益」 加上当前物品所能带来的收益 \(v_i\)。
考虑第二种情况,不选第 \(i\) 种物品,显然能获得的最大收益就等于「只考虑前 \(i-1\) 种物品」所获得的最大收益。
综合两种情况,取最大值即为当前的最大收益,所以我们有:
\]
根据 \(f\) 的定义,最后的答案就是 「考虑前 \(n\) 个物品,背包容量为 \(v\) 的最大收益」,也就是 \(f_{n, v}\)。
下面是一个 \(N=4, V=10, w=\{1, 2, 3, 4\}, v=\{1, 4, 6, 8\}\) 的情况:
int n, v, f[1005][1005], w[1005], val[1005];
int main(void) {
cin >> n >> v;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= v; ++j) {
if (j < w[i])
f[i][j] = f[i - 1][j];
else
f[i][j] = max(f[i - 1][j], f[i - 1][j - w[i]] + val[i]);//这里的 val[i] 对应题目的 v[i]
}
}
}
可这种 DP 显然有一个问题,当 \(n, v \ge 10^4\) 时,程序会 MLE。因为我们使用了一个二维数据来存放 DP 的数据。
有没有什么优化方法?当然有。我们观察代码,发现每一个 f[i][x]
只由 f[i - 1][y]
转移过来,也就是说,\(i\) 逐渐增大的时候, f[i - 2]
及以前的数据我们全部都不需要。
于是我们考虑,只存下 f[i]
与 f[i - 1]
,而不存下以前的数据。这里就需要我们实现滚动数组。
由于 \(i\) 的变化规律一定是 奇数 \(\sim\) 偶数 \(\sim\) 奇数 \(\sim\cdots \sim n\)。那么我们可以使用 · f[i & 1]
来代替 f[i - 1]
, f[!(i & 1)]
来代替 f[i]
,这样保证了使用的交替性。
int n, v, f[2][1005], w[1005], val[1005];
int main(void) {
cin >> n >> v;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= v; ++j) {
if (j < w[i])
f[!(i & 1)][j] = f[(i & 1)][j];
else
f[!(i & 1)][j] = max(f[(i & 1)][j], f[(i & 1)][j - w[i]] + val[i]);//这里的 val[i] 对应题目的 v[i]
}
}
printf("%d", f[n & 1][v]);
return 0;
}
时间复杂度 \(\Theta (nv)\),空间复杂度 \(\Theta(v)\)。
但是,我们还是能优化。
为什么我们不能直接省去 \(f\) 的第一维,只留下第二维呢?我们发现,我们实际上更新 \(f_i\) 的时候,只需要用到 \(f_{i-1}\),而 \(f_{i-1}\) 也只对 \(f_i\) 会产生贡献。所以,我们不妨直接将 \(f_i\) 的数据覆盖到 \(f_{i-1}\) 之前的位置。
于是就有 \(f_j = \max(f_{j-w_i}+v_i, f_j)\)。其中,我们仍需要枚举 \(i\),只不过二维数组中不需要开原先的第一维。
我们很自然的就会写出下面的代码:
for (int i = 1; i <= n; i++)
for (int l = w[i]; j <= v; j++)
f[j] = max(f[j - w[i]] + val[i], f[j]);
哪里错了呢?枚举顺序。
仔细看代码,我们的 \(j\) 是 \(w_i\to v\) 这样子枚举的,而当我们想要去查 \(f_{j - w_i}\) 的时候,查到的其实相当于 \(f_{i, j-w_i}\),而不是我们想要的 \(f_{i-1, j-w_i}\)。
所以我们可以改变枚举顺序,将 \(j\) 的枚举顺序改为 \(v\to w_i\)。这样,我们查询 \(f_{j-w_i}\) 的时候,由于 \(j-w_i < j\),我们查到的必定是更新之前的 \(f_{j-w_i}\)。
所以转移方程为
\]
请务必牢记这个转移方程,在以后的 dp 学习中,我们会经常使用到这种 选、不选 双边决策的问题。
for (int i = 1; i <= n; ++i) {
for (int j = v; j >= w[i]; --j) {
f[j] = max(f[j], f[j - w[i]] + val[i]);
}
}
0x22 完全背包
有 \(n\) 个物品和一个容量为 \(v\) 的背包,每个物品有重量 \(w_i\) 和价值 \(v_i\) 两种属性,要求选若干物品放入背包使背包中物品的总价值最大且背包中物品的总重量不超过背包的容量,每种物品都允许重复选择。
如果你认真思考了为什么 0/1 背包不能顺序(\(j = w_i \sim v\))的话,你就会很容易的想到——完全背包其实就是顺序枚举的 0/1 背包。
对于我们每次更新的 \(f_j\),我们会去查找 \(f_{j - w_i}\),而因为我们是顺序枚举,所以相当于是 \(f_{i, j - w_i}\)。回想一下 \(f\) 数组的定义,\(f_{i, j}\) 表示 「考虑前 \(i\) 个物品,在背包容量为 \(j\) 的情况下,最大收益」。而我们这里查询的是 \(f_{i}\),不就是相当于重复考虑了第 \(i\) 个物品。
所以代码很显然,就是上面 0/1 背包的“错误”代码。
int main(void) {
cin >> n >> v;
for (int i = 1; i <= n; ++i) {
for (int j = w[i]; j <= v; ++j) {
f[j] = max(f[j], f[j - w[i]] + val[i]);
}
}
printf("%d", f[v]);
return 0;
}
0x23 多重背包
有 \(n\) 种物品,一个容量为 \(v\) 的背包,第 \(i\) 种物品有 \(c_i\) 个,重量 \(w_i\),价值 \(v_i\)。要求选出若干个物品放入背包使得在重量之和不超过背包容量的情况下价值最大。
对于这个问题,有很多种解法,如 \(\Theta(n^3)\),\(\Theta(n^2 \log n)\),\(\Theta(n^2)\) 等等,这里只讲前两种做法。
首先考虑最普通的做法,因为第 \(i\) 种物品有 \(c_i\) 个,我们不妨在 0/1 背包的基础上再对每一种物品枚举选的个数 \(k\)。设 \(f_j\) 表示考虑前 \(i\) 种物品,背包容量为 \(j\) 时能获得的最大收益,若第 \(i\) 种选 \(k\) 个,那么所获得的最大收益就是 \(f_j=\max(f_j, f_{j-w_i\times k} + v_i\times k)\)。
考虑优化,第 \(i\) 种物品有 \(c_i\) 个,即相当于有 \(c_i\) 个第 \(i\) 种物品,我们可以直接将第 \(i\) 种物品重复 \(c_i\) 遍,然后跑 0/1 背包,可是这样的时间复杂度和上述方法一样,反而空间复杂度还增加了。
这里我们需要引入一个定理:
对于任意一个正整数 \(x\),必定可以分解成 \(1,2,4,\cdots,2^{t-1},x-2^{t}+1\,(x-2^{t}+1>0)\) 的形式,且 \(1\sim x\) 中的每一个数均可以被表示成 \(1,2,4,\cdots,2^{t-1},x-2^{t}+1\,(x-2^{t}+1>0)\) 中的数的形式。
那么我们就可以通过这种性质来优化上述代码,对于一个有 \(c_i\) 个的物品 \(i\),考虑分解成 \(\log c_i\) 个物品的形式。如将一个 \(c_i=13\) 的物品,拆分成 \(4\) 个物品,其 \(v,w\) 如下:
编号 | \(v\) | \(w\) |
---|---|---|
\(1\) | \(v_i\) | \(w_i\) |
\(2\) | \(2\times v_i\) | \(2\times w_i\) |
\(3\) | \(4\times v_i\) | \(4\times w_i\) |
\(4\) | \(6\times v_i\) | \(6\times w_i\) |
这样子,无论我们实际选出多少件物品 \(i\),他总能被分成上述几件物品的组合,如选出 \(11\) 件物品 \(i\),则相当于选择了上述第 \(1, 3,4\) 物品(\(1+4+6=11\))。
index = 0;
for (int i = 1; i <= m; i++)
{
int c = 1, p, h, k;
cin >> p >> h >> k;
while (k - c > 0)
{
k -= c;
list[++index].w = c * p;
list[index].v = c * h;
c *= 2;
}
list[++index].w = p * k;
list[index].v = h * k;
}
0x24 混合背包
有 \(n\) 种物品,一个容量为 \(v\) 的背包,第 \(i\) 种物品有 \(c_i\) 个,重量 \(w_i\),价值 \(v_i\)。要求选出若干个物品放入背包使得在重量之和不超过背包容量的情况下价值最大。
特别的,当 \(c_i=-1\) 的情况下,可以无限次选择该物品。
对于这个问题,我们很简单的可以想到,若 \(c_i > 0\),则可以按照多重背包的方式来解(\(\Theta(n^3)\) 的解法,0/1 背包可以看做是多重背包的一个 \(c_i = 1\) 特殊情况)。当 \(c_i=-1\),直接按照完全背包的方式来解即可。
0x25 二维代价背包
题意简述:有 \(n\) 个愿望,第 \(i\) 个愿望要耗费 \(m_i\) 的金钱与 \(t_i\) 的时间,总共有 \(M\) 元,\(T\) 时间,问最多可以完成多少个愿望。\(1\le n\le 100,1\le m,t\le 200\)。
乍一看题目似乎和 0/1 背包非常相像,都是有 \(n\) 个物品,每个物品都是只能选或不选,而且选会有费用与价值,只不过这里的价值永远为 \(1\)。唯一不同的点在于,这里的费用有两重——金钱与时间。
我们发现不再能通过单一的一个数字来表示出问题的状态了——我们需要考虑两种费用。我们的问题在于,如何将时间、金钱同时表示出来,或者说一个状态如何同时兼顾时间、金钱。
这个问题的答案非常简单,如果一个维度装不下,那么就多加一个维度即可。设 \(f_{j,k}\) 表示当前考虑前 \(i\) 个愿望,在花费 \(j\) 元钱 \(k\) 时间的情况下,最多能满足多少个愿望,显然有以下递推式:
\]
int main()
{
cin >> n >> M >> T;
for(int i = 1; i <= n; ++i) {
for(int j = M; j >= m[i]; ++j) {
for(int k = T; k >= t[i]; --k) {
f[j][k] = max(f[j][k], f[j - m[i]][k - t[i]] + 1);
}
}
}
cout << f[M][T];
return 0;
}
0x26 分组背包
物品被分为了 \(k\) 组,每组最多只能选 \(1\) 个,问最大利用价值。
若物品被分为了 \(k\) 组,只需要考虑每组选哪个物品即可。从 \(1\sim k\) 遍历一下每组,然后再遍历每组中的物品看选择哪一个即可。
形式化的,若设 \(f_{i,j}\) 表示考虑前 \(i\) 组,背包容量为 \(j\) 的情况下的最大收益,那么有:
\]
0x27 依赖背包
如果选第 \(i\) 件物品,就必须选第 \(j\) 件物品,保证不会循环引用,一部分题目甚至会出现多叉树的引用形式。为了方便,就称不依赖于别的物品的物品称为「主件」,依赖于某主件的物品称为「附件」。
这里的依赖关系形成了森林的形状,如样例就是这样一个森林:
其中 \(a\to b\) 表示 \(a\) 是 \(b\) 的附件。
那么这种情况应该怎么 DP 呢?考虑一下,对于任意一个主件,我们一定会有以下几种处理操作:
- 不选主件
- 选主件,不选附件
- 选择主件 + 附件 1
- 选择主件 + 附件 2
- 选择主件 + 附件 1 + 附件 2
而这几种选择又是不能同时成立的,那么直接用上文提到的分组 DP 即可。
#include <iostream>
#define maxn 32005
using namespace std;
int n, m;
int v, p, q;
int main_item_w[maxn];
int main_item_c[maxn];
int annex_item_w[maxn][3];
int annex_item_c[maxn][3];
int f[maxn];
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
cin >> v >> p >> q;
if (!q)
{
main_item_w[i] = v;
main_item_c[i] = v * p;
}
else
{
annex_item_w[q][0]++;
annex_item_w[q][annex_item_w[q][0]] = v;
annex_item_c[q][annex_item_w[q][0]] = v * p;
}
}
for (int i = 1; i <= m; i++)
for (int j = n; main_item_w[i] != 0 && j >= main_item_w[i]; j--)
{
f[j] = max(f[j], f[j - main_item_w[i]] + main_item_c[i]);
if (j >= main_item_w[i] + annex_item_w[i][1])
f[j] = max(f[j], f[j - main_item_w[i] - annex_item_w[i][1]] + main_item_c[i] + annex_item_c[i][1]);
if (j >= main_item_w[i] + annex_item_w[i][2])
f[j] = max(f[j], f[j - main_item_w[i] - annex_item_w[i][2]] + main_item_c[i] + annex_item_c[i][2]);
if (j >= main_item_w[i] + annex_item_w[i][1] + annex_item_w[i][2])
f[j] = max(f[j], f[j - main_item_w[i] - annex_item_w[i][1] - annex_item_w[i][2]] + main_item_c[i] + annex_item_c[i][1] + annex_item_c[i][2]);
}
cout << f[n] << endl;
return 0;
}
0x28 习题
-
提示:混合背包,分类讨论;
-
提示:背包;转换;
0xFF 参考资料 & 鸣谢
OI Wiki
CSDN
- 动态规划 —— 线性 DP_Alex_McAvoy的博客-CSDN博客_线性dp;
- 动态规划 —— 线性 DP —— 序列问题_Alex_McAvoy的博客-CSDN博客;
- 程序员编程艺术第十一章:最长公共子序列(LCS)问题_v_JULY_v的博客-CSDN博客_lcs 最长公共子序列;
- 最长上升子序列 (LIS) 详解+例题模板 (全)_lxt_Lucia的博客-CSDN博客_最长上升子序列;
- nLogn LCS 算法总结_Non_Cease的博客-CSDN博客;
- 【算法】多重背包问题_AA8j的博客-CSDN博客_多重背包
CN Blog
- 最长公共子序列(LCS) - ranjiewen - 博客园 (cnblogs.com);
- LIS与LCS的nlogn解法 - yzm10 - 博客园 (cnblogs.com);
- LCIS 最长公共上升子序列问题DP算法及优化 - ojnQ - 博客园 (cnblogs.com);
- 最长公共上升子序列 (LCIS) - _Ackerman - 博客园 (cnblogs.com);
洛谷博客
- 题解 P1020 【导弹拦截】 - w1049344862 的博客 - 洛谷博客 (luogu.com.cn);
- 题解 P1855 【榨取kkksc03】 - 早右昕 的博客 - 洛谷博客 (luogu.com.cn);
- 题解 P1064 【金明的预算方案】 - ShawnZhou 的博客 - 洛谷博客 (luogu.com.cn);
其他
- Longest Common Subsequence Simulation (LCS) @jon.andika (sourceforge.net);
- tianyicui/pack: 背包问题九讲 (github.com);
动态规划 Dynamic Programming 学习笔记的更多相关文章
- 动态规划(Dynamic Programming)算法与LC实例的理解
动态规划(Dynamic Programming)算法与LC实例的理解 希望通过写下来自己学习历程的方式帮助自己加深对知识的理解,也帮助其他人更好地学习,少走弯路.也欢迎大家来给我的Github的Le ...
- 动态规划Dynamic Programming
动态规划Dynamic Programming code教你做人:DP其实不算是一种算法,而是一种思想/思路,分阶段决策的思路 理解动态规划: 递归与动态规划的联系与区别 -> 记忆化搜索 -& ...
- PJ可能会用到的动态规划选讲-学习笔记
PJ可能会用到的动态规划选讲-学习笔记 by Pleiades_Antares 难度和速度全部都是按照普及组来定的咯 数位状压啥就先不讲了 这里主要提到的都是比较简单的DP 一道思维数学巧题(补昨天) ...
- 6专题总结-动态规划dynamic programming
专题6--动态规划 1.动态规划基础知识 什么情况下可能是动态规划?满足下面三个条件之一:1. Maximum/Minimum -- 最大最小,最长,最短:写程序一般有max/min.2. Yes/N ...
- 动态规划 Dynamic Programming
March 26, 2013 作者:Hawstein 出处:http://hawstein.com/posts/dp-novice-to-advanced.html 声明:本文采用以下协议进行授权: ...
- [算法]动态规划(Dynamic programming)
转载请注明原创:http://www.cnblogs.com/StartoverX/p/4603173.html Dynamic Programming的Programming指的不是程序而是一种表格 ...
- 最优化问题 Optimization Problems & 动态规划 Dynamic Programming
2018-01-12 22:50:06 一.优化问题 优化问题用数学的角度来分析就是去求一个函数或者说方程的极大值或者极小值,通常这种优化问题是有约束条件的,所以也被称为约束优化问题. 约束优化问题( ...
- 动态规划系列(零)—— 动态规划(Dynamic Programming)总结
动态规划三要素:重叠⼦问题.最优⼦结构.状态转移⽅程. 动态规划的三个需要明确的点就是「状态」「选择」和「base case」,对应着回溯算法中走过的「路径」,当前的「选择列表」和「结束条件」. 某种 ...
- Python算法之动态规划(Dynamic Programming)解析:二维矩阵中的醉汉(魔改版leetcode出界的路径数)
原文转载自「刘悦的技术博客」https://v3u.cn/a_id_168 现在很多互联网企业学聪明了,知道应聘者有目的性的刷Leetcode原题,用来应付算法题面试,所以开始对这些题进行" ...
随机推荐
- C++设计模式 - 总结
一个目标:管理变化,提高复用 掌握设计模式一个核心目标:管理变化,提高复用.在使用设计模式中发现并没有实现复用,这就和设计初衷相违背了,说明代买写的不好. 两种手段:分解VS.抽象 在代码设计中,该开 ...
- Java基础 - 泛型详解
2022-03-24 09:55:06 @GhostFace 泛型 什么是泛型? 来自博客 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了&quo ...
- 《手把手教你》系列基础篇(八十三)-java+ selenium自动化测试-框架设计基础-TestNG测试报告-下篇(详解教程)
1.简介 其实前边好像简单的提到过测试报告,宏哥觉得这部分比较重要,就着重讲解和介绍一下.报告是任何测试执行中最重要的部分,因为它可以帮助用户了解测试执行的结果.失败点和失败原因.另一方面,日志记录对 ...
- Java编程:Lock
在上一篇文章中我们讲到了如何使用关键字synchronized来实现同步访问.本文我们继续来探讨这个问题,从Java 5之后,在java.util.concurrent.locks包下提供了另外一种方 ...
- redis 如何做内存优化?
1.缩减键值对象 缩减键(key)和值(value)的长度, key长度:如在设计键时,在完整描述业务情况下,键值越短越好. value长度:值对象缩减比较复杂,常见需求是把业务对象序列化成二进制数组 ...
- java中的异常体系?throw和throws的区别?
一.java中的异常体系 Thorwable类(表示可抛出)是所有异常和错误的超类,两个直接子类为Error和Exception,分别表示错误和异常.其中异常类Exception又分为运行时异常(Ru ...
- IList和DataSet性能差别 转自 http://blog.csdn.net/ilovemsdn/article/details/2954335
IList和DataSet性能差别 分类: NHibernate/Spring/NetTiers/Castle/Ibatis C# ...
- Python - numpy.clip()函数
np.clip( a, a_min, a_max, out=None): 部分参数解释: 该函数的作用是将数组a中的所有数限定到范围a_min和a_max中.a:输入矩阵:a_min:被限定的最小值, ...
- Java中final的使用
原文链接https://www.cnblogs.com/dolphin0520/p/10651845.html 作者Matrix海 子 本文为笔记 0. 概述 final和static一样都是修饰词, ...
- Clickhouse 用户自定义外部函数
写在前面 Clickhouse 从 21.11 版本开始,除了提供类似SqlServer.MySQL CREATE FUNCTION 的自定义函数之外,还有一个用户自定义函数(UDF),与其说是&qu ...