Leetcode Link: 691. 贴纸拼词 - 力扣(LeetCode)

题目

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

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

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

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

从暴力递归到动态规划记忆化搜索

暴力递归

可以发现,字符和贴纸的顺序都不是重要的,我只要贴纸提供的东西能够和字符里的东西一一对应就行 同时也注意到,字符也不一定要有固定的相对顺序,因为贴纸可以剪的很碎,如果剪成一个字母,实际上就不要求字符的顺序了。

这样的话,我们就可以先对字符排序了

就拿可以拿的贴纸解决第一个字母

class Solution:
    def minStickers(self, stickers: List[str], target: str) -> int:
        res = self.process(stickers, target)
        if res == float('inf'): # 表示都不行,就返回-1
            return -1
        else:
            return res
 
    # 给定当前剩余的字符和所有贴纸,返回最小贴纸数量
    def process(self, stickers, cur):
        # base case: 如果当前的字符一个东西也没有了,那就是需要0张
        if cur=="":  
            return 0
        
        # 给定结果一个初始状态,表示无法拼成(-1不行,会在min处出错)
        res = float('inf')
        # 轮流选择贴纸来作为第一张
        # 如果选择完之后剩余的贴纸有变化,则送到同样的子问题中
        # 反之说明该贴纸没用,就不用送到子问题了。
        for s in stickers:
            rest = self.minus(cur, s)  # 从 cur 中扣掉 s 中的每一个字母
            if len(rest) < len(cur):  
                res = min(res, self.process(stickers, rest))
        
        if res == float('inf'):
            return float('inf')
        else:
            return res + 1 # 加1表示加上本函数中选择的贴纸      
 
    def minus(self, target, strs):
        for s in strs:
            target = target.replace(s, "", 1)
        return target

记忆化搜索

画调用栈来思考能不能记忆化搜索 |400

如果想用一个memo来记录以往的答案,需要意识到字符串内的字母顺序不同,会导致不同的key,所以需要先对其排序,保证key的唯一性后才能使用记忆化搜索。

class Solution:
    def minStickers(self, stickers: List[str], target: str) -> int:
        self.memo = {}
        # 对一个字符串排序(还是得到的字符串) ! 注意这里不是列表!
        target = sorted(target)
        target = "".join(target)
        res = self.process(stickers, target)
        if res == float('inf'):
            return -1
        else:
            return res
 
    # 给定当前的字符和所有贴纸,返回最小贴纸数量
    def process(self, stickers, cur):
        # base case: 如果当前的字符一个东西也没有了,那就是需要0张
        if cur=="":  
            return 0
        
        if cur in self.memo:
            return self.memo[cur]
 
 
        # 给定结果一个初始状态,表示无法拼成(-1不行,会在min处出错)
        res = float('inf')
        # 轮流选择贴纸来作为第一张
        # 如果选择完之后剩余的贴纸有变化,则送到同样的子问题中
        # 反之说明该贴纸没用,就不用送到子问题了。
        for s in stickers:
            rest = self.minus(cur, s)  # 从 cur 中扣掉 s 中的每一个字母
            if len(rest) < len(cur):  
                rest = sorted(rest) # 必需排序
                rest = "".join(rest)
                res = min(res, self.process(stickers, rest))
        
        if res == float('inf'):
            self.memo[cur] = float('inf')
            return float('inf')
        else:
            self.memo[cur] = res + 1 
            return res + 1         
 
    def minus(self, target, strs): 
	    # 这里也可以用词频来做
	    # 两个dict,表示target和strs的词频(每个字母出现多少次)
        for s in strs:
            target = target.replace(s, "", 1)
        return target

至此,我们发现这个不能用dp,因为依赖的是字符串,字符串没法变成严格的表依赖

一种剪枝的方法

利用词频表进行剪枝 同时利用词频表速度也会快一点

任何一个顺序不重要的字符串,都可以用一个[1x26]的数字表示,每个位置表示对应字母的出现次数。

这样后面处理的速度可能会快一点

class Solution:
    def minStickers(self, stickers: List[str], target: str) -> int:
        n = len(stickers)
        dicts = [[0 for _ in range(26)] for _ in range(n)]
        tar = [0 for _ in range(26)]
        # 制作两个词频表
        for idx, word in enumerate(stickers):
            for c in word:
                dicts[idx][ord(c)-ord('a')] += 1
        for c in target:
            tar[ord(c)-ord('a')] += 1
        
        res = self.process(dicts, tar)
        if res == float('inf'):
            return -1
        else:
            return res
 
    # 以词频表的形式给定当前的字符和所有贴纸,返回最小贴纸数量
    def process(self, stickers, cur):
        # base case: 如果当前的字符一个东西也没有了,那就是需要0张
        if sum(cur)==0:  # 这里有更改,词频都是0表示字符串为空
            return 0
 
        res = float('inf')
        for word in stickers:
            flag = 0 # 表示还没走到第一位,亦表示该位不能进入下一个子问题,即剪枝
            tmp_cur = cur.copy()
            for i in range(26):
                if flag == 0 and tmp_cur[i] != 0 and word[i] == 0:
                    break # 走到第一位发现word里没有这一位,剪枝,不走了。
                if flag == 0 and tmp_cur[i] != 0 and word[i] != 0:
                    flag = 1 # 走到第一位发现word里有这一位,可以走
                if flag == 1: # 两个词频表互怼
                    tmp_cur[i] -= word[i]
                    if tmp_cur[i] < 0:
                        tmp_cur[i] = 0
            if flag == 1:
                res = min(res, self.process(stickers, tmp_cur))
        
        if res == float('inf'):
            return float('inf')
        else:
            return res + 1         

emmm,对是对了… 超时了…

启发和联系

  • 遇到那种顺序不重要的字符串,尝试考虑词频表解决一下。
  • 像这种从左往右的模型,不一定有idx来表征位置,表示我从左向右处理某一个东西,本题是字符串。即我就考虑当前最左边的消除掉,但是同时也可能消除掉其他的一些,然后把剩余的传到下一个子问题。