标签: 动态规划

动态规划入门(个人总结)

本篇其实是去年的时候在部门内部做的一次简单的分享的内容,也是对自己坚持学习了一个多月的动态规划内容的一些简单的总结和整理

从斐波那契数列到01背包问题

起始

在正式介绍动态规划之前,我们先从一个简单且不陌生的题目开始
斐波那契数列

首先我们来看下这个题目
斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由01开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

这里我们题面已经明确的给出了实现的逻辑了,是的,就是一眼就能看出来的递归。

最基本的实现代码如下,非常的简单,也很好理解

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

但是,真的就这样结束了么?显然不是,首先我们能直观的看到这个是一个递归的过程,如果输入的n不是很大尚且还好,但是当你的n足够大的时候,比如20000?虽然这也不是非常大,那么会发生什么呢?

此时也许你会说,那我可以改jvm参数,增大可用栈深度,那你的栈深度又能增加到多少呢?200万?还是2000万? 我们不妨仔细分析一下这个过程

F(   n   ) = F(n - 1) + F(n - 2)
F( n - 1 ) = F(n - 2) + F(n - 3)
F( n - 2 ) = F(n - 5) + F(n - 4)
F( n - 3 ) = F(n - 4) + F(n - 5)

可以很容易的看出来,中间经历的大量的重复运算,思考一下,我们是不是有什么办法能把这个中间的大量的重复运算的过程省略过去呢?

这个时候你也许会想到哈希表,是的,这个方向是对的,不过在面对这种int的key的时候,我们一般有一种更加简单更加轻便的作法来实现哈希表的功能,那就是数组。我们可以声明一个int[]的数组来实现我们需要的功能,且其本身比Map型数据结构更加的轻便简单,相对的速度也更快

而且,这个过程中,我们不再是从F(N)F(0)求解,而是正向的从0N求解,那么代码如下

class Solution {
    public int fib(int n) {
        if (n<2){
            return n;
        }
        int[] dp = new int[n+1];
        dp[1] = 1;
        int idx = 1;
        while (++idx <=n){
            dp[idx] = dp[idx-1] + dp[idx-2];
        }
        return dp[n];
    }
}

我们在算法中,给这样这种的数组一个独特的名字叫滚动数组,这样是不是就规避了最大栈深度的限制了呢?那我们入参给个Integer.MAX_VALUE看看会发生什么?内部的值相加是否会超出int型上限我们姑且先不论,直接入参给入跑一下看看

那么接下来我们要对这份代码再做一点优化,而数组长度上限的问题,你可以自己再思考下看看这个问题"java中数组的长度上限是多少"而又为什么会有这个上限。

拓展小知识:循环遍历和递归是可以相互转化的,最经典的例子莫过于二叉树的深度优先搜索(DFS)和广度优先搜索(BFS),一个是递归遍历,一个是循环遍历,这是另外一个不小的话题,这里不做展开讨论了

现在我们再次观察下这份代码,n位置的值,只和n-1n-2位置的值有关,那么我们就确实没有必要声明一个长度为n+1的数组,对此我们做一点点修改

Continue reading

LeetCode刷题【1359】有效的快递序列数目

给你 n 笔订单,每笔订单都需要快递服务。

请你统计所有有效的 收件/配送 序列的数目,确保第 i 个物品的配送服务 delivery(i) 总是在其收件服务 pickup(i) 之后。

由于答案可能很大,请返回答案对 10^9 + 7 取余的结果。

 

示例 1:

输入:n = 1
输出:1
解释:只有一种序列 (P1, D1),物品 1 的配送服务(D1)在物品 1 的收件服务(P1)后。

示例 2:

输入:n = 2
输出:6
解释:所有可能的序列包括:
(P1,P2,D1,D2),(P1,P2,D2,D1),(P1,D1,P2,D2),(P2,P1,D1,D2),(P2,P1,D2,D1) 和 (P2,D2,P1,D1)。
(P1,D2,P2,D1) 是一个无效的序列,因为物品 2 的收件服务(P2)不应在物品 2 的配送服务(D2)之后。

示例 3:

输入:n = 3
输出:90

 

提示:

  • 1 <= n <= 500
