当前位置: 首页 > news >正文

从0开始掌握动态规划

动态规划的核心思想 -- 以空间换时间

复杂点说通过分解问题为子问题并存储子问题解来优化复杂计算的算法策略。

简单看个问题。

一,初始:求最长连续递增子序列

nums = [10,9,2,5,3,7,101,18]

求上面数组中的最长连续递增子序列,输出其长度

暴力解法中我们需要以每个数为开头去遍历数组,得到最长的子数组。

会产生如下遍历

3,7,101

7,101

可以看到,有重复的计算。我们先用java写一下暴力解法。

暴力解法
    public int lengthOfLIS(int[] nums) {if (nums == null || nums.length == 0) {return 0;}int res = 0;for (int i = 0; i < nums.length; i++) {int temp = 1; // 当前子序列的长度,初始为1(单个元素)for (int j = i + 1; j < nums.length; j++) {if (nums[j] > nums[j - 1]) {temp++;} else {break;}}res = Math.max(res, temp); // 更新最长子序列的长度}return res;}

双重for循环,时间复杂度为 (O(n^2)),空间复杂度为 (O(1))。

动态规划

初始化dp的int数组。值为到当前位置的最大连续数。这样整个遍历完dp数组的值应该是

[1,1,1,2,1,2,3,1]。 可以看到最大长度为3。

    /*** 方法二: 动态规划求解连续递增子序列* @param nums* @return*/public int lengthOfLIS2(int[] nums) {if (nums == null || nums.length == 0) {return 0;}int n = nums.length;int[] dp = new int[n];dp[0] = 1; // 单个元素本身就是一个长度为 1 的连续递增子序列int res = 1; // 初始化结果为 1for (int i = 1; i < n; i++) {if (nums[i] > nums[i - 1]) {dp[i] = dp[i - 1] + 1;} else {dp[i] = 1; // 当前元素本身可以作为一个新的连续递增子序列的起点}res = Math.max(res, dp[i]); // 更新最长子序列的长度}return res;}

时间复杂度:(O(n))
空间复杂度:(O(n))

二,升级:求最长递增子序列

nums = [10,9,2,5,3,7,101,18]

求上面数组中的最长递增子序列,输出其长度

将上面一道题进行稍微变化一下,我们可以看出,现在的结果应该是2,3,7,101。输出结果应该是4。

如果不用动态规划,这道题有点难解。需要用到回溯+记忆,单纯的暴力解法并不适用。

比如我再加一个测试用例

nums =[0,1,0,3,2,3]

 回溯法
    /*** 方法一: 暴力解法* @param nums* @return*/public int lengthOfLIS(int[] nums) {if (nums == null || nums.length == 0) return 0;int maxLen = 0;for (int i = 0; i < nums.length; i++) {maxLen = Math.max(maxLen, backtrack(nums, i, new ArrayList<>()));}return maxLen;}// 回溯法生成所有递增子序列private int backtrack(int[] nums, int index, List<Integer> current) {if (index >= nums.length) return current.size();int maxLen = current.size();  // 不选当前元素的默认长度// 选择当前元素的情况if (current.isEmpty() || nums[index] > current.get(current.size() - 1)) {current.add(nums[index]);int lenWith = backtrack(nums, index + 1, current);current.remove(current.size() - 1);maxLen = Math.max(maxLen, lenWith);}// 不选当前元素的情况int lenWithout = backtrack(nums, index + 1, current);return Math.max(maxLen, lenWithout);}

时间复杂度: (O(2^n))

空间复杂度:O(n)

该方法容易超过时间限制

动态规划

这个就需要在上个动态规划样例中稍微改动一下。

我们先模拟一下对照样例生成的dp数组应该的样子。

int[] nums = {10, 9, 2,  5, 3, 7, 101, 18};
int[] dp =   {1,  1, 1,  2, 2, 3,  4,  4};

先举个简单的方便观察的例子

int[] nums = {1, 2, 3, 4, 2, 6, 7};
int[] dp =   {1, 2, 3, 4, 1, 5, 6};

即便当数组为递减数组时,长度也会输出1

  1. 所以我们还是需要先将dp数组全部初始化1
  2. 第一次遍历,2 > 1的,所以dp[1] = dp[0] + 1;
  3. 第二次遍历,3 > 1的,所以dp[2] = dp[0] + 1; 然后 3 > 2的,所以 dp[2] = dp[1] + 1;
  4. ……
  5. 第五次遍历,6 > 4的,所以dp[5] = dp[3] + 1;但是6 > 2,不能得出dp[5] = dp[4] + 1,需要dp[4] + 1与当前的dp[5]的值进行比较。取最大的一个。
  6. ……

所以综上我们可以写出代码

    /*** 方法二: 动态规划求解递增子序列* @param nums* @return*/public int lengthOfLIS2(int[] nums) {if (nums == null || nums.length == 0) {return 0;}int n = nums.length;int[] dp = new int[n];Arrays.fill(dp, 1);int res = 1; // 初始化结果为 1for (int i = 1; i < n; i++) {for (int j = 0; j < i; j++) {if (nums[j] < nums[i]) {dp[i] = Math.max(dp[i], dp[j] + 1);}res = Math.max(res, dp[i]);}}return res;}

 时间复杂度为 ​O(n²),空间复杂度为 ​O(n)。

三,再升级:删除最少元素,使剩余元素先递增后递减

动态规划

还是以刚刚的例子为例

nums = [10,9,2,5,3,7,101,18]

删除最少元素,使剩余元素先递增后递减,输出最少需要删除多少元素。

 通过上面两个例子,我们可以看出一个dp数组只能保存一个状态。

对于这道题,我们可以考虑使用两个dp数组来完成。

nums = [10,9,2,5,3,7,101,18]
int[] dp1 =   {1,  1, 1,  2, 2, 3,  4,  4}; //递增dp
int[] dp2 =   {4,  3, 1,  2, 1, 1,  2,  1}; //递减dp

 可以看出在dp1[6] + dp2[6] = 6的时候是最大的,所以最大长度应该是dp1[6] + dp2[6] - 1 = 5;

因为6这个节点重复计算了,所以需要减1

那么就能得出答案需要删除:nums.length() - 5 = 3;

最少需要删除3个元素。

    /*** 方法二: 动态规划求解删除最少元素,使剩余元素先递增后递减* @param nums* @return*/public int lengthOfLIS3(int[] nums) {if (nums == null || nums.length == 0) {return 0;}int n = nums.length;int[] dp1 = new int[n];int[] dp2 = new int[n];Arrays.fill(dp1, 1);Arrays.fill(dp2, 1);// 递增子序列for (int i = 1; i < n; i++) {for (int j = 0; j < i; j++) {if (nums[j] < nums[i]) {dp1[i] = Math.max(dp1[i], dp1[j] + 1);}}}// 递减子序列for (int i = n - 2; i >= 0; i--) {for (int j = n - 1; j > i; j--) {if (nums[j] < nums[i]) {dp2[i] = Math.max(dp2[i], dp2[j] + 1);}}}// 合并int max = 0;for (int i = 0; i < n; i++) {if (dp1[i] + dp2[i] > max) {max = dp1[i] + dp2[i];}}return n - max - 1;}

 四,看一道力扣原题

322. 零钱兑换

 这道题可以换个角度看看,我们要求amount的最小硬币个数。可以构建dp[amount + 1]数组,

求从1 - amount间的所有最小个数。

换个简单的实例

输入:coins = [1, 2], amount = 5

以外层coins为循环,内层循环amount得到最小值

如上面例子。

初始化dp数组,因为要求最小,所以初始值应该为最大,且dp[0] = 0;

第一次外层循环1得到dp数组[0,1,2,3,4,5]

第二次外层循环2得到dp数组[0,1,1,2,2,3]

构建dp公式:

dp[i] = Math.min(dp[i], dp[i - coin] + 1);

 得到解法

    public int coinChange(int[] coins, int amount) {int[] dp = new int[amount + 1];Arrays.fill(dp, amount + 1); // 初始化为不可达标记dp[0] = 0; // 基础情况// 外层循环遍历硬币for (int coin : coins) {// 内层循环遍历金额(完全背包正序遍历)for (int i = coin; i <= amount; i++) {dp[i] = Math.min(dp[i], dp[i - coin] + 1);}}return dp[amount] > amount ? -1 : dp[amount];}

相关文章:

  • 跟康师傅学Java-面向对象(基础)
  • 秒杀系统解决两个核心问题的思路方法总结:1.库存超卖问题;2.用户重复抢购问题。
  • linux 内核 container_of 宏的原理
  • 批量上传OpenStack镜像
  • python中参数前**的含义
  • 数据结构-前缀树
  • 【Vue 2中的emits声明与Vue 3的defineModel宏函数详解】
  • 蓝牙网关的功能与应用场景
  • Doris的向量化执行如何支撑分布式架构和复杂查询
  • 深度剖析:生成式人工智能备案和登记的关键差异
  • 【HFP】深入解析蓝牙 HFP 协议中呼叫转移、呼叫建立及保持呼叫状态的机制
  • 【强内核论】(6) 核心竞争力:价格不是决定成交的主要因素 | 决定对方买单的是人设 | 老陈头 | 数字游牧社
  • 采用若依vue 快速开发系统功能模块
  • 安全测试的全面知识体系及实现路径
  • visual studio 2022更改项目名称,灾难性故障(异常来自HRESULT)
  • [dp16_两个数组] 通配符匹配 | 交错字符串 | 两个字符串的最小ASCII删除和
  • Linux中NFS服务设置
  • 3.学习笔记--Spring-AOP总结(p39)-Spring事务简介(P40)-Spring事务角色(P41)-Spring事务属性(P42)
  • vue MarkdownIt标签多出了<p>标签导致高度变丑
  • 【实战篇】导入dbc文件
  • 新希望一季度归母净利润4.45亿,上年同期为-19.34亿
  • 准80后湖北省财政厅副厅长徐晶华已调任襄阳市副市长
  • 纪念|海上金石学的兴盛与王昶《金石萃编》
  • 影子调查丨危房之下,百余住户搬离梦嘉商贸楼
  • 国家发展改革委:我们对实现今年经济社会发展目标任务充满信心
  • 人民日报:广东全力推动外贸稳量提质