21天掌握动态规划 第二天

21天掌握动态规划

第二天 线性动态规划

2.1 53. 最大子序和

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

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

示例 2:

输入:nums = [1]
输出:1

示例 3:

输入:nums = [0]
输出:0

示例 4:

输入:nums = [-1]
输出:-1

示例 5:

输入:nums = [-100000]
输出:-100000

提示:

  • 1 < = n u m s . l e n g t h < = 3 ∗ 1 0 4 1 <= nums.length <= 3 * 10^4 1<=nums.length<=3104

  • − 1 0 5 < = n u m s [ i ] < = 1 0 5 -10^5 <= nums[i] <= 10^5 105<=nums[i]<=105

  • 状态描述: d p [ i ] dp[i] dp[i]表示下标为 i i i的最大子序和,那么 d p [ i ] dp[i] dp[i]来源于下标为 i − 1 i - 1 i1的最大子序和 d p [ i − 1 ] dp[i-1] dp[i1]和当前的下标的数组值 n u m s [ i ] nums[i] nums[i]进行相关取舍。

    • 如果当前值 n u m s [ i ] > = 0 nums[i] >= 0 nums[i]>=0,那必然 d p [ i − 1 ] + n u m s [ i ] > = d p [ i − 1 ] dp[i-1] + nums[i] >= dp[i-1] dp[i1]+nums[i]>=dp[i1];反之如果当前值 n u m s [ i ] < 0 nums[i] < 0 nums[i]<0,那必然 d p [ i − 1 ] + n u m s [ i ] < d p [ i − 1 ] dp[i-1] + nums[i] < dp[i-1] dp[i1]+nums[i]<dp[i1].
  • 初始化:第一项 d p [ 0 ] = 0 dp[0] = 0 dp[0]=0

  • 状态转移方程: d p [ i ] = m a x ( d p [ i − 1 ] + n u m s [ i ] , n u m s [ i ] ) dp[i] = max(dp[i-1] + nums[i],nums[i]) dp[i]=max(dp[i1]+nums[i],nums[i])

  • 先根据初始化和状态转移方程可以知道到达每一个下标的最大子序和的值,但是此时还不知道整个数组的整体子序和的最大值,因此需要再遍历一遍 d p dp dp数组,然后进行比较取到最大值。

  • 时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( n ) O(n) O(n)

class Solution {
    public int maxSubArray(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n + 1];
        dp[0] = nums[0];
        for (int i = 1; i < n; i++) {
            dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]); 
        }
        int maxn = nums[0];
        for (int i = 0; i < n; i++) {
            maxn = Math.max(dp[i],maxn);
        }
        return maxn;
    }
}

空间复杂度进行优化

  • 可以发现 d p [ i ] dp[i] dp[i]只跟它的前一项 d p [ i − 1 ] dp[i-1] dp[i1]和当前下标的数组值 n u m s [ i ] nums[i] nums[i]有关,因此可以考虑用滚动数组将空间优化。
  • 时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)
class Solution {
    public int maxSubArray(int[] nums) {
        int pre = 0, res = nums[0];
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            pre = Math.max(pre + nums[i],nums[i]);
            res = Math.max(pre,res);
        }
        return res;
    }
}

2.2 198. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 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 <= nums.length <= 100

  • 0 <= nums[i] <= 400

  • 状态定义: d p [ i ] dp[i] dp[i]表示到达下标为 i i i的房屋一夜之内所能够偷窃到的最高金额

  • 初始化:当 n = = 1 n ==1 n==1的时候,最高金额就是 n u m s [ 0 ] nums[0] nums[0]。当 n = = 2 n == 2 n==2的时候, d p [ 0 ] = n u m s [ 0 ] , d p [ 1 ] = M a t h . m a x ( n u m s [ 0 ] , n u m s [ 1 ] ) dp[0] = nums[0],dp[1] = Math.max(nums[0],nums[1]) dp[0]=nums[0],dp[1]=Math.max(nums[0],nums[1]),比较前面两个值的最大值。

  • 当前能偷窃的最高金额来源于

    • 距离当前房屋距离为2的最高金额 d p [ i − 2 ] dp[i-2] dp[i2] 与当前房屋藏的金额 n u m s [ i ] nums[i] nums[i]的总和
    • 当前房屋相邻的前一间房屋所能够偷得的最高金额
  • 状态转移方程: d p [ i ] = m a x ( d p [ i − 2 ] + n u m s [ i ] , d p [ i − 1 ] ) dp[i] = max(dp[i-2] + nums[i],dp[i-1]) dp[i]=max(dp[i2]+nums[i],dp[i1])

由此,可以写出该题目代码,

Java代码如下:

class Solution {
    
    
    public int rob(int[] nums) {
    
    
        int n = nums.length;
        int[] dp = new int[n];
        dp[0] = nums[0];
        if (n == 1) {
    
    
            return dp[0];
        }
        dp[1] = Math.max(nums[0],nums[1]);
        for (int i = 2; i < n; i++) {
    
    
            dp[i] = Math.max(dp[i - 2] + nums[i],dp[i - 1]);
        }
        return dp[n - 1];
    }
}

空间复杂度优化

n > = 2 n >= 2 n>=2的时候, 当前所能够偷得的最高金额 d p [ i ] dp[i] dp[i]只直接取决于前面两项的 d p [ i − 2 ] + n u m s [ i ] 和 d p [ i − 1 ] dp[i-2] + nums[i]和dp[i-1] dp[i2]+nums[i]dp[i1],因此这里也可以用滚动数组优化空间复杂度

p r e pre pre替代 d p [ i − 2 ] dp[i-2] dp[i2],用 c u r cur cur替代 d p [ i − 1 ] dp[i-1] dp[i1],然后每次往后移的时候(也就是随着 i + + i++ i++),现在的 d p [ i − 2 ] dp[i-2] dp[i2]会变成之前的 d p [ i − 1 ] dp[i-1] dp[i1],因此类似于每次同步往后滚动移位的样子,就也可以类似 p r e = c u r , c u r = t m p pre = cur,cur = tmp pre=cur,cur=tmp,本来最后需要返回的是 d p [ n − 1 ] dp[n-1] dp[n1],空间优化后也就是需要返回 c u r cur cur就行。

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

2.3 213. 打家劫舍 II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

示例 1:

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 3:

输入:nums = [0]
输出:0

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 1000

这个题与198. 打家劫舍题,唯一的区别在于房子首部和房子尾部是连接起来成一个环。大致思路是一样的

  • 状态定义: d p [ i ] dp[i] dp[i]表示到达下标为 i i i的房屋一夜之内所能够偷窃到的最高金额

  • 初始化:当 n = = 1 n ==1 n==1的时候,最高金额就是 n u m s [ 0 ] nums[0] nums[0]。当 n = = 2 n == 2 n==2的时候, d p [ 0 ] = n u m s [ 0 ] , d p [ 1 ] = M a t h . m a x ( n u m s [ 0 ] , n u m s [ 1 ] ) dp[0] = nums[0],dp[1] = Math.max(nums[0],nums[1]) dp[0]=nums[0],dp[1]=Math.max(nums[0],nums[1]),比较前面两个值的最大值。

  • 当前能偷窃的最高金额来源于

    • 距离当前房屋距离为2的最高金额 d p [ i − 2 ] dp[i-2] dp[i2] 与当前房屋藏的金额 n u m s [ i ] nums[i] nums[i]的总和
    • 当前房屋相邻的前一间房屋所能够偷得的最高金额
  • 状态转移方程: d p [ i ] = m a x ( d p [ i − 2 ] + n u m s [ i ] , d p [ i − 1 ] ) dp[i] = max(dp[i-2] + nums[i],dp[i-1]) dp[i]=max(dp[i2]+nums[i],dp[i1])

  • 多一个处理的细节,可以取两部分的下标闭区间[0,n-2],[1,n-1]的最大值。

    • 这里有个细节要注意,按理来说也可以选择不偷第一家,然后最后偷到了倒数第二家,这样子最后一家按理来说也不能偷,也就是我下面这张图的第三部分:“去除头部同时去除尾部”。

      那么这里给出一个证明:

      • 假设 f ( i 0 , i 1 , i 2 , . . . , i n ) f(i_0,i_1,i_2,...,i_n) f(i0,i1,i2,...,in)表示下标从 0 0 0 ~ n n n的房屋所能偷窃的最高金额,那么添加任何一个不为0的金额的房屋进去,即只要满足 n u m s [ i n + 1 ] > = 0 nums[i_{n+1}] >= 0 nums[in+1]>=0 那么 f ( i 0 , i 1 , i 2 , . . . , i n , i n + 1 ) > = f ( i 0 , i 1 , i 2 , . . . , i n ) f(i_0,i_1,i_2,...,i_n,i_{n+1}) >= f(i_0,i_1,i_2,...,i_n) f(i0,i1,i2,...,in,in+1)>=f(i0,i1,i2,...,in).同理可得:添加一个非负数的金额在数组头部,可以得到 f ( i p , i 0 , i 1 , i 2 , . . . , i n ) > = f ( i 0 , i 1 , i 2 , . . . , i n ) f(i_p,i_0,i_1,i_2,...,i_n) >= f(i_0,i_1,i_2,...,i_n) f(ip,i0,i1,i2,...,in)>=f(i0,i1,i2,...,in)关系式。由此可知不需要额外考虑这种同时去除头部或者尾部的情况。

