例题
入门:数字三角形
题目
题目地址:ACwing
题目: 给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
思路+代码
既然需要我们找到一个路径使得路径上经过的数字加起来最大,那么我们可以倒着来。就是我们从第
n
n
n层开始,向上进行选择路径来走。
比如
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
我们以一个二维数组来记录三角形每个结点的数字,命名为
n
u
m
[
i
]
[
j
]
num[i][j]
num[i][j]。
再以一个二维数组
d
p
dp
dp来记录我们从第
n
n
n层开始走,到结点
(
i
,
j
)
(i,j)
(i,j)的数字之和,我们命名其为
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]。
我们以公式
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
+
1
]
[
j
]
+
n
u
m
[
i
]
[
j
]
,
d
p
[
i
+
1
]
[
j
+
1
]
+
n
u
m
[
i
]
[
j
]
dp[i][j] = max(dp[i+1][j]+num[i][j],dp[i+1][j+1]+num[i][j]
dp[i][j]=max(dp[i+1][j]+num[i][j],dp[i+1][j+1]+num[i][j]
来向上得到
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]的值,最后输出
d
p
[
1
]
[
1
]
dp[1][1]
dp[1][1]就是我们需要的解。
下面使代码:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <stdlib.h>
#define INF 0x7fffffff
#define ll long long
#define rei register int
#define mem(s,i) memset(s,i,sizeof(s))
using namespace std;
const int N = 1e6 + 5;
int num[505][505], dp[505][505];
void solve()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
cin >> num[i][j];
//倒着加
for(int i = n;i >= 1;i--){
for(int j = 1;j <= i;j++){
dp[i][j] = max(dp[i+1][j] + num[i][j],dp[i+1][j+1]+num[i][j]);
}
}
cout<<dp[1][1]<<endl;
}
int main()
{
solve();
return 0;
}
基础:摘花生
题目
题目地址:ACwing
题目:
Hello Kitty想摘点花生送给她喜欢的米老鼠。
她来到一片有网格状道路的矩形花生地(如下图),从西北角进去,东南角出来。
地里每个道路的交叉点上都有种着一株花生苗,上面有若干颗花生,经过一株花生苗就能摘走该它上面所有的花生。
Hello Kitty只能向东或向南走,不能向西或向北走。
问Hello Kitty最多能够摘到多少颗花生。
思路+代码
和数字三角形差不多的思路,还是定义两个二维数组,一个
n
u
m
[
i
]
[
j
]
num[i][j]
num[i][j]存储再结点
(
i
,
j
)
(i,j)
(i,j)的花生数量,一个
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]存储走到结点
(
i
,
j
)
(i,j)
(i,j)的已经摘得的花生数量。
由题可知,我们有两种走法,一种是向下走,一种是向右走。那么
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]就从两个方向来进行比较和选择。
如下图:请添加图片描述
那么我们就可以得到
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]的值的获得公式为
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
]
[
j
−
1
]
+
n
u
m
[
i
]
[
j
]
dp[i][j] = max(dp[i-1][j],dp[i][j-1]+num[i][j]
dp[i][j]=max(dp[i−1][j],dp[i][j−1]+num[i][j]
最后得到的
d
p
[
n
]
[
m
]
dp[n][m]
dp[n][m]就是课得到的最多的花生数量的值。
下面展示代码
#include <iostream>
#include <algorithm>
#include <cstring>
#include <stdlib.h>
#define INF 0x7fffffff
#define ll long long
#define rei register int
#define mem(s, i) memset(s, i, sizeof(s))
using namespace std;
const int N = 110;
int num[N][N], dp[N][N];
int t, n, m;
void solve(){
cin >> t;
while (t--){
mem(num, 0);
mem(dp, 0);
cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
cin >> num[i][j];
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + num[i][j];
cout << dp[n][m] << endl;
}
}
int main(){
solve();
return 0;
}
提高:最低通行费
题目
思路+代码
和上一道差不多,只是这道题需要求的是走到终点的最小值,那么我们就还需要做个改变,那就是对于
0
行
0
列
0行0列
0行0列的
d
p
dp
dp数组的处理。
我们需要使
d
p
[
i
]
[
0
]
dp[i][0]
dp[i][0]和
d
p
[
0
]
[
i
]
dp[0][i]
dp[0][i]的值都为
I
N
F
INF
INF,这样是为了方便进行
m
i
n
min
min函数比较,因为如果我们走的位置为
(
i
,
1
)
(i,1)
(i,1)和
(
1
,
i
)
(1,i)
(1,i)时,我们依照公式
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
]
[
j
−
1
]
)
+
n
u
m
[
i
]
[
j
]
dp[i][j] = min(dp[i-1][j],dp[i][j-1])+num[i][j]
dp[i][j]=min(dp[i−1][j],dp[i][j−1])+num[i][j],此时的
d
p
[
i
−
1
]
[
j
]
dp[i-1][j]
dp[i−1][j]或
d
p
[
i
]
[
j
−
1
]
dp[i][j-1]
dp[i][j−1]的值为
0
0
0,那么我们的比较最后得到的
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]就为
n
u
m
[
i
]
[
j
]
num[i][j]
num[i][j]了,那么我们就得不到正确的
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]。
所以我们会有如下处理:
for(int i = 0;i <= n;i++){
dp[0][i] = INF;
dp[i][0] = INF;
}
完整代码:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <stdlib.h>
#define INF 0x7fffffff
#define ll long long
#define rei register int
#define mem(s, i) memset(s, i, sizeof(s))
using namespace std;
const int N = 110;
int num[N][N], dp[N][N];
int n;
void solve(){
cin>>n;
for(int i = 0;i <= n;i++){
dp[0][i] = INF;
dp[i][0] = INF;
}
for(int i = 1;i <= n;i++){
for(int j = 1;j <= n;j++){
cin>>num[i][j];
}
}
for(int i = 1;i <= n;i++){
for(int j = 1;j <= n;j++){
if(i==1&&j==1)dp[i][j] = num[i][j];
else{
dp[i][j] = min(dp[i-1][j],dp[i][j-1])+num[i][j];
}
}
}
cout<<dp[n][n]<<endl;
}
int main(){
solve();
return 0;
}
进阶:方格取数
题目
思路+代码
我们可以确定两条路可以分为如下四种走法。
我们可以知道,对于只走一条路的情况:
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
]
[
j
−
1
]
+
n
u
m
[
i
]
[
j
]
dp[i][j] = max(dp[i-1][j],dp[i][j-1]+num[i][j]
dp[i][j]=max(dp[i−1][j],dp[i][j−1]+num[i][j]
但是对于走两路:
d
p
[
i
1
,
j
1
,
i
2
,
j
2
]
dp[i_1,j_1,i_2,j_2]
dp[i1,j1,i2,j2]表示从
(
1
,
1
)
(
1
,
1
)
(1,1)(1,1)
(1,1)(1,1)分别走到
(
i
1
,
j
1
)
(i_1,j_1)
(i1,j1)和
(
i
2
,
j
2
)
(i_2,j_2)
(i2,j2)的路径的最大值。
那么我们对于两条路径经过的方格里的数只能用一次怎么处理呢?
我们可以知道,当
i
1
+
j
1
=
i
2
+
j
2
i_1+j_1=i_2+j_2
i1+j1=i2+j2时,必然存在两条路方格重合的问题,也就是当
i
1
=
i
2
,
j
1
=
j
2
i_1=i_2,j_1=j_2
i1=i2,j1=j2时,两条路径的方格重合。
这个时候你可能认为就需要两个
d
p
dp
dp数组来分别记录两条路得到的数。
但事实上大可不必,我们可以用
d
p
[
k
]
[
i
1
]
[
i
2
]
dp[k][i_1][i_2]
dp[k][i1][i2],一个三维的数组来记录就行了。
此时,
j
1
=
k
−
i
1
,
j
2
=
k
−
i
2
j_1 = k - i_1,j_2 = k - i_2
j1=k−i1,j2=k−i2。
我们就只需要三重循环就可以了。
for(int k = 2;k <= n*2;k++){//k从2开始是因为我们两条路起始点都是(1,1)
for(int i1 = 1;i1 <= n;i1++){
for(int i2 = 1;i2 <= n;i2++){
int j1 = k-i1, j2 = k-i2;
}
}
}
}
然后我们需要知道求得
d
p
[
k
]
[
i
1
]
[
i
2
]
dp[k][i_1][i_2]
dp[k][i1][i2]的公式。
首先我们先要判断重合的时候该怎么做。
我们先定义一个
t
t
t表示我们可能需要加上的值,那么
t
t
t该为多少呢?我们可以看如下代码:
int t = num[i1][j1];
if(i1!=i2)t += num[i2][j2];
如果两条路方格重合,那么
t
t
t就为所在方格的数,否则
t
t
t就为两条路在同一时间所在方格的数的总和。
接着,我们可以知道先看第一种情况,也就是两条路都是向下走的情况。
对于
d
p
[
k
]
[
i
1
]
[
i
2
]
dp[k][i_1][i_2]
dp[k][i1][i2]我们知道其代表在同一时间,从
(
1
,
1
)
(
1
,
1
)
(1,1)(1,1)
(1,1)(1,1)走到
(
i
1
,
j
1
)
(
i
2
,
j
2
)
(i_1,j_1)(i_2,j_2)
(i1,j1)(i2,j2)的数的和,那么我们如果在这个时候两条路都是从上往下走的,那么就有公式:
d
p
[
k
]
[
i
1
]
[
i
2
]
=
m
a
x
(
d
p
[
k
]
[
i
1
]
[
i
2
]
,
d
p
[
k
−
1
]
[
i
1
−
1
]
[
i
2
−
1
]
+
t
)
dp[k][i_1][i_2] = max(dp[k][i_1][i_2],dp[k-1][i_1-1][i_2-1]+t)
dp[k][i1][i2]=max(dp[k][i1][i2],dp[k−1][i1−1][i2−1]+t)
同理如果我们是第一条路向下走,第二条路向右走,就有公式:
d
p
[
k
]
[
i
1
]
[
i
2
]
=
m
a
x
(
d
p
[
k
]
[
i
1
]
[
i
2
]
,
d
p
[
k
−
1
]
[
i
1
−
1
]
[
i
2
]
+
t
)
dp[k][i_1][i_2] = max(dp[k][i_1][i_2],dp[k-1][i_1-1][i_2]+t)
dp[k][i1][i2]=max(dp[k][i1][i2],dp[k−1][i1−1][i2]+t)
然后依次类推可得,第一条路向右走,第二条路向下走,得到公式:
d
p
[
k
]
[
i
1
]
[
i
2
]
=
m
a
x
(
d
p
[
k
]
[
i
1
]
[
i
2
]
,
d
p
[
k
−
1
]
[
i
1
]
[
i
2
−
1
]
+
t
)
dp[k][i_1][i_2] = max(dp[k][i_1][i_2],dp[k-1][i_1][i_2-1]+t)
dp[k][i1][i2]=max(dp[k][i1][i2],dp[k−1][i1][i2−1]+t)
两条路都向右走,得到公式:
d
p
[
k
]
[
i
1
]
[
i
2
]
=
m
a
x
(
d
p
[
k
]
[
i
1
]
[
i
2
]
,
d
p
[
k
−
1
]
[
i
1
]
[
i
2
]
+
t
)
dp[k][i_1][i_2] = max(dp[k][i_1][i_2],dp[k-1][i_1][i_2]+t)
dp[k][i1][i2]=max(dp[k][i1][i2],dp[k−1][i1][i2]+t)
而我们最后需要求得的
d
p
[
k
]
[
i
1
]
[
i
2
]
dp[k][i_1][i_2]
dp[k][i1][i2]就为由上述公式求得的
4
4
4个
m
a
x
max
max值的
m
a
x
max
max值。也就是
d
p
[
k
]
[
i
1
]
[
i
2
]
=
m
a
x
(
m
a
x
1
,
m
a
x
2
,
m
a
x
3
,
m
a
x
4
)
dp[k][i_1][i_2] = max(max_1,max_2,max_3,max_4)
dp[k][i1][i2]=max(max1,max2,max3,max4)
最后我们输出
d
p
[
k
]
[
n
]
[
n
]
dp[k][n][n]
dp[k][n][n]就是答案
(
k
=
2
×
n
)
(k=2\times n)
(k=2×n)
下面是完整代码:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <stdlib.h>
#define INF 0x7fffffff
#define ll long long
#define rei register int
#define mem(s, i) memset(s, i, sizeof(s))
using namespace std;
const int N = 15;
int num[N][N];
int dp[2*N][N][N];
void solve(){
int n;
cin>>n;
int a, b, c;
while(cin>>a>>b>>c, a||b||c){
num[a][b] = c;
}
for(int k = 2;k <= n*2;k++){//k从2开始是因为我们两条路起始点都是(1,1)
for(int i1 = 1;i1 <= n;i1++){
for(int i2 = 1;i2 <= n;i2++){
int j1 = k-i1, j2 = k-i2;
if(j1>=1&&j1<=n&&j2>=1&&j2<=n){
int t = num[i1][j1];
if(i1!=i2)t += num[i2][j2];
int &x = dp[k][i1][i2];
x = max(x,dp[k-1][i1-1][i2-1]+t);
x = max(x,dp[k-1][i1-1][i2]+t);
x = max(x,dp[k-1][i1][i2-1]+t);
x = max(x,dp[k-1][i1][i2]+t);
}
}
}
}
cout<<dp[2*n][n][n]<<endl;
}
int main(){
solve();
return 0;
}
个人总结:
动态规划是个难度较大板块,所以我们需要多做一下动态规划得题来巩固自己对其的理解