Day 46 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.确定dp数组以及下标的含义

dpi : 字符串长度为i的话,dpi为true,表示可以拆分为一个或多个在字典中出现的单词

​ 2.确定递推公式

​ 如果确定dpj 是true,且 j, i 这个区间的子串出现在字典里,那么dpi一定是true。(j < i )。

​ 所以递推公式是 if(j, i 这个区间的子串出现在字典里 && dpj是true) 那么 dpi = true。

​ 3.dp数组如何初始化

​ 从递推公式中可以看出,dpi 的状态依靠 dpj是否为true,那么dp0一定是true,否则递推下去后面都都是false了;

​ 那么dp0有没有意义呢?dp0表示如果字符串为空的话,能否在字典中找到,很明显应该是false;

​ 但题目中说了"给定一个非空字符串 s" 所以测试数据中不会出现i为0的情况,那么dp0怎样定义其实无所谓了;

​ 下标非0的dpi初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词;

​ 其实很多时候都会出现这种dp0赋值和意义不一致的情况,以递推公式为主;

​ 4.确定遍历顺序

​ 讨论两层for循环的前后顺序。

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

​ 而本题其实求的是排列数, 拿 s = "applepenapple", wordDict = "apple", "pen" 举例;

​ "apple", "pen" 是物品,那么我们要求 物品的组合一定是 "apple" + "pen" + "apple" 才能组成 "applepenapple";

​ "apple" + "apple" + "pen" 或者 "pen" + "apple" + "apple" 是不可以的,此处就是强调物品之间顺序;

​ 所以一定是先遍历背包,再遍历物品

c++ 复制代码
	for(int i = 1; i < s.size(); i++) {
		for(int j = 0; j < i; j++){
            string tempWord = s.substr(j, i - 1);
            if(dict.find(tempWord) != dict.end() && dp[j] == true){
                dp[i] = true;
            }
        }
	}

​ 5.打印dp数组:

cpp 复制代码
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
		unordered_set<string> wordSet(wordDict.begin(), wordDict.end());//转化为unordered_set(即wordSet)的原因是为了提高查找效率
        vector<bool> dp(s.size() + 1, 0);
        dp[0] = true;
        for(int i = 1; i <= s.size(); i++) {
			for(int j = 0; j < i; j++){
            	string tempWord = s.substr(j, i - j);
            	if(wordSet.find(tempWord) != wordSet.end() && dp[j] == true){
                	dp[i] = true;
            	}
        	}
		}
        return dp[s.size()];
    }
};

​ 时间复杂度:O(n^3),因为substr返回子串的副本是O(n)的复杂度(这里的n是substring的长度)

​ 空间复杂度:O(n)

多重背包

​ 多重背包本质上可以视为01背包,因为数量仍然是有限个;

​ 每件物品最多有M件可用,把M件摊开,其实01背包问题了;

​ 但是不能完全按照01背包的代码来写,因为vector扩容是一件非常耗时的事情;

​ 递推公式写成如下的形式,把每种商品遍历的个数放在01背包里面在遍历一遍,再递推,就解决了:

​ d p j = m a x ( d p j , d p j − k ∗ w e i g h t \[ i ] + k ∗ v a l u e i ) dpj = max(dpj, dpj - k \* weight\[i] + k * valuei) dpj=max(dpj,dpj−k∗weight\[i]+k∗valuei);

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;
int main() {
    int bagWeight,n;
    cin >> bagWeight >> n;
    vector<int> weight(n, 0);
    vector<int> value(n, 0);
    vector<int> nums(n, 0);
    for (int i = 0; i < n; i++) cin >> weight[i];
    for (int i = 0; i < n; i++) cin >> value[i];
    for (int i = 0; i < n; i++) cin >> nums[i];

    vector<int> dp(bagWeight + 1, 0);

    for(int i = 0; i < n; i++) { // 遍历物品
        for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
            // 以上为01背包,然后加一个遍历个数
            for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) { // 遍历个数
                dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i]);
            }
        }
    }

    cout << dp[bagWeight] << endl;
}

​ 时间复杂度:O(m × n × k),m:物品种类个数,n背包容量,k单类物品数量

背包问题总结

递推公式

​ 问能否能装满背包(或者最多装多少):dpj = max(dpj, dpj - nums\[i] + numsi); ,对应题目如下:

动态规划:416.分割等和子集(opens new window)

动态规划:1049.最后一块石头的重量 II(opens new window)

问装满背包有几种方法:dpj += dpj - nums\[i] ,对应题目如下:

动态规划:494.目标和(opens new window)

动态规划:518. 零钱兑换 II(opens new window)

动态规划:377.组合总和Ⅳ(opens new window)

动态规划:70. 爬楼梯进阶版(完全背包)(opens new window)

问背包装满最大价值:dpj = max(dpj, dpj - weight\[i] + valuei); ,对应题目如下:

动态规划:474.一和零(opens new window)

问装满背包所有物品的最小个数:dpj = min(dpj - coins\[i] + 1, dpj); ,对应题目如下:

动态规划:322.零钱兑换(opens new window)

动态规划:279.完全平方数

遍历顺序

对于01背包

​ 二维dp数组的两个for遍历的先后循序是可以颠倒的;

​ 一维dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量

​ 对于完全背包

​ 因为dpj 是根据下标j之前所对应的dpj计算出来的;

​ 只要保证下标j之前的dpj都是经过计算的就可以了,颠倒是不会影响结果的;

​ 但如果题目有所变动,不再是求纯完全背包问题:

如果求组合数就是外层for循环遍历物品,内层for遍历背包

for循环先后循序一定是先遍历物品,再遍历背包容量

​ 对于完全背包

​ 因为dpj 是根据下标j之前所对应的dpj计算出来的;

​ 只要保证下标j之前的dpj都是经过计算的就可以了,颠倒是不会影响结果的;

​ 但如果题目有所变动,不再是求纯完全背包问题:

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

相关推荐
江畔柳前堤10 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
Forever Nore11 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考13 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX13 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面14 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号14 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽14 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI15 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者16 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者16 小时前
【征程6】校准量化中HistogramObserver解析
算法