动态规划(基础版)

文章目录

    • 题目分类
    • 算法思想步骤
    • 基础题目
      • A.凑硬币
      • B.爬楼梯
      • C.数塔问题
      • D.有多少不同的路
      • E.青蛙能跳到吗
      • F.判断子序列
      • G.最长上升子序列(LIS)长度
      • H.最长公共子序列 (LCS)长度
      • I.最大子序和
      • J.按摩师
      • K.选值凑数


题目分类

DP一般题目:最值问题/计数问题/存在性问题

算法思想步骤

  1. 假设最后一步,找出相同问题,分析子问题
  2. 根据子问题与原问题的相同部分找出状态方程
  3. 根据状态方程找出转移方程
  4. 确定初始条件:用转移方程算不出来但真实存在的情况
  5. 确定边界情况:一般考虑数组越界问题
  6. 确定计算顺序

基础题目


A.凑硬币

现在有给定面值的硬币,问你~~最少~~ 可以用几枚硬币组合成 n 元钱,所有面值的硬币无限多。(下面用现有2、5、7面值的硬币与凑27元为例解释)

  1. 假设最后一步分析子问题
    原问题:最少几枚硬币凑27
    必定有k枚硬币面值分别为a1、a2、a3…ak 使得他们面值加起来为27
    必有最后一枚硬币k的面值为ak,除掉这枚硬币,前面k-1枚硬币面值加起来为27-ak
    现问题即子问题:最少几枚硬币凑27-ak
  2. 找状态方程
    f(x)=最少用多少枚硬币凑出x
    ak只能为2、5、7
    f(27)=min{f(27-2)+1,f(27-5)+1,f(27-7)+1}
  3. 找转移方程
    设数组f[x]=最少用几枚硬币凑出x
    对于任意x f[x]=min{f[x-2]+1,f[x-5]+1,f[x-7]+1}
  4. 初始条件
    定义f[0]=0。因为据转移方程可得f[0]=正无穷即凑不出来,但事实上不需要硬币即可凑出0即与计算结果不符需提前定义。
  5. 边界情况
    如果凑不出n,定义f[n]为正无穷,即f[-1]=f[-2]=正无穷,数组一旦越界返回正无穷即凑不出来。例如n为1根据转移方程可得f[1]=正无穷即凑不出来。
  6. 计算顺序
    知小才能求大。所有计算顺序为由前向后。
#include 
#include 
#include 
using namespace std; 
#define INF 0x3fffffff
int main()
{
   vector<int> a; //现有面值放在一个数组
    int kind=a.size(); //已知面值种类数量
    int n; //要凑的面值大小 
    cin>>n;
    int f[n+1]; //用0至n,数组开n+1
    f[0]=0; //初始化
    for(int i=1;i<=n;i++)
    {
        f[i]=INF;
        for(int j=0;j<kind;j++)
        {
            if(i>=a[j]&&f[i-a[j]]!=INF)
            f[i]=min(f[i-a[j]]+1,f[i]);
        }  
    }
    if(f[n]==INF)
    f[n]=-1;
    cout<<f[n]<<endl;
}

B.爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有~~多少种不同的方法~~ 可以爬到楼顶呢?
注意:给定 n 是一个正整数。

  1. 假设最后一步分析子问题
    原问题:有多少种方法爬到第n阶楼梯
    要爬到楼顶,最后一步必然是爬1个或两个台阶。假设最后一步爬一阶楼梯,前n-1阶楼梯有X种爬的方法;假设最后一步爬两阶楼梯,前n-2阶楼梯有Y种爬的方法。所以爬n阶楼梯的方法就是X+Y。
    现问题即子问题:有多少种方法爬到第n-1阶楼梯和第n-2阶楼梯
  2. 找状态方程
    f(x)=有多少种方法爬到第x阶楼梯
  3. 找转移方程
    对于任意一阶楼梯 f[x]=f[x-1]+f[x-2]
  4. 初始条件
    爬到第一阶楼梯的方法只有一种,即f[1]=1。
    已经在第0阶楼梯上,所以爬到第0阶楼梯的方法只有1种,即f[0]=1。
  5. 边界情况
    已在初始条件下考虑过。
  6. 计算顺序
    知前才能算后,即由大到小计算顺序。结果为f[n]。
