DP(动态规划)

  • DP(普通一维问题)
  • 背包DP
  • 树形DP
  • 状压DP
  • 数位DP
  • DP的常见优化

动态规划和递归的区别就是——》动态规划实现了对子问题的存储

动态规划 题目特点:

  • 1.计数(问:how many ways。。。)
    • a.有多少种方式 走到右下角
    • b.有多少种方法 选出k个数使得和是sum
  • 2.求最大值、最小值(最大的一个解题类型)
    • a.从左上角走到右下角 路径的最大数字和
    • b.最长上升子序列的长度
  • 3.求存在性
    • a.取石子游戏,先手是否必胜
    • b.能不能选出k个数 使得和是sum

一、引入:例题

1.(LintCode 669):Coin Change——第【2】种类型 求最大最小值的DP
有三种硬币,分别面值2元,5元,7元,每种硬币都足够多
买一本书需要27元
求:如何用最少的硬币组合正好付清,不需要对方找钱

1.动态规划组成部分一:确定状态

  • 简单的说,解动态规划的时候需要开一个数组,数组的每个元素f[i] 或 f[i][j] 代表着什么(类似于 数学中的,设x,y,z)
  • 怎么找 这个状态?——找到“最后一步”(结束条件)
  • 虽然我们不知道最优策略是什么,但 最优策略肯定是K枚硬币 :a1+a2+…+ak = 27
  • 所以,一定有一枚“最后的”硬币:ak
  • 除掉这枚硬币,前面硬币的面值加起来是 27-ak

图示如下:
在这里插入图片描述

关键点:

  • 我们不关心前面的 k-1枚硬币是怎么拼出 27-ak 的(可能有很多种拼法),我们甚至不知道 ak 和 k 的(值)是什么。
    但是 我们确定 前面的硬币拼出了 27-ak

  • 因为是最优策略,所以拼出27-ak的硬币数也一定是最少的(也就是说,27-ak 也一定是最优策略),否则这就不是最优策略了
    (由此归纳出来) 子问题:

  • 我们将原问题转化成了一个子问题,而且规模更小:最少用多少枚硬币可以拼出 27-ak

  • 归纳上述子问题,我们可以得出: 设 状态 f(x) = 最少用多少枚硬币拼出X 【f(x)是硬币数,x是总钱数】

      可是,我们还不知道最后那枚硬币ak是多少。
      但 我们可以确定:最后那枚硬币只能是2,5或7
      所以
      if(ak == 2) f(27) = f(27-2) + 1 ;    //加上最后这一枚 硬币2
      if(ak == 5) f(27) = f(27-5) + 1 ;    //。。。硬币5
      if(ak == 7) f(27) = f(27-7) + 1 ;    //。。。硬币7
      除此以外,没有其他的可能了
      我们要求最少的硬币数(求三种可能的最小值 即可),所以:
      f(27) = min{ f(27-2)+1 , f(27-5)+1 , f(27-7)+1 }
    
  • 到这,可能会有一个疑问——》这不是用的 递归 算法吗?

  • 我们先来用递归写一下:

int f(int x){     //f(x) = 最少要用多少硬币拼出x 
    //结束条件
    if(x==0) return 0;
    int res = MAX_VALUE;    //因为要求最小值,所以初始化先为最大值
    if(x>=2){    //最后一枚硬币是2元
        res = Math.min(res,f(x-2)+1);    
    }
    if(x>=5){    //最后一枚硬币是5元
        res = Math.min(res,f(x-5)+1);    
    }
    if(x>=7){    //最后一枚硬币是7元
        res = Math.min(res,f(x-7)+1);    
    }
    return res;
}
  • 代码执行时的思路:
    在这里插入图片描述我们可以看出,有很多数被重复算了很多次—-》这些重新计算完全没必要(浪费时间)—-》所以,我们想要优化这段代码—-》也就是动态规划的思想

动态规划 = 递归+记忆化

动态规划 会将计算过的结果保留下来,需要用的时候直接获取,以此来避免重复的计算

2.动态规划组成部分二:转移方程

其实这一步很简单,我们根据 上一步“确定状态” 归纳出的f(x)就可以写出 转移方程:

  • 设 状态f[x] = 最少用多少枚硬币拼出x
  • 对于任意x, f[x] = min{ f[x-2]+1 , f[x-5]+1 , f[x-7]+1 } 注意:这里的 f[] 是方括号—-》一开始就说了,动态规划是要定义一个数组来解决问题的

3.动态规划组成部分三:初始条件+边界情况

  • 两个问题:

  • x-2 , x-5 , x-7 小于 0 怎么办?

  • 递归什么时候停下来?

  • 解决:

  • 我们定义:如果不能拼出x,那么f[x] = 正无穷

      为什么要定义成 正无穷,而不是其他值呢?
      —-》因为我们的状态转移方程为:f[x] = min{ f[x-2]+1,f[x-5]+1,f[[x-7]+1] };
      例如,x=2时
      f[2] = min{ f[0]+1,f[-3]+1,f[-5]+1 };
      而题中,x<0 是不允许出现的(越界条件)—-》f[x]是算不出来的
      所以,必须给 f[x] 定义一个值,
      但这个值定义为其他任何数都是有风险的(可能会出现另两个值比 f[0]+1 小的情况)-—-》因此,我们定义:拼不出来x f[x] = 正无穷
    

例如:f[-1]=f[-2]=…=正无穷
f[1] = min{ f[-1]+1 , f[-4]+1 , f[-6]+1 } = 正无穷,表示拼不出来 1

  • 初始条件:f[0] = 0 ; //初始条件 就是:用状态转移方程算不出来的,但这个值还有意义,需要手动定义的值
    (这里我们清楚的知道,f[0]是得0的—-》x=0,那么不需要硬币去拼)

