【C/C++】691. 贴纸拼词

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第10天,点击查看活动详情


题目链接:691. 贴纸拼词

题目描述

我们有 n 种不同的贴纸。每个贴纸上都有一个小写的英文单词。

您想要拼写出给定的字符串 target ,方法是从收集的贴纸中切割单个字母并重新排列它们。如果你愿意,你可以多次使用每个贴纸,每个贴纸的数量是无限的。

返回你需要拼出 target 的最小贴纸数量。如果任务不可能,则返回 -1

注意: 在所有的测试用例中,所有的单词都是从 1000 个最常见的美国英语单词中随机选择的,并且 target 被选择为两个随机单词的连接。

提示:

  • n = = s t i c k e r s . l e n g t h n == stickers.length
  • 1 n 50 1 \leqslant n \leqslant 50
  • 1 s t i c k e r s [ i ] . l e n g t h 10 1 \leqslant stickers[i].length \leqslant 10
  • 1 t a r g e t . l e n g t h 15 1 \leqslant target.length \leqslant 15
  • stickers[i] 和 target 由小写英文单词组成

示例 1:

输入: stickers = ["with","example","science"], target = "thehat"
输出:3
解释:
我们可以使用 2 个 "with" 贴纸,和 1 个 "example" 贴纸。
把贴纸上的字母剪下来并重新排列后,就可以形成目标 “thehat“ 了。
此外,这是形成目标字符串所需的最小贴纸数量。
复制代码

示例 2:

输入:stickers = ["notice","possible"], target = "basicbasic"
输出:-1
解释:我们不能通过剪切给定贴纸的字母来形成目标“basicbasic”。
复制代码

整理题意

题目给定了一组单词 stickers ,定义为每个单词在一张贴纸上,每个单词贴纸有无数张,又给定了目标字符串 target ,现在要求利用这些单词贴纸,来拼凑这个字符串,要求返回使用贴纸最少的数量,如果不能拼凑成目标字符串 target 返回 -1

解题思路分析

首先观察题目数据范围,目标字符串 target 长度不超过 15,看到这个数据范围,通常要联想到使用 状态压缩 ,所谓状态压缩也就是将状态采用整数的形式进行表示,比如这里我们需要表示目标字符串 target 的构造状态,对于每个字符有两种状态,已构造和未构造两种状态,所以可以采用二进制进行状态表示,该二进制整数有 15 位(target 长度),每一位对应目标字符串 target 中的字符的构造状态,0 表示未构造,1 表示已构造。因为 2 15 2^{15} 小于 int 型表示范围,所以可以直接采用 int 整数来表示状态,初始状态全为 00000……0000)表示都还未构造,目标状态全为 11111……1111)表示构造完成。

利用整数来表示状态就是状态压缩,同样我们还可以扩展到三进制、四进制等等

方法一:状态压缩 + 记忆化搜索

对于每个状态可以采用爆搜的方法进行判断所需的最少贴纸数量,但是考虑到爆搜在时间复杂度上面会超时 TLE,通常会对搜索进行 记忆化 处理,这也就是所谓的 记忆化搜索 了。我们记录每个状态到构造完成所需的最少贴纸数,当再次到达相同状态时(也就是已经搜索过的状态),我们可以直接返回答案,而不需要再次搜索答案。

方法二:状态压缩 + 动态规划

对于每个状态可以在 n 个单词贴纸中选取一个贴纸转移至下一个状态,同样可以从状态 0 开始,遍历每一种状态所需的最小贴纸数量,直至目标状态。

优化

在记忆化搜索和动态规划中仍然存在重复操作,比如对于 贴纸1贴纸2 来说,先选取 贴纸1 再选取 贴纸2 和先选取 贴纸2 再选取 贴纸1 最后到达的状态是一样的,也就是殊途同归的。我们不考虑选取的顺序,而是考虑选取包含当前状态缺少的字符贴纸。

