目录
学习目标
- 掌握背包 DP
- 掌握最大连续子段和、最长上升子序列、最长公共子序列等问题
- 掌握区间 DP、棋盘 DP、金字塔 DP 等其他 DP
学习时间
- 周三晚上 18:00~21:00
DP 类型题目
数字三角形 - 金字塔 DP
题目内容
观察下面的数字金字塔。
写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
在上面的样例中,从 7 → 3 → 8 → 7 → 5 7\rightarrow3\rightarrow8\rightarrow7\rightarrow5 7→3→8→7→5 的路径产生的答案最大。
题解
思路:金字塔 DP,状态转移方程为:
d
p
[
i
]
[
j
]
=
max
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
1
]
)
+
a
[
i
]
[
j
]
dp[i][j]=\max(dp[i-1][j],dp[i-1][j-1])+a[i][j]
dp[i][j]=max(dp[i−1][j],dp[i−1][j−1])+a[i][j]
代码:
#include <bits/stdc++.h>
using namespace std;
long long n, a[1005][1005], dp[1005][1005];
int main() {
cin >> n;
for (int i = 1; i <= n; i++) for (int j = 1; j <= i; j++) cin >> a[i][j];
dp[1][1] = a[1][1];
for (int i = 2; i <= n; i++) for (int j = 1; j <= i; j++) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1]) + a[i][j];
long long ans = 0;
for (int i = 1; i <= n; i++) ans = max(ans, dp[n][i]);
cout << ans << endl;
return 0;
}
骨头收藏家 - 01背包
题目内容
许多年前,在泰迪的家乡有一个人,他被称为 “骨头收藏家” 。这个人喜欢收集各种各样的骨头,比如狗的,牛的,他也去了坟墓…… .
收集骨头的人有一个体积为 V V V 的大袋子,在他收集的过程中有很多骨头,很明显,不同的骨头有不同的价值,不同的体积,现在给出每根骨头在他的过程中的价值,你能计算出收集骨头的人能得到的最大总价值吗?
题解
思路:01背包,状态转移方程为:
d
p
[
j
]
=
d
p
[
j
−
w
[
i
]
]
+
v
[
i
]
dp[j]=dp[j-w[i]]+v[i]
dp[j]=dp[j−w[i]]+v[i](一维 DP 数组更省空间)
代码:
#include <bits/stdc++.h>
using namespace std;
int n, V, dp[1005], w[1005], v[1005];
int main() {
int t;
cin >> t;
while (t--) {
cin >> n >> V;
for (int i = 1; i <= n; i++) cin >> v[i];
for (int i = 1; i <= n; i++) cin >> w[i];
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) {
for (int j = V; j >= w[i]; j--) dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
cout << dp[V] << endl;
}
return 0;
}
疯狂的采药 - 多重背包
题目内容
LiYuxiang 是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。
医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同种类的草药,采每一种都需要一些时间,每一种也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是 LiYuxiang,你能完成这个任务吗?
此题和原题的不同点:
-
每种草药可以无限制地疯狂采摘。
-
药的种类眼花缭乱,采药时间好长好长啊!师傅等得菊花都谢了!
题解
思路:多重背包,和01背包做法 一模一样 相似,状态转移方程为:
d
p
[
j
]
=
d
p
[
j
−
a
[
i
]
]
+
b
[
i
]
dp[j]=dp[j-a[i]]+b[i]
dp[j]=dp[j−a[i]]+b[i]
代码:
#include <bits/stdc++.h>
using namespace std;
long long T, m, a[10005], b[10005], dp[10000005];
int main() {
cin >> T >> m;
for (long long i = 1; i <= m; i++) cin >> a[i] >> b[i];
for (long long i = 1; i <= m; i++) {
for (long long j = a[i]; j <= T; j++) {
dp[j] = max(dp[j], dp[j - a[i]] + b[i]);
}
}
cout << dp[T] << endl;
return 0;
}
最大连续子段和
题目内容
给定有 n n n 个整数(可能为负整数)组成的序列 a 1 , a 2 , … , a n a_1,a_2,\dots,a_n a1,a2,…,an,求该序列连续的子段和的最大值。
如果该序列的所有元素都是负整数时定义其最大子段和为 0 0 0。例如,当 ( a 1 , a 2 , a 3 , a 4 , a 5 ) = ( − 5 , 11 , − 4 , 13 , − 4 , − 2 ) (a_1,a_2,a_3,a_4,a_5)=(-5,11,-4,13,-4,-2) (a1,a2,a3,a4,a5)=(−5,11,−4,13,−4,−2) 时,最大子段和为 11 + ( − 4 ) + 13 = 20 11+(-4)+13=20 11+(−4)+13=20。
题解
思路:最大连续子段和,状态转移方程为:
d
p
[
i
]
=
{
d
p
[
i
−
1
]
≥
0
d
p
[
i
−
1
]
+
a
[
i
]
d
p
[
i
−
1
]
<
0
a
[
i
]
dp[i] = \begin{cases} dp[i-1]\ge0 & dp[i-1]+a[i] \\ dp[i-1]<0 & a[i] \end{cases}
dp[i]={dp[i−1]≥0dp[i−1]<0dp[i−1]+a[i]a[i]
代码:
#include <bits/stdc++.h>
using namespace std;
const int inf = 1000000000;
int a[100005], dp[100005];
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i], dp[i] = a[i];
for (int i = 1; i <= n; i++) {
if (dp[i - 1] >= 0) dp[i] += dp[i - 1];
}
int ans = -inf;
for (int i = 1; i <= n; i++) ans = max(ans, dp[i]);
cout << ans << endl;
return 0;
}
最长上升子序列
题目内容
给定 N N N 个数,求这 N N N 个数的最长上升子序列的长度。
什么是最长上升子序列? 就是给你一个序列,请你在其中求出一段不断严格上升的部分,它不一定要连续。
比如: 2 , 3 , 4 , 7 2,3,4,7 2,3,4,7 和 2 , 3 , 4 , 6 2,3,4,6 2,3,4,6 就是序列 2 , 5 , 3 , 4 , 1 , 7 , 6 2,5,3,4,1,7,6 2,5,3,4,1,7,6 的两种选取方案。最长的长度是4。
题解
思路:最长上升子序列,状态转移方程为:
(
1
≤
j
<
i
&
a
[
j
]
<
a
[
i
]
)
d
p
[
i
]
=
max
(
d
p
[
i
]
,
d
p
[
j
]
+
1
)
(1\le j<i\ \&\ a[j]<a[i])\quad dp[i]=\max(dp[i],dp[j]+1)
(1≤j<i & a[j]<a[i])dp[i]=max(dp[i],dp[j]+1)
注意:
d
p
[
i
]
dp[i]
dp[i] 需要初始化为
1
1
1。
代码:
#include <bits/stdc++.h>
using namespace std;
int a[5005], dp[5005];
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) {
dp[i] = 1;
for (int j = 1; j < i; j++) {
if (a[j] < a[i]) dp[i] = max(dp[i], dp[j] + 1);
}
}
int ans = 0;
for (int i = 1; i <= n; i++) ans = max(ans, dp[i]);
cout << ans << endl;
return 0;
}
最长公共子序列
题目内容
给定两个长度为 n n n 的数列,求他们的最长公共子序列的长度。
题解
思路:最长上升子序列,状态转移方程为:
d
p
[
i
]
[
j
]
=
{
a
[
i
]
=
b
[
j
]
d
p
[
i
−
1
]
[
j
−
1
]
+
1
a
[
i
]
≠
b
[
j
]
max
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
]
[
j
−
1
]
)
dp[i][j] = \begin{cases} a[i]=b[j] & dp[i-1][j-1]+1 \\ a[i]\ne b[j] & \max(dp[i-1][j],dp[i][j-1]) \end{cases}
dp[i][j]={a[i]=b[j]a[i]=b[j]dp[i−1][j−1]+1max(dp[i−1][j],dp[i][j−1])
#include <bits/stdc++.h>
using namespace std;
int a[3005], b[3005], dp[3005][3005];
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) cin >> b[i];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (a[i] == b[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
cout << dp[n][n] << endl;
return 0;
}
最长锯齿子序列
题目内容
给定 n n n 个整数组成的序列 a 1 , a 2 , … , a n a_1,a_2,\dots,a_n a1,a2,…,an,请从中挑出尽量长的子序列,形成一个锯齿序列。所谓锯齿序列,就是它的差分序列(由相邻数字的差组成的序列)是正负交替的。为了避免差为 0 0 0 时不方便区分正负,保证给定的每个数字都不相同。
例如给定的序列是 1 , 3 , 5 , 2 , 4 , 6 1,3,5,2,4,6 1,3,5,2,4,6,那么它的子序列 1 , 5 , 2 , 6 1,5,2,6 1,5,2,6 是一个锯齿序列,因为它的差分序列是 4 , − 3 , 4 4,-3,4 4,−3,4;而 1 , 3 , 5 1,3,5 1,3,5 不是,因为这三个数字是递增的。
题解
思路:最长锯齿子序列,状态转移方程为:
(
1
≤
j
<
i
)
{
a
[
i
]
>
b
[
j
]
d
p
[
1
]
[
i
]
=
max
(
d
p
[
1
]
[
i
]
,
d
p
[
0
]
[
j
]
+
1
)
a
[
i
]
<
b
[
j
]
d
p
[
0
]
[
i
]
=
max
(
d
p
[
0
]
[
i
]
,
d
p
[
1
]
[
j
]
+
1
)
(1\le j<i)\quad \begin{cases} a[i]>b[j] & dp[1][i]= \max(dp[1][i],dp[0][j]+1)\\ a[i]<b[j] & dp[0][i]= \max(dp[0][i],dp[1][j]+1) \end{cases}
(1≤j<i){a[i]>b[j]a[i]<b[j]dp[1][i]=max(dp[1][i],dp[0][j]+1)dp[0][i]=max(dp[0][i],dp[1][j]+1)
代码:
#include <bits/stdc++.h>
using namespace std;
int n, a[10005], dp[2][10005];
int main() {
cin >> n;
if (n <= 2) {
cout << n << endl;
return 0;
}
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) {
dp[0][i] = dp[1][i] = 1;
for (int j = 1; j < i; j++) {
if (a[j] < a[i]) dp[1][i] = max(dp[1][i], dp[0][j] + 1);
else dp[0][i] = max(dp[0][i], dp[1][j] + 1);
}
}
cout << max(dp[0][n], dp[1][n]) << endl;
return 0;
}
合唱队 - 区间 DP
题目内容
为了在即将到来的晚会上有更好的演出效果,作为 AAA 合唱队负责人的小 A 需要将合唱队的人根据他们的身高排出一个队形。假定合唱队一共 n n n 个人,第i个人的身高为 h i h_i hi 米 ( 1000 ≤ h i ≤ 2000 ) (1000\le h_i\le2000) (1000≤hi≤2000),并已知任何两个人的身高都不同。假定最终排出的队形是 A A A 个人站成一排,为了简化问题,小 A 想出了如下排队的方式:他让所有的人先按任意顺序站成一个初始队形,然后从左到右按以下原则依次将每个人插入最终棑排出的队形中:
第一个人直接插入空的当前队形中。
对从第二个人开始的每个人,如果他比前面那个人高( h h h 较大),那么将他插入当前队形的最右边。如果他比前面那个人矮( h h h 较小),那么将他插入当前队形的最左边。
当n个人全部插入当前队形后便获得最终排出的队形。
例如,有 6 6 6 个人站成一个初始队形,身高依次为 1850 , 1900 , 1700 , 1650 , 1800 , 1750 1850,1900,1700,1650,1800,1750 1850,1900,1700,1650,1800,1750,那么小 A 会按以下步骤获得最终排出的队形:
1850 1850 1850。
1850 , 1900 1850,1900 1850,1900,因为 1900 > 1850 1900>1850 1900>1850。
1700 , 1850 , 1900 1700,1850,1900 1700,1850,1900,因为 1700 < 1900 1700<1900 1700<1900。
1650 , 1700 , 1850 , 1900 1650,1700,1850,1900 1650,1700,1850,1900,因为 1650 < 1700 1650<1700 1650<1700。
1650 , 1700 , 1850 , 1900 , 1800 1650,1700,1850,1900,1800 1650,1700,1850,1900,1800,因为 1800 > 1650 1800>1650 1800>1650。
1750 , 1650 , 1700 , 1850 , 1900 , 1800 1750,1650,1700,1850,1900,1800 1750,1650,1700,1850,1900,1800,因为 1750 < 1800 1750<1800 1750<1800。
因此,最终排出的队形是 1750 , 1650 , 1700 , 1850 , 1900 , 1800 1750,1650,1700,1850,1900,1800 1750,1650,1700,1850,1900,1800。
小 A 心中有一个理想队形,他想知道多少种初始队形可以获得理想的队形。
请求出答案对 19650827 19650827 19650827 取模的值。
题解
思路:区间 DP,这类题的模板:
for (int len = 1; len <= n; len++) {
for (int l = 1, r = len; r <= n; l++, r++) {
dp[l][r] = 0x3f3f3f3f;
for (int k = l; k <= r; k++) dp[l][r] = max(dp[l][r], dp[l][k] + dp[r][k];
}
}
代码:
#include <bits/stdc++.h>
using namespace std;
long long n, a[1005], sum[1005], dp[1005][1005][2];
long long mod = 19650827;
int main() {
cin >> n;
for (long long i = 1; i <= n; i++) cin >> a[i], dp[i][i][0] = 1;
for (long long len = 1; len < n; len++) {
for (long long l = 1, r = l + len; r <= n; l++, r++) {
if (a[l] < a[l + 1]) dp[l][r][0] += dp[l + 1][r][0];
if (a[l] < a[r]) dp[l][r][0] += dp[l + 1][r][1];
if (a[r] > a[r - 1]) dp[l][r][1] += dp[l][r - 1][1];
if (a[r] > a[l]) dp[l][r][1] += dp[l][r - 1][0];
dp[l][r][1] %= mod;
dp[l][r][0] %= mod;
}
}
cout << (dp[1][n][0] + dp[1][n][1]) % mod << endl;
return 0;
}
过河卒 - 棋盘 DP
题目内容
棋盘上
A
A
A 点有一个过河卒,需要走到目标
B
B
B 点。卒行走的规则:可以向下、或者向右。同时在棋盘上
C
C
C 点有一个对方的马,该马所在的点和所有跳跃一步可达的点称为对方马的控制点。因此称之为 “马拦过河卒”。
棋盘用坐标表示,
A
A
A 点
(
0
,
0
)
(0,0)
(0,0) 、
B
B
B 点
(
n
,
m
)
(n,m)
(n,m) ,同样马的位置坐标是需要给出的。如果马能够控制
A
A
A 点,那么卒将寸步难行。
现在要求你计算出卒从 A A A 点能够到达 B B B 点的路径的条数,假设马的位置是固定不动的,并不是卒走一步马走一步。
题解
思路:棋盘 DP,状态转移方程为:
d
p
[
i
]
[
j
]
=
d
p
[
i
]
[
j
]
+
{
i
=
0
d
p
[
i
−
1
]
[
j
]
j
=
0
d
p
[
i
]
[
j
−
1
]
i
≠
0
&
j
≠
0
d
p
[
i
]
[
j
−
1
]
+
d
p
[
i
−
1
]
[
j
]
dp[i][j] = dp[i][j]+ \begin{cases} i=0 & dp[i-1][j]\\ j=0 & dp[i][j-1]\\ i\ne0\ \&\ j\ne0 & dp[i][j-1]+dp[i-1][j] \end{cases}
dp[i][j]=dp[i][j]+⎩
⎨
⎧i=0j=0i=0 & j=0dp[i−1][j]dp[i][j−1]dp[i][j−1]+dp[i−1][j]
注意: 特判马是否控制点
A
A
A 或点
B
B
B,若成立则直接输出
0
0
0。
代码:
#include <bits/stdc++.h>
using namespace std;
bool v[25][25];
long long f[25][25];
int dir[8][2] = {{2, 1}, {2, -1}, {1, 2}, {1, -2}, {-2, 1}, {-2, -1}, {-1, 2}, {-1, -2}};
int main() {
int n, m, x, y;
cin >> n >> m >> x >> y;
v[x][y] = true;
for (int i = 0; i < 8; i++) {
int sx = x + dir[i][0];
int sy = y + dir[i][1];
if (sx >= 0 && sx <= n && sy >= 0 && sy <= m) v[sx][sy] = true;
}
if (v[n][m] || v[0][0]) {
cout << 0 << endl;
return 0;
}
f[0][0] = 1;
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
if (j) f[i][j] += f[i][j - 1]);
if (i) f[i][j] += f[i - 1][j]);
if (v[i][j]) f[i][j] = 0;
}
}
cout << f[n][m] << endl;
return 0;
}
总结
DP 的题目实际并不难,考验的是找到正确的 DP 方式。