zoukankan      html  css  js  c++  java
  • leetcode691:Stickers to Spell Word

    题目链接
    给定一个字符串target和一个字符串数组stickers[],要求从stickers中选取尽量少的字符串,以这些字符串所包含的字母为原材料,拼出target。
    其中:
    对于stickers中的每个字符串,可以使用多次。
    target和stichers中的字符串字符集全部为小写字母集。

    分析

    初看这道题,这是一个整数规划问题。
    target为aabbb,stickers为[abb,aab]。
    设需要x1个abb,x2个aab,则:
    min z=x1+x2

    1*x1+2*x2>=2(提供的a的个数应该大于target中包含的a的个数)
    2*x1+x2>=3(提供的b的个数应该大于target中包含的b的个数)
    x1和x2为非负整数

    原题目等价于整数规划问题,这是一个NP问题。
    但是题目给的数据范围有提示:target的长度小于15。

    方法一:暴力搜索

    import collections
    
    
    class Solution(object):
        def minStickers(self, stickers, target):
            # 统计target中各个字符的个数
            t_count = collections.Counter(target)
            # 统计stickers中每个字符串中各个字符的个数,去掉target中不包含的字符
            A = [collections.Counter(sticker) & t_count
                 for sticker in stickers]
            # 如果sticker1包含sticker2,那么sticker2是无论如何不会选择的
            # 这个循环必须倒着写,因为要删除元素
            for i in range(len(A) - 1, -1, -1):
                if any(A[i] == A[i] & A[j] for j in range(len(A)) if i != j):
                    A.pop(i)
    
            self.best = len(target) + 1
    
            def search(ans=0):
                if ans >= self.best: return
                if not A:
                    if all(t_count[letter] <= 0 for letter in t_count):
                        self.best = ans
                    return
                # A一直进行pop操作,直到A为空的时候,得到答案
                sticker = A.pop()
                # 在当前的target中至多需要多少个sticker,当前的target用t_count来表示
                used = max((t_count[letter] - 1) // sticker[letter] + 1
                           for letter in sticker)
                used = max(used, 0)
    
                for c in sticker:
                    t_count[c] -= used * sticker[c]
    
                search(ans + used)
                # 实际使用该sticker的个数可以是0~used之间的任意一个数字,都要尝试一遍
                for i in range(used - 1, -1, -1):
                    for letter in sticker:
                        t_count[letter] += sticker[letter]
                    search(ans + i)
    
                A.append(sticker)
    
            search()
            return self.best if self.best <= len(target) else -1
    
    
    

    方法二:动态规划

    使用Python语言编写动态规划代码时,很容易超时。必须加上一个优化,对stickers进行“瘦身”,如果sticker A完爆sticker B,那么无论如何都不能选择B,这样就可以把B从stickers中删掉。使用C++是不需要这个优化就能通过的。

    import collections
    
    
    class Solution:
        def minStickers(self, stickers, target):
            """
            :type stickers: List[str]
            :type target: str
            :rtype: int
            """
            # 首先,进行一些优化,对stickers进行“瘦身”操作,没有这一步,会超时的
            t_count = collections.Counter(target)
            A = [collections.Counter(sticker) & t_count
                 for sticker in stickers]
    
            for i in range(len(A) - 1, -1, -1):
                if any(A[i] == A[i] & A[j] for j in range(len(A)) if i != j):
                    A.pop(i)
    
            stickers = ["".join(s_count.elements()) for s_count in A]
            # stickers瘦身完毕,开始执行动规
            N = 1 << len(target)
            big = 0xffffff
            dp = [big] * N
            dp[0] = 0
            for i in range(N):
                if dp[i] == big:
                    continue
                # 当前状态为i,让当前状态加上每一个sticker,为将来的状态进行赋值
                for j in stickers:
                    now = i
                    # 怎么让当前状态now+当前sticker j呢?
                    # 要让sticker j中的每一个字符都发挥作用
                    for k in j:
                        for r in range(len(target)):
                            if now & (1 << r):  # 如果now中已经有了第r个字符,那就不需要了
                                continue
                            # now中没有,并且target中有,sticker j中也有,那就可以应用字符k了
                            if k == target[r]:
                                now |= (1 << r)
                                break
                    dp[now] = min(dp[i] + 1, dp[now])
            return -1 if dp[-1] > len(target) else dp[-1]
    
    

    这是一种很常见的、动态规划解决NP问题的模式!每个状态用一个数字表示,然后从0开始循环,把每个状态的答案保存起来。用当前状态+操作产生未来状态,并给未来状态进行赋值。

    dp=[1<<N]
    dp[0]=0
    for cur in range(1<<N):#当前状态
        for op in operations:#所有操作
            nextState,value=apply(cur,op)#对当前状态应用操作
            dp[nextState]=min(value,dp[nextState])#更新将来的状态
    
    

    从下面往上面赋值要比从上面往下面赋值简单清晰的多!
    更关键的是,有时候只能自下而上的更新状态,因为上面根本不知道自己前面有哪些状态。
    这个数组dp虽然是一维数组,实际上它是一个有向无环图。如果状态A可以推出状态B,那么就有一条从A指向B的有向边。将此一维数组一展开,就是一个有向无环图。在这个有向无环图中,寻找某个节点的后继结点可能很容易,而求解某个结点的前向结点就很复杂(关键是要找到全部的前向结点)。这种问题就像二极管一样,从一个方向到另一个方向可以很容易通过,从另一方向回来就难了。就像中国象棋中的马一样,跳过去容易,跳回来难。但是,不排除有时候寻找前向结点的可能性。也许,有些问题就是对状态减去某个操作比较简单。

    dp=[1<<N]
    dp[0]=0 
    for cur in range(1,1<<N):
        for op in operations:
            lastState=sub(cur,op)
            dp[cur]=min(dp[lastState]+1,dp[cur])
    

    方法三:一种超时算法

    统计字符个数之后进行动规。
    动规的关键在于如何定义每一个状态,定义完状态之后,状态之间通过状态转移来联系起来,就会产生一个有向无环图。
    方法二中,没有对输入的各个字符串进行处理,导致循环中需要循环O(字符串长度)次。如果预先统计出每个字符串中各个字符的个数就可以循环O(字符种类数)次。

    import collections
    import functools
    from queue import Queue
    
    
    class Solution:
        def minStickers(self, stickers, target):
            """
            :type stickers: List[str]
            :type target: str
            :rtype: int
            """
    
            def solve(A, b):
                ks = list(b.keys())
                vs = [b[i] for i in ks]
                START = tuple([0] * len(b))  # 当前状态
                ma = dict()
                ma[START] = 0
                states = Queue()  # 状态队列
                states.put(START)
                while states.qsize():
                    nowTuple = states.get()
                    nowStep = ma[nowTuple]
                    # print(nowTuple, nowStep)
                    for sticker in A:  # 当前状态加上每一个sticker产生未来的状态
                        state = list(nowTuple)
                        for j, c in enumerate(ks):
                            if c in sticker:
                                state[j] = min(state[j] + sticker[c], vs[j])
                        t = tuple(state)
                        if t not in ma or nowStep + 1 < ma[t]:
                            if t not in ma:
                                states.put(t)
                            ma[t] = nowStep + 1
    
                return ma[tuple(vs)]
    
            A = [collections.Counter(i) for i in stickers]
            b = collections.Counter(target)
            # 对stickers进行瘦身
            for i in range(len(A) - 1, -1, -1):
                if any(A[i] == A[i] & A[j] for j in range(len(A)) if i != j):
                    A.pop(i)
            # stickers包含的字母集合
            s = functools.reduce(lambda s, i: s | i, A, collections.Counter())
            return -1 if not all(i in s for i in b)else solve(A, b)
    

    这种方法虽然超时了,但是我认为当字符串长度足够长的时候,这种算法效率更高些。

  • 相关阅读:
    第一章 搭建Qt开发环境
    Git
    spring和springmvc
    spring注解
    转:spring mvc 设置@Scope("prototype")
    转:win10完美去除快捷方式小箭头
    数据库操作之DBUtils
    java.awt.headless系统设定
    POI 导出Exicel, 报Could not initialize class sun.awt.X11FontManager错误
    ORA-01950: no privileges on tablespace
  • 原文地址:https://www.cnblogs.com/weiyinfu/p/7639064.html
Copyright © 2011-2022 走看看