对于每个状态中缺少的字符,我们希望能够快速从所有贴纸中找到包含当前缺少字符的贴纸,对所有单词贴纸进行预处理,得到一个二维不定长数组 visvis[i] 中包含当前所需字母 i 的单词贴纸编号。

那么对于每个状态来说,我们只需找到当前状态第一个所缺少的是一个字母,通过字母找到对应包含该字母的所有贴纸进行遍历和转移状态,这样就避免了 1 -> 22 -> 1 殊途同归的问题了。

具体实现

方法一:状态压缩 + 记忆化搜索

  1. 首先初始化记忆数组,将每个状态记录为不可达状态(-1),将状态 0 记录为 0,表示无需贴纸即可到达该状态。
  2. 从状态 0 出发进行搜索;
  3. 对于每个状态进行判断是否为目标状态(停止搜索),是否已经搜索过(返回之前记录的答案)。
  4. 对于每个状态尝试使用 n 个贴纸进行状态转移,对下一个状态继续进行搜索,选取到达目标状态所需贴纸数最少的作为当前状态的答案。
  5. 返回当前状态所需最少贴纸数量并记录。

方法二:状态压缩 + 动态规划(带优化)

  1. 预处理从 a - z 每个字母被哪些贴纸所包含,记录贴纸编号。
  2. 创建并初始化 dp[i] 一维数组所有状态所需贴纸数为 m + 1mtarget 长度),表示所有状态不可达,这是因为长度为 m 的字符串最多需要 m 张贴纸。并将 dp[0] 初始化为 0 表示无需贴纸。
  3. 遍历 [0, mask - 1) 左闭右开区间(mask == 1 << m),因为 mask - 1 为最终状态,无缺少字符,所以无需遍历。
  4. 查找当前状态下第一个缺少的字符,包含当前缺少字符的贴纸编号在预处理的时候已经处理过了,直接遍历即可。
  5. 通过字母数组哈希表记录每个贴纸上的字母个数,并对当前状态下所缺少的字符进行更新。
  6. 更新能够通过当前状态到达的下一个状态的最少贴纸数即可。

复杂度分析

方法一:状态压缩 + 记忆化搜索

  • 时间复杂度: O ( 2 m × n × ( c + m ) ) O(2 ^ m \times n \times (c + m)) ,其中 mtarget 的长度,c 为每个 sticker 的平均字符数。一共有 O ( 2 m ) O(2 ^ m) 个状态。计算每个状态时,需要遍历 nsticker。遍历每个 sticker 时,需要遍历它所有字符和 target 所有字符。
  • 空间复杂度: O ( 2 m ) O(2 ^ m) ,记忆化时需要保存每个状态的贴纸数量。

方法二:状态压缩 + 动态规划(带优化)

  • 时间复杂度: O ( 2 m × n × ( c + m ) ) O(2 ^ m \times n \times (c + m)) ,由于预处理了贴纸编号,所以时间复杂度是远远小于理论值的。
  • 空间复杂度: O ( 2 m ) O(2 ^ m) ,记忆化时需要保存每个状态的贴纸数量。

代码实现

方法一:状态压缩 + 记忆化搜索

class Solution {
private:
    int n, m;
    vector<int> vis;
    vector<string> stickers;
    string target;
    int dfs(int state){
        if(state == (1 << m) - 1) return 0;
        if(vis[state] != -1) return vis[state];
        //最多m张贴纸,因为target长度为m,所以设置m+1为最大值
        int ans = m + 1;
        for(int i = 0; i < n; i++){
            int nxt = state;
            int len = stickers[i].length();
            for(int j = 0; j < len; j++){
                char c = stickers[i][j];
                for(int k = 0; k < m; k++){
                    //如果当前字符与target中字符相同且当前状态没有这个字符
                    if(c == target[k] && ((1 << k) & nxt) == 0){
                        nxt |= (1 << k);
                        break;
                    }
                }
            }
            if(nxt != state) ans = min(ans, dfs(nxt) + 1);
        }
        //记录最小贴纸数同时返回最小贴纸数
        return vis[state] = ans;
    }
public:
    int minStickers(vector<string>& sti, string tar) {
        stickers = sti;
        target = tar;
        n = stickers.size();
        m = target.length();
        //vis数组记录当前状态state到达target所需最小贴纸数量,初始化为-1表示不可达
        vis.resize(1 << m, -1);
        //二进制表示target状态state,0表示没有,1表示有
        int res = dfs(0);
        return res == m + 1 ? -1 : res;
    }
};
复制代码

