目标和问题:从回溯到动态规划的旅程

news2024/12/23 6:19:38

目录

引言

题目描述

示例

初步思路:回溯法

回溯法实现

分析

转变思路:动态规划

问题转换

状态定义

状态转移方程

二维动态规划实现

压缩到一维动态规划

一维动态规划实现

详细讲解:从回溯到动态规划的旅程

1. 从回溯到动态规划的转变

2. 问题转换的关键

3. 状态定义与转移

4. 压缩到一维的优化

二维动态规划

一维动态规划

举例说明

从后往前遍历

从前往后遍历

总结

总结


引言

算法学习之路充满了挑战和乐趣,其中背包问题更是经典中的经典。今天,我们一起探讨一道有趣的题目——“目标和”,看一看它如何从回溯变身为动态规划。

494. 目标和 - 力扣(LeetCode)

题目描述

给定一个非负整数数组 nums 和一个整数 target。我们可以向数组中的每个整数前添加 '+' 或 '-',然后把它们串联起来形成表达式。我们的任务是找出所有可能的表达式,使其结果等于 target

示例

输入:nums = [1, 1, 1, 1, 1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

初步思路:回溯法

第一眼看到这道题,或许你会想用回溯法。没错,这也是大多数人的第一反应:通过递归地添加 '+' 或 '-' 来尝试所有可能的表达式。

回溯法实现

class Solution {
    int result = 0;

    public int findTargetSumWays(int[] nums, int target) {
        if (nums.length == 0) return 0;
        backtrack(nums, 0, target);
        return result;
    }

    void backtrack(int[] nums, int i, int remain) {
        if (i == nums.length) {
            if (remain == 0) {
                result++;
            }
            return;
        }
        backtrack(nums, i + 1, remain - nums[i]);
        backtrack(nums, i + 1, remain + nums[i]);
    }
}

分析

回溯法通过递归地尝试每一种可能的组合,最终统计满足条件的组合数目。虽然直观,但时间复杂度为 O(2^n),在输入规模较大时效率较低。

转变思路:动态规划

接下来,我们将这道题转换为动态规划的问题。这里有一个巧妙的转换,让我们一起来看。

问题转换

其实,这个问题可以转化为一个子集划分问题,而子集划分问题又是一个典型的背包问题

首先,如果我们把 nums 划分成两个子集 A 和 B,分别代表分配 + 的数和分配 - 的数,那么他们和 target 存在如下关系:

sum(A) - sum(B) = target
sum(A) = target + sum(B)
sum(A) + sum(A) = target + sum(B) + sum(A)
2 * sum(A) = target + sum(nums)

我们可以将问题转换为找到两个子集,使得一个子集和为 (sum + target) / 2,另一个子集和为 sum - (sum + target) / 2,其中 sum 是数组 nums 的总和。

状态定义

  • 定义 dp[i][j] 表示使用前 i 个数字,是否可以组成和为 j 的子集数。

状态转移方程

  • 如果不使用当前数字 nums[i-1],即 dp[i][j] = dp[i-1][j]

  • 如果使用当前数字 nums[i-1],即 dp[i][j] = dp[i-1][j-nums[i-1]](前提是 j >= nums[i-1]

二维动态规划实现

int findTargetSumWays(int[] nums, int target) {
    int sum = 0;
    for (int n : nums) sum += n;
    // 这两种情况,不可能存在合法的子集划分
    if (sum < Math.abs(target) || (sum + target) % 2 == 1) {
        return 0;
    }
    return subsets(nums, (sum + target) / 2);
}

int subsets(int[] nums, int sum) {
    int n = nums.length;
    int[][] dp = new int[n + 1][sum + 1];
    dp[0][0] = 1; // 初始状态,和为 0 的子集只有一个,即空集

    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= sum; j++) {
            if (j >= nums[i-1]) {
                dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];
            } else {
                dp[i][j] = dp[i-1][j];
            }
        }
    }
    return dp[n][sum];
}

压缩到一维动态规划

为了进一步优化空间复杂度,我们可以将二维数组压缩到一维数组。

对照二维 dp,只要把 dp 数组的第一个维度全都去掉就行了,唯一的区别就是这里的 j 要从后往前遍历,原因如下

因为二维压缩到一维的根本原理是,dp[j] 和 dp[j-nums[i-1]] 还没被新结果覆盖的时候,相当于二维 dp 中的 dp[i-1][j] 和 dp[i-1][j-nums[i-1]]

那么,我们就要做到:在计算新的 dp[j] 的时候,dp[j] 和 dp[j-nums[i-1]] 还是上一轮外层 for 循环的结果

