动态规划算法精解(Java实现):从入门到精通

一、动态规划概述

动态规划(Dynamic Programming,DP)是一种解决复杂问题的高效算法,通过将问题分解为相互重叠的子问题,并存储子问题的解来避免重复计算。它在众多领域如计算机科学、运筹学、经济学等都有广泛应用,能够显著提升问题的求解效率。

核心思想:

  1. 最优子结构:问题的最优解包含子问题的最优解。这意味着可以通过求解子问题的最优解来得到原问题的最优解。例如,在求解最短路径问题时,从起点到终点的最短路径必然包含了从起点到中间某点的最短路径。
  2. 重叠子问题:不同的问题会重复使用相同的子问题解。如果不进行处理,这些子问题会被多次求解,造成大量的时间浪费。动态规划通过记录子问题的解,避免了这种重复计算。
  3. 状态转移:通过状态转移方程描述问题间的递推关系。状态转移方程是动态规划的核心,它定义了如何从已知的子问题解推导出当前问题的解。

适用场景:

  • 最优化问题(最大值 / 最小值):例如求最大利润、最短路径等。在资源分配问题中,需要在一定的约束条件下,找到使某个目标函数达到最大值或最小值的方案。
  • 计数问题(多少种方式):比如计算排列组合的数量、走楼梯的不同方式等。这类问题通常需要统计满足特定条件的所有可能情况的数量。
  • 存在性问题(是否可行):判断是否存在满足某种条件的解。例如,在背包问题中,判断是否能在给定的背包容量下装入某些物品。

二、动态规划三要素

  1. DP 数组定义:明确dp[i]dp[i][j]表示的含义。dp数组用于存储子问题的解,其定义需要根据具体问题来确定。例如,在斐波那契数列问题中,dp[i]表示第i个斐波那契数。
  2. 状态转移方程:描述问题间的递推关系。它是动态规划的关键,通过状态转移方程,可以从已知的子问题解推导出当前问题的解。状态转移方程的推导需要对问题进行深入分析,找出问题之间的内在联系。
  3. 初始条件:确定最小子问题的解。初始条件是动态规划的基础,它为状态转移提供了起点。在很多问题中,初始条件通常是边界情况的解。

三、经典 DP 问题实现

3.1 斐波那契数列(LeetCode 509)

斐波那契数列是一个经典的数学序列,其定义为:F(0)=0,F(1)=1,F(n)=F(n−1)+F(n−2)(n≥2)。

class Solution {public int fib(int n) {if (n <= 1) return n;int[] dp = new int[n+1];dp[0] = 0;dp[1] = 1;for (int i = 2; i <= n; i++) {dp[i] = dp[i-1] + dp[i-2];}return dp[n];}// 空间优化版public int fibOptimized(int n) {if (n <= 1) return n;int prev = 0, curr = 1;for (int i = 2; i <= n; i++) {int sum = prev + curr;prev = curr;curr = sum;}return curr;}
}

在上述代码中,fib方法使用了一个一维数组dp来存储中间结果,避免了重复计算。而fibOptimized方法则对空间进行了优化,只使用了两个变量prevcurr来保存必要的信息,将空间复杂度从O(n)降低到了O(1)。

3.2 爬楼梯(LeetCode 70)

假设你正在爬楼梯,需要n阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

class Solution {public int climbStairs(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];}
}

这个问题可以转化为斐波那契数列问题。到达第n阶楼梯的方法数等于到达第n - 1阶楼梯的方法数加上到达第n - 2阶楼梯的方法数。

四、二维 DP 问题

4.1 最小路径和(LeetCode 64)

给定一个包含非负整数的m x n网格grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。每次只能向下或者向右移动一步。