#include 
using namespace std; 
int main()
{
    int n; //n阶楼梯 
    cin>>n;
	int f[n+1];  //利用0...n,数组开到n+1
	f[0]=f[1]=1;
	for(int i=2;i<n+1;i++)
	{
		f[i]=f[i-1]+f[i-2];
	}
	cout<<f[n]<<endl;
}

C.数塔问题

设有一个三角形的数塔,顶点结点称为根结点,每个结点有一个整数数值。从顶点出发,在每一个结点可以选择向左走或者向右走一直走到底层,要求找出一条路径,使路径上的值~~最大~~。
example(示意图,内部存储依然整齐存储):
                          13
                 11               8
          12             7              26
      6          14            15              8
12       7           13               24           11

Sample Output
max=86

  1. 分析子问题
    由上往下推答案不好确定,所以采取将数塔倒过来即从下往上推。
    原问题:从顶点出发到底部数值最大为多少
    顶点到底部最大数值一定是从前面n-1层最大路径值即从左上下来路径的数值总和与从右上下来数值总和加此位置的数值。
    子问题:从顶点出发到第n-1层数值最大为多少
  2. 找状态方程
    f(i)(j)=位置为i,j的数到底部的最大数值和
  3. 找转移方程
    f[i][j]=max{f[i+1][j],f[i+1][j+1]}+a[i][j]
  4. 初始条件
    先将最后一行存入到f数组中。
  5. 边界情况
    暂无数组越界情况。
  6. 计算顺序
    由底部向顶部。答案为f[0][0]。
#include
#include 
using namespace std;
int main()
{
    int a[105][105],f[105][105];
    int n;
    cin>>n;
    for(int i=0;i<n;i++)
    {
        for(int j=0;j<=i;j++)
            cin>>a[i][j];
    }
    for(int i=0;i<n;i++)
        f[n-1][i]=a[n-1][i];             //先将最后一行存入到f数组中
    for(int i=n-1;i>=0;i--)
    {
        for(int j=0;j<=i;j++)
            f[i][j]=max(f[i+1][j],f[i+1][j+1])+a[i][j];     
    }
    cout<<f[0][0];
}

D.有多少不同的路

机器人位于m x n 网格的左上角(下图中标记为"开始")。机器人只能在任何时间点向下或右转。机器人试图到达网格的右下角(下图中标有"完成")。~~有多少~~可能的唯一路径?
动态规划(基础版)_第1张图片
横坐标为(0…m-1)左上角(0,0)
纵坐标为(0…n-1) 右下角(m-1,n-1)

  1. 假设最后一步分析子问题
    原问题:多少种方法从(0,0)走到(m-1,n-1)
    如果问题成立则机器人最后一步可由(m-2,n-1)走到(m,n)或由(m-1,n-2)走到(m,n)。假如机器人有X种方式从(0,0)走到(m-2,n-1),有Y种方式从(0,0)走到(m-1,n-2),则原问题的答案为X+Y。所以可知
    现问题即子问题为:多少种方法从(0,0)走到(m-2,n-1)和(m-1,n-2)
  2. 找状态方程
    f(i,j)=有多少种方法从(0,0)走到(i,j)
  3. 找转移方程
    对于任意一个格子 f[i][j]=f[i-1][j]+f[i][j-1]
  4. 初始条件
    f[0][0]=1,即只有一种方法到(0,0)。
  5. 边界情况
    第0列左边没有格子,第0行上面没有格子,因此走到第0行的格子或走到第0列的格子均只有一个方法。即i=0或j=0时,f[i][j]=1。
  6. 计算顺序
    知左知上才能求,所以从左到右、从上到下计算。即计算第0行…计算第一行…计算第m-1行。结果为f[m-1][n-1]。
#include 
using namespace std; 
int main()
{
    int m,n;
    cin>>m>>n;
    int f[m][n];
    for(int i=0;i<m;i++)
    {
        for(int j=0;j<n;j++)
        {
            if(i==0||j==0)
            f[i][j]=1;
            else
            f[i][j]=f[i-1][j]+f[i][j-1];
        }
    }
    cout<<f[m-1][n-1]<<endl;
}

E.青蛙能跳到吗

