算法:动态规划——线性DP(C++)

动态规划——线性DP

  • 概述
  • 经典的问题
    • 1.最大连续子序列和
    • 2.最长不下降子序列
    • 3.最长公共子序列
    • 4.最长回文子串
  • 相关习题(持续更新中)

  • 博客主要参考书:胡凡《算法笔记》

概述

在我看来动态规划就是将一个问题的最优问题分解为子问题的最优解来获得真正的最优解。动态规划问题当中重要的就是:

  • 状态的转化方程:目标态=F(某一状态)。
  • 有边界状态和其值

使用条件:1.有重复的子问题;2.有最优子结构

经典的问题

1.最大连续子序列和

题目:给定一个序列:S1,S2,S3…Sn,有i,j使得sum=Si+Si+1…+Sj-1+Sj最大,求最大值。
这很明显是一个最优解问题,同时我们在考虑当前的数
我们设数组dp[n]当中存放的是以当前的i,即dp[i]是以Si为结尾的最大连续子列的。那么对于dp[i]有两种情况:

  • 将Si放入前序列:dp[i]=dp[i-1]+Si
  • Si不放入前序列:dp[i]=Si,即此时以Si结尾的最大序列和就是以Si开头的。

所以dp[i]=max(dp[i-1]+Si,Si),也就是我们的状态转换方程,现在的状态可以由另一个状态转换而来。
边界:dp[1]=S1;
当我们获得dp数组的所有值之后,也就意味着我们获得了以每一个数字结尾的连续子序列的最大和,再次遍历选出其中最大的值,就是我们需要的结果(我们并不需要知道这个连续序列是谁,只要他的值就可以)
代码如下:

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1010;
int S[N], dp[N];
int main() {
	int n;
	cin >> n;//读入序列的个数
	for (int i = 0; i < n; i++) {
		cin >> S[i];//读入序列
	}
	dp[0] = S[0];//边界条件
	for (int i = 1; i < n; i++) {
		//状态转换方程
		dp[i] = max(S[i], dp[i - 1] + S[i]);
	}
	//找出最大的
	int Max_sum = 0;
	for (int i = 0; i < n; i++) {
		Max_sum = max(Max_sum, dp[i]);
	}
	cout << Max_sum;
	return 0;
}

2.最长不下降子序列

问题:给定一个序列:S1,S2,S3…Sn,找到一个最长的子序列,可以是不连续的,使得其是不下降的序列(非递减的),求此序列的长度。
这题先直接给出代码,然后对比上一题的不同进行分析:
代码:

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1010;
int S[N], dp[N];
int main() {
	int n;
	cin >> n;//读入序列的个数
	for (int i = 0; i < n; i++) {
		cin >> S[i];//读入序列
	}
	//改变了的代码部分begin
	for (int i = 0; i < n; i++) {
		dp[i] = 1;//边界条件,每一个以Si为结尾的最长不下降子序列都是由自己开始的
		for (int j = 0; j < i; j++) {
		//状态转换方程
		if(S[i]>=S[j]&&dp[j]+1>dp[i])//从第一个开始依次看是否有比自己小的(有才可以加进去嘛S[i]>=S[j]),如果有->
		dp[i] = dp[j]+1;//->还得考虑加入目前可以进入的队列是否会使得以自己结尾的序列长度的值增加(dp[j]+1>dp[i]),如果是才更新,这样每次更新以后都是目前的最大值
	}
	}
	//end

	//找出最大的
	int Max_sum = 0;
	for (int i = 0; i < n; i++) {
		Max_sum = max(Max_sum, dp[i]);
	}
	cout << Max_sum;
	return 0;

}

可以看到相比上一题我们的循环多了一层,这是为什么呢?
原因就是我们要获得目前的Si结尾的最长不下降子序列不能只在前一个Si-1的基础上考虑,我觉得主要是可以不连续造成的,比如序列:1,1,3,4,2,以4结尾的满足序列为:{1,1,3,4}若此时考虑2,只看前一个状态4的话,2是无法加进去的,但是由于可以不连续,就可以是{1,1,2},所以我们还需要一个循环去看在2之前的序列,看有没有小于等于2的如果有比如是第一个1,那么现在就可以是:dp[0]+1,然后是第二个1,那么是dp[1]+1。这个时候我们需要比较是否比前一个加入的dp大,因为有可能是不大的,我们需要的是可以加入且长度最大的。

  • 边界条件:以Si为结尾的最长不下降子序列的长度至少是1(只含有他自己)
  • 状态转换方程:if(S[i]>=S[j]&&dp[j]+1>dp[i])
    dp[i] = dp[j]+1;