这里可以拿198. 打家劫舍题的结论,先对 n = 1 n=1 n=1的时候特判一下,然后在主函数里取两部分下标为闭区间[0,n-2]与[1,n-1]的结果的最大值。

Java代码如下:

class Solution {
    
    
    public int rob(int[] nums) {
    
    
        if (nums.length == 1) {
    
    
            return nums[0];
        }
        return Math.max(myrob(Arrays.copyOfRange(nums,0,nums.length-1)),myrob(Arrays.copyOfRange(nums,1,nums.length)));
    }

    public int myrob(int[] nums) {
    
    
        int pre = 0;
        int cur = 0;
        for (int i = 0; i < nums.length; i++) {
    
    
            int tmp = Math.max(cur,pre + nums[i]);
            pre = cur;
            cur = tmp;
        }
        return cur;
    }
}

2.4 740. 删除并获得点数

给你一个整数数组 nums ,你可以对它进行一些操作。

每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除 所有 等于 nums[i] - 1nums[i] + 1 的元素。

开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。

示例 1:

输入:nums = [3,4,2]
输出:6
解释:
删除 4 获得 4 个点数,因此 3 也被删除。
之后,删除 2 获得 2 个点数。总共获得 6 个点数。

示例 2:

输入:nums = [2,2,3,3,3,4]
输出:9
解释:
删除 3 获得 3 个点数,接着要删除两个 2 和 4 。
之后,再次删除 3 获得 3 个点数,再次删除 3 获得 3 个点数。
总共获得 9 个点数。

删除并获得 n u m s [ i ] nums[i] nums[i]的点数,同时失去 n u m s [ i ] − 1 nums[i] - 1 nums[i]1 n u m s [ i ] + 1 nums[i] + 1 nums[i]+1的值,可以想象成一个新的数组 d p dp dp,然后 d p [ j ] = n u m s [ i ] dp[j] = nums[i] dp[j]=nums[i],那么就是不能同时选择 d p [ j − 1 ] dp[j - 1] dp[j1] d p [ j + 1 ] dp[j+1] dp[j+1],这样子就把此问题具体成了198. 打家劫舍的版本了。

  • 第一步先构造新数组 d p dp dp。找到数组 n u m s nums nums中的最大值 m a x n maxn maxn,然后构造长度为 m a x n maxn maxn的数组 d p dp dp
  • 第二步填充初始化数组 d p dp dp。采用的是类似于 n u m s nums nums中的每个元素填充到该数值 n u m s [ i ] nums[i] nums[i]的对应位置即下标为 j = n u m s [ i ] j = nums[i] j=nums[i]上去,使得 d p [ j ] = n u m s [ i ] dp[j] = nums[i] dp[j]=nums[i]
  • 第三步确定重复元素的填充,因为只要删除了 n u m s [ i ] nums[i] nums[i],那么所有等同于 n u m s [ i ] − 1 nums[i]-1 nums[i]1 n u m s [ i ] + 1 nums[i]+1 nums[i]+1的元素也都会删除,假设 n u m s [ i ] nums[i] nums[i]出现的次数用 c i c_i ci表示,那么就是要删除 c i − 1 ∗ ( n u m s [ i ] − 1 ) + c i + 1 ∗ ( n u m s [ i ] + 1 ) c_{i-1}*(nums[i]-1) + c_{i+1}*(nums[i]+1) ci1(nums[i]1)+ci+1(nums[i]+1),选择了 c i ∗ n u m s [ i ] c_i*nums[i] cinums[i],于是我们可以把 c i ∗ n u m s [ i ] c_i*nums[i] cinums[i]累加到同一个下标元素上去,将 c i ∗ n u m s [ i ] c_i*nums[i] cinums[i]看作是一个数 d p [ n u m s [ i ] ] ∗ c i dp[nums[i]]*c_i dp[nums[i]]ci
  • 第四步确定 r o b rob rob函数

Java代码如下:

class Solution {
    
    
    public int deleteAndEarn(int[] nums) {
    
    
        int maxn = nums[0];
        int n = nums.length;
        for (int i = 0; i < n; i++) {
    
    
            maxn = Math.max(maxn,nums[i]);
        }
        int[] dp = new int[maxn + 1];
        for (int x : nums) {
    
    
            dp[x] += x;
        }
        return rob(dp);
    }

    public int rob(int[] nums) {
    
    
        int n = nums.length;
        int pre = 0, cur = 0;
        int sum = 0;
        for (int i = 0;i < n; i++) {
    
    
            sum = Math.max(pre + nums[i],cur);
            pre = cur;
            cur = sum;
        }
        return cur;
    }
}

喜欢的可以关注微信公众号『进阶的算法小白』,每天更新一点算法知识,一起学习。

猜你喜欢

转载自blog.csdn.net/qq_41688840/article/details/118446064