赞
踩
动态规划应用于子问题重叠的情况:
给定一段钢条,和不同长度的价格,问如何切割使得总价格最大。
为了求解规模为n的原问题,我们先求解形式完全一样,但规模更小的子问题。 即当完成首次切割后,我们将两段钢条看成两个独立的钢条切割问题实例。 我们通过组合相关子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大者,构成原问题的最优解。
最优子结构:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。
动态规划的两种实现方法:
算导用子问题图上按照逆拓扑序求解问题,引出记忆化搜索。
重构解(输出方案):转移的时候记录最优子结构的位置。
给出 n个矩阵的序列,希望计算他们的乘积,问最少需要多少次乘法运算?
(认为
p
×
q
p\times q
p×q 的矩阵与
q
×
r
q\times r
q×r 的矩阵相乘代价是
p
×
q
×
r
p \times q \times r
p×q×r 。)
完全括号化方案是指要给出谁先和谁乘。
两个要素:
具有最优子结构也可能是适合用贪心的方法求解。
注意要确保我们考察了最优解中用到的所有子问题。
最优子结构的不同体现在两个方面:
子问题空间要足够小,即问题的递归算法会反复地求解相同的子问题,而不是一直生成新的子问题。
存表记录最优分割的位置,就不用重新按照代价来重构。
子序列允许不连续。
每个 c [ i ] [ j ] c[i][j] c[i][j] 只依赖于 c [ i − 1 ] [ j ] c[i-1][j] c[i−1][j] 、 c [ i ] [ j − 1 ] c[i][j-1] c[i][j−1] 和 c [ i − 1 ] [ j − 1 ] c[i-1][j-1] c[i−1][j−1] 。
记录最优方案的时候可以不需要额外建表(优化空间),因为重新选择一遍(转移过程)也是 O ( 1 ) O(1) O(1) 的。
给二叉搜索树的每个节点定义一个权值,问如何安排使得权值和深度的乘积最小。
考虑当一棵子树成为了一个节点的子树时,答案(期望搜索代价)有何变化?
由于每个节点的深度都增加了 1,这棵子树的期望搜索代价的增加值应为所有概率之和。
tD/eD 动态规划: 状态空间是 O ( n t ) O(n^{t}) O(nt) 的,每一项依赖其他 O ( n e ) O(n^{e}) O(ne) 项。
我们的目标是求出给定序列的一个最长的连续子序列,满足这个序列中的后一个元素 不小于 前一个元素。
因为是连续的,所以只要与上一个元素进行比较即可。
int a[MAXN];
int dp() {
int now = 1, ans = 1;
for (int i = 2; i <= n; i++) {
if (a[i] >= a[i - 1])
now++;
else
now = 1;
ans = max(now, ans);
}
return ans;
}
与最长连续不下降子序列不同的是,不需要这个子序列是连续的了。
求最长子序列的方法有两种。
最简单的第一种
O
(
n
2
)
O(n^{2})
O(n2)的算法。每一次从头扫描找出最佳答案。
int a[MAXN], d[MAXN];
int dp() {
d[1] = 1;
int ans = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j < i; j++)
if (a[j] <= a[i]) {//判断j能否添加到该序列中
d[i] = max(d[i], d[j] + 1);//是否将j添加到该序列中
ans = max(ans, d[i]);
}
}
return ans;
}
稍复杂的第二种 n n n long n n n的算法, 参考了这篇文章
首先,定义 a 1 . . . . . . a n a_{1}......a_{n} a1......an为原始序列, d为当前的不下降子序列, len为子序列的长度,那么 d l e n d_{len} dlen 就是长度为 len 的不下降子序列末尾元素。
初始化: d 1 = a 1 、 l e n = 1 d_{1}=a_{1} 、len=1 d1=a1、len=1。
现在我们已知最长的不下降子序列长度为 1,那么我们让 i 从 2 到 n循环,依次求出前 i个元素的最长不下降子序列的长度,循环的时候我们只需要维护好 这个数d组还有 len就可以了。关键在于如何维护。
考虑进来一个元素 a i a_{i} ai :
1.元素大于等于
d
l
e
n
d_{len}
dlen,直接
d
+
+
l
e
n
=
a
i
d_{++len}=a_{i}
d++len=ai 即可,这个比较好理解。
元素小于
d
l
e
n
d_{len}
dlen ,找到 第一个 大于它的元素,插入进去,其他小于它的元素不要。
那么代码如下:
for (int i = 0; i < n; ++i) scanf("%d", a + i);
memset(dp, 0x1f, sizeof dp);
mx = dp[0];
for (int i = 0; i < n; ++i) {
*std::upper_bound(dp, dp + n, a[i]) = a[i];
}
ans = 0;
while (dp[ans] != mx) ++ans;
d p [ i ] = m a x ( d p [ j ] + 1 ) ( ( i , j ) ∈ E ) dp[i]=max(dp[j]+1)((i,j)\in E) dp[i]=max(dp[j]+1)((i,j)∈E)
d
p
[
i
]
[
i
+
l
e
n
]
dp[i][i+len]
dp[i][i+len]=
{
m
a
x
(
d
p
[
i
+
1
]
[
i
+
l
e
n
]
,
d
p
[
i
]
[
i
+
l
e
n
−
1
]
)
,
e
l
s
e
d
p
[
i
+
1
]
[
i
+
l
e
n
−
1
]
+
1
,
i
f
s
[
i
]
=
s
[
i
+
l
e
n
]
\left \{ ^{dp[i+1][i+len-1]+1 ,if s[i]=s[i+len]}_{max(dp[i+1][i+len],dp[i][i+len-1]),else} \right.
{max(dp[i+1][i+len],dp[i][i+len−1]),elsedp[i+1][i+len−1]+1,ifs[i]=s[i+len]
边界:
d
p
[
i
]
[
j
]
=
1
dp[i][j]=1
dp[i][j]=1。
注意: d p [ i ] [ j ] dp[i][j] dp[i][j]表示的是闭区间。
也可以转化为 LCS 问题,只需要把a 串反转当做b ,对 a和 b求 LCS 即可。
注意区分子串(要求连续)的问题。
O ( n 2 ) : d p [ i ] = m a x ( d p [ j ] + 1 , d p [ i ] ) , s ( j + 1... i ) O(n^{2}):dp[i]=max(dp[j]+1,dp[i]),s(j+1...i) O(n2):dp[i]=max(dp[j]+1,dp[i]),s(j+1...i)是回文
p[i]表示从i 向两侧延伸(当然要保证两侧对应位置相等)的最大长度。
为了处理方便,我们把原串每两个字符之间加一个(不包含在原串中的)#,开头加一个 $。
这样得到的回文串长度就保证是奇数了
考虑如果按顺序得到了
p
[
1...
i
−
1
]
p[1...i-1]
p[1...i−1] ,如何计算p[i] 的值?
如果之前有一个位置比如说是id ,有 p[id]+id>i那么 i 这个位置是被覆盖了的,根据id 处的对称性,我们找
p
[
i
d
×
2
−
i
]
p[id \times 2-i]
p[id×2−i] 延伸的部分被p[id] 延伸的部分所覆盖的那段,显然这段对称回去之后是可以从i 处延伸出去的长度。
如果找不到呢?就先让p[id]=1 吧。
之后再暴力延伸一下.
可以证明是
O
(
n
)
O(n)
O(n) 的。
至于如何找是否有这么一个 id 呢?递推的时候存一个max 就好了。
当选取的状态难以进行递推时(分解出的子问题和原问题形式不一样),考虑将问题状态分类细化,增加维度。
(1)How many ways HDU - 1978(记忆化搜索关于求多少种方式模板)
(2)FatMouse and Cheese HDU - 1078(记忆化搜索入门模板)
方法一
d p [ i ] = m a x ( d p [ i ] , d p [ j + 1 ] ) ( 最 长 上 升 子 序 列 ) dp[i]=max(dp[i],dp[j+1])(最长上升子序列) dp[i]=max(dp[i],dp[j+1])(最长上升子序列)
int dfs(int i) {
if (mem[i] != -1) return mem[i];
int ret = 1;
for (int j = 1; j < i; j++)
if (a[j] < a[i]) ret = max(ret, dfs(j) + 1);
return mem[i] = ret;
}
int main() {
memset(mem, -1, sizeof(mem));
// 读入部分略去
cout << dfs(n) << endl;
}
方法 II
山洞里有 M株不同的草药,采每一株都需要一些时间 t i t_{i} ti ,每一株也有它自身的价值 v i v_{i} vi 。给你一段时间 T,在这段时间里,你可以采到一些草药。让采到的草药的总价值最大。
int n, t; int tcost[103], mget[103]; int ans = 0; void dfs(int pos, int tleft, int tans) { if (tleft < 0) return; if (pos == n + 1) { ans = max(ans, tans); return; } dfs(pos + 1, tleft, tans); dfs(pos + 1, tleft - tcost[pos], tans + mget[pos]); } int main() { cin >> t >> n; for (int i = 1; i <= n; i++) cin >> tcost[i] >> mget[i]; dfs(1, t, 0); cout << ans << endl; return 0; }
举例:用常规 dp 写“合并石子”需要先枚举区间长度然后枚举起点,但记忆化搜索直接枚举断点(就是枚举当前区间由哪两个区间合并而成)然后递归下去就行
Jin Ge Jin Qu hao UVA - 12563 (劲歌金曲)01背包,求装入的东西最多(相同多时价值大)
意概要:有 n个物品和一个容量为 W 的背包,每个物品有重量 w i w_{i} wi 和价值 v i v_{i} vi 两种属性,要求选若干物品放入背包使背包中物品的总价值最大且背包中物品的总重量不超过背包的容量。
在上述例题中,由于每个物体只有2 种可能的状态(取与不取),正如二进制中的 0 和 1,这类问题便被称为「0-1 背包问题」。
例题中已知条件有第 i个物品的重量 w i w_{i} wi,价值 v i v_{i} vi,以及背包的总容量W 。
设 DP 状态 f i , j f_{i,j} fi,j为在只能放前 i 个物品的情况下,容量为j 的背包所能达到的最大总价值。
考虑转移。假设当前已经处理好了前 i-1个物品的所有状态,那么对于第 i个物品,当其不放入背包时,背包的剩余容量不变,背包中物品的总价值也不变,故这种情况的最大价值为
f
i
−
1
,
j
f_{i-1,j}
fi−1,j ;当其放入背包时,背包的剩余容量会减小
w
i
w_{i}
wi ,背包中物品的总价值会增大
v
i
v_{i}
vi ,故这种情况的最大价值为
f
i
−
1
,
j
−
w
i
+
v
i
f_{i-1,j-w_{i}}+v_{i}
fi−1,j−wi+vi。
由此可以得出状态转移方程:
f
i
,
j
=
m
a
x
(
f
i
−
1
,
j
,
f
i
−
1
,
j
−
w
i
+
v
i
)
f_{i,j}=max(f_{i-1,j},f_{i-1,j-w_{i}}+v_{i})
fi,j=max(fi−1,j,fi−1,j−wi+vi)
这里如果直接采用二维数组对状态进行记录,会出现 MLE。可以考虑改用滚动数组的形式来优化。
由于对
f
i
f_{i}
fi有影响的只有
f
i
−
1
f_{i-1}
fi−1,可以去掉第一维,直接用
f
i
f_{i}
fi 来表示处理到当前物品时背包容量为 的最大价值,得出以下方程:
f
j
=
m
a
x
(
f
j
,
f
j
−
w
i
+
v
i
)
f_{j}=max(f_{j},f_{j-w_{i}}+v_{i})
fj=max(fj,fj−wi+vi)
如果第二层循环正着遍历,对于当前处理的物品 i 和当前状态
f
i
,
j
f_{i,j}
fi,j ,在
j
≥
w
i
j\geq w_{i}
j≥wi时,
f
i
,
j
f_{i,j}
fi,j是会被
f
i
,
j
−
w
−
i
f_{i,j-w-{i}}
fi,j−w−i 所影响的。这就相当于物品i 可以多次被放入背包,与题意不符。(事实上,这正是完全背包问题的解法)
为了避免这种情况发生,我们可以改变枚举的顺序,从W 枚举到 w i w_{i} wi ,这样就不会出现上述的错误,因为 f i , j f_{i,j} fi,j 总是在 f i , j − w i f_{i,j-w_{i}} fi,j−wi 前被更新。
for (int i = 1; i <= n; i++)
for (int l = W; l >= w[i]; l--)
f[l] = max(f[l], f[l - w[i]] + v[i])
完全背包模型与 0-1 背包类似,与 0-1 背包的区别仅在于一个物品可以选取无限次,而非仅能选取一次。
Piggy-Bank POJ - 1384(完全背包+背包放满)
这道题是很明显的 0-1 背包问题,可是不同的是选一个物品会消耗两种价值(经费、时间)。这种问题其实很简单:方程基本不用变,只需再开一维数组,同时转移两个价值就行了!(完全、多重背包同理)
这时候就要注意,再开一维存放物品编号就不合适了,因为容易 MLE。
for (int k = 1; k <= n; k++) {
for (int i = m; i >= mi; i--) // 对经费进行一层枚举
for (int j = t; j >= ti; j--) // 对时间进行一层枚举
dp[i][j] = max(dp[i][j], dp[i - mi][j - ti] + 1);
}
所谓分组背包,就是将物品分组,每组的物品相互冲突,最多只能选一个物品放进去。
这种题怎么想呢?其实是从「在所有物品中选择一件」变成了「从当前组中选择一件」,于是就对每一组进行一次 0-1 背包就可以了。
再说一说如何进行存储。我们可以将
t
k
,
i
t_{k,i}
tk,i 表示第 k组的第i 件物品的编号是多少,再用
c
n
t
k
cnt_{k}
cntk 表示第k 组物品有多少个。
for (int k = 1; k <= ts; k++) // 循环每一组
for (int i = m; i >= 0; i--) // 循环背包容量
for (int j = 1; j <= cnt[k]; j++) // 循环该组的每一个物品
if (i >= w[t[k][j]])
dp[i] = max(dp[i],dp[i - w[t[k][j]]] + c[t[k][j]]); // 像0-1背包一样状态转移
输出方案其实就是记录下来背包中的某一个状态是怎么推出来的。我们可以用 g i , v g_{i,v} gi,v 表示第 i 件物品占用空间为 v的时候是否选择了此物品。然后在转移时记录是选用了哪一种策略(选或不选)。输出时的伪代码:
int v = V; // 记录当前的存储空间
for (
从最后一件循环至第一件) // 因为最后一件物品存储的是最终状态,所以从最后一件物品进行循环
{
if (g[i][v]) {
选了第 i 项物品;
v -= 第 i 项物品的价值;
} else
未选第 i 项物品;
}
对于给定的一个背包容量、物品费用、其他关系等的问题,求装到一定容量的方案总数。
这种问题就是把求最大值换成求和即可。
例如 0-1 背包问题的转移方程就变成了:
d
p
i
=
Σ
(
d
p
i
,
d
p
i
−
c
i
)
dp_{i}=\Sigma(dp_{i},dp_{i-c_{i}})
dpi=Σ(dpi,dpi−ci)
初始条件:
d
p
0
=
1
dp_{0}=1
dp0=1
因为当容量为 0 时也有一个方案:什么都不装!
for (int i = 0; i < N; i++) { for (int j = V; j >= v[i]; j--) { int tmp = max(dp[j], dp[j - v[i]] + w[i]); int c = 0; if (tmp == dp[j]) c += cnt[j]; // 如果从dp[j]转移 if (tmp == dp[j - v[i]] + w[i]) c += cnt[j - v[i]]; // 如果从dp[j-v[i]]转移 dp[j] = tmp; cnt[j] = c; } } int max = 0; // 寻找最优解 for (int i = 0; i <= V; i++) { max = max(max, dp[i]); } int res = 0; for (int i = 0; i <= V; i++) { if (dp[i] == max) { res += cnt[i]; // 求和最优解方案数 } }
(1)Round Numbers POJ - 3252(数位dp+判断前导零)
(2)数位dp总结 之 从入门到模板(stO)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。