3.最长公共子序列

题目:给定两个字符串(也可以是数字序列),求出两字符串的最长公共部分(可以不连续)。
设dp[i][j]表示符串A的前i个字符和字符串B的前j个字符的最长公共子序列长度 。
分析dp[i][j]时,情况有:

  • A[i]=B[j],此时dp[i][j]=dp[i-1]dp[j-1]+1;
  • A[i]!=B[j],此时dp[i][j]=max(dp[i-1][j],dp[i][j-1])
    状态转换方程

此处想不通的可以自己画一个二维的图辅助理解或者好好看看代码,代码注释很详细。
边界:dp[i][0]=dp[0][j]=0

代码:

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio> 
using namespace std;
const int N=1010;
int dp[N][N];//dp[i][j]表示字符串A的前i个字符和字符串B的前j个字符的最长公共子序列长度 
char A[N],B[N];
int main(){
    gets(A+1);//数组的下标从1开始
	gets(B+1);
	int lenA=strlen(A+1);//注意此时下标是从一开始的所以读取长度也要从一开始
	//gets(char *) and strlen(char *):因此此处我们要给出开始的首地址。
	int lenB=strlen(B+1);
	 //填充边界值
	 for(int i=0;i<=lenA;i++){
	 	dp[i][0]=0;//即不论字符串A到第几个字符,B串是第0个字符时,最长公共子序列长度为0 
	 } 
	 //同理
	 for(int j=0;j<=lenB;j++){
	 	dp[0][j]=0;
	 } 
	 //状态转化方程 
	 for(int i=1;i<=lenA;i++){
	 	for(int j=1;j<=lenB;j++){
	 		if(A[i]==B[j]){
	 			dp[i][j]=dp[i-1][j-1]+1;
			 } else{
			 	dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
			 }
		 }
	 }
	
	cout << dp[lenA][lenB];//最大值肯定是已经考虑了两个字符串的所有部分的 
	
	return 0;
}

4.最长回文子串

题目:给出一个字符串,需要的出此字符串的最长回文子串的长度。
使用动态规划解决回文时间复杂度在O(n^2),还有其他比此复杂度更小的算法,请看文章:
设dp[i][j]表示从i到j的最长回文子串的长度
边界:dp[i][i]=0,if(s[i]=s[i+1]) dp[i][i+1]=1;else =0;
状态转换

  • s[i]=s[j]:dp[i][j]=dp[i+1][j-1]+1;
  • s[i]!=s[j]:dp[i][j]=dp[i+1][j-1];
    但是此时存在一个问题:如:
    dp[1][1]=1;
    dp[1][2]=(s[i]==s[i+1])?2:1
    dp[1][3]=dp[2][2];
    dp[1][4]=dp[2][3];//此时dp[2][3]并没有被计算过,也就是说前一个状态并没得到。
    采用以下办法来解决:
    也就是固定长度来看在该长度下是否有回文串,长度1和长度2时都已经初始化完成则只需从长度为3开始枚举,每一个状态所需要的前一个状态的长度必然比自己短1,这样保证一定已经被遍历过。

代码:

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio> 
using namespace std;
const int N=1010;
int dp[N][N];//dp[i][j]表示以i为开始,j为结尾的字符串是否是回文串 ,1代表是,0代表不是 
char S[N];

int main(){
    gets(S);
    int ans=0; 
	int len=strlen(S);
   //memset():memset(void *buffer, int c, int count) c:赋给buffer的值,count:buffer的长度.
   memset(dp,0,sizeof(dp));
	 //填充边界值

	 for(int i=0;i<len;i++){
	 dp[i][i]=1;
	 if(i<len-1){
	 	if(S[i]=S[i+1]){
	 		dp[i][i+1]=1;
	 		ans=2;
		 }
	 }
	 } 

	 //状态转化方程 
	 //patzjujztaccbcc
	 for(int L=3;L<=len;L++){
	 	for(int i=0;i+L-1<len;i++){//枚举起始端点 
	 	int j=i+L-1;//终点 
	 		if(S[i]==S[j]&&dp[i+1][j-1]==1){//前一种状态回文存在(他的前一种状态回文的长度必然是他的减一,必然已经遍历过) 
	 		ans=L;//找到了该长度之下的字符串 ,跟新ans 
	 		dp[i][j]=1;
		 }
	 }
}
	
	cout << ans;
	
	return 0;
}

相关习题(持续更新中)

你可能感兴趣的:(算法学习,动态规划,算法,c++)