如果你从前往后遍历一维 dp 数组,dp[j] 显然是没问题的,但是 dp[j-nums[i-1]] 已经不是上一轮外层 for 循环的结果了,这里就会使用错误的状态,当然得不到正确的答案。

也就是说。在将二维动态规划压缩到一维动态规划时,需要确保在计算新的 dp[j] 时,dp[j]dp[j - nums[i-1]] 不会在本轮内循环中被更新。这就是为什么我们在一维动态规划中需要从后往前遍历的原因。

一维动态规划实现

int findTargetSumWays(int[] nums, int target) {
    int sum = 0;
    for (int n : nums) sum += n;
    // 这两种情况,不可能存在合法的子集划分
    if (sum < Math.abs(target) || (sum + target) % 2 == 1) {
        return 0;
    }
    return subsets(nums, (sum + target) / 2);
}

/* 计算 nums 中有几个子集的和为 sum */
int subsets(int[] nums, int sum) {
    int n = nums.length;
    int[] dp = new int[sum + 1];
    // base case
    dp[0] = 1;
    
    for (int i = 1; i <= n; i++) {
        // j 要从后往前遍历
        for (int j = sum; j >= 0; j--) {
            // 状态转移方程
            if (j >= nums[i-1]) {
                dp[j] = dp[j] + dp[j-nums[i-1]];
            } else {
                dp[j] = dp[j];
            }
        }
    }
    return dp[sum];
}

详细讲解:从回溯到动态规划的旅程

1. 从回溯到动态规划的转变

最初的回溯法思路简单直观,但其时间复杂度为 O(2^n),在处理大规模数据时效率低下。为了提高效率,我们需要找到一种方法,将其转变为动态规划问题。

2. 问题转换的关键

通过将问题转换为背包问题,我们引入了动态规划的思想。具体来说,我们将问题转化为寻找两个子集,使得其中一个子集和为 (sum + target) / 2

3. 状态定义与转移

  • 状态定义dp[i][j] 表示使用前 i 个数字,是否可以组成和为 j 的子集数。

  • 状态转移方程:通过递推关系,逐步求解子问题,从而解决原问题。

4. 压缩到一维的优化

为了进一步优化空间复杂度,我们将二维动态规划压缩到一维动态规划。这一过程的关键在于从后往前更新数组,确保在计算新的 dp[j] 时,dp[j]dp[j - nums[i]] 仍然是上一轮的结果,避免状态覆盖问题。

二维动态规划

在二维动态规划中,状态转移方程是: [ dpi = dpi-1 + dpi-1] ]

这个方程表明:

  • dp[i][j] 是在第 i 轮计算的。

  • dp[i-1][j]dp[i-1][j-nums[i-1]] 都是上一轮(即第 i-1 轮)计算的结果。

一维动态规划

在一维动态规划中,状态转移方程是: [ dp[j] = dp[j] + dp[j-nums[i]] ]

为了确保在计算新的 dp[j] 时,dp[j]dp[j - nums[i]] 都是上一轮的结果,我们需要从后往前遍历 dp 数组。这样可以保证 dp[j - nums[i]] 在当前轮次还没有被更新。

举例说明

假设 nums = [1, 2, 3]sum = 4

从后往前遍历
  1. 初始化

    dp = [1, 0, 0, 0, 0]
  2. 遍历 nums[0] = 1

    for (int j = 4; j >= 1; j--) {
        dp[j] += dp[j - 1];
    }

    更新后:

    dp = [1, 1, 0, 0, 0]
  3. 遍历 nums[1] = 2

    for (int j = 4; j >= 2; j--) {
        dp[j] += dp[j - 2];
    }

    更新后:

    dp = [1, 1, 1, 1, 0]
  4. 遍历 nums[2] = 3

    for (int j = 4; j >= 3; j--) {
        dp[j] += dp[j - 3];
    }

    更新后:

    dp = [1, 1, 1, 2, 1]
从前往后遍历
  1. 初始化

    dp = [1, 0, 0, 0, 0]
  2. 遍历 nums[0] = 1

    for (int j = 1; j <= 4; j++) {
        dp[j] += dp[j - 1];
    }

    更新后:

    dp = [1, 1, 1, 1, 1]
  3. 遍历 nums[1] = 2

    for (int j = 2; j <= 4; j++) {
        dp[j] += dp[j - 2];
    }

    更新后:

    dp = [1, 1, 2, 2, 2]
  4. 遍历 nums[2] = 3

    for (int j = 3; j <= 4; j++) {
        dp[j] += dp[j - 3];
    }

    更新后:

    dp = [1, 1, 2, 3, 3]

通过对比可以看出,从前往后遍历会导致在当前轮次中使用已经更新过的 dp[j - nums[i]],从而得到错误的结果。而从后往前遍历可以确保在计算新的 dp[j] 时,dp[j]dp[j - nums[i]] 都是上一轮的结果。