Related Topics
  • 数学
  • 动态规划
  • 组合数学

  • 👍 44
  • 👎 0
  • 数学求解推导过程


    简单来说,第n对快递在原来的排序中插入放置的情况有先PD的依赖性,不妨来分析下摆放情况,如上图,分别确定D的位置为蓝色,P的情况为绿色的可能性,

    那么总和的结果为

    (2n-1) + (2n-2) + (2n-3) + ... + 1

    这就是一个从1到(2n-1)的求和过程,转化之后为

    (2n-1) * 2n / 2

    2 * n^2 - n

    这是第n对快递的情况,而第(n-1)的情况可以由之前求得,这里简单的用f(n-1)表示

    所以最终我们知道

    f(n) = f(n-1) * ( 2 * n^2 - n )

    这样我们从1开始滚动求解得到f(n)的具体数值中途记得模1e9+7

    代码

    class Solution {
        public int countOrders(int n) {
            long ans = 1;
            long mod = (long) (1e9+7);
            for (int i = 2; i <= n; i++) {
                ans *= (2 * i * i - i);
                ans %= mod;
            }
            return (int)ans;
        }
    }

    LeetCode刷题【1155】掷骰子的N种方法

    这里有 n 个一样的骰子,每个骰子上都有 k 个面,分别标号为 1 到 k

    给定三个整数 nk 和 target ,返回可能的方式(从总共 kn 种方式中)滚动骰子的数量,使正面朝上的数字之和等于 target 。

    答案可能很大,你需要对 109 + 7 取模 。

     

    示例 1:

    输入:n = 1, k = 6, target = 3
    输出:1
    解释:你扔一个有6张脸的骰子。
    得到3的和只有一种方法。
    

    示例 2:

    输入:n = 2, k = 6, target = 7
    输出:6
    解释:你扔两个骰子,每个骰子有6个面。
    得到7的和有6种方法1+6 2+5 3+4 4+3 5+2 6+1。
    

    示例 3:

    输入:n = 30, k = 30, target = 500
    输出:222616187
    解释:返回的结果必须是对 109 + 7 取模。

     

    提示:

    • 1 <= n, k <= 30
    • 1 <= target <= 1000
    Related Topics
    • 动态规划

  • 👍 139
  • 👎 0
  • 二维简单DP
    简单DP算法,想要得到结果target,那么我们单独拿出来假定的最后一个骰子,这个骰子有1k种可能性
    继而对应在得到target之前,没有这个骰子的时候需要算出的target-1target-k种情况的总和,
    那么我们就很容易的知道了转移方程

    dp[i][target] = dp[i-1][target-1] + .... + dp[i-1][target-k]

    初始一个情况,只有一个骰子的时候,1k的值都只有1种组合情况

    代码

    class Solution {
    
        /**
         * dp[i][target] = dp[i-1][target-1] + .... + dp[i-1][target-k]
         */
        public int numRollsToTarget(int n, int k, int target) {
            int mod = (int) 1e9 + 7;
            int[][] dp = new int[n][target+1];
            for (int i = 1; i <= k && i <= target; i++) {
                dp[0][i] = 1;
            }
            for (int row = 1; row < n; row++) {
                for (int col = row+1; col <= target; col++) {
                    for (int i = 1; i <= k; i++) {
                        if (i>col){
                            continue;
                        }
                        dp[row][col] += dp[row-1][col-i];
                        dp[row][col] %= mod;
                    }
                }
            }
            return dp[n-1][target];
        }
    }

    LeetCode刷题【剑指 Offer 43】1~n 整数中 1 出现的次数

    输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。

    例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。

     

    示例 1:

    输入:n = 12
    输出:5
    

    示例 2:

    输入:n = 13
    输出:6

     

    限制:

    • 1 <= n < 2^31

    注意:本题与主站 233 题相同:https://leetcode-cn.com/problems/number-of-digit-one/

    Related Topics
    • 递归
    • 数学
    • 动态规划

  • 👍 343
  • 👎 0
  • 按位计算统计,(【左侧数值】当前位1【右侧数值】)
    直接拿一个数字31261分析下,过程如下

        3      1      2      6      1
                                    3126 + 1
                             3130
                      3200
                3000 + 261 + 1
        10000
    1. 最低位当前数字是1,这个位置上1粗线的次数由他前面的数字决定,所以我们可以非常明白的知道,这个时候这一位一共出现了3127次字符1
      • 分别为有前缀的3126 次
      • 和无前缀的 当数字n = 1的时候的1次
    2. 倒数第二位数字6,这个数字大于1,根据上面算最低位的时候的情况分析依据,我们可以知道这个位置一共出现了3130次,这个结果可以也可以分为两部分
      • 有前缀的时候3120种情况,数字xxx1x的情况,当前位前面有312种情况,而又因为当前位大于1,后面一位的0-9的10种情况,即为3120种
      • 无前缀的时候1019的10种情况
      • 这样,我们暂时把这部分换为另外一种算法,即为( 312 + 1 ) * 10 种。
    3. 再往前一位数字2,同前面的数字6的情况,因为大于1所以结果为(31 + 1) * 100
    4. 再次往前一位,数字1,虽然前面为1的情况我们已经分析过了,但是那个是不完整的,在这里,我们将完整分析下为1的时候的情况
      • 因为后面还有其他数字,整个31xxx区段是还没完全结束的,所以我们不能直接得到结果为4000
      • 那么前面部分的情况为012的3000种情况,也可以认为如果左侧数字是k,那么现在就有k000
      • 又因为之前说了这个31xxx区段是还没完全结束,所以应当还有当前位后面部分的261种情况,外加如果后面都为0的情况
      • 那么这个位置数字1出现的次数就是3000 + 261 + 1 = 3262
    5. 最高位为3,大于1,可以直白的知道有10000次出现了字符1,那么按照前面的规律,我们假定最高位为0,按照之前的方法可以得到( 0 + 1 ) * 10000
    6. 举例数据中没有出现的当前位为0的情况,这个就比较简单了,直接就是左侧数据出现的次数了
      • 如 30261 中0的位置,
      • 就是数字1xxx,11xxx,21xxx的3000种情况

    计算中间的问题

    我们在计算中使用了一个辅助变量10,100,100010000,这个数值是比入参数字大一位的

    又因为题目给的入参条件1 <= n < 2^31

    所以中间计算过程我就偷了个懒给转成了long型数据

    另外,葱低往高取每一位的数字的时候可以直接用余10,之后再除10的方法,我这边一开始是准备转成数组直接根据数组计算左右侧数据影响的当前位置1出现次数的,最后就还是没改了

    代码

    class Solution {
        /*
        3      1      2      6      1
                                    3127
                             3130
                      3200
                3000 + 261 + 1
         10000
         */
    
    
        final static int [] sizeTable = { 9, 99, 999, 9999, 99999, 999999, 9999999,
                99999999, 999999999, Integer.MAX_VALUE };
    
        public int countDigitOne(int n) {
            long ans = 0;
            long num = (long) n;
            int size = numSize(n);
            int[] arr = new int[size];
            int idx = size-1;
            while (n != 0){
                arr[idx] = n%10;
                n /= 10;
                idx--;
            }
            idx = size-1;
            long tmp = 10;
            while (idx >= 0){
                long r = 0;
                if (arr[idx] == 0){
                    r = ( num / tmp ) * ( tmp / 10 );
                }
                if (arr[idx] == 1){
                    r = ( num / tmp ) * ( tmp / 10 ) + ( num % (tmp / 10) ) + 1;
                }
                if (arr[idx] > 1){
                    r = ( (num / tmp)+1 ) * ( tmp / 10 );
                }
                ans += r;
                tmp *= 10;
                idx--;
            }
    
            return (int)ans;
        }
    
    
    
        static int numSize(int x) {
            for (int i=0; ; i++)
                if (x <= sizeTable[i])
                    return i+1;
        }
    }

    LeetCode刷题【689】三个无重叠子数组的最大和

    给你一个整数数组 nums 和一个整数 k ,找出三个长度为 k 、互不重叠、且全部数字和(3 * k 项)最大的子数组,并返回这三个子数组。

    以下标的数组形式返回结果,数组中的每一项分别指示每个子数组的起始位置(下标从 0 开始)。如果有多个结果,返回字典序最小的一个。

     

    示例 1:

    输入:nums = [1,2,1,2,6,7,5,1], k = 2
    输出:[0,3,5]
    解释:子数组 [1, 2], [2, 6], [7, 5] 对应的起始下标为 [0, 3, 5]。
    也可以取 [2, 1], 但是结果 [1, 3, 5] 在字典序上更大。
    

    示例 2:

    输入:nums = [1,2,1,2,1,2,1,2,1], k = 2
    输出:[0,2,4]
    

     

    提示:

    • 1 <= nums.length <= 2 * 104
    • 1 <= nums[i] < 216
    • 1 <= k <= floor(nums.length / 3)
    Related Topics
    • 数组
    • 动态规划

  • 👍 320
  • 👎 0
  • 动态规划【图解】

    解题思路

    1. 根据题面长度k区间,自然会想到的是滑动窗口来构建一个新的数组sumK
    2. 接下来的思路稍微棘手点,但是如果你做过42. 接雨水,并且是用动态规划的方法来做的话,那么就会非常好理解了学了好久的动态规划再来做这题
    3. 接雨水的思路是:找到左侧的最大值,同时找到右侧的最大值
    4. 而这里,不能单纯的从左边右边移动一位,而是对应的需要移动距离k之外的最大值
    5. 看图示例

    1. 首先求得sumK数组 3 3 3 8 13 12 6
    2. 接下来分别求左侧和右侧的最大值,下标0,1没有左侧区间,下标5,6也没有右侧区间
    3. 左侧最大值从下标2开始,对应值为sumK[0],之后往右移动,下标5的左侧最大值为sumK[3],下标6同理
    4. 右侧最大值为从右往左,原理同左侧最大值
    5. 最终只遍历中间部分左右侧最大值都有的区间,找到和最大的那个值,并记下对应的下标索引

    代码

    class Solution {
    
        /*
        1,  2,  1,  2,  6,  7,  5,  1    k = 2
        3   3   3   8  13  12   6
                3   3   3   8  13
       13  13  13  12   6
         */
    
    
        public int[] maxSumOfThreeSubarrays(int[] nums, int k) {
            int[] sumK = new int[nums.length-k+1];
            for (int i = 0; i < k; i++) {
                sumK[0] += nums[i];
            }
            for (int i = 1; i < sumK.length; i++) {
                sumK[i] = sumK[i-1] + nums[i+k-1] - nums[i-1];
            }
    //        System.out.println(Arrays.toString(sumK));
            int[][] dp = new int[sumK.length][2];
            boolean flag = false;
            for (int i = k; i < sumK.length - k; i++) {
                int j = sumK.length - i - 1;
                if (flag){
                    dp[i][0] = sumK[i-k] > sumK[dp[i-1][0]] ? i-k : dp[i-1][0];
                    dp[j][1] = sumK[j+k] >= sumK[dp[j+1][1]] ? j+k : dp[j+1][1];
                }else{
                    flag = true;
                    dp[i][0] = i-k;
                    dp[j][1] = j+k;
                }
    //            System.out.println(sumK[dp[i][0]] + "    "+sumK[dp[j][1]]);
            }
    //        for (int[] ints : dp) {
    //            System.out.println(Arrays.toString(ints));
    //        }
            int[] res = new int[3];
            int max = 0;
            for (int i = k; i + k < sumK.length; i++) {
                int sum = sumK[i] + sumK[dp[i][0]] + sumK[dp[i][1]];
    //            System.out.println("i  "+i + "  sum"+ sum);
                if ( sum > max){
                    max = sum;
                    res[0] = dp[i][0];
                    res[1] = i;
                    res[2] = dp[i][1];
                }
            }
            return res;
        }
    }

    LeetCode刷题【剑指 Offer 49】丑数

    我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。

     

    示例:

    输入: n = 10
    输出: 12
    解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。

    说明:  

    1. 1 是丑数。
    2. n 不超过1690。

    注意:本题与主站 264 题相同:https://leetcode-cn.com/problems/ugly-number-ii/

    Related Topics
    • 哈希表
    • 数学
    • 动态规划
    • 堆(优先队列)

  • 👍 356
  • 👎 0
  • dp,算是dp吧

    解题思路

    此处撰写解题思路

    代码

    class Solution {
        public int nthUglyNumber(int n) {
            int[] dp = new int[n];
            dp[0] = 1;
            int[] numArr = new int[]{2,3,5};
            int[] pArr = new int[3];
            for (int i = 1; i < n; i++) {
                dp[i] = Math.min(Math.min(dp[pArr[0]]*numArr[0],dp[pArr[1]]*numArr[1]),dp[pArr[2]]*numArr[2]);
                for (int p = 0; p < pArr.length; p++) {
                    if (dp[i] == dp[pArr[p]]*numArr[p]){
                        pArr[p]++;
                    }
                }
            }
            return dp[n-1];
        }
    }

    LeetCode刷题【32】最长有效括号

    给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

     

    示例 1:

    输入:s = "(()"
    输出:2
    解释:最长有效括号子串是 "()"
    

    示例 2:

    输入:s = ")()())"
    输出:4
    解释:最长有效括号子串是 "()()"
    

    示例 3:

    输入:s = ""
    输出:0
    

     

    提示:

    • 0 <= s.length <= 3 * 104
    • s[i]'('')'
    Related Topics
  • 字符串
  • 动态规划

  • 👍 1880
  • 👎 0
  • 栈模拟,java

    class Solution {
        public int longestValidParentheses(String s) {
            Stack<Integer> stack = new Stack<>();
            stack.push(-1);
            int idx = -1;
            int ans = 0;
            while (++idx < s.length()) {
                //表示上一个未被匹配的左括号'('
                if (s.charAt(idx) == '(') {
                    stack.push(idx);
                } else {
                    //这里是右括号‘)’
                    stack.pop();
                    if (!stack.isEmpty()) {
                        ans = Math.max(ans, idx - stack.peek());
                    }else{
                        stack.push(idx);
                    }
                }
            }
            return ans;
        }
    }
    
    1. 栈底元素是有效区间的起始位置
    2. 之后栈内只插入左括号的位置
    3. 每遇到一个右括号,则从栈顶弹出一个元素
    4. 如果弹出的是左括号,那么就能知道当前的有效区间的长度
    5. 如果弹出的右括号,此时栈内为空,之前的有效区间不存在了,后面再能遇到的是新的有效区间,需要从当前位置重新开始表示,那么就把当前值插入到栈底部
    6. 在每次得到有效区间的时候,依次比较取较大值