一,概念篇
1,动态规划:通过计算出小问题的最优解,可以推出大问题的最优解,从而可以推出更大问题的最优解,最小问题即是边界情况。
2,子问题(小问题):子问题是一个与原问题有着类似的结构,但规模比原问题小的问题。
3,动态规划的基本思想:将待求解的问题划分为若干个阶段(子问题),按顺序求解子问题,子问题的求解为更大子问题的求解提供信息,由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次。
4,状态表示和最优化值。
状态表示是对当前子问题的解的局面的(条件)一种全面的描述。
最优化值是该状态表示下的最优化值(方案值),我们最终能通过其直接或间接得到答案。
5,状态的设计
具有最优化总结构,能够全面描述某一个局面,尽量简洁。
设计状态的关键是充分描述,尽量简洁。
6,动态规划的精髓——状态转移:通过已知的较小问题的最优值得出较大问题的最有值的过程。状态的转移需要满足要考虑到所有的可能性。
7,动态规划的时间复杂度估计
O=状态数*状态转移的复杂度
二,入门例题篇
1,数字三角形(洛谷P1216)
观察下面的数字金字塔。
写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
分析:这道题的状态是当前节点,最优化值是从底部某元素到该点的路径数字最大和,边界条件为,最后一排所有节点的最优化值是其自身的值,状态转移:题干中所说每一步可以走到它下方的点也可以走到它右下方的点,所以第i个节点的状态是由它下方的两个节点的子问题转移而来,因为要求路径数字和最大,所以可以得到状态转移方程dp[i][j]=max(dp[i-1][j],dp[i-1][j-1])+a[i][j]。
#include<iostream>
#include<algorithm>
using namespace std;
int r,g[1005][1005],dp[1005][1005];
int main()
{
std::ios::sync_with_stdio(false);
cin>>r;
for(int i=1;i<=r;i++)
for(int j=1;j<=i;j++)
cin>>g[i][j];
dp[1][1]=g[1][1];
for(int i=2;i<=r;i++)
for(int j=1;j<=i;j++)
dp[i][j]=max(dp[i-1][j],dp[i-1][j-1])+g[i][j];
int ans=0;
for(int i=1;i<=r;i++)
ans=max(ans,dp[r][i]);
cout<<ans<<endl;
return 0;}
2,过河卒(洛谷P1002)
棋盘上AA点有一个过河卒,需要走到目标B点。卒行走的规则:可以向下、或者向右。同时在棋盘上C点有一个对方的马,该马所在的点和所有跳跃一步可达的点称为对方马的控制点。因此称之为“马拦过河卒”。
棋盘用坐标表示,A点(0, 0)(0,0)、B点(n, m)(n,m为不超过2020的整数),同样马的位置坐标是需要给出的。
现在要求你计算出卒从A点能够到达B点的路径的条数,假设马的位置是固定不动的,并不是卒走一步马走一步。
分析:这道题的状态依然是当前节点,最优化值是从(0,0)出发到当前节点的路径总数,边界条件为(0,0)点到本身的方案数为1(最小子问题),第一列,第一行的点的方案数都为1(因为只能往右和往下走),状态转移:因为只能往右走和往下走,所以当前节点的方案数是由其左边的节点和上面的节点转移而来,所以当前节点的方案数就等于左边节点的方案数加上右边节点的方案数所以我们就得到了状态转移方程,dp[i][j]=dp[i-1][j]+dp[i][j-1];如果是马控制的区域那么那个点的dp值为0。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
typedef unsigned long long ULL;
ULL dp[25][25];
bool danger[25][25];
ULL n,m,x,y;
const int dx[8]={-1,-2,-2,-1,+1,+2,+2,+1};
const int dy[8]={-2,-1,+1,+2,+2,+1,-1,-2};
int main()
{
scanf("%lld%lld%lld%lld",&n,&m,&x,&y);
n++;m++;x++;y++;
danger[x][y]=true;
for(int i=0;i<8;i++)
{
ULL nx=x+dx[i]; ULL ny=y+dy[i];
if(nx>=1&&nx<=n&&ny>=1&&ny<=m) danger[nx][ny]=true;
}
dp[1][1]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
if(danger[i][j]) continue;
if(i==1&&j==1) continue;
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
printf("%lld",dp[n][m]);
return 0;}
3,切割钢材
有一根长度为n条,想把它切成若干小段,每段长度都为整数,不同长度的收益不同,给出数组 p[1…n],长度为len的钢条能产生p[len]的价值,求一种切割方案,使得最终总收益最大。
分析:这道题的状态时,dp[i]表示长度为i的钢材能产生的最大价值。边界条件为长度为i的钢材不切割时的价值为p[i],长度为1的钢材的最大价值为dp[1]=p[1],状态转移:dp[i]的状态是由dp[1…i-1]转移而来,从考虑切最后一刀的角度考虑,dp[i]=max(dp[i],dp[i-j]+p[j]|1<=j<i);因为以前的dp[i]一定是最优解,故该状态转移方程成立。//这个状态转移方程的某些类似于背包