线性动态规划
一、意义
动态规划(dynamic programming),将一个目标大问题“大事化小,小事化了”,分成很多的子问题,得出子问题的解后得到目标大问题的解。动态规划相当于地狱难度的递推。
二、例题
1. 最大子段和
1.1 审题
题目描述
给出数组 a [ ] a[] a[] 的值,如果我们取连续且非空的一段,那么这段的和最大是多少?
输入描述
第 1 1 1 行,是一个正整数 n n n,数组 a a a 的长度。
第 2 2 2 行,用空格隔开的 n n n 个整数,依次是 a [ 1 ] , a [ 2 ] , ⋯ , a [ n ] a[1],a[2],⋯,a[n] a[1],a[2],⋯,a[n] 的值。
输出描述
1 1 1 个整数,为所求的最大的和。
样例1
输入
6 1 -6 5 -4 2 4
输出
7
提示
【样例解释】
取 [ 5 , − 4 , 2 , 4 ] [5,−4,2,4] [5,−4,2,4] 可以得到最大的和 7 7 7。
【数据范围】
对于 60 % 60\% 60% 的数据, n ≤ 100 n≤100 n≤100。
对于 80 % 80\% 80% 的数据, n ≤ 5000 n≤5000 n≤5000。
对于 100 % 100\% 100% 的数据, n ≤ 100000 n≤100000 n≤100000, 1 ≤ i ≤ n 1≤i≤n 1≤i≤n, − 10000 ≤ a [ i ] ≤ 10000 −10000≤a[i]≤10000 −10000≤a[i]≤10000,且数组 a [ ] a[] a[] 中至少有 1 1 1 个正数
1.2 思路
一开始的时候我们考虑尺取,但是右指针没有终止条件了,因此是不可以的。
接下来考虑动态规划。我们一起列举所有最大的子段和( n = 4 n=4 n=4):
ans[1] | ans[2] | ans[3] | ans[4] | ans[x] |
---|---|---|---|---|
a[1] | a[2] | a[3] | a[4] | a[x] |
a[1]+a[2] | a[2]+a[3] | a[3]+a[4] | a[x-1]+a[x] | |
a[1]+a[2]+a[3] | a[2]+a[3]+a[4] | a[x-2]+a[x-1]+a[x] | ||
a[1]+a[2]+a[3]+a[4] | a[x-3]+a[x-2]+a[x-1]+a[x] | |||
a[1] | max(a[2], a[1]+a[2]) | max(a[3], ans[2]+a[3]) | max(a[4], ans[3]+a[4]) | max(a[x], ans[x-1]+a[x]) |
不难发现,在 ans[3] 列的 a[2] 和 a[1]+a[2] 已经被计算过最大值了,那么直接用算过的最大值加上 a[3] 就可以了。所以我们推出了递推公式(即状态转移方程):
a
n
s
[
i
]
=
{
a
n
s
[
i
]
=
a
[
1
]
i
=
=
1
a
n
s
[
i
]
=
m
a
x
(
a
[
i
]
,
a
n
s
[
i
−
1
]
+
a
[
i
]
)
i
>
=
1
ans[i] = \begin{cases} \text ans[i]=a[1] & i==1 \\ \text ans[i]=max(a[i], ans[i-1]+a[i]) & i>=1 \end{cases}
ans[i]={ans[i]=a[1]ans[i]=max(a[i],ans[i−1]+a[i])i==1i>=1
这不就能做了嘛(ps,一般在动态规划里面 ans[]
叫 dp[]
)。
1.3 参考答案
#include <iostream>
using namespace std;
int n;
int a[100005];
int dp[100005];
int maxn = -1e9;
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
dp[1] = a[1];
for (int i = 2; i <= n; i++)
{
dp[i] = max(a[i], dp[i-1]+a[i]);
maxn = max(maxn, dp[i]);
}
cout << maxn;
return 0;
}
2. 路径数量
2.1 审题
题目描述
一个机器人位于一个 m × n m \times n m×n 网格的左上角,机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角。问,总共有多少条不同的路径?(
我是不会告诉你 BFS 和 DFS 也阔以滴)
输入描述
输入共一行, 2 2 2 个整数分别表示 m m m 和 n n n。
输出描述
输出不同路径的数量。
样例1
输入
3 2
输出
3
样例2
输入
3 7
输出
28
提示
【样例解释1】
从左上角开始,总共有 3 3 3 条路径可以到达右下角。
- 向右->向下->向下
- 向下->向下->向右
- 向下->向右->向下
【数据说明】
1 ≤ m , n ≤ 100 1 \le m, n \le 100 1≤m,n≤100
题目数据保证答案 ≤ 2 × 1 0 9 \le2 \times 10^9 ≤2×109
2.2 思路
还是要把递推公式求出来:
d
p
[
i
]
[
j
]
=
{
d
p
[
i
]
[
j
]
=
1
i
=
=
1
∣
∣
j
=
=
1
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
+
d
p
[
i
]
[
j
−
1
]
i
>
=
1
&
&
j
>
=
1
dp[i][j] = \begin{cases} \text dp[i][j]=1 & i==1||j==1 \\ \text dp[i][j]=dp[i-1][j]+dp[i][j-1] & i>=1\& \& j>=1 \end{cases}
dp[i][j]={dp[i][j]=1dp[i][j]=dp[i−1][j]+dp[i][j−1]i==1∣∣j==1i>=1&&j>=1
这道题目我们在之前的知识点总结已经刷过了 * 遍了。
2.3 参考答案
#include <iostream>
using namespace std;
int m, n;
int dp[105][105];
int main()
{
cin >> m >> n;
for (int i = 1; i <= m; i++)
dp[i][1] = 1;
for (int j = 1; j <= n; j++)
dp[1][j] = 1;
for (int i = 2; i <= m; i++)
for (int j = 2; j <= n; j++)
dp[i][j] = dp[i-1][j]+dp[i][j-1];
cout << dp[m][n];
return 0;
}
3. 摘花生
3.1 审题
题目描述
小红想摘点花生送给她喜欢的米老鼠。她来到一片有网格状道路的矩形花生地,从西北角进去,东南角出来。地里每个道路的交叉点上都有种着一株花生苗,上面有若干颗花生,经过一株花生苗就能摘走该它上面所有的花生。可多只能向东或向南走,不能向西或向北走。问可多最多能够摘到多少颗花生。
输入描述
第一行是一个整数 T T T,代表一共有多少组数据( 1 ≤ T ≤ 100 1≤T≤100 1≤T≤100)。接下来是T组数据,每组数据的第一行是两个整数,分别代表花生苗的行数 R R R 和列数 C C C( 1 ≤ R , C ≤ 100 1≤R,C≤100 1≤R,C≤100)。每组数据的接下来 R R R 行数据,从北向南依次描述每行花生苗的情况。每行数据有 C C C 个整数,按从西向东的顺序描述了该行每株花生苗上的花生数目 M M M( 0 ≤ M ≤ 1000 0≤M≤1000 0≤M≤1000)。
输出描述
对每组输入数据,输出一行,内容为可多能摘到得最多的花生颗数。
样例1
输入
2 2 2 1 1 3 4 2 3 2 3 4 1 6 5
输出
8 16
3.2 思路
递推公式:
d
p
[
i
]
[
j
]
=
{
d
p
[
i
]
[
j
]
=
1
i
=
=
1
&
&
j
=
=
1
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
]
[
j
−
1
]
)
+
a
[
i
]
[
j
]
i
>
=
1
&
&
j
>
=
1
dp[i][j] = \begin{cases} \text dp[i][j]=1 & i==1\& \& j==1 \\ \text dp[i][j]=max(dp[i-1][j], dp[i][j-1])+a[i][j] & i>=1\& \& j>=1 \end{cases}
dp[i][j]={dp[i][j]=1dp[i][j]=max(dp[i−1][j],dp[i][j−1])+a[i][j]i==1&&j==1i>=1&&j>=1
3.3 参考答案
#include <iostream>
using namespace std;
int T;
int n, m;
int a[105][105];
int dp[105][105];
int main()
{
cin >> T;
while (T--)
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
{
cin >> a[i][j];
dp[i][j] = max(dp[i-1][j], dp[i][j-1])+a[i][j];
}
cout << dp[n][m] << endl;
}
return 0;
}
4. 金字塔路径
4.1 审题
题目描述
写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到下方的点也可以到达右下方的点。
输入描述
输入有 n + 1 n+1 n+1 行
第一行是一个整数 n n n,表示行的数目
接下来是 n n n 行数字塔主体,数字塔的第 k k k 行有 k k k 个数字。
输出描述
第一行,包含那个可能得到的最大的和。
第二行,表示这个数塔从最顶层到最底层的行走路径。最优路径越左越好。
每个数字和箭头之间用一个空格分开。
样例1
输入
5 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5
输出
30 7 -> 3 -> 8 -> 7 -> 5
提示
对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 1000 1 ≤ n ≤ 1000 1≤n≤1000,所有数塔的数据输入值在 [ 0 , 100 ] [0,100] [0,100] 范围内。
4.2 参考答案
#include <iostream>
using namespace std;
int n;
int a[1005][1005];
int 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];
// 需求1
for (int j = 1; j <= n; j++) // 基础项
dp[n][j] = a[n][j];
for (int i = n-1; i >= 1; i--)
for (int j = 1; j <= i; j++)
dp[i][j] = max(dp[i+1][j], dp[i+1][j+1])+a[i][j];
cout << dp[1][1] << endl;
// 需求2
int x = 1, y = 1;
cout << a[x][y];
for (int i = 1; i < n; i++)
{
if (dp[x+1][y] >= dp[x+1][y+1]) // 最优路线优先靠左!!
{
x++;
}
else
{
x++;
y++;
}
cout << " -> " << a[x][y];
}
return 0;
}
5. 最大正方形
5.1 审题
题目描述
在一个 n × m n\times m n×m 的只包含 0 0 0 和 1 1 1 的矩阵里找出一个不包含 0 0 0 的最大正方形,输出边长。
输入描述
第一行为两个整数 n , m n,m n,m( 1 ≤ n , m ≤ 1000 1≤n,m≤1000 1≤n,m≤1000),接下来 n n n 行,每行 m m m 个数字,用空格隔开, 0 0 0 或 1 1 1。
输出描述
一个整数,最大正方形的边长。
样例1
输入
4 4 0 1 1 1 1 1 1 0 0 1 1 0 1 1 0 1
输出
2
5.2 参考答案
#include <iostream>
#include <algorithm>
using namespace std;
int n, m, maxn;
int a[1005][1005];
int dp[1005][1005];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
{
cin >> a[i][j];
if (a[i][j])
{
dp[i][j] = min({dp[i][j-1], dp[i-1][j], dp[i-1][j-1]})+1;
maxn = max(maxn, dp[i][j]);
}
}
cout << maxn;
return 0;
}