Leetcode【60、79、93、131、842】

时间:2022-06-25
本文章向大家介绍Leetcode【60、79、93、131、842】,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
问题描述:【Math】60. Permutation Sequence
解题思路:

这道题是一个从 1 到 n 的数组,共有 n! 个全排列序列,找到第 k 个全排列序列。

刚开始没仔细读题就之间 DFS 回溯,很快写好但超时了哈哈哈。再读一下题目,发现如果 k 很大,要一个个找过去,所以会很慢。

实际上,这是一道数学题(找规律)。以 n = 5 举例,共有 5!= 120 个排列,我们发现:当 k <= 24,排列全部以 1 开头;24 < k <= 48,排列全部以 2 开头,以此类推。因此,我们可以一位一位的构造答案,根据 k 值判断其落在哪个区间,找到开头数字加入结果;然后,从数组中删除该开头数字,并确定 k 值位于当前区间的第几个,更新 k 值;按照上述方法进行操作,直到得到一个全排列,就是最后的答案。

以 n = 5,k = 31 为例(nums = [1,2,3,4,5]):

  • k ∈ (4!, 2*4!],则确定 ans = "2",数组中删除 2(nums=[1,3,4,5]),k 更新为 k = 31 - 4! = 7 (k 位于该区间的第 7 个);
  • k ∈ (3!, 2*3!],则确定 ans = "23",数组中删除 3(nums=[1,4,5]),k 更新为 k = 7 - 3! = 1 (k 位于该区间的第 1 个);
  • k ∈ (0, 1*2!],则确定 ans = "231",数组中删除 1(nums=[4,5]),k 更新为 k = 1 - 0 = 1 (k 位于该区间的第 1 个);
  • k ∈ (0, 1*1!],则确定 ans = "2314",数组中删除 4(nums=[4]),k 更新为 k = 1 - 0 = 1 (k 位于该区间的第 1 个);
  • k ∈ (0, 1*0!],则确定 ans = "23145",数组中删除 5(nums=[]),k 更新为 k = 1 - 0 = 1 (k 位于该区间的第 1 个);
  • 得到一个全排列,最终结果 ans = "23145"
Python3 实现:
class Solution:
    def getPermutation(self, n: int, k: int) -> str:
        ans = ""
        nums = [(i + 1) for i in range(n)]
        while len(ans) != n:  # 迭代方法,逐个字符加入到结果中
            nf = math.factorial(len(nums) - 1)
            for i in range(len(nums)):
                if k <= (i + 1) * nf:  # 找到k位于哪个区间
                    ans += str(nums[i])
                    del nums[i]
                    k -= i * nf  # k位于当前区间的第几个
                    break
        return ans

解题思路:

这道题是给一个 m*n 的字符矩阵 board 和一个单词 word,判断 word 是否存在字符矩阵中。

这道题很明显用 DFS 回溯法去解决。做法如下:

  • 在主函数中,遍历字符矩阵的每个坐标 (i, j),如果发现 board[i][j] == word[0],则将 board[i][j] 位置先改为 ""(因为每个位置字符只能使用一次),然后进入回溯函数 search(i, j, path, wind) (path 记录单词路径,wind 为 word 索引)进行深搜判断。如果没有在 search() 函数中找到,说明没有以当前字符 board[i][j] 开头的单词,要恢复原来该位置的字符。
  • 在回溯函数中,对于每个字符的上下左右四个位置进行深搜(要保证不越界),如果 board 的下一个位置的字符匹配 word 的下一个字符,则修改 board 中当前字符为 "" 进行递归调用。递归调用结束后,要先恢复原来该位置的字符,再去判断返回值是 True 还是 False。如果找到(返回值为 True,则返回 True),否则继续查找下一个位置。
Python3 实现:
class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        def search(i, j, path, wind):  # (i,j)是匹配word第一个字符的坐标,path记录结果,wind为word索引
            isfind = False
            if path == word:  # 找到了该单词
                return True
            for p in pos:
                if 0 <= i+p[0] < m and 0 <= j+p[1] < n and board[i+p[0]][j+p[1]] == word[wind]:
                    tmp = board[i+p[0]][j+p[1]]
                    board[i+p[0]][j+p[1]] = ""
                    isfind = search(i+p[0], j+p[1], path+word[wind], wind+1)
                    board[i+p[0]][j+p[1]] = tmp  # 先恢复原来的字符
                    if isfind == True:  # 再判断返回结果,如果是True直接返回;如果是False继续查找
                        return True
            return False
        
        if len(word) == 0 or len(board) == 0:
            return False
        pos = [[-1,0], [1,0], [0,-1], [0,1]]  # 上下左右四个位置
        m = len(board)
        n = len(board[0])
        for i in range(m):
            for j in range(n):
                if board[i][j] == word[0]:
                    tmp = board[i][j]
                    board[i][j] = ""
                    if search(i, j, word[0], 1):
                        return True
                    board[i][j] = tmp  # 如果没有找到,恢复原来的字符
        return False