class Solution {public int minPathSum(int[][] grid) {int m = grid.length, n = grid[0].length;int[][] dp = new int[m][n];// 初始化dp[0][0] = grid[0][0];for (int i = 1; i < m; i++) dp[i][0] = dp[i-1][0] + grid[i][0];for (int j = 1; j < n; j++) dp[0][j] = dp[0][j-1] + grid[0][j];// 状态转移for (int i = 1; i < m; i++) {for (int j = 1; j < n; j++) {dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + grid[i][j];}}return dp[m-1][n-1];}
}

在这个问题中,dp[i][j]表示从左上角到达坐标(i, j)的最小路径和。通过初始化第一行和第一列的dp值,然后使用状态转移方程dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + grid[i][j]来更新其他位置的dp值。

4.2 最长公共子序列(LeetCode 1143)

给定两个字符串text1text2,返回这两个字符串的最长公共子序列的长度。

class Solution {public int longestCommonSubsequence(String text1, String text2) {int m = text1.length(), n = text2.length();int[][] dp = new int[m+1][n+1];for (int i = 1; i <= m; i++) {for (int j = 1; j <= n; j++) {if (text1.charAt(i-1) == text2.charAt(j-1)) {dp[i][j] = dp[i-1][j-1] + 1;} else {dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);}}}return dp[m][n];}
}

dp[i][j]表示text1的前i个字符和text2的前j个字符的最长公共子序列的长度。如果text1的第i个字符和text2的第j个字符相等,则dp[i][j] = dp[i-1][j-1] + 1;否则,dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])

五、背包问题系列

5.1 0 - 1 背包问题

给定一组物品,每个物品有对应的重量weights和价值values,以及一个容量为capacity的背包。要求在不超过背包容量的前提下,选择一些物品放入背包,使得背包中物品的总价值最大。每个物品只能选择放入或不放入背包(即 0 - 1 选择)。

class Knapsack {public int maxValue(int[] weights, int[] values, int capacity) {int n = weights.length;int[][] dp = new int[n+1][capacity+1];for (int i = 1; i <= n; i++) {for (int j = 1; j <= capacity; j++) {if (j < weights[i-1]) {dp[i][j] = dp[i-1][j];} else {dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-weights[i-1]] + values[i-1]);}}}return dp[n][capacity];}// 空间优化版(一维数组)public int maxValueOptimized(int[] weights, int[] values, int capacity) {int[] dp = new int[capacity+1];for (int i = 0; i < weights.length; i++) {for (int j = capacity; j >= weights[i]; j--) {dp[j] = Math.max(dp[j], dp[j-weights[i]] + values[i]);}}return dp[capacity];}
}

maxValue方法中,dp[i][j]表示前i个物品在背包容量为j时的最大价值。通过两层循环遍历物品和背包容量,根据当前物品是否能放入背包来更新dp值。maxValueOptimized方法对空间进行了优化,使用一维数组dp来保存中间结果,将空间复杂度从O(n∗capacity)降低到了O(capacity)。

5.2 完全背包问题(LeetCode 322 零钱兑换)

给定不同面额的硬币coins和一个总金额amount,编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。每种硬币的数量是无限的。

import java.util.Arrays;class Solution {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];}
}

dp[i]表示凑成金额i所需的最少硬币个数。通过遍历每种硬币,更新dp数组。如果dp[amount]仍然大于amount,说明无法凑成总金额,返回 -1。

六、DP 解题方法论

6.1 解题步骤

  1. 确定 DP 状态:明确状态表示什么含义。这是解决动态规划问题的关键一步,需要根据问题的特点来定义合适的状态。
  2. 定义 DP 数组:选择一维 / 二维数组存储状态。根据问题的复杂度和状态的维度,选择合适的数组来存储中间结果。
  3. 状态转移方程:找出状态间的关系式。通过分析问题的最优子结构,推导出状态转移方程。
  4. 初始化:确定边界条件。初始化是状态转移的起点,需要根据问题的定义来确定边界情况的解。
  5. 计算顺序:确定填表顺序。根据状态转移方程的依赖关系,确定计算dp数组的顺序。
  6. 空间优化:考虑是否可降维。在一些情况下,可以通过优化空间来降低算法的空间复杂度。

6.2 经典问题分类

问题类型典型例题特点
线性 DP最长递增子序列 (300)单序列或双序列问题
区间 DP最长回文子串 (5)涉及子区间的最优解
树形 DP打家劫舍 III (337)在树结构上进行状态转移
状态机 DP买卖股票最佳时机 (121)状态间存在多种转移可能
数位 DP数字 1 的个数 (233)处理数字位上的计数问题

七、高频面试题精选