方法二:状态压缩 + 动态规划(带优化)

class Solution {
public:
    int minStickers(vector<string>& stickers, string target) {
        int n = stickers.size();
        int m = target.length();
        //记录包含所需字母的贴纸序号
        vector<vector<int>> vis(26);
        for(int i = 0; i < 26; i++) vis[i].clear();
        for(int i = 0; i < n; i++){
            int len = stickers[i].length();
            for(int j = 0; j < len; j++){
                int c = stickers[i][j] - 'a';
                //将当前贴纸序号压入对应字母列表中,保证不重不漏
                if(vis[c].empty() || vis[c].back() != i){
                    vis[c].push_back(i);
                }
            }
        }
        //状态总数mask
        int mask = 1 << m;
        //初始化为m+1,因为最多m张贴纸即可完成
        vector<int> dp(mask, m + 1);
        //初始化状态 0 无需贴纸
        dp[0] = 0;
        //遍历每一种状态
        for(int pre = 0; pre < mask - 1; pre++){
            //无法通过状态0到达该状态
            if(dp[pre] == m + 1) continue;
            //寻找当前状态下第一个缺少的字符
            int c = -1;
            for(int i = 0; i < m; i++){
                if((pre & (1 << i)) == 0){
                    c = i;
                    break;
                }
            }
            //因为小于 mask - 1 所以 c 一定是可以找到缺少的字符
            c = target[c] - 'a';
            //遍历包含当前字母的贴纸序号
            int sz = vis[c].size();
            for(int i = 0; i < sz; i++){
                int k = vis[c][i];
                int len = stickers[k].length();
                //字母数组哈希表记录贴纸上的字母个数
                int num[26];
                memset(num, 0, sizeof(num));
                for(int j = 0; j < len; j++) num[stickers[k][j] - 'a']++;
                //now 表示从状态 pre 出发通过该贴纸可以到达的状态
                int now = pre;
                for(int j = 0; j < m; j++){
                    //对比当前状态是否缺少该字符,以及当前贴纸是否有
                    if(((1 << j) & now) == 0 && num[target[j] - 'a'] > 0){
                        num[target[j] - 'a']--;
                        now |= (1 << j);
                    }
                }
                //更新答案
                dp[now] = min(dp[now], dp[pre] + 1);
            }
        }
        //如果不能达到状态mask - 1,dp[mask - 1]的值为-1
        return dp[mask - 1] == m + 1 ? -1 : dp[mask - 1];
    }
};
复制代码

总结

  • 该题核心思想为 状态压缩搜索DFS逆向搜索 的过程,动态规划是 正向搜索 的过程)。当我们看见数据范围较小(小于 20 时),同时这道题目中有涉及到是否选取、是否使用这样的二元状态,那么这道题目很可能就是一道状态压缩的题目。
  • 其次需要注意优化,考虑到选取贴纸顺序与最终状态无关紧要,只是殊途同归的问题,考虑如何优化顺序导致的重复状态成为难点。首先确定每次需要找的字符为第一个缺少的字符,其次考虑如何快速寻找包含缺少字符的贴纸编号,这里就自然能够想到预处理出每个字母被哪些贴纸包含。
  • 优化前后的时间复杂度差异还是很明显的:

微信截图_20220603200409.png

结束语

人生难免会面对各种烦恼,若一直耿耿于怀,只会让自己难受。何不把往事清零,学会轻装前行,放下烦恼,才能腾出手来拥抱当下的幸福。愿我们都能拥有好状态,去笑对生活中的风风雨雨。

猜你喜欢

转载自juejin.im/post/7104987872444284936