算法(四)——动态规划

news/2025/2/25 23:33:01

文章目录

    • 基本思想
    • 适用条件
      • 最优子结构
      • 子问题重叠
      • 状态转移方程
    • 解题步骤
    • 应用
      • 斐波那契数列
      • 背包问题
      • 最大子数组和

基本思想

动态规划的核心思想在于将一个复杂的问题分解为一系列相互关联的子问题,通过求解子问题并保存其解,避免对相同子问题的重复计算,从而提高算法的效率。具体来说,动态规划通常会利用已经解决的子问题的结果来推导出更大规模子问题的解,直至最终解决原问题。

适用条件

最优子结构

问题的最优解包含其子问题的最优解。也就是说,一个问题的最优解可以由其子问题的最优解组合而成。例如,在求解最短路径问题时,从起点到终点的最短路径必然包含从起点到中间某个节点的最短路径。

子问题重叠

在求解问题的过程中,会多次遇到相同的子问题。通过保存这些子问题的解,避免重复计算,从而提高算法的效率。例如,在斐波那契数列的计算中,计算较大的斐波那契数时会多次用到较小的斐波那契数。

状态转移方程

用于描述问题的状态和状态之间的关系,通过状态的转移得到最终问题的解。

解题步骤

定义状态:确定问题的状态表示,即如何用一个或多个变量来描述问题的子问题。状态的定义需要能够准确地刻画问题的特征,并且要满足最优子结构性质。

找出状态转移方程:根据问题的最优子结构性质,找出状态之间的递推关系,即如何从已知的子问题的解推导出当前问题的解。状态转移方程是动态规划算法的核心。

确定初始状态:找出问题的边界条件,即最简单的子问题的解。初始状态是递推的起点,后续的状态都是基于初始状态逐步推导出来的。

计算顺序:根据状态转移方程,确定状态的计算顺序。通常有两种计算顺序:自顶向下(记忆化搜索)和自底向上(迭代)。自顶向下方法从原问题出发,递归地求解子问题,并保存已经求解的子问题的解;自底向上方法则从初始状态开始,逐步计算出所有子问题的解,直到得到原问题的解。

返回结果:根据问题的要求,从计算得到的状态中提取出最终的解。

应用

斐波那契数列

 public class Fibonacci {
    public static int fibonacci(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 static void main(String[] args) {                       
        System.out.println(fibonacci(10));  
      }
    }
  • 子问题定义:定义 dp[i] 表示第 i 个斐波那契数。
  • 状态转移方程: dp[i] = dp[i - 1] + dp[i - 2] ,其中 i >= 2 , dp[0] = 0 , dp[1] = 1 。- 时间复杂度:使用了一个循环来计算 dp 数组的每个元素,循环次数为 n ,因此时间复杂度为 O(n) 。
  • 空间复杂度:使用了一个大小为 n + 1 的数组 dp 来存储子问题的解,所以空间复杂度为 O(n)。
  • 实际上,由于计算 dp[i] 时只需要 dp[i - 1] 和 dp[i - 2] 的值,我们可以进一步优化空间复杂度到 ,只使用两个变量来存储前两个斐波那契数。

背包问题

 public class Knapsack {
    public static int knapsack(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 (weights[i - 1] <= j) {                    
             dp[i][j] = Math.max(values[i - 1] + dp[i - 1][j - weights[i - 1]], dp[i - 1][j]);                
           } else {                    
              dp[i][j] = dp[i - 1][j];               
            }            
          }      
        }        
       return dp[n][capacity];    
       }    
 public static void main(String[] args) {        
      int[] weights = {2, 3, 4, 5};        
      int[] values = {3, 4, 5, 6};        
      int capacity = 8;        
      System.out.println(knapsack(weights, values, capacity));    
      }
   }
  • 子问题定义:定义 dp[i][j] 表示在前 i 个物品中选择,且背包容量为 j 时能获得的最大价值。
  • 状态转移方程:
    • 当 weights[i - 1] <= j 时(即第 i 个物品可以放入背包), dp[i][j] = Math.max(values[i - 1] + dp[i - 1][j - weights[i - 1]], dp[i - 1][j]) ,表示取放入第 i 个物品和不放入第 i 个物品两种情况的较大值。
    • 当 weights[i - 1] > j 时(即第 i 个物品放不下), dp[i][j] = dp[i - 1][j] ,即不放入第 i 个物品。
  • 时间复杂度:有两个嵌套的循环,外层循环遍历物品数量 n 次,内层循环遍历背包容量 capacity 次,所以时间复杂度为 O(n×capacity) 。
  • 空间复杂度:使用了一个二维数组 dp[n + 1][capacity + 1] 来存储子问题的解,因此空间复杂度为O(n×capacity) 。通过优化,可以将空间复杂度降低到 ,因为每次计算 dp[i][j] 时只依赖于 dp[i - 1][…] 的值。

