解决实用编程题目:单词拆分和分割等和子集--动态规划方式深度呈现

139. 单词拆分

题目描述

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。

示例 2:

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。

注意,你可以重复使用字典中的单词。

示例 3:

输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

提示:

  • 1 <= s.length <= 300
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 20
  • s 和 wordDict[i] 仅由小写英文字母组成
  • wordDict 中的所有字符串 互不相同

解题思路

dp[i] 表示字符串 s 的前 i 个字符是否可以用 wordDict 中的单词拼接得到。

解题步骤如下:

  1. 初始化一个大小为 s.length() + 1 的布尔数组 dp,并将 dp[0] 设置为 true,表示空字符串总是可以拼接的。

  2. 对于每个从 1s.length() 的索引 i,遍历检查:

    • 对于每个在 wordDict 中的单词 word,如果 word 的长度小于等于 idp[i - word.length()]true,则检查 s 从位置 i - word.length()i 的子串是否等于 word
    • 如果这个子串等于 word,则将 dp[i] 设置为 true
  3. 在完成上述步骤后,dp[s.length()] 将给出答案,表示整个字符串 s 是否可以由 wordDict 中的单词拼接得到。

代码实现

import java.util.List;


public class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        boolean[] dp = new boolean[s.length() + 1];
        dp[0] = true;

        for (int i = 1; i <= s.length(); i++) {
            for (String word : wordDict) {
                if (word.length() <= i) {
                    if (dp[i - word.length()]) {
                        if (s.substring(i - word.length(), i).equals(word)) {
                            dp[i] = true;
                            break;
                        }
                    }
                }
            }
        }
        return dp[s.length()];
    }
}

416. 分割等和子集

题目描述

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。

示例 2:

输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

二维动态规划

状态转移方程:很多时候,状态转移方程思考的角度是「分类讨论」,对于「0-1 背包问题」而言就是「当前考虑到的数字选与不选」。

dp[i][j]表示从数组的 [0, i] 这个子区间内挑选一些正整数,每个数只能用一次,使得这些数的和恰好等于 j

  1. 不选择 nums[i],如果在 [0, i - 1] 这个子区间内已经有一部分元素,使得它们的和为 j ,那么 dp[i][j] = true;
  2. 选择 nums[i],如果在 [0, i - 1] 这个子区间内就得找到一部分元素,使得它们的和为 j - nums[i]。

状态转移方程:

dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]]

以下是解决这个问题的步骤: 

  1. 计算总和:首先,计算数组 nums 的总和。如果总和是奇数,那么不能分割成两个和相等的子集,因为两个整数和不能为奇数。

  2. 初始化动态规划数组:创建一个布尔型动态规划数组 dp,其大小为 sum / 2 + 1dp[i][j]表示从数组的 [0, i] 这个子区间内挑选一些正整数,每个数只能用一次,使得这些数的和恰好等于 j。初始化 dp[0]true,因为总和为 0 总是可能的。

  3. 填充动态规划数组:对于 nums 中的每个数字 num,从 sum / 2num 倒序遍历 dp 数组,更新 dp[j] = dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]]

  4. 检查结果:最后,返回 dp[len - 1][target]的值。如果 dp[len - 1][target]true,表示可以分割数组成两个和相等的子集。 

public class Solution {
    public boolean canPartition(int[] nums) {
        int len = nums.length;
        // 计算数组总和
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }

        // 如果总和是奇数,无法平分为两个和相等的子集
        if ((sum & 1) == 1) {
            return false;
        }

        int target = sum / 2;
        // 创建动态规划数组,dp[i][j] 表示使用前 i 个数字能否得到总和 j
        boolean[][] dp = new boolean[len][target + 1];

        //初始化第一行,只使用第一个数字 nums[0] 时,能达到的总和
        //防止数组越界要加个判断
        if (nums[0] <= target) {
            dp[0][nums[0]] = true;
        }