7.1 编辑距离(LeetCode 72)

给你两个单词word1word2,请你计算出将word1转换成word2所使用的最少操作数。你可以对一个单词进行如下三种操作:插入一个字符、删除一个字符、替换一个字符。

class Solution {public int minDistance(String word1, String word2) {int m = word1.length(), n = word2.length();int[][] dp = new int[m+1][n+1];// 初始化for (int i = 1; i <= m; i++) dp[i][0] = i;for (int j = 1; j <= n; j++) dp[0][j] = j;// 状态转移for (int i = 1; i <= m; i++) {for (int j = 1; j <= n; j++) {if (word1.charAt(i-1) == word2.charAt(j-1)) {dp[i][j] = dp[i-1][j-1];} else {dp[i][j] = Math.min(Math.min(dp[i-1][j], dp[i][j-1]),dp[i-1][j-1]) + 1;}}}return dp[m][n];}
}

dp[i][j]表示将word1的前i个字符转换成word2的前j个字符所需的最少操作数。通过初始化第一行和第一列的dp值,然后使用状态转移方程来更新其他位置的dp值。

7.2 打家劫舍(LeetCode 198)

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组nums,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

class Solution {public int rob(int[] nums) {int n = nums.length;if (n == 0) return 0;if (n == 1) return nums[0];int[] dp = new int[n];dp[0] = nums[0];dp[1] = Math.max(nums[0], nums[1]);for (int i = 2; i < n; i++) {dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);}return dp[n-1];}// 空间优化版public int robOptimized(int[] nums) {int prev = 0, curr = 0;for (int num : nums) {int temp = Math.max(curr, prev + num);prev = curr;curr = temp;}return curr;}
}

rob方法使用一维数组dp来存储中间结果,dp[i]表示偷窃前i间房屋能够获得的最高金额。robOptimized方法对空间进行了优化,只使用两个变量prevcurr来保存必要的信息。

八、DP 优化技巧

  1. 空间压缩:二维转一维(滚动数组)。在一些问题中,dp数组的更新只依赖于前一行或前几行的信息,此时可以使用滚动数组将二维数组压缩为一维数组,从而降低空间复杂度。
  2. 状态压缩:用位运算表示状态(如 TSP 问题)。对于一些状态数量较少的问题,可以使用位运算来表示状态,从而减少空间的使用。
  3. 单调队列优化:优化滑动窗口最值问题。在求解滑动窗口的最值问题时,可以使用单调队列来优化时间复杂度。
  4. 斜率优化:优化特定形式的状态转移方程。对于一些具有特定形式的状态转移方程,可以使用斜率优化来降低时间复杂度。
// 示例:使用滚动数组优化空间
int[][] dp = new int[2][n]; // 只保留前两行

九、常见误区与调试技巧

常见错误:

  1. 未正确处理边界条件。边界条件是动态规划的基础,如果处理不当,会导致结果错误。
  2. 状态转移方程推导错误。状态转移方程是动态规划的核心,如果推导错误,整个算法将无法得到正确的结果。
  3. 数组越界访问。在使用dp数组时,需要注意数组的下标范围,避免越界访问。
  4. 空间复杂度过高。在一些问题中,如果没有进行空间优化,可能会导致空间复杂度过高,从而超出内存限制。

调试建议:

  1. 打印 DP 表格检查中间结果。通过打印dp数组的中间结果,可以帮助我们理解状态转移的过程,发现问题所在。
  2. 从小规模测试用例开始验证。先使用小规模的测试用例来验证算法的正确性,逐步增加测试用例的规模。
  3. 使用 IDE 的调试功能逐步跟踪。利用 IDE 的调试功能,逐步执行代码,观察变量的变化,找出问题所在。
  4. 对比暴力解法的结果。如果可能的话,可以实现一个暴力解法,将其结果与动态规划的结果进行对比,验证算法的正确性。