总结
  • 从后往前遍历:确保在计算新的 dp[j] 时,dp[j]dp[j - nums[i]] 仍然是上一轮循环的结果,避免在当前轮次中使用已更新的值。

  • 从前往后遍历:可能会使用当前轮次已经更新的 dp[j - nums[i]],导致错误的结果。

希望这个解释能够帮助你更好地理解为什么在一维动态规划中需要从后往前遍历。如果还有任何问题,请随时告诉我!

总结

通过这道“目标和”问题,我们展示了如何从回溯法转换为动态规划。这不仅提高了算法的效率,也拓宽了我们的思路。在算法的世界里,转换思维方式,寻求优化方案,是解决复杂问题的重要手段。

希望这篇博客能够帮助你理解背包问题的变种及其动态规划的应用。如果你有任何问题或建议,欢迎在评论区留言。

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

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

相关文章

AGI 之 【Hugging Face】 的【Transformer】的 [ 解码器 ] / [ 认识 Transformer ]的简单整理

AGI 之 【Hugging Face】 的【Transformer】的 [ 解码器 ] / [ 认识 Transformer ]的简单整理 目录 AGI 之 【Hugging Face】 的【Transformer】的 [ 解码器 ] / [ 认识 Transformer ]的简单整理 一、简单介绍 二、Transformer 三、解码器 四、认识Transformer 1、Transf…

io_contextttttttttttt

创建上下文——io_context_t 它是一个上下文结构&#xff0c;在内部它包含一个完成队列&#xff0c;在线程之间是可以共享的。 提交请求——iocb io回调数据结构&#xff0c;和io_submit配合使用。 处理结果 通过io_event处理结果&#xff0c; struct io_event {void *data…

智胜未来:AI如何重塑SaaS用户增长战略

在当今这个数字化时代&#xff0c;SaaS&#xff08;软件即服务&#xff09;已成为企业运营的重要支柱&#xff0c;而人工智能&#xff08;AI&#xff09;技术的迅猛发展&#xff0c;正以前所未有的方式重塑着SaaS行业的面貌&#xff0c;特别是对其用户增长战略产生了深远影响。…

洛谷P7044 「MCOI-03」括号(栈括号的贡献 组合数经典问题)

题目 思路来源 P7044 「MCOI-03」括号 题解 - 洛谷专栏 题解 统计一对括号的贡献&#xff0c;对于一个左括号i&#xff0c;找到其右第一个匹配的括号的位置j 这样对于不包含这个(i,j)对的区间[l,r]&#xff08;1<l<i&#xff0c;i<r<j&#xff09;来说&#xff…

在docker配置Nginx环境配置

应用于商业模式集中&#xff0c;对于各种API的调用&#xff0c;对于我们想要的功能进行暴露&#xff0c;对于不用的进行拦截进行鉴权。用于后面的付费 开发环境 正式上线模式 一、常用命令 停止&#xff1a;docker stop Nginx重启&#xff1a;docker restart Nginx删除服务&a…

【已解决】ip2region解析ip获取地区位置 在linux部署出现java文件操作报错:java.io.FileNotFoundException

1、依赖 <dependency><groupId>org.lionsoul</groupId><artifactId>ip2region</artifactId><version>2.7.0</version></dependency>2.加入ip2region.xdb文件 ip2Region下载地址 3.加入到项目里面去 把 ip2region.xdb 文件放…

Windows环境使用SpringBoot整合Minio平替OSS

目录 配置Minio环境 一、下载minio.exe mc.exe 二、设置用户名和密码 用管理员模式打开cmd 三、启动Minio服务器 四、访问WebUI给的地址 SpringBoot整合Minio 一、配置依赖&#xff0c;application.yml 二、代码部分 FileVO MinioConfig MinioUploadService MinioController 三…

Python (Ansbile)脚本高效批量管理服务器和安全

1、简介 在现代 IT 基础设施中&#xff0c;管理大量服务器是一项复杂而繁琐的任务。特别是在检查服务器的存活状态以及 SSH 登录等任务上&#xff0c;手动操作非常耗时且容易出错。本文将介绍如何使用 Python 脚本实现对多台服务器的批量检查和管理&#xff0c;包括检查服务器…

用dify实现简单的Agent应用(AI信息检索)

这篇文章里&#xff0c;我们来聊聊如何使用字节最新的豆包大模型&#xff0c;在 Dify 上来快速完成一个具备理解需求、自主规划、自主选择工具使用的简单智能体&#xff08;Agent&#xff09;。 准备工作 完整准备过程分为&#xff1a;准备 Docker 环境、启动 Dify 程序、启动…

