动态规划(DP)

动态规划(DP)
用来优化加速,时间复杂度从指数(Exponential) 加速到多项式时间(Polynomial)。
何时使用DP?

  • 计数问题:求有多少种方法
  • 优化问题:max和min问题

使用DP的要求

  • 最优子结构
  • 重复子问题:求解子问题时,会出现重复计算(overlapping)。如果没有重复计算,则退化成分治算法(divide and conquer)
  • 无后效性:调用子问题最优解时,子问题的最优解不发生改变。

使用方法

  1. 带有记忆性的递归
    举例:斐波那契序列
  2. DP
    举例:LIS(最长上升子序列),LCS(最长公共子序列)

典型题目

斐波那契序列

分析
递推公式和边界条件已知。存在重复计算的问题,类似带有记忆性的递归,即使用数组保存先前的结果,减少重复计算。

开一个数组dp[]
for i in range(3,n)
dp[i]=dp[i-1]+dp[i-2]
仅需一个for loop

代码

int DP(int n)
{
	int dp[n+1];
	dp[1]=1;dp[2]=1;
	for(int i=3;i<=n;i++)
		dp[i]=dp[i-1]+dp[i-2];
	return dp[n];
}

三角形数

题目
给定一个由行数字组成的数字三角形。试着设计一个算法,计算出从三角形的顶到底的一条路径,使得该路径经过的数字总和最大。
测试数据

1
2 3
20 5 6
7 8 9 10
最大值 31

思路1:
自底向上方向,MaxSum[ i ] [ j ]表示第i行第j列的数到最底下路径和的最大值。
MaxSum[ i] [ j] 和MaxSum[ i ][ j+1]中的较大者,作为第i-1行的转移项。
代码

#include<iostream>

using namespace std;

const int maxn=1e2+2;

int D[maxn][maxn],MaxSum[maxn][maxn]; 
int main()
{
	int i,j;
	int N;
	cin>>N;//输入N行测试数据
	for(i=1;i<=N;i++)
		for(j=1;j<=i;j++) 
			cin>>D[i][j];

	for(i=1;i<=N;i++)//最大权值和初始化为最后一列 
		MaxSum[N][i]=D[N][i];
		
	for(i=N;i>1;i--)
		for(j=1;j<i;j++)
		{
			if(MaxSum[i][j]>MaxSum[i][j+1])//比较左右两边数据大小 
				MaxSum[i-1][j]=D[i-1][j]+MaxSum[i][j];//更新上一行最大权值的和 
			else
				MaxSum[i-1][j]=D[i-1][j]+MaxSum[i][j+1];
		}
	//最大值在MaxSum[1][1]
	cout<<MaxSum[1][1]<<endl;	
} 

需要注意
注意数组的边界,数组不是从0开始,这里使用从1开始。
对于第i行,需要使用第i+1行的数据,对于for(i=N;i>1;i–)这里

思路2:自上到下,思路在下方代码上面。

下面这道题目可能包含负数。

Acwing898. 数字三角形

输入格式
第一行包含整数n,表示数字三角形的层数。

接下来n行,每行包含若干整数,其中第 i 行表示数字三角形第 i 层包含的整数。

输出格式
输出一个整数,表示最大的路径数字和。

数据范围
1≤n≤500,
−10000≤三角形中的整数≤10000
输入样例:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出样例:
30


/*

数字三角形思路分析;

状态表示f[i,j]
    集合:所有从起点到(i,j)的路径
    属性:max

状态计算
f[i,j]可以来自左上和右上两种情况:
    左上:f[i-1][j-1]+a[i][j]
    右上:f[i-1][j]+a[i][j]

*/


/*
    dp问题的时间复杂度分析:  状态数量 × 转移的计算量
    
    本题:状态数量n^2 ,转移计算量O(1),所以本题时间复杂度O(n^2)
*/

#include<bits/stdc++.h>
using namespace std;
const int N=510,INF=1e9;
int n;
int f[N][N];

int a[N][N];

