个人笔记-动态规划

文章目录

  • 思想
  • 过程
  • 实现的套路
      • 1.自底向上
      • 2.自顶向下
  • 题目
      • 1. 经典的数字三角形问题
      • 2. 最大连续子序列和
      • 3. 最长公共子序列
    • 背包问题
      • 1.01背包
      • 2.多重背包
      • 3.完全背包

思想

首先,动态规划最重要的是掌握他的思想,动态规划的核心思想是把原问题分解成子问题进行求解,也就是分治的思想。

那么什么问题适合用动态规划呢?我们通过一个现实中的例子,来理解这个问题。大家可能在公司里面都有一定的组织架构,可能有高级经理、经理、总监、组长然后才是小开发,今天我们通过这个例子,来讲讲什么问题适合使用动态规划。又到了一年一度的考核季,公司要挑选出三个最优秀的员工。一般高级经理会跟手下的经理说,你去把你们那边最优秀的3个人报给我,经理又跟总监说你把你们那边最优秀的人报给我,经理又跟组长说,你把你们组最优秀的三个人报给我,这个其实就动态规划的思想!

过程

动态规划问题,大致可以通过以下四部进行解决。

1.划分状态, 即划分子问题,例如上面的例子,我们可以认为每个组下面、每个部门、每个中心下面最优秀的3个人,都是全公司最优秀的3个人的子问题

2.状态表示, 即如何让计算机理解子问题。上述例子,我们可以实用f[i][3]表示第i个人,他手下最优秀的3个人是谁。

3.状态转移, 即父问题是如何由子问题推导出来的。上述例子,每个人大Leader下面最优秀的人等于他下面的小Leader中最优秀的人中最优秀的几个。

4.确定边界, 确定初始状态是什么?最小的子问题?最终状态又是什么。例如上述问题,最小的子问题就是每个小组长下面最优秀的人,最终状态是整个企业,初始状态为每个领导下面都没有最优名单,但是小组长下面拥有每个人的评分。

实现的套路

我们实现动态规划算法,常用的是2个实现套路,一个是自底向上,另外一个是自顶向下。无论是何种方式,我们都要明确动态规划的过程,把状态表示、状态转移、边界都考虑好。

1.自底向上

简单来说就是根据初始状态,逐步推导到最终状态,而这个转移的过程,必定是一个拓扑序。如何理解这个拓扑序问题呢,甲总监下面有X,Y,Z两个小组,甲总监不会一拿到X组最优秀的三个人,就立马去跟A经理汇报,而是要等到Y,Z小组也选出来之后,也就是自己下面所有子问题都解决了,才会继续向汇报。如果推导的过程不是一个拓扑序,那么要么得到错误的结果,要么算法就要退化。

自底向上一般用来解决什么问题呢?那就是可以轻松确定拓扑序的问题,例如线性模型,都是从左往右进行转移,区间模型,一般都是从小区间推导到大区间。自底向上的一个经典实现是斐波那楔数列的递推实现,即F[i] = F[i - 1] + F[i - 2] 。

2.自顶向下

也就是从最终状态出发,如果遇到一个子问题还未求解,那么就先求解子问题。如果子问题已经求解,那么直接使用子问题的解,所以自顶向下动态规划又有一个形象生动的名字,叫做记忆化搜索,一般我们采用递归的方式进行求解。

题目

1. 经典的数字三角形问题

个人笔记-动态规划_第1张图片

输入格式:
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5

思路:

由格式知每一次都是向下或者向右下。
那么下一个状态的最大值由上两个状态的最大值决定
因为第一行的必须选,是固定的,固从下往上推
sum[i][j]=max(dfs(i+1,j)+dfs(i+1,j+1)+a[i][j]
即为动态转移方程

代码:

const int MAX=1005;
int sum[MAX][MAX],a[MAX][MAX];//sum要初始为-1,因为a值可能为0
int dfs(int i,int j){
	if(sum[i][j]!=-1)
		return sum[i][j];
	if(i==n)//最后一行,返回数组值
		sum[i][j]=a[i][j];
	else
		sum[i][j]=a[i][j]+max(dfs(i+1,j),dfs(i+1,j+1));
	return sum[i][j];
}

2. 最大连续子序列和

题目

给定一个数组,求数组中和最大的连续子序列的和。 如a[4]={2,6,-1,2},max=2+6-1+2=9。

思路

可以用sum[i] 表示以 i 结尾的最大和。则
1.sum[i-1]>=0,那么前子序列和对以 i 结尾的子序列和有贡献,即sum[i]=sum[i-1]+a[i]
2.sum[i-1]<0,那么a[i]与其相加之后反而变小,即对答案没有贡献,此时sum[i]=a[i]
可以得出转移方程 sum[i]=max(sum[i-1)+a[i],a[i]) 。最后遍历寻找最大sum[i]即可。

代码

//hdu 1003
void maxsum(){
//	dp[i]=max(dp[i-1]+a[i],a[i])
	int t,n,dp[1005];cin>>t;
	while(t--){
		cin>>n;
		for(int i=1;i<=n;i++)cin>>dp[i];
		int l=1,r=1,temp=1,maxn=dp[1];
		for(int i=1;i<=n;i++){
			if(dp[i-1]>=0)
				dp[i]+=dp[i-1];
			else//没有贡献,记录新序列左区间
				temp=i;
			if(dp[i]>maxn){//记录区间以及更新最大值
				l=temp;
				r=i;
				maxn=dp[i];
			}
		}
		printf("%d %d %d\n",l,r,maxn);
	}
}

3. 最长公共子序列

子序列定义

给定一个字符串s,子序列就是是其任意子串(可不连续,但有顺序)
如12345.子串可是 123、234、2345等。21、541不是其子序列。
问题
给定两个字符串,求他们最长的那个公共子序列。
s1="12345",s2="12523",最长公共子序列为125 或者 123(最长公共子序列子序列不唯一长度唯一)
思路
给定两个数列A=a1,a2…an;B=b1,b2,…bm。设LCS(n,m)为长为n和m的最长公共子序列。我们从后往前看:
1.若有 an=bm 。那么就有 LCS(n,m)=LCS(n-1,m-1)+1。
2.如果它们不相等,那么LCS(n,m)=max(LCS(n-1,m),LCS(n,m-1)),就是当前两个字符不匹配,那么分别将A前移一位和将B前移一位在进行比较,即LCS(n-1,m)和LCS(n,m-1)。
3.边界即n==0 || m==0,LCS(0,0)=0。

可得动态转移方程
个人笔记-动态规划_第2张图片
代码

#include
using namespace std;
const int maxn=1005;
int a[maxn][maxn],b[maxn][maxn];
//b数组用来记录转移的方向 
void LCS(string s1,string s2){
	int len1=s1.size();
	int len2=s2.size();
	for(int i=1;i<=len1;i++){
		for(int j=1;j<=len2;j++){
			if(s1[i-1]==s2[j-1]){
				a[i][j]=a[i-1][j-1]+1;
				b[i][j]=0;//左上 
			}
			else if(a[i-1][j]>=a[i][j-1]){
				a[i][j]=a[i-1][j];
				b[i][j]=1;//上 
			}
			else{
				a[i][j]=a[i][j-1];
				b[i][j]=2;//左 
			}
		}
	}
	for(int i=0;i<=len1;i++){
		for(int j=0;j<=len2;j++)
			cout<<a[i][j]<<" ";
		cout<<endl;
	}
	//反向遍历输出公共字符串 
	stack<char>q;
	int i=len1,j=len2;
	while(i>=1&&j>=1){
		if(b[i][j]==0){//左上说明字符一样,入栈 
			i--;j--;
			q.push(s1[i]);
		}
		else if(b[i][j]==1)
			i--;
		else j--;
	}
	while(!q.empty()){
		cout<<q.top();
		q.pop();
	}
}
/*
ABCPDSFJGODIHJOFDIUSHGD
OSDIHGKODGHBLKSJBHKAGHI
*/
int main(){
	string s1,s2;
	cin>>s1>>s2;
	LCS(s1,s2);
	return 0;
}

背包问题

1.01背包

问题

在N件物品中,每件物品有其价值Vi和重量Wi且仅有一件,任意选取物品放在最大容量为M的背包,求能装下的最大价值(可不装满)。

思路

dp[n][m]表示n个物品放入最大容量为m的背包中。
二维:dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i])前者表示不拿,后者表示拿的代价和所得价值(前提是背包容量够可以拿得下)。
一维:dp[j]=max(dp[j],dp[j-w[i]]+v[i]
代码:

//二维
for(int i=1;i<=n;i++)
	for(int j=m;j>=1;j--){
		if(j>=w[i])
			dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);//能拿得下 
		else
			dp[i][j]=dp[i-1][j];                          //拿不下 
	}
//二维下逆向寻找路径
void findwhat(int i,int j,int find[]){
	if(i>=1){
		if(dp[i][j]==dp[i-1][j]){				//没有拿第i件物品
			find[i]=0;
			findwhat(i-1,j,find);
		}
		else if(dp[i][j]==dp[i-1][j-w[i]]+v[i]){//拿了第i件物品 
			find[i]=1;
			findwhat(i-1,j-w[i],find);
		}
	}
}

//一维
for(int i=1;i<=n;i++)
	for(int j=m;j>=w[i];j--)//保证拿得下
		dp[j]=max(dp[j],dp[j-w[i]]+v[i]); 

2.多重背包

//2020/3/24
//将物品数量做一层循环,就类似于01背包 
#include
using namespace std;
typedef long long ll;
const int maxn=105;
int main(){
	int n,m,v[maxn],w[maxn],num[maxn],dp[maxn];
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>v[i]>>w[i]>>num[i];
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=n;i++)
		for(int k=1;k<=num[i];k++)
			for(int j=m;j>=w[i];j--)
				dp[j]=max(dp[j-1],dp[j-w[i]]+v[i]);
	cout<<dp[m]<<endl;
	return 0;
}

3.完全背包

//2020/3/24
#include
using namespace std;
typedef long long ll;
const int maxn=105;
int main(){
	int n,m,v[maxn],w[maxn],dp[maxn];
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>v[i]>>w[i];
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=n;i++)
		for(int j=w[i];j<=m;j++)//01背包为逆序,完全背包为正序 
			dp[j]=max(dp[j-1],dp[j-w[i]]+v[i]);
	cout<<dp[m]<<endl;
	return 0;
}

你可能感兴趣的:(算法集,算法)