        // 动态规划,填充剩余的表格
        for (int i = 1; i < len; i++) {
            for (int j = 0; j <= target; j++) {
                // 先从上一行继承结果
                dp[i][j] = dp[i - 1][j];

                // 如果当前数字等于目标值 j,直接标记为 true
                if (nums[i] == j) {
                    dp[i][j] = true;
                    continue;
                }
                // 如果当前数字小于 j,考虑两种情况:不取或取当前数字
                if (nums[i] < j) {
                    dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
                }
            }
        }
        // 返回最终结果,即使用所有数字是否能达到总和 target
        return dp[len - 1][target];
    }
}

状态压缩(一维)

本质上,这个问题是一个子集求和问题,类似于背包问题。问题可以被转化为:是否可以从数组中选择一些数字,使得这些数字的总和等于数组所有元素总和的一半。

dp[i] 表示数组是否可以形成总和为 i 的子集。初始化 dp[0] 为 true,因为总和为 0 总是可能的。

public class Solution {
    public boolean canPartition(int[] nums) {
        // 计算数组总和
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }

        // 如果总和是奇数,则不能分割成两个和相等的子集
        if (sum % 2 != 0) {
            return false;
        }

        // 计算子集总和目标值(数组总和的一半)
        sum /= 2;

        // 初始化动态规划数组
        boolean[] dp = new boolean[sum + 1];
        dp[0] = true; // 总和为0总是可能的

        // 遍历数组中的每个数字
        for (int i = 1; i < len; i++) {
            // 从子集总和目标值向下遍历到当前数字
            for (int j = sum; nums[i] <= j; j--) {
                // 更新动态规划数组的值
                // dp[j] = true 代表不包含当前考虑的 num 就已经能组成子集

                //dp[j - num] = true  代表的是包含当前考虑的 num 的解
                dp[j] = dp[j] || dp[j - nums[i]];
            }
        }

        // 如果dp[sum]为true,则表示可以分割数组成两个和相等的子集
        return dp[sum];
    }
}

思考:为什么压缩到一维时,内循环要采用逆序?

因为在一维情况下,是根据 dp[j] || dp[j - nums[i]]来推d[j]的值,如不逆序,就无法保证在外循环 i 值保持不变 j 值递增的情况下,dp[j - num[i]]的值不会被当前所放入的nums[i]所修改,当j值未到达临界条件前,会一直被nums[i]影响,也即是可能重复的放入了多次nums[i],为了避免前面对后面产生影响,故用逆序。

举个例子,数组为[2,2,3,5],要找和为6的组合

  • i = 0时,dp[2]为true,
  • 当i自增到1,j = 4时,nums[i] = 2,dp[4] = (dp[4] || dp[4 - 2]) 为true,
    • 当i不变,j = 6时,dp[6] = dp [6] || dp [6 - 2],而dp[4]为true,

所以dp[6] = true,显然是错误的。 如果是正序的话,后面dp访问前面的dp时得到的是已经更新的内容,此时求的是完全背包问题。故必须得纠正在正序时,i值不变时多次放入nums[i]影响后续判断的情况。

但如果我们逆序遍历 j

  • i = 1nums[i] = 2,逆序遍历 j
    • j = 6dp[6] = dp[6] || dp[6 - 2],此时 dp[6 - 2]dp[4] 还没有被当前的 nums[i] 更新,所以这是基于 i = 0 时的结果。
    • 接下来,当 j = 4,更新 dp[4],但这个更新不会影响到 j = 6 的计算。

逆序遍历保证了在计算 dp[j] 时,我们使用的 dp[j - nums[i]] 是基于之前元素的结果,而不是当前元素 nums[i] 的结果。这样就避免了重复计算同一个元素。

引用资料 

动态规划(转换为 0-1 背包问题)

「力扣」上的 0-1 背包问题:

「力扣」第 416 题:分割等和子集(中等);
「力扣」第 474 题:一和零(中等);
「力扣」第 494 题:目标和(中等);
「力扣」第 879 题:盈利计划(困难);