int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=i;j++)
            cin>>a[i][j];
    }
    
    //初始化多初始化两侧两列:因为计算 f[i][1]需要用到f[i-1][0],这里第0列需要初始化
     for(int i=1;i<=n;i++){
        for(int j=0;j<=i+1;j++)
            f[i][j]=-INF;
    }
    int res=-INF;
    
    f[1][1]=a[1][1];
    
    for(int i=2;i<=n;i++){
        for(int j=1;j<=i;j++)
        {
                f[i][j]= max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j]);
        }
             
    }
    //结果在最后一行取
    for(int i=1;i<=n;i++) res=max(res,f[n][i]);
    cout<<res<<endl;
    
    
    
    
    
}

最小路径和

Leetcode 64
题目
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。

示例:

输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。

ac代码

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int m=grid.size();
        int n=grid[0].size();
        vector<vector<int>> f(m,vector<int>(n,0));//m*n的两维vector,全0
        //状态
         f[0][0]=grid[0][0];
        for(int i=1;i<n;i++)//第一行
            f[0][i]+=grid[0][i]+f[0][i-1];
            
        f[0][0]=grid[0][0];
        for(int i=1;i<m;i++)//第一列
            f[i][0]+=grid[i][0]+f[i-1][0];
        //转移
        for(int i=1;i<m;i++)
            for(int j=1;j<n;j++)
                 f[i][j]=grid[i][j]+min(f[i-1][j],f[i][j-1]);            
        return f[m-1][n-1];

    }
};

最长上升子序列(LIS)

题目链接:Acwing895. 最长上升子序列
问题描述
一个数的序列bi,当b1 < b2 < … < bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, …, aN),我们可以得到一些上升的子序列(ai1, ai2, …, aiK),这里1 <= i1 < i2 < … < iK <= N。比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序列中最长的长度是4,比如子序列(1, 3, 5, 8)。你的任务,就是对于给定的序列,求出最长上升子序列的长度。
输入数据
输入的第一行是序列的长度N(1<=N<=1000)。第二行给出的序列中的N个整数,这些整数的取值范围都是0-10000。
输出要求
最长上升子序列的长度。

输入样例
7
1 7 3 5 9 4 8
输出样例
4

分析
求 a 1 _1 1,a 2 _2 2,…,a k _k k,…,a n _n n 的最长上升子序列
子问题是求 a 1 _1 1,a 2 _2 2,…,a k _k k的最长上升子序列

dp[i]表示终点是a i _i i 的最长上升子序列的长度
动态规划

  1. 状态转移 : dp[ i ] = max { dp[ j ]+1 , dp[ i ] } , 对于所有的 j< i ,并且 f j _j j<f i _i i
    含义: 以a j _j j为终点的最长子序列长度加上 a i _i i本身 就是 以 a i _i i结尾的最长子序列的长度, 即 dp[ j ]+1
  2. base cases :dp[ i ] =1 ,i ∈ [0,n)
    含义:初始时每个位置的最长上升子序列长度都为1,即它本身

时间复杂度 O(n 2 ^2 2)

代码

#include<iostream>
using namespace std;

const int maxn=1010;
int f[maxn],dp[maxn];
//dp[i]表示终点是i的最长上升子序列的长度 
int ans=1;//保存结果 
int main()
{
	int N;
	cin>>N;
	for(int i=1;i<=N;i++)
	{
		cin>>f[i];
		dp[i]=1;//边界处理 
	}

	//转移 
	for(int i=1;i<=N;i++)
	{
		for(int j=1;j<i;j++)
		{
			if(f[j]<f[i]) dp[i]=max(dp[i],dp[j]+1);
		}		
		ans=max(ans,dp[i]);
	}
	cout<<ans;		
} 

补充最长下降子序列(LDS)和最长上升子序列(LIS)分装成函数:

#include<iostream>

using namespace std;
const int maxn=1010;

long long a[maxn],dp[maxn];