问题描述:【DFS】93. Restore IP Addresses
解题思路:

这道题是给一个数字字符串,返回所有可能的 IP 地址组合。

这道题和下面的 Leetcode 131 以及 Leetcode 842 做法是类似的,也是使用回溯法 DFS 对字符串前缀进行划分。注意该深搜函数 search(s, path)(s 为后半部分字符串,path 为划分的 IP 子段)的几个出口:

  • 如果 len(path) > 4,不符合 IP 地址,提前终止,返回;
  • 如果 s 为空串,但是 len(path) < 3,说明划分结束,但是不符合 IP 地址,也返回;
  • 如果 s 为空串,且 len(path) == 4,说明找到一组解,就加入到结果列表 ans 中。

在 for 循环中,还要注意去除前导 0 以及字符串前缀数字 > 255 的情况,才能进行递归调用深搜。

Python3 实现:
class Solution:
    def restoreIpAddresses(self, s: str) -> List[str]:
        def search(s, path):
            if len(path) > 4:  # 不满足题意
                return
            if not s and len(path) < 4:  # 不满足题意
                return
            if not s and len(path) == 4:  # 找到一组解
                ans.append('.'.join(path))
                return
            for i in range(1, len(s) + 1):
                pre = s[:i]  # 划分前缀
                if (len(pre) > 1 and pre[0] == '0') or int(pre) > 255:  # 不满足题意
                    continue
                search(s[i:], path + [pre])
        
        ans = []
        search(s, [])
        return ans

问题描述:【Brute Force、DFS】131. Palindrome Partitioning
解题思路:

这道题是给一个字符串,将字符串分割成一些子串,使得所有子串都是回文串,求所有划分的方案数。

一个子串是否是回文串可以使用 s == s[::-1] 来判断。

方法1(Brute Force):

首先想到一种暴力解法,就是对于字符串的每个字符 s[i],依次将 s[i] 加入到回文串前缀列表中每个回文串前缀的后面,然后再判断 s[i] 的加入能否形成新的回文串前缀。如果可以,拓展回文串前缀列表。最后,遍历完所有字符后,列表中存储的就是最终结果。

以 s = "abba" 举例:

  • s[0] = a,a 本身是回文串,加入到结果列表 ans = [[a]];
  • s[1] = b,b 加入回文串前缀的后面,得到 ans = [[a,b]];
  • s[2] = b,b 先加入回文串前缀的后面,得到 ans = [[a,b,b]];然后发现,b 的加入可以形成新的回文串 "bb"(从最后一个 b 开始往前形成子串 bb),因此拓展结果列表得到 ans = [[a,b,b], [a,bb]];
  • s[3] = a,a 先加入回文串前缀的后面,得到 ans = [[a,b,b,a], [a,bb,a]];然后发现,a 的加入可以形成新的回文串 "abba"(注意到 ans 中两个都可以构成 "abba",所以还要判断新加入的回文串是否之前已经加入过),因此拓展结果列表得到 ans = [[a,b,b,a], [a,bb,a], [abba]]。

这种做法时间复杂度为 O(n^4),空间复杂度为 O(n^2),能够 AC

Python3 实现:

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        ans = [[]]
        for i in range(len(s)):
            for a in ans:  # 将当前字符加入到每个字符串前缀列表的后面
                a.append(s[i])
            for a in ans:
                N = len(a)
                st = a[-1]
                for j in range(N-2, -1, -1):
                    st = a[j] + st  # 从最后一个字符开始构造新的子串
                    if st == st[::-1] and a[:j] + [st] not in ans:  # 判断新的子串是否是回文串,且构成的回文串前缀是否之前出现过
                        ans.append(a[:j] + [st])  # 将新的回文串前缀加入到结果列表中
        return ans

方法2(DFS):

其实这道题一看是求所有结果,很明显用 DFS 回溯法求解,但是刚开始没有思路,找不到如果求解的方法,最后看了别人的做法才明白。