最大子数组和

假设有一个数组 nums,求解其连续子数组的最大和

  • 定义状态: 设 dp[i] 为以 nums[i] 结尾的连续子数组的最大和。
  • 状态转移方程: dp[i] = max(dp[i-1] + nums[i], nums[i]),即当前位置的最大和要么是之前的最大和加上当前元素,要么是当前元素本身。
  • 初始化: dp[0] = nums[0],数组的第一个元素作为初始值。
  • 遍历: 从数组的第二个元素开始遍历,更新 dp[i]。
  • 最终结果: 最大的 dp[i] 即为所求。
public class MaxSubarraySum {
    public static int maxSubarraySum(int[] nums) {
        int n = nums.length;
        // 创建一个数组 dp 用于保存子问题的解
        int[] dp = new int[n];
        // 初始化 dp 数组的第一个元素
        dp[0] = nums[0];

        // 遍历数组,根据状态转移方程更新 dp 数组
        for (int i = 1; i < n; i++) {
            dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
        }

        // 找出 dp 数组中的最大值
        int max = dp[0];
        for (int i = 1; i < n; i++) {
            if (dp[i] > max) {
                max = dp[i];
            }
        }

        return max;
    }

    public static void main(String[] args) {
        int[] nums = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
        int result = maxSubarraySum(nums);
        System.out.println(result); // 输出 6,对应子数组 [4, -1, 2, 1]
    }
}

http://www.niftyadmin.cn/n/5866965.html

相关文章

回合制游戏文字版(升级)

//在上一篇博客的基础上&#xff0c;加了细节的改动 //改动&#xff1a;添加了外貌&#xff0c;性别&#xff0c;招式的细节描绘&#xff1b;添加了个人信息展示界面 //一创建java文件1&#xff0c;命名为playGame package test2;import java.util.Random;public class play…

PHP入门基础学习四(PHP基本语法)

运算符 运算符&#xff0c;专门用于告诉程序执行特定运算或逻辑操作的符号。根据运算符的作用&#xff0c;可以将PHP语言中常见的运算符分为9类 算数运算符&#xff1a; 是用来处理加减乘除运算的符号 也是最简单和最常用的运算符号 赋值运算符 1. 是一个二元运算符&#x…

LabVIEW齿轮箱故障分析系统

在运维过程中&#xff0c;某大型风电场发现多台2.5MW风力发电机组在低速重载工况下频繁出现异常振动&#xff0c;导致齿轮箱温度升高和发电效率下降。传统的FFT频谱分析无法准确定位故障源&#xff0c;人工排查耗时且成本高昂。经初步检查&#xff0c;怀疑是行星齿轮箱内齿圈局…

常用的HTML meta标签有哪些

meta是 HTML 中的一个元数据标签&#xff0c;位于 <head> 标签内&#xff0c;不会在页面上直接显示&#xff0c;但能为浏览器和搜索引擎提供关于网页的重要信息。以下是一些常用的 <meta> 标签及其用途&#xff1a; 1. 字符编码声明 用于指定 HTML 文档的字符编码…

【1】VS Code 新建上位机项目---C#基础语法

VS Code 新建上位机项目---C#基础语法 1 基本概念1.1 准备工具1.2 新建项目2 C#编程基础2.1 命名空间和类2.2 数据类型2.3 控制台输入输出2.3.1 输入输出: write 与 read2.3.2 格式化 : string.Foramt() 与 $2.3.3 赋值与运算2.4 类型转换2.4.1 数值类型之间的转换:(int)2.4…

python中格式化输出知识点汇总

在Python中&#xff0c;格式化输出是一种常见的操作&#xff0c;用于将数据以特定的格式展示。以下是Python中格式化输出的主要方法&#xff1a; 1. 使用 % 操作符 这是Python早期版本中常用的格式化方法&#xff0c;类似于C语言中的 printf 。 基本语法 &#xff1a; "…

在PyCharm中集成AI编程助手并嵌入本地部署的DeepSeek-R1模型:打造智能开发新体验

打造智能开发新体验&#xff1a;DeepSeekPycharmollamaCodeGPT 目录 打造智能开发新体验&#xff1a;DeepSeekPycharmollamaCodeGPT前言一、什么是ollama&#xff1f;二、如何使用1.进入ollama官方网站:2.点击下载ollama安装包3.根据默认选项进行安装4.安装成功5.打开命令提示符…

lua-游戏红点提示系统抽象设计

文章目录 前言一、定义红点节点类型二、节点注册与管理三、状态更新与冒泡机制 四、示例配置与使用五、结构示意图六、关键机制说明总结 前言 在游戏开发中&#xff0c;红点提示系统可以通过树形结构和策略模式进行抽象&#xff0c;实现高扩展性。以下是基于Lua的实现方案&…