//最长下降子序列 
long long LDS( long long a[],int N)
{
	long long  temp=0;
	for(int i=1;i<=N;i++)
	 dp[i]=1;
	 for(int i=1;i<=N;i++)
	 {
	 	for(int j=1;j<i;j++)
	 	{
	 		if(a[j]>a[i])
	 			dp[i]=max(dp[j]+1,dp[i]);
	 	}
	 	temp=max(dp[i],temp);
	 }
	 
	 return temp;
}
//最长上升子序列 
long long LIS( long long a[],int N)
{
	long long  temp=0;
	for(int i=1;i<=N;i++)
	 dp[i]=1;
	 for(int i=1;i<=N;i++)
	 {
	 	for(int j=1;j<i;j++)
	 	{
	 		if(a[j]<a[i])
	 			dp[i]=max(dp[j]+1,dp[i]);
	 	}
	 	temp=max(dp[i],temp);
	 }
	 
	 return temp;
}

int main()
{
	
	int N;
	cin>>N;
	for(int i=1;i<=N;i++) 
		{
			cin>>a[i];
		}
	cout<<LDS(a,N)<<endl;
	cout<<LIS(a,N)<<endl; 
}

最长公共子序列(LCS)

Leetcode1143. 最长公共子序列
问题描述
输入两个字符串, 要你求出两个字符串的最长公共子序列长度。
输入
输入两行不超过200的字符串。
输出
给出两个字符串的最大公共字符串的长度。

样例输入
abcfbc
abfcab
样例输出
4

分析
状态转移
我们需要看字符串 s1 第i个字母 和 字符串 s2 第j个字母的关系
if s1 i _i i == s2 j _j j
同时去掉各自最后的字母,看s1 i _i i- 1 _1 1 和 s2 j _j j- 1 _1 1
得到 dp[ i ] [ j ] = dp [ i-1 ] [ j -1 ]+1

if s1 i _i i != s2 j _j j
若去掉 s1 i _i i ,看 s1 i _i i- 1 _1 1 和 s2 j _j j 的公共子序列 与
若去掉 s2 j _j j,看s1 i _i i和 s2 j _j j- 1 _1 1的公共子序列
取两者最大值 dp[ i ] [ j ] = max (dp [ i ] [ j-1 ],dp [ i-1] [ j ] );

时间复杂度 O ( m × n ) O(m \times n) O(m×n),两个字符串的长度。

代码

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m=text1.size();
        int n=text2.size();
        int dp[m+1][n+1];
        
       memset(dp,0,sizeof(dp));//置零
        for(int i=1;i<=m;i++)
            for(int j=1;j<=n;j++)
            {
                if(text1[i-1]!=text2[j-1])
                dp[i][j]=max(dp[i][j-1],dp[i-1][j]);

                if(text1[i-1]==text2[j-1])
                dp[i][j]=dp[i-1][j-1]+1;
            }
        return dp[m][n]; 
    }
};

板子题目链接:Acwing897. 最长公共子序列

ac代码

#include<bits/stdc++.h>
using namespace std;

const int N=1000;
int n,m;
int dp[N+1][N+1];
int main(){
    string t1,t2;
    cin>>n>>m;
    cin>>t1>>t2;
   
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(t1[i-1]==t2[j-1])
                dp[i][j]=dp[i-1][j-1]+1;
            else{
                dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
            }
        }
        
    }
    cout<<dp[n][m]<<endl;
    
}

将字符串翻转成单增

Leetcode 926
如果一个由 ‘0’ 和 ‘1’ 组成的字符串,是以一些 ‘0’(可能没有 ‘0’)后面跟着一些 ‘1’(也可能没有 ‘1’)的形式组成的,那么该字符串是单调递增的。

我们给出一个由字符 ‘0’ 和 ‘1’ 组成的字符串 S,我们可以将任何 ‘0’ 翻转为 ‘1’ 或者将 ‘1’ 翻转为 ‘0’。

返回使 S 单调递增的最小翻转次数。