给定一个非负整数数组a[],青蛙最初定位在数组的第一个索引处即石头0处。数组中的每个元素表示该位置的最大跳转长度即青蛙在石头i处最大可向右跳的距离为ai。确定青蛙~~是否能够~~到达最后一个索引即跳到最后一个石头n-1上。

  1. 假设最后一步分析子问题
    原问题:青蛙能否跳到石头n-1
    如果青蛙能跳到最后一块石头,那考虑最后一跳即从别的石头i即i 现问题即子问题:青蛙是否能跳到石头i
  2. 找状态方程
    f(x)=青蛙是否能跳到石头x
  3. 找转移方程
    f[x]=枚举0<=i=x))即为true
  4. 初始条件
    青蛙一开始就在第0块石头上即f[0]=true。
  5. 边界情况
    无数组越界情况。
  6. 计算顺序
    由前之后才可算,即从小到大求解。答案为f[n-1]。
#include 
using namespace std; 
int main()
{
    int n; //有多少石头 
    cin>>n;
    int a[n],f[n];  
    for(int i=0;i<n;i++)
    cin>>a[i];
	f[0]=true; 
    for(int x=1;x<n;x++)
    {
    	f[x]=false;
    	for(int i=0;i<x;i++)
    	{
    		if(f[i]&&i+a[i]>=x)
    		{
    			f[x]=true;
    		    break;
			}
		}
	}
    cout<<f[n-1]<<endl;
}

F.判断子序列

给定字符串 s 和 t ,判断 s ~~是否~~为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(如"ace"是"abcde"的一个子序列,而"aec"不是)。

暴力算法:直接对比 O(n)

class Solution {
public:
    bool isSubsequence(string s, string t)
 {
        int sLen = s.length();
        int tLen = t.length();
        if (sLen <= 0)
            return true;
        else if (sLen > tLen) 
            return false;
        int ss = 0;
        for (int i = 0; i < tLen; i++) 
        {
            if (s[ss] == t[i]) 
            {
                ss++;
                if (ss == sLen)
                {
                    return true;
                }
            }
         }
        return false;
    }
};

动态规划: O(n 2 ^{2} 2)

  1. 分析子问题
    原问题:s是否为t的子序列
    若s是t的子序列,从字串最后一个字符开始考虑,若s中最后一个字符与t中任意字符相等,且s中除最后一个字符外是t的子序列,则s是t的子序列。
    子问题:除最后一个字符外的字符串是否为t的子序列
  2. 找状态方程
    f(i)(j)=s中前i个字符是否是t中前j个字符的子序列
  3. 找转移方程
    f[i][j]= if (f[i][j- 1])   f[i][j] = true;
        else if (s[i-1]==t[j-1]&&f[i-1][j-1])  f[i][j] = true;
        else      f[i][j] = false;
  4. 初始条件
    f数组需从1开始存放,但字符串以0开始,所以需考虑f数组第一行与第一列的值,即i为0或j为0的情况,因为空字符串是任意字符串的字串,任意字符串不是空字符串的字串。所以f[i][0]=false,f[0][j]=true。
    若s为空,则答案直接为true。
    若s的长度大于t的长度,则答案直接为false。
  5. 边界情况
    已考虑到4中。但需注意f数组的大小。
  6. 计算顺序
    由前知后。所以由左向右,由上向下计算。答案为f[m][n]。

状态转换示意图
动态规划(基础版)_第2张图片
练习演示推导链接:https://alchemist-al.com/algorithms/is-subsequence

class Solution {
public:
    bool isSubsequence(string s, string t) {
        int m=s.length();
        int n=t.length();
         if (m <= 0) 
            return true;         
         if (m > n)
            return false;        
       bool f[m+1][n+1];
        for(int i=0;i<=m;i++)
        {
            for(int j=0;j<=n;j++)
            {
                if(i==0) f[i][j]=true;
                else if(j==0)  f[i][j]=false;
                else if(i==0&&j==0) f[i][j]=true;
                else
              {           
                if(f[i][j-1]) f[i][j]=true;
                else if(s[i-1]==t[j-1]&&f[i-1][j-1])
                    f[i][j]=true;
                else f[i][j]=false;
              }
            }
        }    
        return f[m][n];
    }
};

G.最长上升子序列(LIS)长度