4.动态规划组成部分四:计算顺序

  • 计算顺序: f[1] , f[2] , f[3] , … , f[27]

      - 为什么计算顺序(递归顺序)为:f[1],f[2],…,f[27] ?
      - 难道动态规划的计算顺序都是这样的?
      答:并不是,但大多是。
      本题 :当我们计算到 f[x] 时,f[x-2],f[x-5],f[x-7]一定都是已经得到结果的了
      (因为计算 f[x] 要用到后面那堆东西)
    
  • 小结(求动态规划题的基本思路)

    • 1.确定状态
      • 最后一步(最优策略中 使用的最后一枚硬币ak)
      • 化成子问题(最少的硬币拼出更小的面值27-ak)
    • 2.转移方程
      • 根据状态,确定转移方程(f[x] = min{ f[x-2]+1 , f[x-5]+1 , f[x-7]+1 }
    • 3.初始条件和边界情况
      • f[0] = 0
      • 如果不能拼出x , f[x] = 正无穷
    • 4.计算顺序
      • f[0] , f[1] , … , f[x]
        ========================================

以上为b站视频的总结;以下是我自己的理解

【重点】经典例题(简单一维dp)

1.斐波那契数列

1 1 2 3 5 8 …

这是最经典的递归问题,
但 如果用递归求解,会重复计算一些子问题。
那如何用 动态规划 求解呢。

题目描述:求斐波那契数列的第n项,n<39。
在这里插入图片描述

  • 递归法
    根据递推公式:f(n) = f(n-1)+f(n-2)
int fib(int n){
	if(n<2) return n;
	return fib(n-1)+fib(n-2);
}
  • dp
    • 1.状态 : 最后一步是求f[n]
    • 2.转移方程:f[n] = f[n-1]+f[n-2]
    • 3.初始化:f[1]=1 ;边界条件:n<=1
    • 4.计算顺序:1—>n
public int Fibonacci(int n){
	if(n <= 1) return n;	//边界条件
	int[] fib = new int[n+1];
	fib[1] = 1;		//初始化
	fib[2] = 1;
	for(int i=2;i<=n;i++){	//计算顺序
		 fib[i] = fib[i-1] + fib[n-2];	//状态方程
	return fib[n];
}

2.矩形覆盖

题目描述:我们可以用2*1的小矩形横着或竖着去覆盖更大的矩形。请问用n2*1的小矩形无重叠的覆盖一个2*n的大矩形,总共有多少种方法?

  • 分析:dp[1] = 1 ; dp[2] = 2

      要覆盖2*n的大矩形,
      可以先覆盖一个2*1的矩形,再覆盖2*(n-1)的矩形;
      也可以先覆盖两个个2*2的矩形,再覆盖2*(n-2)的矩形。
      而覆盖2*(n-1)和2*(n-2) 可以看做是子问题,传递下去
    

在这里插入图片描述

- 最后一步:求 dp[n]
- 初始化:dp[1] = 1 ; dp[2] = 2;  边界条件:n<=2
- 转移方程(递归表达式):dp[n] = dp[n-1] + dp[n-2]
- 计算顺序:1-->n
  • 递归法
public int rectCover(int n){
	if(n<=2) return n;
	return rectCover(n-1)+rectCover(n-2);
}
  • dp算法
public int rectCover(int n){
	if(n<=2) return n;
	int[] dp = new int[n+1];
	dp[1] = 1;
	dp[2] = 2;
	for(int i=3;i<=n;i++){
		dp[i] = dp[i-1]+dp[i-2];
	}
	return dp[n];
}

3.跳台阶

题目描述:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级台阶总共有多少种跳法。
在这里插入图片描述

  • 分析

      int[] dp = new int[n]; //dp[i]表示跳到第i级台阶有多少种跳法
      
      状态(最后一步):d[n]
      初始化:dp[1] = 1 ; dp[2] = 1 ;
      边界条件:n<=2;
      状态转移方程:dp[i] = dp[i-1]+dp[i-2]; 	//dp[i]的状态,要么从i-1的台阶跳1级到i	; 要么从i-2级台阶一次跳2级到i
      计算顺序:1-->n 	//计算 dp[i] 需要先计算 dp[i-1] 和 dp[i-2]
    
public int jumpFloor(int n){
	if(n<=2) return n;
	int[] dp = new int[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];
}

4.变态跳台阶

题目描述:一只青蛙可以跳上1级台阶,也可以跳上2级…它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
在这里插入图片描述

  • 分析

      最后一步:求 dp[n] //跳上n级台阶的方案数
      初始化:dp[1] = 1,dp[2] = 1,...,dp[n] = 1
      状态转移方程:dp[i] = dp[i-1]+dp[i-2]+...+dp[1]	//从所有台阶上都可以调到i级台阶上去
      计算顺序:1-->n
    
  • 代码实现

public int jumpFloorII(int n){
	int[] dp = new int[n+1];
	Arrays.fill(dp,1);	//把dp数组中所有元素初始化为1
	//对于每一级台阶,方案数都是前面所有台阶的方案数的和
	for(int i=1;i<=n;i++){	
		for(int j=1;j<i;j++){
			dp[i] += dp[j];
		}
	}
	return dp[n];
}

参考资料

  • 【动态规划专题班】ACM总冠军、清华+斯坦福大神带你入门动态规划算法-哔哩哔哩】 https://b23.tv/jmLSCrC
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值