示例 1:
输入:“00110”
输出:1
解释:我们翻转最后一位得到 00111.
示例 2:
输入:“010110”
输出:2
解释:我们翻转得到 011111,或者是 000111。
示例 3:
输入:“00011000”
输出:2
解释:我们翻转得到 00000000。
提示:
1 <= S.length <= 20000
S 中只包含字符 ‘0’ 和 ‘1’

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/flip-string-to-monotone-increasing

分析
求最长不降子序列(LIS),字符串长度减去LIS 即可
使用 DP算法求解LIS超时。

超时代码(思路是对的)
时间复杂度O( n 2 n^2 n2)

class Solution {
public:
    int minFlipsMonoIncr(string S) {
        int len=S.size();
        int dp[len+1];
        for(int i=0;i<=len;i++)
            dp[i]=1;
         int ans=0;
        for(int i=0;i<len;i++)
        {
            for(int j=0;j<i;j++)
            {
                if(S[j]<=S[i])
                dp[i]=max(dp[i],dp[j]+1);
            }
            ans=max(dp[i],ans);
        }
        return len-ans;
    }
     
};

优化代码(AC)
维护单调数列,使用二分法。

采用upper_bound()函数,返回第一个大于x的位置这样的话,遇到相等的元素,则一直会添加到数组中来
时间复杂度O(nlogn)
空间复杂度O(n)

class Solution {
public:
    int minFlipsMonoIncr(string S) {
        int len=S.size();
        vector<char> vec;
        for(int i=0;i<len;i++)
        {
            //p是大于S[i]的下标
            int p = upper_bound(vec.begin(),vec.end(),S[i])-vec.begin();
            if(vec.size()==p) //新来的是最大的
                vec.push_back(S[i]);//加入数组
            else//新来的比原位置的小
                vec[p]=S[i];替换掉
        }
       return len-vec.size();//返回字符串长度-LIS
    }
     
};


迷雾森林

补充一道简单的dp题

链接:迷雾森林
来源:牛客网

赛时提示:保证出发点和终点都是空地

帕秋莉掌握了一种木属性魔法
这种魔法可以生成一片森林(类似于迷阵),但一次实验时,帕秋莉不小心将自己困入了森林
帕秋莉处于地图的左下角,出口在地图右上角,她只能够向上或者向右行走

现在给你森林的地图,保证可以到达出口,请问有多少种不同的方案

答案对2333取模

输入描述:
第一行两个整数m , n表示森林是m行n列
接下来m行,每行n个数,描述了地图
0 - 空地
1 - 树(无法通过)
输出描述:
一个整数表示答案
此题需要在计算过程中%2333,同时在处理边界的时候,出现1的话后面的都不能通过;这里处理是多用了一行的一个格,省掉了一行和一列的边界处理。

#include<bits/stdc++.h>
using namespace std;
const int maxn=3010;

int a[maxn][maxn];
int n,m;
long long  dp[maxn][maxn];


template<class T>inline void read(T &res)
{
	char c;T flag=1;
	while((c=getchar())<'0'||c>'9')if(c=='-')flag=-1;res=c-'0';
	while((c=getchar())>='0'&&c<='9')res=res*10+c-'0';res*=flag;
}


int main(){
	cin>>m>>n;
	for(int i=1;i<=m;i++){
		for(int j=1;j<=n;j++)
			read(a[i][j]);
	}
	memset(dp,0,sizeof(dp));
	dp[m+1][1]=1;
	
	for(int i=m;i>=1;i--){
		for(int j=1;j<=n;j++){
			if(a[i][j]!=1)
			dp[i][j]=(dp[i+1][j]+dp[i][j-1])%2333;
		}
	}
	
		cout<<dp[1][n]<<endl;
//	cout<<endl;
//	for(int i=1;i<=m;i++){
//		for(int j=1;j<=n;j++)
//			cout<<dp[i][j]<<" ";
//		cout<<endl; 
//	}
	
	

	
	
}

/*
3 5
0 1 0 0 0
0 0 0 0 0
0 0 1 0 0


*/

感谢您阅读到最后,点击下面链接可轻松跳转。

祝您阅读愉快。

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值