数据结构与算法之动态规划

数据结构与算法系列

数据结构与算法之哈希表

数据结构与算法之跳跃表

数据结构与算法之字典树

数据结构与算法之平衡二叉树

数据结构与算法之十大经典排序

数据结构与算法之二分查找三模板

数据结构与算法之动态规划

数据结构与算法之动态规划

前言

动态规划即Dynamic Programming,简称DP,无论是在日常生活还是在工程问题中都有着十分广泛的应用,比如最短路径问题,购物满减问题等等。
动态规划也是算法中较难的一个模块,而其中最大的问题在于如何确定状态以及状态转移方程,“状态”这一词在后面说明。
本文将从递归开始一步一步讲解到动态规划。

定义

动态规划是一种将复杂问题拆分成相对简单的子问题自下而上求解每个子问题最优解从而求解初始问题最优解的模型。
以下是维基百科对Dynamic Programming 的定义:

Dynamic Programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems.

复杂问题拆分子问题

与分治法以及递归操作一样,我们需要对问题进行拆分成子问题进行解决,不同在于递归操作将这一拆分过程展现出来并且当出现大量重复子问题时,递归依然会重新解决这些问题,而DP算法则是利用子问题之间的关系直接由子问题开始入手解决,每个子问题只计算一次,并且重复子问题不会再次计算。
这里也暗示了我们动态规划的使用前提之一,即拆分子问题之间有着某种关系且拆分过程会出现大量重复子问题。

自下而上

在说自下而上之前,需要提及一下递归操作,递归操作是从当前需要求解的问题(顶)向下分解成子问题(下),一直分解到子问题满足止归条件,开始从下往上返回结果到当前需要求解的问题。
过程如下,从求解问题向其子问题分解推进,即为自顶而下。

在这里插入图片描述
自下而上则是一个相反的过程,不从求解问题出发,而是从求解问题的子问题出发,一步一步向上推进到顶端需要求解的问题。
在这里插入图片描述
以计算10由多少个1组成为例
自顶而下可以理解为:
10 = 1 + 9,
9 = 1 + 8,

2 = 1 + 1, 
于是计算出2由两个1组成,3由1个1和1个2组成即1个1+2个1=3个1…

自下而上则理解为:
1 + 1 = 2,
1 + 2 = 1 + 1 + 1 = 3

1+ 9 = 1 + 1 + … + 1 = 10
虽然这个例子很简单,但是从其中理解自下而上与自顶而下的区别还是比较容易的,即相当于去除了子问题分解的步骤,直接从子问题倒推到需求解问题。
当然也可以看出其实我们的子问题分解步骤并没有真的舍去,正如前文所述,其实是找到了子问题之间存在的某种关系才从子问题倒推出需求解的问题。那么这一关系数学上我们称之为数列的递推公式在动态规划中称之为状态转移方程。那么以后的问题就是如何定义子问题的状态以及如何确定状态转移方程。

最优解

在自下而上的过程中,我们对于每个子问题都是求解其最优解,因此我们得到的最终要求解的问题当然也是全局最优解。

走四定三

DP问题虽然因题而已,但是呢,在确定是否使用DP解决问题,以及解决问题所需要的步骤还是比较固定的,相信大家在多次练习之后就明白如何去解决这类问题,当然问题有难有简,多学习熟能生巧嘛。
走四定三是博主自己的一种称呼,意思是DP最基本可以按照四个步骤进行求解,等熟练之后如果确定三点即可直接写出动态规划。

走四

走四,四个基本步骤(内心问问自己下面四个问题):

  1. 使用递归的方法进行解决(内心OS:递归能不能做?)
  2. 递归时,是不是多次计算重复子问题(内心OS:递归时候是不是算了好多重复的问题?)
  3. 使用备忘录策略记录每个子问题,从而进行剪枝操作(内心OS:拿个备忘录保存下这些子问题的解,遇到子问题直接掏出这个大宝贝翻翻有没有记录过这个解)
  4. 改用动态规划,自下而上构建递推方程式(内心OS:备忘录都能用,那说明我这边肯定可以动态规划搞一波事情,看看递推还有备忘录策略中有没有能找到的规律)

定三

定三,三个需要确定的点:

  1. 有没有重复子问题,如果有,基本上可以用动态规划
  2. 确定1之后,则确定进行构建递推方程式的状态量并构建状态转移方程
  3. 确定2之后,确定最小子问题的最优解(其实就是使用底层常数个子问题的最优解作为初始值)
    下面就举两个例子来进行示范。
    在这里插入图片描述

经典DP

斐波那契数列

斐波那契数列是最经典且十分简单的一道DP运算题了。

剑指offer
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项。斐波那契数列的定义如下:
  F(0) = 0, F(1) = 1
  F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
  输入:n = 2
  输出:1
示例 2:
  输入:n = 5
  输出:5

走四

走1:
能递归吗?肯定可以呀,上图
在这里插入图片描述

class Solution {
    public int fib(int n) {
        if (n < 2)
            return n;
    return fib(n - 1) + fib(n - 2);
    }
}