「力扣」上的 完全背包问题:

「力扣」第 322 题:零钱兑换(中等);
「力扣」第 518 题:零钱兑换 II(中等);
「力扣」第 1449 题:数位成本和为目标值的最大数字(困难)。
这里要注意鉴别:「力扣」第 377 题,不是「完全背包」问题。


背包问题资料下载,链接:百度云下载,密码:sjop 。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

OpenCV图像处理——C++实现亚像素尺寸标定板边缘轮廓提取

前言 标定模板&#xff08;Calibration Target&#xff09;在机器视觉、图像测量、摄影测量以及三维重建等应用中起着重要的作用。它被用于校正相机的畸变&#xff0c;确定物理尺寸和像素之间的换算关系&#xff0c;并建立相机成像的几何模型。通过使用相机拍摄带有固定间距图…

JavaScript中的数据缓存与内存泄露:解密前端性能优化与代码健康

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f525;系列专栏&#xff1a;JavaScript篇 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来JavaScript篇专栏内容:JavaScript-数据缓存与内存泄露 目录 说说你对事件循环的理解 一、是什么 二、宏…

Linux操作系统基础(7):Linux的文件路径

1. Linux文件路径的介绍 在Linux系统中&#xff0c;文件路径是用来定位文件或目录在文件系统中位置的一种表示方法&#xff0c;它能够帮助用户快速、准确地定位文件或目录&#xff0c;并进行相应的操作。 文件路径可以分为 绝对路径 和 相对路径 两种类型&#xff1a; 绝对路…

H266/VVC熵编码技术概述

熵编码 熵编码&#xff1a; 是指按信息熵原理进行的无损编码方式&#xff0c;无损熵编码也是有损视频编码中的一个关键模块&#xff0c;它处于视频压缩系统的最末端。熵编码把一列系用来表示视频序列的元素符号转变为一个用来传输或存储的压缩码流&#xff0c;输入的符号可能包…

提高开发效率的必备!超实用的VSCode插件推荐

前言 作为一个前端程序猿&#xff0c;我经常使用 VSCode 代码编辑器&#xff0c;但是每个开发者都有自己独特的工作风格和偏好。为了满足不同开发者的需求&#xff0c;VSCode 提供了丰富的插件生态系统。在本文中&#xff0c;我将为大家推荐一些强大的 VSCode 插件&#xff0c;…

Linux学习之系统编程3(进程及wait函数)

写在前面&#xff1a; 我的Linux的学习之路非常坎坷。第一次学习Linux是在大一下的开学没多久&#xff0c;结果因为不会安装VMware就无疾而终了&#xff0c;可以说是没开始就失败了。第二次学习Linux是在大一下快放暑假&#xff08;那个时候刚刚过完考试周&#xff09;&#xf…

SpringFrameWork

SpringFrameWork简介 介绍springFrameWork框架 Spring Framework是一个为企业级应用程序开发提供全面基础设施支持的开源框架&#xff0c;通过集成IoC、DI和AOP等技术&#xff0c;使得应用程序的开发更加灵活、可维护和可扩展。Spring MVC、SpringBoot、Spring Cloud、Spring D…

java实现大文件分片上传

背景&#xff1a; 公司后台管理系统有个需求&#xff0c;需要上传体积比较大的文件&#xff1a;500M&#xff0d;1024M&#xff1b;此时普通的文件上传显然有些吃力了&#xff0c;加上我司服务器配置本就不高&#xff0c;带宽也不大&#xff0c;所以必须考虑多线程异步上传来提…

数据结构与算法python版本之线性结构之队列Quene

什么是队列&#xff1f; 队列是一种有次序的数据集合&#xff0c;其特征是&#xff1a;新数据项的添加总发生在一端&#xff08;通常称为“尾rear”端&#xff09;&#xff0c;而现存数据项的移除总发生在另一端&#xff08;通常称为“首front”端&#xff09;&#xff1b;当数…

