一、动态规划的四个基本步骤:
二、动态规划的常用方法:
(1)自底向上求解:
最常见的dp算法,由dp初值导出问题规模次小的最优解,记录在数组中,然后不断扩大问题规模,循环求出规模更大的子问题,每次求解当前问题时,都会用到以前求出过的解,所以把前面的解都保存一下,避免对相同的子问题重复多次计算。
(2)自顶向下求解:
又称为备忘录法。可以认为是优化过的递归求解算法。即开辟一块数组空间,在递归过程中,查看该问题是否以及得到过解,若前面已经求得解,则不需要递归,直接可以得到该子问题的解,否则才递归下去求解。实质上是前一种算法的递归实现,在真正求解中也是先求解规模最小的子问题,然后再依次扩大问题规模的,且因为程序递归,还要多付出额外的程序运行代价。
三、动态规划解决问题的举例:
(1)数字三角形问题
在数字三角形中寻找一条从顶部到底边的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或 右下走。只需要求出这个最大和,以及路径。三角形的行数大于1小于等于100,数字为 0 - 99
输入格式:
5 //表示三角形的行数 接下来输入三角形
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
要求输出最大和和路径
样例输入
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
样例输出
MAX : 30
7 to left 3 to left 8 to right 7 to left 5
分析:
数字三角形问题状态转移方程为 dp[i][j]=max(dp[i-1][j-1],dp[i-1][j])+a[i][j]; dp[i][j]意义是表示到(i,j)这个点的最大的可能值。分析可得,只有左上角(i-1,j-1)和右上角(i-1.j)两个点可能到达当前点(i,j),在两者中取最大,并加上a[i][j]即得到dp[i][j]。同时记录s[i][j]表示dp[i][j]是由哪个方向的点得来的。初始化dp[k][0]、dp[k][k]、s[k][0]、s[k][k] 其中0<=k<n。遍历最底层dp[n-1][k]寻找最大值即为所求。通过s[i][j]递归求解可得路径。
代码:
#include<iostream>
using namespace std;
int a[100][100];
int dp[100][100];
int s[100][100];
void init(int n){//dp初始化
dp[0][0]=a[0][0];
for(int i=1;i<n;i++){
dp[i][i]=dp[i-1][i-1]+a[i][i];
dp[i][0]=dp[i-1][0]+a[i][0];
s[i][i]=-1;
s[i][0]=1;
}
}
int solve(int n,int &t){//依次求出每个位置的最优解
init(n);
for(int i=1;i<n;i++){
for(int j=1;j<i;j++){
dp[i][j]=max(dp[i-1][j-1],dp[i-1][j])+a[i][j];
if(dp[i-1][j-1]>dp[i-1][j])s[i][j]=-1;//用s[i][j]记录选择
else s[i][j]=1;
}
}
int max=dp[n-1][0];
t=0;
for(int i=1;i<n;i++){
if(dp[n-1][i]>max){
max=dp[n-1][i];
t=i;
}
}
return max;//返回最底层的最优解
}
void print(int i,int j){//利用s[i][j]重构最优解
if(i==0){
cout<<a[i][j]<<' ';
return ;
}
if(s[i][j]>0){
print(i-1,j);
cout<<"to left"<<' ';
}
else{
print(i-1,j-1);
cout<<"to right"<<' ';
}
cout<<a[i][j]<<' ';
}
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++){
for(int j=0;j<=i;j++){
cin>>a[i][j];
}
}
int t;
cout<<"MAX : "<<solve(n,t)<<endl;
print(n-1,t);
return 0;
}
/*
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
*/
运行结果:
(2)最长公共子序列问题:
给定两个序列 X={x1,x2,…,xm} 和 Y={y1,y2,…,yn},找出X和Y的最长公共子序列。
输入:
第一行给出一个整数N(0<N<100)表示待测数据组数。接下来每组数据两行,分别为待测的两组字符串。每个字符串长度不大于1000.。
输出:
每组测试数据输出一个整数,表示最长公共子序列长度,同时输出最长公共子序列。
样例输入
2
asdf
adfsd
123abc
abc123abc
样例输出
3
adf
6
123abc
分析:
最长公共子序列的状态转移方程为:
if(s1[i-1]==s2[j-1])
dp[i][j]= dp[i-1][j-1]+1;
else
dp[i][j]=max(dp[i-1][j], dp[i][j-1]);
dp[i][j]物理意义为:s1[0]到s1[i]和s2[0]到s2[j]两个串的最长公共子序列。dp过程为,查看两串的最后一位,若相同,则可以认为两串长度均减去最后一位,然后 dp[i][j]= dp[i-1][j-1]+1;
若最后一位不同,则要根据两种情况选择最优解,选择去掉s1串最后一位或者s2最后一位,再进行判断(去掉后不影响最终解)。最后问题规模缩小到其中一个串长度为0,则最长公共子序列也为0,据此可以设置dp的初始条件(全局变量就没初始化)。因为要输出最优解,所以要记录一下最后一位不相等的情况下,选择了去掉s1串还是s2串的末尾。用1 2 3表示三种选择的状态,存在b[i][j]中。LCS函数递归求解,输出结果。
代码:
#include<iostream>
using namespace std;
string s1;
string s2;
int dp[1000][1000];
int b[1000][1000];//记录dp过程中的选择
int solve(int i,int j){//计算最优值
if(s1[i-1]==s2[j-1]){
b[i][j]=1;
return dp[i-1][j-1]+1;
}
else{
if(dp[i-1][j]>dp[i][j-1]){
b[i][j]=2;
return dp[i-1][j];
}
else{
b[i][j]=3;
return dp[i][j-1];
}
}
}
void LCS(int i,int j){//1 2 3 表示三种不同dp方式
if(b[i][j]==1){
LCS(i-1,j-1);
cout<<s1[i-1];
}
if(b[i][j]==2){
LCS(i-1,j);
}
if(b[i][j]==3){
LCS(i,j-1);
}
}
int main(){
int N;
cin>>N;
while(N--){
cin>>s1;
cin>>s2;
for(int i=1;i<=s1.length();i++){
for(int j=1;j<=s2.length();j++){
dp[i][j]=solve(i,j);//dp
}
}
cout<<dp[s1.length()][s2.length()]<<endl;//输出最优值
LCS(s1.length(),s2.length());//构造最优解
cout<<endl;
}
return 0;
}
/*
2
asdf
adfsd
123abc
abc123abc
*/
运行结果:
(3)0-1背包问题
给定n种物品和一背包。物品i的重量是wi,其价值为vi,背包的容量为W。问:应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
输入:第一行有两个正整数n和W,n是物品种数,W是背包容量,接下来的一行中有n个正整数,表示物品的价值,第三行中有n个正整数,表示物品的重量。
输出:
将计算的装入背包物品的最大价值和最优装入方案
输入样例:
5 10
6 3 5 4 6
2 2 6 5 4
输出样例:
15
1 1 0 0 1
分析:
0-1背包问题状态转移方程为dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i-1]]+v[i-1]);dp[i][j]的物理意义是考虑前i件物品,当背包容量为j时的最大价值是多少。需要考虑特殊情况,若当前物品超过了可用的背包容量,则不考虑当前物品。print函数检查dp[i][j]与dp[i-1][j]是否相同,相同则表示第i件物品没有选。若选了,则考虑前i-1种物品在容量为j-w[i]的背包下的选取情况,递归求解。初始化:前0种物品,不管多大背包,价值均为0,背包容量为0,任意种物品考虑价值均为0。
代码:
#include<iostream>
using namespace std;
int dp[1000][1000];
void init(int n,int W){
for(int i=0;i<=n;i++){
dp[i][0]=0;
}
for(int i=0;i<=W;i++){
dp[0][i]=0;
}
}
void solve(int n,int W,int* w,int* v){
for(int i=1;i<=n;i++){
for(int j=1;j<=W;j++){
if(w[i-1]>j){//当前物品装不下
dp[i][j]=dp[i-1][j];
}
else{//可装下,取最优
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i-1]]+v[i-1]);
}
}
}
}
void print(int i,int j,int* w){
if(i==0) return ;
if(dp[i][j]==dp[i-1][j]){//相同表示没选
print(i-1,j,w);
cout<<0<<' ';
}
else{//不同表示选了
print(i-1,j-w[i-1],w);
cout<<1<<' ';
}
}
int main(){
int n,W;
cin>>n>>W;
int v[n];//valve
int w[n];//weight
for(int i=0;i<n;i++){
cin>>v[i];
}
for(int i=0;i<n;i++){
cin>>w[i];
}
init(n,W);
solve(n,W,w,v);//填表
cout<<dp[n][W]<<endl;//输出最优值
print(n,W,w);//输出最优解
return 0;
}
/*
5 10
6 3 5 4 6
2 2 6 5 4
*/
运行结果:
四、 总结:
动态规划问题最困难的地方就是写出正确的状态转移方程。需要大家多多训练dp思维,充分考虑dp初始值以及dp数组之间状态转移的关系,找到了正确的递推关系,加上精确的变量边界划分,即可得到状态转移方程,之后就是编码过程了。