走2:
递归过程有重复子问题嘛,当然,继续上图
走3:
有的话,直接来个备忘录记录子问题

在这里插入图片描述

class Solution {
    Map<Integer, Integer> map = new HashMap();
    public int fib(int n) {
        if(map.containsKey(n)){
            return map.get(n);    
        }
        if (n < 2){
            if(!map.containsKey(n)){
                map.put(n, n);
            }
            return n;
            }
    int m = fib(n - 1)%1000000007 + fib(n - 2)%1000000007;
    if(!map.containsKey(n)){
        map.put(n, m%1000000007);
    }
    return m%1000000007;
    }
}

走4:
走四,就放到定三里一并讲解。

定三

首先,自顶而下肯定有重复子问题 => 定1
其次,状态量即为F(N),状态转移方程为F(N) = F(N - 1) + F(N - 2), 其中 N > 1 => 定2
最后,初始值0, 1 => 定3
那么动态规划就是
0      1     1
g      f
g            f = f + g
g            f
      g= f - g    f
      g     f
如上进行移动,仅需两个变量即可完成.

class Solution {
    public int numWays(int n) {
        if(n == 0) return 1;
        int f = 1, g= 0;
        for(int i = 1; i <= n; i++){
            f = f  % 1000000007 + g;
            g = f  - g;
        }
        return f % 1000000007;
    }
}

打家劫舍

今天入门到小进阶:一种最为简单的打家劫舍,具体题目在力扣上有三个难度,自行查看。

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
  输入:[1,2,3,1]
  输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
   偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
  输入:[2,7,9,3,1]
  输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
   偷窃到的最高金额 = 2 + 9 + 1 = 12 。

问题分析
偷还是不偷,这是一个问题

其实打家劫舍就是一个取舍的问题,当前房子偷还是不偷的问题

  1. 如果偷了,那么相邻房屋不能再偷
  2. 如果没有偷,那么可以从相邻房屋继续偷
求解问题

偷的金币最大值

走四

走1:
递归可以做吗?当然可以,根据问题分析,我们只需比较当前房屋偷和不偷所得的金额谁更大选择谁即可,那么迭代也按照条件,当前偷了则跳过相邻房屋,否则继续从相邻房屋进行访问。

class Solution {
    public int rob(int[] nums) {
        if(nums.length==0)
            return 0;
        return myrob(nums, 0);
    }
    
        public int myrob(int[] nums,int cur)
    {
        if(cur >= nums.length)
            return 0;
        
        int steal = nums[cur];
        steal += myrob(nums, cur + 2);
        
        int notSteal=0;
        notSteal += myrob(nums, cur + 1);
        
        int max=Math.max(steal, notSteal);
        return max;
    }
}

走2:
有大量重复子问题吗?当然有,比如第三个房屋如果偷,那么跳到第五个房间进行计算,然而当第四个房间不偷,同样也会跳到第五个房间进行计算。
走3:
备忘录带上,查小本本啦!

class Solution {
    Map<Integer,Integer> map = new HashMap<>();
    public int rob(int[] nums) {
        if(nums.length==0)
            return 0;
        return myrob(nums, 0);
    }
    
        public int myrob(int[] nums,int cur)
    {
        if(cur >= nums.length)
            return 0;
            
        if(map.containsKey(cur))
            return map.get(cur);
        
        int steal = nums[cur];
        steal += myrob(nums, cur + 2);
        
        int notSteal=0;
        notSteal += myrob(nums, cur + 1);
        
        int max=Math.max(steal, notSteal);
        map.put(cur, max);
        return max;
    }
}

走4:
动态规划,当然可以,放到定三一起分析。

定三

定三:当然此处我是做了空间优化,所以只使用了三个变量
首先,确定了肯定有重复子问题的出现 => 定1
其次,状态量确定dp0代表着前一房屋不偷到该房屋后所得金额,dp1代表着前一房屋偷了到该房屋不偷所得金额,dp则取其中最大值最为求解结果,若此时处于第i屋,那么根据问题分析则dp = max{dp0 + nums[i], dp1} => 定2
最后,最底层初始值三者皆为0 => 定3

class Solution {
    public int rob(int[] nums) {
        int dp0 = 0, dp1 = 0, dp = 0;
        for(int i=0; i<nums.length; i++) {
            dp = Math.max(dp1, dp0 + nums[i]); 
            dp0 = dp1;
            dp1 = dp;
        }
        return dp1;
    }
}

总结

动态规划初步讲解就到这里结束了,其实内容不是很多,主要在于练习。
谨记走四定三就可以解决大部分DP问题,总结下来即为以下几个问题:

  1. 是否可以递归?
  2. 递归是否有重复子问题?
  3. 我先用备忘录搞一波事情试试?
  4. 状态量是什么?
  5. 状态转移方程如何构建?
  6. 初始条件是什么?
    自下而上的思维,使得复杂重复问题变得十分简单,动态规划通常在时间复杂度以及空间复杂度上有着很大的优化。

如有兴趣,可以关注我的公众号,每周和你一起修炼数据结构与算法。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT 涓涓清泉

感谢打赏,我会更加努力写更好的

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值