十、学习路径建议

  1. 基础阶段

    • 斐波那契数列
    • 爬楼梯问题
    • 最小路径和
      这些问题是动态规划的基础,通过解决这些问题,可以帮助我们理解动态规划的基本思想和解题步骤。
  2. 进阶阶段

    • 背包问题系列
    • 股票买卖问题
    • 字符串 DP 问题
      这些问题具有一定的复杂度,需要我们深入理解状态设计和转移方程的构建。
  3. 高手阶段

    • 树形 DP
    • 状态压缩 DP
    • 数位 DP
      这些问题是动态规划的高级应用,需要我们具备较强的思维能力和编程技巧。

结语

动态规划是算法学习中的难点也是重点,需要大量练习才能掌握其精髓。建议从简单问题入手,逐步理解状态设计和转移方程的构建,最终达到能够独立解决陌生 DP 问题的水平。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/78402.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【JLINK调试器】适配【大华HC32F4A0芯片】的完整解决方案

JLINK调试器适配 大华HC32F4A0芯片的完整解决方案 文章目录 JLINK调试器适配 大华HC32F4A0芯片的完整解决方案一、问题背景1.1 HC32F4A0芯片特性1.2 为何需要J-Link支持1.3 未适配的影响 二、解决方案2.1 问题复现2.2 手动配置2.3 结果验证 三、常见问题四、固件烧入 一、问题背…

AVOutputFormat 再分析