Spring框架的学习SpringMVC(1)

1.什么是MVC (1)MVC其实就是软件架构的一种设计模式&#xff0c;它将软件的系统分为&#xff0c;&#xff08;视图&#xff0c;模型&#xff0c;控制器&#xff09;三个部分 1.1View(视图) 视图也就是&#xff0c;在浏览器显示的那一个部分&#xff0c;是后端数据的呈现 1.…

02-部署LVS-DR群集

1.LVS-DR工作原理 LVS-DR模式&#xff0c;Director Server作为群集的访问入口&#xff0c;不作为网购使用&#xff0c;节点Director Server 与 Real Server 需要在同一个网络中&#xff0c;返回给客户端的数据不需要经过Director Server 为了响应对整个群集的访问&#xff0c;…

实验一 MATLAB \ Python数字图像处理初步

一、实验目的&#xff1a; 1&#xff0e;熟悉及掌握在MATLAB\Python中能够处理哪些格式图像。 2&#xff0e;熟练掌握在MATLAB\Python中如何读取图像。 3&#xff0e;掌握如何利用MATLAB\Python来获取图像的大小、颜色、高度、宽度等等相关信息。 4&#xff0e;掌握如何在M…

电脑录音怎么录?简单四个方法轻松搞定!

在电脑上录制音频是一项非常实用的技能&#xff0c;适合多种场合的需求。例如&#xff0c;你可能需要录制自己的声音&#xff0c;用于录音广播、演示或视频制作&#xff1b;也可能需要录制电脑中的声音&#xff0c;如音乐、游戏音效或在线直播&#xff1b;或者需要捕捉浏览器中…

2024 年 亚太赛 APMCM (A题)中文赛道国际大学生数学建模挑战赛 | 飞行器外形的优化 | 数学建模完整代码+建模过程全解全析

当大家面临着复杂的数学建模问题时&#xff0c;你是否曾经感到茫然无措&#xff1f;作为2022年美国大学生数学建模比赛的O奖得主&#xff0c;我为大家提供了一套优秀的解题思路&#xff0c;让你轻松应对各种难题&#xff01; 完整内容可以在文章末尾领取&#xff01; 第一个问…

ScaleCache: A Scalable Page Cache for Multiple Solid-State Drives——论文泛读

EuroSys 2024 Paper 论文阅读笔记整理 问题 高性能存储设备&#xff0c;如具有GB/s级I/O带宽的NVMe SSD&#xff0c;已被广泛应用于企业服务器中。对于处理大量数据&#xff0c;在RAID配置中使用多个SSD很有吸引力&#xff0c;这可以提高I/O性能、可靠性和容量。尽管多个SSD为…

隐私集合求交(PSI)原理深入浅出

隐私集合求交技术是多方安全计算领域的一个子问题&#xff0c;通常也被称为安全求交、隐私保护集合交集或者隐私交集技术等&#xff0c;其目的是允许持有各自数据集的双方或者多方&#xff0c;执行两方或者多方集合的交集计算&#xff0c;当PSI执行完成&#xff0c;一方或者两方…

SQL Server 2022的组成

《SQL Server 2022从入门到精通&#xff08;视频教学超值版&#xff09;》图书介绍-CSDN博客 SQL Server 2022主要由4部分组成&#xff0c;分别是数据库引擎、分析服务、集成服务和报表服务。本节将详细介绍这些内容。 1.2.1 SQL Server 2022的数据库引擎 SQL Server 2022的…

90%的铲屎官必遇到家里猫毛满天飞问题,热门宠物空气净化器分享

作为一名资深猫奴&#xff0c;一到换毛季节家中就会忍受猫毛飞舞、异味四溢的双重困扰&#xff1f;花粉加上宠物的毛发和体味&#xff0c;过敏和不适似乎成了家常便饭。尝试过很多半方法&#xff0c;用过空气净化器去除毛和异味&#xff0c;虽然普通空气净化器可能提供一定程度…

MySQL数据库的备份-恢复-日志

一、备份 1.1数据备份的重要性 备份的主要目的是灾难恢复。 在生产环境中&#xff0c;数据的安全性至关重要。 任何数据的丢失都可能产生严重的后果。 1.2造成数据丢失的原因 程序错误人为操作错误运算错误磁盘故障灾难&#xff08;如火灾、地震&#xff09;和盗窃 1.3数…

Flutter——最详细(Drawer)使用教程

背景 应用左侧或右侧导航面板&#xff1b; 属性作用elevation相当于阴影的大小 import package:flutter/material.dart;class CustomDrawer extends StatelessWidget {const CustomDrawer({Key? key}) : super(key: key);overrideWidget build(BuildContext context) {return…