给定n个数,求这n个数的~~最长~~上升子序列的长度。什么是最长上升子序列? 就是给你一个序列,请你在其中求出一段不断严格上升的部分,它不一定要连续。就像这样:2,3,4,7和2,3,4,6就是序列2 5 3 4 1 7 6的两种选取方案。最长的长度是4。

  1. 分析子问题
    原问题:整个序列最长上升子序列的长度是多少,即以最后一个元素结尾的序列的最长上升子序列长度是多少
    若最后一个数比前n-1个数的最长上升子序列的最后一个数要大,则整个序列的最长上升子序列长度为前n-1个数的最长上升子序列长度+1
    子问题:前n-1个数的最长上升子序列的长度是多少
  2. 找状态方程
    f(x)=以a[x]结尾的序列的最长上升子序列长度
  3. 找转移方程
    f[x]=max{f[p]}(p
  4. 初始条件
    第一个数的最长上升子序列长度为1,即f[0]=1。
  5. 边界情况
    无数组越界情况。
  6. 计算顺序
    知前算后。答案为f数组中的最大值。
class Solution {
public:
    int LIS(vector<int> a) 
 {
   int n=a.size();
   int f[n];
   f[0]=1;
   for(int i=1;i<n;i++)
	  //每次求以第i个数为终点的最长上升子序列的长度 
	{
		int temp=0;   //记录满足条件的,第i个数左边的上升子序列的最大长度 
		for(int j=0;j<i;j++)
		{  //查看以第j个数为终点的最长上升子序列 
			if(a[i]>a[j])
			{
				if(temp<f[j])
					temp=f[j];
			}
		}
		f[i]=temp+1;
	}
	int result;
	for(int i=0;i<n-1;i++)
		result=max(f[i],f[i+1]);
    return result;
   }
  };

H.最长公共子序列 (LCS)长度

给定序列长度为m的字符串s,长度为n的字符串t求s,t的~~最长~~公共子序列长度。例如,{1,3,4,5,6,7,7,8},{3,5,7,4,8,6,7,8,2}的最长公共子序列长度为5。

  1. 分析子问题
    原问题:长度为m的s与长度为n的t最长公共子序列长度是多少
    存在最长公共子序列z,从最后一个字符开始考虑,若s的最后一个字符与t的最后一个字符相同,则z长度=s的前m-1个字符和t的前n-1个字符最长公共子序列长度+1;若不相同,z长度为s的前m-1个字符和t的前n个字符最长公共子序列长度与s的m个字符和t的t前n-1个字符最长公共子序列长度的最大值。
    子问题:s的前m-1个字符和t的前n个字符最长公共子序列长度;s的m个字符和t的t前n-1个字符最长公共子序列长度
  2. 找状态方程
    f(i)(j)=s的前i个字符与t的前j个字符最长公共子序列长度
  3. 找转移方程
    f[i][j]=若s的第i个字符与t的第j个字符相同 f[i-1][j-1]+1
       若s的第i个字符与t的第j个字符不同 max{f[i-1][j],f[i][j-1]}
  4. 初始条件
    空字符串与所有字符串的最长公共子序列长度均为0。为方便计算,从f数组第0行对应的是空字符与t的最长公共子序列长度,第0列对应的是空字符与s的最长公共子序列长度。即f[0][j]=f[i][0]=0。
  5. 边界情况
    暂无数组越界情况。
  6. 计算顺序
    由左到右,由上到下。答案为f[m][n]。

状态转换示意图
动态规划(基础版)_第3张图片

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
  int m=text1.length();
  int n=text2.length();
  int f[m+1][n+1]; //多用一个空字符串的空间
  for(int i=0;i<=m;i++)
  {
  	for(int j=0;j<=n;j++)
  	{
  		if(i==0||j==0)  f[i][j]=0;
  		else
  		{
  			if(text1[i-1]==text2[j-1])
  			f[i][j]=f[i-1][j-1]+1;
  			else
  			f[i][j]=max(f[i-1][j],f[i][j-1]);
		  }
	  }
   } 
   return f[m][n];
    }
};

"选不选"策略

I.最大子序和

