classSolution: deffindNumOfValidWords(self, words: List[str], puzzles: List[str]) -> List[int]: # 初始化答案数组 result = {} for p in puzzles: result[p] = 0 sets = {}
for w in words: for p in puzzles:
if p in sets: p_set = sets[p] else: p_set = set([i for i in p]) sets[p] = p_set first_c = False c_c = True for c in w: #检查第一个字母 if c == p[0]: first_c = True if c notin p_set: c_c = False break if first_c and c_c: result[p] += 1 return [ result[key] for key in puzzles ]
好吧,只能看答案了。原来这道题目的套路叫“状态压缩”,我自己主动放弃的一个已知条件是,“没有顺序也没有频率的要求”,与其说是“放弃”,不如说是“不会用”。还有个条件是,word 和 puzzle 里只会出现小写字母。然后这题目就成了判断 word 里出现的字母,是不是都在 puzzle 里有,至于 word 里每个字母出现位置和次数都不重要了。
因为字母一共只有 26 个,所以我用一个 26 个 bit 的位图,就可以表达每个字母的有无了。一个整型是 32 位,正好可以用一个整数表达。于是,所有的 word 都可以表达成一个整数的位图。显然有些 word 虽然字面不一样,表达成位图后是一样的。于是我们可以用一个字典记录下来次数。
classSolution: defreverseVowels(self, s: str) -> str: ss = [ x for x in s ] i = 0 l = len(ss) j = l - 1 vowels = ['a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'] while i < j: while i < l and ss[i] notin vowels: i += 1 while j > 0and ss[j] notin vowels: j -= 1
if i <= j: ss[i], ss[j] = ss[j], ss[i] i += 1 j -= 1 return"".join(ss)
classSolution: deftranspose(self, matrix: List[List[int]]) -> List[List[int]]: m = len( matrix ) n = len( matrix[0] ) result = [] for i inrange( n ): result.append( [0] * m ) for i inrange( m ): for j inrange( n ): result[j][i] = matrix[i][j] return result
classSolution: defremoveDuplicates(self, nums: List[int]) -> int: l = len(nums) if l == 0or l == 1: return l r = 0 p = r + 1 while p < l: if nums[p] != nums[r]: nums[r+1] = nums[p] r += 1 p += 1 return r + 1
第一个信封选择,理论上有 N 种,而第二个信封选择,理论上有 N - 1 种。只有 w 值相等时候才能剪枝。这就看出来,上面的算法,时间复杂度是 O( n! )。果然是无法 AC 的,超出了时间限制。
我的智慧估计就到这里了,能写出来不错了,还没有能力想到更短的算法。然后看了官方解法。我们设想,w 有序后,我们彻底摒弃 w 这个维度的影响,单纯看 h 这个维度的影响,应该怎么做呢?其实就是求剩下 h 的序列里,最长严格递增子序列。但是,这里其实有个问题,就是如果在 w 值相等的情况下,h 的最长严格递增子序列长度是错的。解决方法也很简单,在 w 相等的时候,h 按降序排列,就好了,这样就无法形成严格递增子序列了,只能在 w 不同的情况下增加子序列长度。
到了这一步,就可以设计一个动态规划的算法,其实,这里也是递推法。状态怎么转换的呢?第 i 个状态,就是前 i 个数构成的最长严格递增子序列长度。这时候,新来的第 i + 1 个数,就要去和前 i 个数里的每个数去比,如果更大,就能得到一个比当前这个状态长 1 的子序列,这里最大的一个,就是第 i + 1 个状态的值。太绕了,举个例子:
再来看最后一个解法,官方解法的说明,说实在的太抽象了,都是用的代数,我看不懂。我自己想了个。我们朴素来想想,如果我们想要套得更多层,到底怎么挑选信封比较有效呢?比如第一个信封是 (w:1, h:1),这时候第二个信封有两个选择(w:2, h:5)和 (w:2, h:2),你选哪个呢?不傻的话,都会选后者吧,因为 h 更小,意味着将来可以被下一个信封套进去的概率更大。
这个新的动态规划算法,状态这样设计的,假如,我想组成一个层数为 i 的套娃,那么用到的最小的那个信封高度,记为第 i 个状态值。注意这个算法仍然建立在 w 已经是升序,而 h 是降序的基础上。这时候,新来一个信封。如果它的高度,比 i 大,那只能套在 i 的外面,其高度变成第 i + 1 个状态。如果其高度比 i 小的时候呢?注意我们的策略,组成一个套娃的时候,尽可能选满足条件最小的一个信封,将来套入新信封的概率才会变大。所以,如果其高度比 i 小的话,理论上我们应该把第 i 个状态的值调小,也即没有扩展新状态。不过,我们真应该更新第 i 个么?如果前面还有比这个值大的呢?那这个肯定是套不上去的,所以得往前找一个位置,正好是前一个状态比它小,后一个状态比它大,然后把比它大的那个给更新了。这个位置可以用 二分查找 来搜索。
f = [envelopes[0][1]] for i inrange(1, n): if (num := envelopes[i][1]) > f[-1]: f.append(num) else: index = bisect.bisect_left(f, num) f[index] = num returnlen(f)
于是,我们得到了这个算法,这是我抄的,不是我自己手撸的。我们可以看看这个算法的复杂度是什么,排序是 O( n logn ),二分查找是 O( logn ),所以动态规划的过程,复杂度还是 O( n logn ),总的时间复杂度是 O( n logn )。比上一个算法又进步了。