AVOutputFormat 结构体 /*** addtogroup lavf_encoding* {*/ typedef struct AVOutputFormat {const char *name;/*** Descriptive name for the format, meant to be more human-readable* than name. You should use the NULL_IF_CONFIG_SMALL() macro* to define it.*/const…

4.29-4.30 Maven+单元测试

单元测试&#xff1a; BeforeAll在所有的单元测试方法运行之前&#xff0c;运行一次。 AfterAll在所有单元测试方法运行之后&#xff0c;运行一次。 BeforeEach在每个单元测试方法运行之前&#xff0c;都会运行一次 AfterEach在每个单元测试方法运行之后&#xff0c;都会运行…

具身系列——Q-Learning算法实现CartPole游戏(强化学习)

完整代码参考&#xff1a; rl/qlearning_cartpole.py 陈先生/ailib - Gitee.com 部分训练得分&#xff1a; Episode 0 Reward: 19.0 Avg Reward: 19.00 Time: 0.00s Episode 1 Reward: 17.0 Avg Reward: 18.98 Time: 0.00s Episode 2 Reward: 10.0 Avg Reward: 18.89 Time:…

2.2 矩阵

考点一&#xff1a;方阵的幂 1. 计算方法 (1) ​找规律法​ ​适用场景​&#xff1a;低阶矩阵或具有周期性规律的矩阵。​示例​&#xff1a; 计算 A ( 0 1 1 0 ) n A \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}^n A(01​10​)n&#xff1a; 当 n n n 为奇…

一个完整的神经网络训练流程详解(附 PyTorch 示例)

&#x1f9e0; 一个完整的神经网络训练流程详解&#xff08;附 PyTorch 示例&#xff09; &#x1f4cc; 第一部分&#xff1a;神经网络训练流程概览&#xff08;总&#xff09; 在深度学习中&#xff0c;构建和训练一个神经网络模型并不是简单的“输入数据、得到结果”这么简…

从入门到登峰-嵌入式Tracker定位算法全景之旅 Part 0 |Tracker 设备定位概览与系统架构

Part 0 |Tracker 设备定位概览与系统架构 在开始算法与代码之前,本章将从“高空视角”全面剖析一个嵌入式 Tracker 定位系统的整体架构:背景、目标与规划、关键约束、开发环境配置、硬件清单与资源预算、逻辑框图示意、通信链路与协议栈、软件架构与任务划分,以及低功耗管…

【自然语言处理与大模型】大模型意图识别实操

本文先介绍一下大模型意图识别是什么&#xff1f;如何实现&#xff1f;然后通过一个具体的实战案例&#xff0c;详细演示如何运用大模型完成意图识别任务。最后&#xff0c;对大模型在该任务中所发挥的核心作用进行总结归纳。 一、意图识别的定义与核心任务 意图识别是自然语言…

HTML打印设置成白色,但是打印出来的是灰色的解决方案

在做浏览打印的时候&#xff0c;本来设置的颜色是白色&#xff0c;但是在浏览器打印的时候却显示灰色&#xff0c;需要在打印的时候勾选选项“背景图形”即可正常展示。

PyCharm中全局搜索无效

发现是因为与搜狗快捷键冲突了&#xff0c;把框选的那个勾选去掉或设置为其他键就好了

Nginx 核心功能02

目录 一、引言 二、正向代理 &#xff08;一&#xff09;正向代理基础概念 &#xff08;二&#xff09;Nginx 正向代理安装配置 &#xff08;三&#xff09;正向代理配置与验证 三、反向代理 &#xff08;一&#xff09;反向代理原理与应用场景 &#xff08;二&#xf…

探索 C++23 std::to_underlying:枚举底层值获取的利器

文章目录 引言基本概念作用使用示例与之前方法的对比在 C23 中的意义总结 引言 在 C 的发展历程中&#xff0c;每一个新版本都带来了许多令人期待的新特性和改进&#xff0c;以提升代码的安全性、可读性和可维护性。C23 作为其中的一个重要版本&#xff0c;也不例外。其中&…

WGDI-分析WGD及祖先核型演化的集成工具-文献精读126

WGDI: A user-friendly toolkit for evolutionary analyses of whole-genome duplications and ancestral karyotypes WGDI&#xff1a;一款面向全基因组重复事件与祖先核型演化分析的易用工具集 摘要 在地球上大多数主要生物类群中&#xff0c;人们已检测到全基因组复制&…

C# 方法(控制流和方法调用)

本章内容: 方法的结构 方法体内部的代码执行 局部变量 局部常量 控制流 方法调用 返回值 返回语句和void方法 局部函数 参数 值参数 引用参数 引用类型作为值参数和引用参数 输出参数 参数数组 参数类型总结 方法重载 命名参数 可选参数 栈帧 递归 控制流 方法包含了组成程序的…

「Mac畅玩AIGC与多模态16」开发篇12 - 多节点串联与输出合并的工作流示例

一、概述 本篇在输入变量与单节点执行的基础上,扩展实现多节点串联与格式化合并输出的工作流应用。开发人员将掌握如何在 Dify 工作流中统一管理输入变量,通过多节点串联引用,生成规范统一的最终输出,为后续构建复杂逻辑流程打下基础。 二、环境准备 macOS 系统Dify 平台…

解锁Windows异步黑科技:IOCP从入门到精通

在当今快节奏的数字化时代&#xff0c;软件应用对性能的追求可谓永无止境。无论是高并发的网络服务器&#xff0c;还是需要快速处理大量文件的桌面应用&#xff0c;都面临着一个共同的挑战&#xff1a;如何在有限的系统资源下&#xff0c;实现高效的数据输入输出&#xff08;I/…

Java学习手册:Spring 生态其他组件介绍

一、微服务架构相关组件 Spring Cloud 服务注册与发现 &#xff1a; Eureka &#xff1a;由 Netflix 开源&#xff0c;包含 Eureka Server 和 Eureka Client 两部分。Eureka Server 作为服务注册表&#xff0c;接收服务实例的注册请求并管理其信息&#xff1b;Eureka Client 负…

VMware Workstation 创建虚拟机并安装 Ubuntu 系统 的详细步骤指南

VMware Workstation 创建虚拟机并安装 Ubuntu 系统 的详细步骤指南 一、准备工作1. 下载 Ubuntu 镜像2. 安装 VMware Workstation 二、创建虚拟机1. 新建虚拟机向导2. 选择虚拟机配置类型3. 加载安装镜像4. 系统类型配置5. 虚拟机命名与存储6. 磁盘容量分配7. 硬件自定义&#…

串口的缓存发送以及缓存接收机制

#创作灵感# 在我们实际使用MCU进行多串口任务分配的时候&#xff0c;我们会碰到这样一种情况&#xff0c;即串口需要短间隔周期性发送数据&#xff0c;且相邻两帧之间需要间隔一段时间&#xff0c;防止连帧。我们常常需要在软件层面对串口的发送和接受做一个缓存的处理方式。 …

时间交织(TIADC)的失配误差校正处理(以4片1GSPS采样率的12bitADC交织为例讲解)

待写…有空再写&#xff0c;有需要的留言。 存在失配误差的4GSPS交织 校正完成后的4GSPS交织