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

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 中的所有字符串 互不相同

解题思路

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;
        // 创建二维状态数组,行:物品索引,列:容量(包括 0)
        boolean[][] dp = new boolean[len][target + 1];

        // 先填表格第 0 行,第 1 个数只能让容积为它自己的背包恰好装满
        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];

                if (nums[i] == j) {
                    dp[i][j] = true;
                    continue;
                }
                if (nums[i] < j) {
                    dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
                }
            }
        }
        return dp[len - 1][target];
    }
}

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 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

二维动态规划

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

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

  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]]

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;
        // 创建二维状态数组,行:物品索引,列:容量(包括 0)
        boolean[][] dp = new boolean[len][target + 1];

        // 先填表格第 0 行,第 1 个数只能让容积为它自己的背包恰好装满
        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];

                if (nums[i] == j) {
                    dp[i][j] = true;
                    continue;
                }
                if (nums[i] < j) {
                    dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
                }
            }
        }
        return dp[len - 1][target];
    }
}

状态压缩(一维)

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

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

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

  2. 初始化动态规划数组:创建一个布尔型动态规划数组 dp,其大小为 sum / 2 + 1dp[i] 表示数组是否可以形成总和为 i 的子集。初始化 dp[0]true,因为总和为 0 总是可能的。

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

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

//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 num : nums) {
            // 从子集总和目标值向下遍历到当前数字
            for (int i = sum; i >= num; i--) {
                // 更新动态规划数组的值
                // dp[i] = true 如果不包含当前数字就已经能组成子集(dp[i])
                // 或者包含当前数字就能组成子集(dp[i - num])
                //考虑 dp[i] 时,dp[i - num] 代表的是不包含当前考虑的 num 的解.num在外层循环,内层循环中对同一个位置的dp只有影响1次
                dp[i] = dp[i] || dp[i - num];
            }
        }

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

你可能感兴趣的:(#,算法,力扣,动态规划)