缓存数据一致性策略如何分类?

一、概述 数据库与缓存数据一致性问题&#xff0c;一直以来都是大家比较关注的问题。针对一致性的解决方案也是非常多&#xff0c;以下主要针对方案的梳理与分类&#xff1a; 数据库数据与缓存数据一致性的方案&#xff0c;可以从不同的角度来分类&#xff0c;比如&#xff1…

稳定币记录

稳定币&#xff1a; 稳定币&#xff08;Stablecoin&#xff09;是一种加密货币&#xff0c;其设计目的是维持相对稳定的价值&#xff0c;通常与某种法定货币&#xff08;如美元、欧元&#xff09;或其他资产&#xff08;如黄金&#xff09;挂钩。稳定币通过将加密货币与相应的…

Flink-【时间语义、窗口、水位线】

1. 时间语义 1.1 事件时间&#xff1a;数据产生的事件&#xff08;机器时间&#xff09;&#xff1b; 1.2 处理时间&#xff1a;数据处理的时间&#xff08;系统时间&#xff09;。 &#x1f330;&#xff1a;可乐 可乐的生产日期 事件时间&#xff08;可乐产生的时间&…

计算机毕业设计 SpringBoot的停车场管理系统 Javaweb项目 Java实战项目 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

逻辑回归算法到底能做什么

逻辑回归&#xff08;Logistic Regression&#xff09;是一种广义的线性回归分析模型&#xff0c;常用于数据挖掘、疾病自动诊断、经济预测等领域。它根据给定的自变量数据集来估计事件的发生概率。变量的范围在0和1之间&#xff0c;通常用于二分类问题&#xff0c;最终输出的预…

Opencv(C++)学习之cv::calcHist 任意bin数量进行直方图计算

**背景&#xff1a;**当前网上常见的直方图使用方法都是默认使用256的范围&#xff0c;而对于使用特定范围的直方图方法讲的不够清楚。仔细研究后总结如下&#xff1a; 1、常见使用方法&#xff0c;直接对灰度图按256个Bin进行计算。 Mat mHistUn; int channels[1] { 0 }; {…

键盘数字键打不出来怎么解锁?收藏好这4个简单方法!

“我在使用电脑进行办公时&#xff0c;突然发现我电脑键盘的数字键无法输入&#xff0c;这该怎么办呢&#xff1f;我应该如何解锁呢&#xff1f;请给我出出主意吧&#xff01;” 在日常使用电脑时&#xff0c;很多用户都需要使用键盘输入文字。但有时候部分用户也会遇到键盘数字…

你知道vue中key的原理吗?说说你对它的理解

一、Key是什么 开始之前&#xff0c;我们先还原两个实际工作场景 当我们在使用v-for时&#xff0c;需要给单元加上key <ul><li v-for"item in items" :key"item.id">...</li> </ul>用new Date()生成的时间戳作为key&#xff0c…

Docker 网络管理

一、Docker网络简介 Docker网络是容器化应用程序的重要组成部分&#xff0c;它使得容器之间可以互相通信和连接&#xff0c;同时也提供了容器与外部环境之间的隔离和连接。 二、Docker网络网络模式 Docker 提供了多种网络模式&#xff0c;可以通过docker network ls 命令查看…

springboot实现ChatGPT式调用(一次调用,持续返回)

下边实现了一个持续返回100以内随机数的接口&#xff0c;在接口超时之前会每隔1秒返回一个随机数 GetMapping(value "/getRandomNum", produces MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter getRandomNum() {SseEmitter emitter new SseEmitter();Th…

五、Spring AOP面向切面编程(基于注解方式实现和细节)

本章概要 Spring AOP底层技术组成初步实现获取通知细节信息切点表达式语法重用&#xff08;提取&#xff09;切点表达式环绕通知切面优先级设置CGLib动态代理生效注解实现小结 5.5.1 Spring AOP 底层技术组成 动态代理&#xff08;InvocationHandler&#xff09;&#xff1a;…