概念
动态规划,解决问题的一种方法。将很多问题转换成多个子问题求解,先计算子问题,到达边界直接返回问题的值,最后得到最终答案的一种方法。动态规划分为两大类:记忆化搜索和递推。记忆化搜索更好写,但常数更高;递推不太好写,但是常数低。二者时间复杂度无特殊情况基本相同。
状态转移方程:将一个问题转换成子问题计算得到结果的方程。
d
p
dp
dp:动态规划的简称。
数字三角形,就是一个三角形,每一个点都有一个数字,找一条路径满足题目要求。形如:
1
1
1
2
3
2 3
2 3
4
5
6
4 5 6
4 5 6这样的一种图形被称为数字三角形。一般题目中要求最大值或者最小值。
方法
现在一个题目,要求下面数字三角形的路径最大值。 1 1 1 3 2 3 2 3 2 1 1 10 1 1 10 1 1 10因为我们是求最大值,则很容易想到一种方案:每次贪心选择两个点中最大的,但是在上面的三角形中会有一个问题:贪心选择的路径: 1 → 3 → 1 1\to 3\to 1 1→3→1,答案是 5 5 5。然而这个题的最优路径是 1 → 2 → 10 1\to 2\to 10 1→2→10,答案是 13 13 13。因为我们一个较小的下面可能藏了一个大的,然而较大的下面却全是小的。所以一个完美的方式就出来了:不难发现,到达一个数的路径,就是前面两个路径的最大值加上自己。因为以自己结束的只能是从上面两个点下来才可能。但是有一个点从它结束的值是固定的:第 1 1 1排第 1 1 1个。不管怎么走,都要从它开始,这就是边界情况。设 i i i为行数, j j j为列数,从 1 1 1开始编号,于是,我们的方案就出来了: f i , j = max ( f i − 1 , j , f i − 1 , j − 1 ) + a i , j f_{i,j}=\max(f_{i-1,j},f_{i-1,j-1})+a_{i,j} fi,j=max(fi−1,j,fi−1,j−1)+ai,j,我们假定 f i , j = 0 , j > i ∣ ∣ j = 0 f_{i,j}=0,j>i||j=0 fi,j=0,j>i∣∣j=0。最后,我们考虑顺序。发现,到达一个数只需要知道上面两个数的 f f f即可,于是,顺序就是从第一排从上往下计算,又因为同一排的互补干扰,并不需要知道同一排的值,则在一排是什么顺序无所谓。
例题
AcWing 1015
这个题虽然变成了一个矩阵,但是仍然可以发现,每个点只能从北或西过来。只有左上角的点是一定要走的,这就是边界条件。现在,知道了解决每个点的子问题是什么,那么就很简单了。状态转移方程: f i , j = max ( f i − 1 , j , f i , j − 1 ) + a i , j f_{i,j}=\max(f_{i-1,j},f_{i,j-1})+a_{i,j} fi,j=max(fi−1,j,fi,j−1)+ai,j。因为解决一个点的子问题是上面或者左边,所以从上到下,从左到右即可。
#include<bits/stdc++.h>
using namespace std;
const int NN=104;
int f[NN][NN];
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
int x;
scanf("%d",&x);
f[i][j]=max(f[i-1][j],f[i][j-1])+x;
}
printf("%d\n",f[n][m]);
}
return 0;
}
AcWing 1018
这个题发现并不是刚才的模型。但是不难发现,想要时间最短就只能往下或往右走。于是,就变成了刚才的题目。需要注意的是,第一排和第一列需要先计算,因为这里变成了求最小,如果越界了可能不对。当然,也可以先把数组初始化为正无穷,求 min \min min自然就不会选。
#include<bits/stdc++.h>
using namespace std;
const int NN=104;
int f[NN][NN];
int main()
{
int n;
scanf("%d",&n);
memset(f,0x3f,sizeof(f));
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
int x;
scanf("%d",&x);
if(i==1&&j==1)
f[1][1]=x;
else
f[i][j]=min(f[i][j-1],f[i-1][j])+x;
}
printf("%d",f[n][n]);
return 0;
}
AcWing 1027
法一
不难发现,这个题目和上上一道题差不多,但是要计算两次。于是可以想到一种方法:我们可以假设这个是同步进行的两个路径,毕竟两个路径怎么走除了同一点也没有什么影响。于是我们可以设 f i 1 , j 1 , i 2 , j 2 f_{i1,j1,i2,j2} fi1,j1,i2,j2为第一条路径走到 ( i 1 , j 1 ) (i1,j1) (i1,j1),第二条走到 ( i 2 , j 2 ) (i2,j2) (i2,j2)的最大值。注意,我们两个路径长度必须一样,这样才是同步进行。因为只能往下或往右走,所以长度就是 i + j i+j i+j,判断两个是否一样即可。然后,如果走到了同一点,则只加上一个点的值,反之两个路径的值都加上。然后计算状态转移,和上上题一样,只不过是两个加在一起。
#include<bits/stdc++.h>
using namespace std;
const int NN=14;
int f[NN][NN][NN][NN],a[NN][NN];
int main()
{
int n,x,y,w;
scanf("%d",&n);
while(scanf("%d%d%d",&x,&y,&w)&&(x||y||w))
a[x][y]=w;
for(int i1=1;i1<=n;i1++)
for(int j1=1;j1<=n;j1++)
for(int i2=1;i2<=n;i2++)
for(int j2=1;j2<=n;j2++)
if(i1+j1==i2+j2)
{
int &d=f[i1][j1][i2][j2];
if(i1==i2&&j1==j2)
d=a[i1][j1];
else
d=a[i1][j1]+a[i2][j2];
d+=max(f[i1-1][j1][i2-1][j2],max(f[i1-1][j1][i2][j2-1],max(f[i1][j1-1][i2-1][j2],f[i1][j1-1][i2][j2-1])));
}
printf("%d",f[n][n][n][n]);
return 0;
}
法二
可以发现,这样会浪费计算很多东西,比如很多 i 1 + j 1 ≠ i 2 + j 2 i1+j1\not= i2+j2 i1+j1=i2+j2的情况是浪费的。于是,为了避免浪费,可以发现两个和一样,那么就统计一个和为 k k k,就有 f k , i 1 , i 2 f_{k,i1,i2} fk,i1,i2,要计算 j j j就直接用 k k k减去即可。这样,想判断是否为同一个点也十分简单,只要求 i 1 ≠ i 2 i1\not= i2 i1=i2即可。我们再来看看前面的状态转移方程分别对应什么: f i 1 − 1 , j 1 , i 2 − 1 , j 2 = f k − 1 , i 1 − 1 , i 2 − 1 f_{i1-1,j1,i2-1,j2}=f_{k-1,i1-1,i2-1} fi1−1,j1,i2−1,j2=fk−1,i1−1,i2−1 f i 1 − 1 , j 1 , i 2 , j 2 − 1 = f k − 1 , i 1 − 1 , i 2 f_{i1-1,j1,i2,j2-1}=f_{k-1,i1-1,i2} fi1−1,j1,i2,j2−1=fk−1,i1−1,i2 f i 1 , j 1 − 1 , i 2 − 1 , j 2 = f k − 1 , i 1 , i 2 − 1 f_{i1,j1-1,i2-1,j2}=f_{k-1,i1,i2-1} fi1,j1−1,i2−1,j2=fk−1,i1,i2−1 f i 1 , j 1 − 1 , i 2 , j 2 − 1 = f k − 1 , i 1 , i 2 f_{i1,j1-1,i2,j2-1}=f_{k-1,i1,i2} fi1,j1−1,i2,j2−1=fk−1,i1,i2所以,这些都可以一一对应,代码也是可行的,空间少了,浪费时间也少了。
#include<bits/stdc++.h>
using namespace std;
const int NN=14;
int f[NN*2][NN][NN],a[NN][NN];
int main()
{
int n,x,y,w;
scanf("%d",&n);
while(scanf("%d%d%d",&x,&y,&w)&&(x||y||w))
a[x][y]=w;
for(int k=2;k<=n*2;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(k-i>=1&&k-i<=n&&k-j>=1&&k-j<=n)
{
f[k][i][j]=a[i][k-i];
if(i!=j)
f[k][i][j]+=a[j][k-j];
f[k][i][j]+=max(f[k-1][i-1][j-1],max(f[k-1][i][j-1],max(f[k-1][i-1][j],f[k-1][i][j])));
}
printf("%d",f[n*2][n][n]);
return 0;
}
AcWing 275
法一
和上一题法一基本一模一样。本题不让走到同一点,那么就判断是否是同一点,是同一点就不计算。如果是从同一点过来的,那么那个点之前就没有更新,所以是零,最大值绝对不是它,所以也不会用它更新。因为两条路都会到最后一个点,所以最后一个点不会更新,直接输出差一步到达终点的两条路径即可。注意,这样的两条路径只有一个:从左边和上边来的两条路径。
#include<bits/stdc++.h>
using namespace std;
const int NN=54;
int f[NN][NN][NN][NN],a[NN][NN];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&a[i][j]);
for(int i1=1;i1<=n;i1++)
for(int j1=1;j1<=m;j1++)
for(int i2=1;i2<=n;i2++)
for(int j2=1;j2<=m;j2++)
if(i1+j1==i2+j2&&(i1!=i2||j1!=j2))
f[i1][j1][i2][j2]=a[i1][j1]+a[i2][j2]+max(f[i1-1][j1][i2-1][j2],max(f[i1-1][j1][i2][j2-1],max(f[i1][j1-1][i2-1][j2],f[i1][j1-1][i2][j2-1])));
printf("%d",f[n-1][m][n][m-1]);
return 0;
}
法二
同上一题的法二,相同点的处理同本题法一。
#include<bits/stdc++.h>
using namespace std;
const int NN=54;
int f[NN*2][NN][NN],a[NN][NN];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&a[i][j]);
for(int k=3;k<=n+m;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(k-i>=1&&k-i<=m&&k-j>=1&&k-j<=m&&i!=j)
f[k][i][j]=a[i][k-i]+a[j][k-j]+max(f[k-1][i-1][j-1],max(f[k-1][i][j-1],max(f[k-1][i-1][j],f[k-1][i][j])));
printf("%d",f[n+m-1][n-1][n]);
return 0;
}