第三章 动态规划 复习总结

一、动态规划的四个基本步骤: 

二、动态规划的常用方法:

 (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数组之间状态转移的关系,找到了正确的递推关系,加上精确的变量边界划分,即可得到状态转移方程,之后就是编码过程了。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值