给定一个整数数组 nums ,找到一个具有~~最大~~和的连续子数组(子数组最少包含一个元素),求其最大和。

  1. 分析子问题
    原问题:以第i个数结尾的连续子序列中,最大和为多少
    两种选择:选择与第i个数合并即前i个数最大和加此数或不合并即本数
    选两者中的最大值即为答案
    现问题即子问题:以第i-1个数结尾的连续子序列中,最大和为多少
  2. 找状态方程
    f(x)=以第i个数结尾的连续子数组最大和为多少
  3. 找转移方程
    f[x]=max{f[x-1]+nums[i],nums[i]}
  4. 初始条件
    第0个数最大一定是自己本身,即f[0]=nums[0]。
  5. 边界情况
    数组不会越界。
  6. 计算顺序
    知前算后。
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
    int l=nums.size();
	int f[l],maxsum=nums[0]; //保存最大连续子序列的和
	f[0]=nums[0];
	for(int i=1;i<l;i++)
	{
		f[i]=max(f[i-1]+nums[i],nums[i]);
		maxsum=max(maxsum,f[i]);
	}
	return maxsum;
    }
};

J.按摩师

一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列nums,替按摩师找到最优的预约集合(总预约时间~~最长~~),返回总的分钟数。

  1. 分析子问题
    原问题:对于所有即前n个预约即第n个预约,总预约时间最长为多少
    如果选第n个预约,因为不能选相邻时间,所以最长为前n-2个预约的最长时间+第n个预约的预约时间;如果不选,最长为前n-1个预约的最长时间。答案为两者最大值。
    子问题:对于前n-1个预约,前n-2个预约,总预约时间最长为多少
  2. 找状态方程
    f(x)=前x个预约的总预约最长时间
  3. 找转移方程
    对于第任意一个预约 f[x]=max{f[x-1],f[x-2]+nums[i]}
  4. 初始条件
    前一个即第0个预约时间最长为此预约本身时长,即f[0]=nums[0]。
    前两个即第1个预约时间最长为此预约本身与第0个预约时长的最大值,即f[1]=max{f[0],f[1]}。
  5. 边界情况
    若没有预约即n==0或只有一个预约直接返回nums[0]。(提交好几次不通过才考虑到)
  6. 计算顺序
    知前算后。答案为f[n-1]。
class Solution {
public:
    int massage(vector<int>& nums) {
    int n=nums.size();
    if(n == 0) return 0; 
    if(n == 1) return nums[0];
	int f[n];
	f[0]=nums[0];
	f[1]=max(nums[0],nums[1]);
	for(int i=2;i<n;i++)
	{
		f[i]=max(f[i-2]+nums[i],f[i-1]);		
	}
	return f[n-1];
    }
};

K.选值凑数

给定一组数arr(如:3, 34, 4, 12, 5, 2)和S(如:9),若~~~~从所给的数中,选出若干个数使它们的和为S,则输出true,否则输出false。

  1. 分析子问题
    原问题:对于所有数是否存在能凑出S的可能
    如果存在能够凑出的可能,有选和不选两种方法。如果选择当前数字,则需判断是否存在能凑出S-当前数值的可能;如果不选当前数字,则继续考虑其他数字能否凑出S。
    子问题:对于部分数是否存在凑出S-某一数值的可能
  2. 找状态方程
    f(i)(j)=对于第i个数选和不选能否凑成j
  3. 找转移方程
    f[i][j]=若选该数 f[i-1][j-arr[i]]
            OR
        不选该数 f[i-1][j]
        (两者一种成立即可,用or连接)
        前面所有数已经将目标数值凑好 f[i-1][j]
  4. 初始条件
    当需要凑的数为0时,一定可以凑出来,即f[i][0]=true。
  5. 边界情况
    当选第一个数时,能凑出来的只有和他大小相同的数,其他都凑不出来,即f[0][arr[0]]=true,f[0][除arr[0]以外的数]=false。
  6. 计算顺序
    从左到右,从上到下。答案为f[arr的大小][S]。

状态示意图

i\j 0     1     2     3     4     5     6     7     8     9
3
34
4
12
5
2
class Solution {
public:
    bool func(vector<int>& arr,int S) 
  {       
   int n=arr.size();
   bool f[n][S+1];
   for(int i=0;i<n;i++)
   f[i][0]=true;
   for(int j=0;j<=S;j++)
   {
   	if (arr[0]==j)
   	f[0][j]=true;
   	else f[0][j]=false;
   }
   for(int i=1;i<n;i++)
   {
   	for(int j=1;j<=S;j++)
   	{
   		if(arr[i]>j) //已经凑得与前面状态相同
		   f[i][j]=f[i-1][j];
	    else	   
	    	f[i][j]=f[i-1][j]|f[i-1][j-arr[i]];
	}
   }
   return f[n-1][S];    
   }
};

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