使用回溯法的解题思路是对于字符串 s 的前缀进行划分,然后判断前缀是否是回文子串。如果是,形成临时结果,将 s 的后半部分和临时结果传入到下一层(深搜);如果不是,那就继续划分下一个前缀。最后,传入的 s 会变成空串,这时形成的结果必定是回文串的一个划分,加入到结果列表中即可。

以 s = "aab" 举例,用 path 记录一种划分结果:

  • 1、划分 s = "aab" 前缀 a,a 是回文串,将其加入 path = [a],并且同 s = "ab" 传入到下一层;
  • 2、划分 s = "ab" 前缀 a,a 是回文串,将其加入 path = [a,a],并且同 s = "b" 传入到下一层;
  • 3、划分 s = "b" 前缀 b,b 是回文串,将其加入 path = [a,a,b],并且同 s = "" 传入到下一层;
  • 4、因为 s = "",说明划分完毕,将结果 path = [a,a,b] 加入到 ans 中,直接返回到步骤 3(没有前缀,继续返回)、步骤 2;
  • 5、在步骤 2 中,划分 s = "ab" 前缀 ab,ab 不是回文串,继续划分下一个前缀;没有前缀,返回步骤 1;
  • 6、在步骤 1 中,划分 s = "aab" 前缀 aa,aa 是回文串,将其加入 path = [aa],并且同 s = "b" 传入到下一层;
  • 7、划分 s = "b" 前缀 b,b 是回文串,将其加入 path = [aa,b],并且同 s = "" 传入到下一层;
  • 8、因为 s = "",说明划分完毕,将结果 path = [aa,b] 加入到 ans 中,直接返回到步骤 7(没有前缀,继续返回)、步骤 6;
  • 9、在步骤 6 中,划分 s = "aab" 前缀 aab,aab 不是回文串,继续划分下一个前缀;没有前缀,结束。
  • 10、最终得到结果 ans = [[a,a,b], [aa,b]]。

优化:可以通过用区间 DP 来计算任意 s[i:j] 之间是否是回文串 【区间DP、双指针】647. Palindromic Substrings,并保存结果;然后再执行DFS,如果发现某条子串不是回文,就可以直接退出,从而减少计算量。

Python3 实现:

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        def search(s, path):
            if not s:  # s 变为空串,将形成的一种结果path加入到ans中
                ans.append(path[:])
            for i in range(1, len(s) + 1):
                pre = s[:i]  # 对于s的每一个前缀
                if pre == pre[::-1]:  # 如果前缀是回文串,则深搜
                    search(s[i:], path + [pre])  # 将后半部分和临时结果传入到下一层
        
        ans = []
        search(s, [])
        return ans

问题描述:【DFS】842. Split Array into Fibonacci Sequence
解题思路:

这道题是给一个数字字符串 S,将其划分成斐波那契序列。

很容易想到用 DFS 回溯法去找斐波那契序列的一种划分。类似于上面的 Leetcode 131,对于数字字符串 S 的前缀进行划分,如果 S 的前缀合法(没有前导 0 并且数字没有越界),就将 S 的后半部分和临时结果 path 传入,进行递归调用。

递归出口:

  • 如果临时结果 path 长度大于等于 3,但是不满足 f[i-2] + f[i-1] = f[i],返回 False;
  • 如果临时结果 path 长度大于等于 3,且 S 为空串,说明划分完毕,保存结果ans.extend(path),返回 True。

第一次提交时,WA 了,报错如下:

检查了一下发现没什么问题啊?然后重新读题,很敏感的发现斐波那契序列数值要求为 <= 2 ** 31 - 1(2147483647),然后发现报错结果的最后一项 11336528511 > 2 ** 31 - 1,因此在代码中加入了数值范围的判断,就 AC 了。

Python3 实现:
class Solution:
    def splitIntoFibonacci(self, S: str) -> List[int]:
        def search(S, path):
            if len(path) >= 3 and path[-3] + path[-2] != path[-1]:  # 不满足斐波那契条件
                return False
            if not S and len(path) >= 3:  # S为空串,说明可以划分
                ans.extend(path)  # 找到一种划分,保存结果到ans中,返回
                return True
            for i in range(1, len(S) + 1):
                pre = S[:i]  # 划分前缀
                if (len(pre) > 1 and pre[0] == '0') or (int(pre) > 2 ** 31 - 1):  # 不满足题意
                    continue
                if search(S[i:], path + [int(pre)]):  # 将S后半部分和临时结果传入进行递归调用
                    return True
            return False  # S本身长度小于3
                
        ans = []
        search(S, [])
        return ans