LeetCode刷题笔记(算法思想 三)

LeetCode刷题笔记(算法思想 三)

  • 五、分治
      • 241. 为运算表达式设计优先级
      • 96. 不同的二叉搜索树
      • 95. 不同的二叉搜索树 II
  • 六、搜索
    • BFS
      • 1091. 二进制矩阵中的最短路径
      • 279. 完全平方数
      • 127. 单词接龙
    • DFS
      • 695. 岛屿的最大面积
      • 200. 岛屿数量
      • 547. 朋友圈
      • 130. 被围绕的区域
      • 417. 太平洋大西洋水流问题
    • Backtracking
      • 17. 电话号码的字母组合
      • 93. 复原IP地址
      • 79. 单词搜索
      • 257. 二叉树的所有路径
      • 46. 全排列
      • 47. 全排列 II
      • 77. 组合
      • 39. 组合总和
      • 40. 组合总和 II
      • 216. 组合总和 III
      • 78. 子集
      • 90. 子集 II
      • 131. 分割回文串
      • 37. 解数独
      • 51. N皇后

五、分治

241. 为运算表达式设计优先级

给定一个含有数字和运算符的字符串,为表达式添加括号,改变其运算优先级以求出不同的结果。你需要给出所有可能的组合的结果。有效的运算符号包含 +, - 以及 * 。

示例 1:

输入: "2-1-1"
输出: [0, 2]
解释: 
((2-1)-1) = 0 
(2-(1-1)) = 2

示例 2:

输入: "2*3-4*5"
输出: [-34, -14, -10, -10, 10]
解释: 
(2*(3-(4*5))) = -34 
((2*3)-(4*5)) = -14 
((2*(3-4))*5) = -10 
(2*((3-4)*5)) = -10 
(((2*3)-4)*5) = 10

思路: 以运算符作为分隔,递归的求解左右两侧的算式。利用分治思想三步走:1.分解:按运算符分成左右两部分,分别求解;2.解决:实现一个递归函数,输入算式,返回算式解;3.合并:根据运算符,合并左右两部分的解,得出最终解。

代码实现:

class Solution:
    def diffWaysToCompute(self, input: str):
        if input.isdigit():   # 如果只有数字,直接返回
            return [int(input)]
        res = []
        for index, char in enumerate(input):
            if char in ['+','-','*']:
                # 1.分解:遇到运算符,计算左右两侧的结果集
                # 2.解决:diffWaysToCompute 递归函数求出子问题的解
                left = self.diffWaysToCompute(input[:index])
                right = self.diffWaysToCompute(input[index+1:])
                # 3.合并:根据运算符,合并子问题的解
                for l in left:
                    for r in right:
                        if char == '+':
                            res.append(int(l) + int(r))  # py3 input类型默认是字符串
                        elif char == '-':
                            res.append(int(l) - int(r))
                        elif char == '*':
                            res.append(int(l) * int(r))
        return res

96. 不同的二叉搜索树

给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?

示例:

输入: 3
输出: 5
解释:
给定 n = 3, 一共有 5 种不同结构的二叉搜索树:

   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 \
   2     1         2                 3

思路: 使用动态规划。假设n个节点存在G(n)个不同的二叉搜索树,遍历每个节点,以每个节点为根构建二叉搜索树,令 f(i) 为以i为根的二叉搜索树的个数,则:G(n) = f(1) + f(2) + f(3) + … + f(n)。例如当n=7,[1,2,3,4,5,6,7]时,假设 i=3 为根节点,其左子树节点个数为2个,其右子树节点个数为4个,可表示为:f(i) = G(2) * G(4),写成通式则为:f(i) = G(i-1) * G(n-i)。综合上面两个公式可得著名的卡特兰数公式:G(n) = G(0) * G(n-1) + G(1) * G(n-2) + … + G(n-1) * G(0)。对于本题,dp[0]和dp[1]很容易得到均为1,根据动态规划递推式可得dp[n]的值。

代码实现:

class Solution:
    def numTrees(self, n: int) -> int:
        dp = [0 for _ in range(n+1)]
        dp[0] = 1   # 空二叉树也算一种
        dp[1] = 1   # 一个节点

        for i in range(2,n+1):
            for j in range(1,i+1):
                dp[i] += dp[j-1] * dp[i-j]

        return dp[n]

95. 不同的二叉搜索树 II

给定一个整数 n,生成所有由 1 … n 为节点所组成的 二叉搜索树 。

示例:

输入:3
输出:
[
  [1,null,3,2],
  [3,2,null,1],
  [3,1,null,null,2],
  [2,1,3],
  [1,null,2,null,3]
]
解释:
以上的输出对应以下 5 种不同结构的二叉搜索树:

   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 \
   2     1         2                 3

提示: 0 <= n <= 8

思路: 根据96题的思路,首先定义一个用于生成树的函数build_Trees(left, right),表示生成 [left,…,right]的所有可能二叉搜索树。若left > right,说明为空树,返回None;遍历每个节点,递归的生成左右子树,所有可能的左子树列表 left_trees = build_Trees(left, i-1);所有可能的右子树列表 right_trees = build_Trees(i+1, right),用两层循环组合左右子树,生成二叉搜索树。

代码实现:

# Definition for a binary tree node.
class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

class Solution:
    def generateTrees(self, n: int):
        if (n == 0):
            return []

        def build_Trees(left, right):
            all_trees = []
            if (left > right):  # 说明是空树
                return [None]
            for i in range(left, right + 1):
                left_trees = build_Trees(left, i - 1)
                right_trees = build_Trees(i + 1, right)
                for l in left_trees:
                    for r in right_trees:
                        cur_tree = TreeNode(i)
                        cur_tree.left = l
                        cur_tree.right = r
                        all_trees.append(cur_tree)
            return all_trees

        res = build_Trees(1, n)
        return res

六、搜索

BFS

1091. 二进制矩阵中的最短路径

在一个 N × N 的方形网格中,每个单元格有两种状态:空(0)或者阻塞(1)。

一条从左上角到右下角、长度为 k 的畅通路径,由满足下述条件的单元格 C_1, C_2, …, C_k 组成:

相邻单元格 C_i 和 C_{i+1} 在八个方向之一上连通(此时,C_i 和 C_{i+1} 不同且共享边或角)
C_1 位于 (0, 0)(即,值为 grid[0][0])
C_k 位于 (N-1, N-1)(即,值为 grid[N-1][N-1])
如果 C_i 位于 (r, c),则 grid[r][c] 为空(即,grid[r][c] == 0)

返回这条从左上角到右下角的最短畅通路径的长度。如果不存在这样的路径,返回 -1 。

示例 1:

输入:[[0,1],[1,0]]

输出:2

示例 2:

输入:[[0,0,0],[1,1,0],[1,1,0]]

输出:4

提示:

1 <= grid.length == grid[0].length <= 100
grid[i][j] 为 0 或 1

思路: 使用广度优先搜索BFS,把要搜索的点加入队列,对每层进行遍历判断,每个点都向8个方向进行扩展,探索通路,新的通路的点再加入队列,直到走到终点。BFS中,最先到达终点的就是最短路径,每一层遍历的节点都与根节点距离相同。

代码实现:

class Solution:
    def shortestPathBinaryMatrix(self, grid):
        from collections import deque
        if len(grid) == 1:
            return 1
        # 起点或终点是堵塞状态,没有路
        if grid[0][0] == 1 or grid[len(grid)-1][len(grid)-1] == 1:
            return -1
        res = 1   # res代表路径长度
        path = deque()
        path.append([0,0])   # 先把起点加入队列
        while path:  # BFS广度优先搜索
            for _ in range(len(path)):  # 对BFS的某一层的中所有点向8个方向进行扩展
                x, y = path.popleft()
                for new_x, new_y in [(x-1, y-1),(x, y-1),(x+1, y-1),(x+1, y),
                    (x+1, y+1),(x, y+1),(x-1, y+1),(x-1, y)]:
                    if new_x == len(grid)-1 and new_y == len(grid)-1: # 若走到了终点
                        return res + 1
                    if new_x < 0 or new_x > len(grid)-1 or new_y < 0 or new_y > len(grid)-1: # 扩展的点超出边界
                        continue
                    if grid[new_x][new_y] == 1 or grid[new_x][new_y] == -1: # 扩展的点为阻塞或已经访问过
                        continue
                    if grid[new_x][new_y] == 0:  # 扩展的点是通路
                        grid[new_x][new_y] = -1  # 将该点设置成已访问
                        path.append([new_x,new_y])  # 将该点加入队列
            res += 1 # 对某一层的元素都判定后,距离加1(同一个层中所有点距起点距离相等)
        return -1

279. 完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

示例 1:

输入: n = 12
输出: 3 
解释: 12 = 4 + 4 + 4.

示例 2:

输入: n = 13
输出: 2
解释: 13 = 4 + 9.

思路:
LeetCode刷题笔记(算法思想 三)_第1张图片
本题可以转化为BFS思路,使用广度优先遍历顺序遍历每一行,所以当节点差出现0时,此时一定是最短的路径。如绿色所示,若此时节点为0,表示根节点可以由路径上的平方数{1,1,9}构成,返回此时的路径长度为3,后续不再执行。如红色所示,若节点值在之前已经出现,则不需要再计算,一定不会比最短路径短,最短路径还未出现。实现广度优先搜索一般都需要借助队列,循环遍历,每次计算 x = num - i^2,i 的范围是[1, int(num**0.5)+1)。若x==0说明找到了路径;若x不在visited里说明当前节点未出现过,将该节点假如visited并加入队列。

此题另一种解法见:动态规划 -> 分割整数 -> 279. 完全平方数。

代码实现:

class Solution:
    def numSquares(self, n: int):
        from collections import deque
        queue = deque()
        queue.append(n)
        visited = set()
        step = 1
        while queue:
            for _ in range(len(queue)):
                num = queue.popleft()
                for i in range(1, int(num**0.5)+1):
                    x = num - i*i
                    if x == 0:
                        return step
                    if x not in visited:
                        visited.add(x)
                        queue.append(x)
            step += 1
        return step

127. 单词接龙

给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:

每次转换只能改变一个字母。
转换过程中的中间单词必须是字典中的单词。

说明:

如果不存在这样的转换序列,返回 0。
所有单词具有相同的长度。
所有单词只由小写字母组成。
字典中不存在重复的单词。
你可以假设 beginWord 和 endWord 是非空的,且二者不相同。

示例 1:

输入:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]

输出: 5

解释: 一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog",
     返回它的长度 5。

示例 2:

输入:
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]

输出: 0

解释: endWord "cog" 不在字典中,所以无法进行转换。

思路:
LeetCode刷题笔记(算法思想 三)_第2张图片
单词接龙问题可以把每个单词看作节点,差距只有一个字母的两个单词连成一条边,问题变成找到从起点到终点的最短路径,使用广度优先搜索BFS。其中最重要的步骤是找到相邻的节点,也就是只差一个字母的单词,我们可以将wordlist中单词处理成通用状态,将单词中的某个字母用_代替例如:hot 有三个通用状态分别是:_ot, h_t, ho_。通过遍历每个单词的所有通用状态,并在字典里找到并遍历该通用状态对应的所有单词,字典里对应的这些单词都和当前单词有同样的通用状态,所以是当前单词的相邻节点,同时也处于BFS中当前节点的下一层。

代码实现:

class Solution:
    def ladderLength(self, beginWord: str, endWord: str, wordList):
        from collections import defaultdict
        from collections import deque

        if endWord not in wordList or not wordList:
            return 0

        L = len(beginWord)  # 所有单词具有相同长度
        all_combo_dict = defaultdict(list)  # defaultdict(list)构建一个默认值为list的字典
        for word in wordList:   # 对wordList中的单词做预处理
            for i in range(L):
                # 字典的键是通用状态,例如:d*g
                # 字典的值是一个列表,里面存着拥有相同通用状态的所有单词
                all_combo_dict[word[:i] + "*" + word[i+1:]].append(word)

        queue = deque()
        queue.append([beginWord,1])
        visited = {beginWord: True}   # Visited 确保不会重复处理同一个单词
        while queue:
            current_word, level = queue.popleft()
            for i in range(L):
                intermediate_word = current_word[:i] + "*" + current_word[i + 1:]
                for word in all_combo_dict[intermediate_word]:
                    if word == endWord:
                        return level + 1
                    if word not in visited:
                        visited[word] = True  # 该单词标记为已访问
                        queue.append((word, level+1))
                all_combo_dict[intermediate_word] = []
        return 0

DFS

695. 岛屿的最大面积

给定一个包含了一些 0 和 1 的非空二维数组 grid 。

一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。

找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为 0 。)

示例 1:

[[0,0,1,0,0,0,0,1,0,0,0,0,0],
 [0,0,0,0,0,0,0,1,1,1,0,0,0],
 [0,1,1,0,1,0,0,0,0,0,0,0,0],
 [0,1,0,0,1,1,0,0,1,0,1,0,0],
 [0,1,0,0,1,1,0,0,1,1,1,0,0],
 [0,0,0,0,0,0,0,0,0,0,1,0,0],
 [0,0,0,0,0,0,0,1,1,1,0,0,0],
 [0,0,0,0,0,0,0,1,1,0,0,0,0]]

对于上面这个给定矩阵应返回 6。注意答案不应该是 11 ,因为岛屿只能包含水平或垂直的四个方向的 1 。

示例 2:

[[0,0,0,0,0,0,0,0]]

对于上面这个给定的矩阵, 返回 0。

注意: 给定的矩阵grid 的长度和宽度都不超过 50。

思路: 利用深度优先搜索DFS,把接下来要遍历的土地加入到栈里。访问每一片土地时,都对他的四个方向进行探索,找到未访问的土地,加入到栈中,只要栈不为空,就说明还有土地待访问,那么就从栈中取出一个元素并访问。为了确保每个土地访问不超过一次,我们每次经过一块土地时,将这块土地的值置为 0。这样我们就不会多次访问同一土地。

代码实现:

class Solution:
    def maxAreaOfIsland(self, grid):
        ans = 0
        for i, m in enumerate(grid):
            for j, n in enumerate(m):
                area = 0
                stack = [(i,j)]
                while stack:  # 当栈为空表示无路可走,返回上一层循环
                    x, y = stack.pop()
                    if x<0 or y<0 or x == len(grid) or y == len(grid[0]) or grid[x][y] != 1:
                        continue
                    area += 1  # 连通岛屿面积+1
                    grid[x][y] = 0  # 将已经访问过的点置为0,避免再次访问
                    for dx, dy in [(0,-1),(0,1),(-1,0),(1,0)]:
                        new_x = x + dx
                        new_y = y + dy
                        stack.append((new_x,new_y))
                ans = max(ans, area)
        return ans

200. 岛屿数量

给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

示例 1:

输入:
11110
11010
11000
00000
输出: 1

示例 2:

输入:
11000
11000
00100
00011
输出: 3
解释: 每座岛屿只能由水平和/或竖直方向上相邻的陆地连接而成。

思路: 使用DFS,遍历整个矩阵,当遇到 grid[ i ][ j ] == '1’时,对该点的上下左右进行深度优先搜索,岛屿数num+1。探索岛屿的同时将 grid[ i ][ j ] == ‘0’,避免之后重复搜索相同的岛屿。

代码实现:

class Solution:
    def numIslands(self, grid):
        def dfs(grid, i, j):
            if i < 0 or j < 0 or i >= len(grid) or j >= len(grid[0]) or grid[i][j] != '1':
                return
            grid[i][j] = '0'  # 已访问过的节点置为0
            # for new_i,new_j in [(i,j+1),(i,j-1),(i-1,j),(i+1,j)]:
            #     if 0<= new_i < len(grid) and 0<= new_j < len(grid[0]) and grid[new_i][new_j] == '1':
            #         dfs(grid,new_i,new_j)
            dfs(grid, i+1, j)
            dfs(grid, i, j+1)
            dfs(grid, i-1, j)
            dfs(grid, i, j-1)

        num = 0
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                if grid[i][j] == '1':
                    dfs(grid, i, j)
                    num += 1
        return num

547. 朋友圈

班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。

给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。

示例 1:

输入: 
[[1,1,0],
 [1,1,0],
 [0,0,1]]
输出: 2 
说明:已知学生0和学生1互为朋友,他们在一个朋友圈。
第2个学生自己在一个朋友圈。所以返回2。

示例 2:

输入: 
[[1,1,0],
 [1,1,1],
 [0,1,1]]
输出: 1
说明:已知学生0和学生1互为朋友,学生1和学生2互为朋友,所以学生0和学生2也是朋友,所以他们三个在一个朋友圈,返回1。

注意:

N 在[1,200]的范围内。
对于所有学生,有M[i][i] = 1。
如果有M[i][j] = 1,则有M[j][i] = 1。

思路: 本题中的每个人都可以看作一个节点,如果两个人互为朋友,那么这两个节点之间就有一条边相连。因为该题表示的是朋友关系,所以二维表的长宽一定相等(长度宽度都表示共有几个人),利用深度优先搜索dfs,先访问一个节点,再访问其相邻的任一节点,再访问这一节点的任一相邻节点,不断遍历没有访问过的节点。这一过程相当于去询问每个人,让他说出跟谁是朋友,每个人只用问一遍,所以用一个一维列表记录是否访问过该节点即可。所以在主代码设置一层循环,dfs函数里设置一层循环;在主代码首先访问第0个人,进入dfs深度优先搜索,如果在dfs中把剩下的所有人都访问到了,说明他们几个都是朋友,只在主代码里调用了一次dfs,就有一个朋友圈;如果在dfs中谁也访问不到,就说明第0个人没有朋友,他自己是一个朋友圈,就接着在主代码遍历第1个人,以此类推。

代码实现:

class Solution:
    def findCircleNum(self, M) -> int:
        def dfs(i, visited):
            visited[i] = 1
            for j in range(len(M)):
                if visited[j] == 0 and M[i][j] == 1:
                    dfs(j, visited)

        num = 0
        visited = [0 for _ in range(len(M))]
        for i in range(len(M)):
            if visited[i] == 0:
                dfs(i, visited)
                num += 1
        return num

130. 被围绕的区域

给定一个二维的矩阵,包含 ‘X’ 和 ‘O’(字母 O)。

找到所有被 ‘X’ 围绕的区域,并将这些区域里所有的 ‘O’ 用 ‘X’ 填充。

示例:

X X X X
X O O X
X X O X
X O X X

运行你的函数后,矩阵变为:

X X X X
X X X X
X X X X
X O X X

解释: 被围绕的区间不会存在于边界上,换句话说,任何边界上的 ‘O’ 都不会被填充为 ‘X’。 任何不在边界上,或不与边界上的 ‘O’ 相连的 ‘O’ 最终都会被填充为 ‘X’。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。

思路: 从边界开始深度优先搜索,把"O"和与边界"O"连通的"O"置为"Y",然后再遍历整个二维数组,把"O"置为"X",把"Y"置为"O"。

代码实现:

class Solution:
    def solve(self, board):
        """
        Do not return anything, modify board in-place instead.
        """
        def dfs(board, i, j):
            if i<0 or j<0 or i >= len(board) or j >= len(board[0]) or board[i][j] != "O":
                return
            board[i][j] = "Y"   # 把与边界连通的"O"变成"Y"
            dfs(board, i, j-1)
            dfs(board, i, j+1)
            dfs(board, i-1, j)
            dfs(board, i+1, j)

        if len(board) == 0:
            return []
        for i in range(len(board)):
            if board[i][0] == "O":   # 第一列
                dfs(board, i, 0)
            if board[i][len(board[0])-1] == "O":   # 最后一列
                dfs(board, i, len(board[0])-1)
        for j in range(len(board[0])):
            if board[0][j] == "O":   # 第一行
                dfs(board, 0, j)
            if board[len(board)-1][j] == "O":   # 最后一行
                dfs(board, len(board)-1, j)

        for i in range(len(board)):
            for j in range(len(board[0])):
                if board[i][j] == "O":   # 把"O"变成"X"
                    board[i][j] = "X"
                elif board[i][j] == "Y":   # 把"Y"变成"O"
                    board[i][j] = "O"

        print(board)

417. 太平洋大西洋水流问题

给定一个 m x n 的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。

规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。

请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。

提示:

输出坐标的顺序不重要
m 和 n 都小于150

示例:

给定下面的 5x5 矩阵:

  太平洋 ~   ~   ~   ~   ~ 
       ~  1   2   2   3  (5) *
       ~  3   2   3  (4) (4) *
       ~  2   4  (5)  3   1  *
       ~ (6) (7)  1   4   5  *
       ~ (5)  1   1   2   4  *
          *   *   *   *   * 大西洋

返回: [[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (上图中带括号的单元).

思路: 本题按照水流方向正向找比较困难,而从大西洋或太平洋出发逆着水流找相对容易,利用DFS,找下一个高度大于等于当前点的位置,最后取同时被大西洋出发和太平洋出发遍历到的点,就是答案。

代码实现:

class Solution:
    def pacificAtlantic(self, matrix):
        m = len(matrix)
        if m == 0: return []
        n = len(matrix[0])
        if n == 0: return []
        res1, res2 = set(), set()  # 流向太平洋与大西洋的位置

        def dfs(i, j, res):
            res.add((i,j))
            for x, y in [(-1,0),(1,0),(0,-1),(0,1)]:
                new_i = i + x
                new_j = j + y
                if 0<=new_i<m and 0<=new_j<n and matrix[i][j] <= matrix[new_i][new_j] \
                        and (new_i,new_j) not in res:
                    dfs(new_i, new_j, res)

        for i in range(m):   # 从左侧的太平洋开始
            dfs(i, 0, res1)
        for j in range(n):   # 从上方的太平洋开始
            dfs(0, j, res1)
        for i in range(m):   # 从右侧的大西洋开始
            dfs(i, n-1, res2)
        for j in range(n):   # 从下方的大西洋开始
            dfs(m-1, j, res2)
        return res1 & res2   # 两个位置结果取交集

Backtracking

17. 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

LeetCode刷题笔记(算法思想 三)_第3张图片
示例:

输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].

说明:
尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。

思路:
LeetCode刷题笔记(算法思想 三)_第4张图片这类让求全排列的问题,通常都是使用回溯法。关于回溯法,推荐看这篇文章:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-xiang-jie-by-labuladong-2/

代码实现:

class Solution:
    def letterCombinations(self, digits: str):
        if len(digits) == 0:
            return []
        hash = {"2":["a","b","c"],"3":["d","e","f"],"4":["g","h","i"],
                "5":["j","k","l"],"6":["m","n","o"],"7":["p","q","r","s"],
                "8":["t","u","v"],"9":["w","x","y","z"]}

        def backtrack(conbination, nextdigit):
            if len(nextdigit) == 0:
                res.append(conbination)
                return
            for letter in hash[nextdigit[0]]:
                backtrack(conbination + letter, nextdigit[1:])

        res = []
        backtrack('', digits)
        return res

93. 复原IP地址

给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。

有效的 IP 地址正好由四个整数(每个整数位于 0 到 255 之间组成),整数之间用 ‘.’ 分隔。

示例:

输入: "25525511135"
输出: ["255.255.11.135", "255.255.111.35"]

思路: 使用回溯法,根据IP地址的规则限制,每次只能截取一位、两位或三位;截取一位没有规则限制;截取两位需要保证最左不能为0;截取三位需要保证最左不能为0且这三位数不能大于256。

代码实现:

class Solution:
    def restoreIpAddresses(self, s: str):
        r = []
        def restore(count=0, ip='', s=''):
            if count == 4:
                if s == '':
                    r.append(ip[:-1])   # 最后一个不取是因为最后一个是"."
                return
            if len(s) > 0:   # 截取一位
                restore(count+1, ip+s[0]+'.', s[1:])
            if len(s) > 1 and s[0] != '0':  # 截取两位,且最左不能为0
                restore(count+1, ip+s[:2]+'.', s[2:])
            if len(s) > 2 and s[0] != '0' and int(s[0:3]) < 256:  # 截取三位,且最左不能为0,且这三位数不能大于256
                restore(count+1, ip+s[:3]+'.', s[3:])
        restore(0, '', s)
        return r

79. 单词搜索

给定一个二维网格和一个单词,找出该单词是否存在于网格中。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例:

board =
[
  ['A','B','C','E'],
  ['S','F','C','S'],
  ['A','D','E','E']
]

给定 word = "ABCCED", 返回 true
给定 word = "SEE", 返回 true
给定 word = "ABCB", 返回 false

提示:

    board 和 word 中只包含大写和小写英文字母。
    1 <= board.length <= 200
    1 <= board[i].length <= 200
    1 <= word.length <= 10^3

思路: 对每个格子都要从头搜索,如果搜索不到返回False,只要有一个搜索到就返回True。在DFS时,如果遇到不匹配的需要回溯并且状态重置。

代码实现:

class Solution:
    direction = [(-1, 0), (0, -1), (1, 0), (0, 1)]
    def exist(self, board, word: str) -> bool:
        def backtrack(x, y, index):
            if index == len(word)-1:   # 递归终止条件
                return board[x][y] == word[index]

            if board[x][y] == word[index]:
                mark[x][y] = True   # 先把位置标记为已访问,如果下一个不匹配则要释放
                for dir in self.direction:
                    new_x = x + dir[0]
                    new_y = y + dir[1]
                    if 0 <= new_x < m and 0 <= new_y < n and not mark[new_x][new_y] \
                            and backtrack(new_x,new_y,index+1):
                        # 说明一路搜索到最后
                        return True
                mark[x][y] = False  # 回溯,将这个位置释放为未访问状态
            return False

        m = len(board)
        n = len(board[0])
        mark = [[False for _ in range(n)] for _ in range(m)]
        for i in range(m):
            for j in range(n):
                # 对每个格子都从头开始搜
                if backtrack(i, j, 0):
                    return True
        return False

257. 二叉树的所有路径

给定一个二叉树,返回所有从根节点到叶子节点的路径。

说明: 叶子节点是指没有子节点的节点。

示例:

输入:

   1
 /   \
2     3
 \
  5

输出: ["1->2->5", "1->3"]

解释: 所有根节点到叶子节点的路径为: 1->2->5, 1->3

思路: 利用path存储每次走的路径,用res存储最终结果。从根节点开始一直往下找,假如到达叶子节点则把这条路径添加到res;假如不是叶子节点就继续递归,每次递归都在路径里加上 “->” 作为连接符。

代码实现:

# Definition for a binary tree node.
class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

class Solution:
    def binaryTreePaths(self, root: TreeNode):
        def backtrack(root, path):
            if root:
                path += str(root.val)
                if not root.left and not root.right:  # 叶子节点
                    res.append(path)
                else:   # 非叶子节点
                    path += "->"
                    backtrack(root.left, path)
                    backtrack(root.right, path)

        res = []
        backtrack(root,"")
        return res

46. 全排列

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

输入: [1,2,3]
输出:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]

思路: 全排列问题,用回溯法,每次递归循环都将当前值加入列表,把除去当前值的剩下值再作为参数传入,进行下一个递归。

代码实现:

class Solution:
    def permute(self, nums):
        def backtrack(nums, li):
            if not nums:
                res.append(li)
                return
            for i in range(len(nums)):
                backtrack(nums[:i]+nums[i+1:], li+[nums[i]])  # 利用列表的加法

        res = []
        backtrack(nums,[])
        return res

47. 全排列 II

给定一个可包含重复数字的序列,返回所有不重复的全排列。

示例:

输入: [1,1,2]
输出:
[
  [1,1,2],
  [1,2,1],
  [2,1,1]
]

思路: 本题在46题基础上,增加了重复数,为了提高效率,需要在深搜过程中合理剪枝。首先把数组排序,使得重复的数挨在一起,在深搜函数遍历数组那里,先对每个数进行判断,如果当前数和他前一个数相等,就不对他进行深搜。

代码实现:

class Solution:
    def permuteUnique(self, nums) :
        def backtrack(nums,li):
            if not nums:
                res.append(li)
                return
            for i in range(len(nums)):
                if i > 0 and nums[i] == nums[i-1]:  # 不对重复的数进行深搜
                    continue
                backtrack(nums[:i] + nums[i+1:], li+[nums[i]])
        res = []
        nums.sort()   # 这里排序可以保证把重复的数放在一起以便被检测到
        backtrack(nums,[])
        return res

77. 组合

给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。

示例:

输入: n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

思路: 利用回溯,用i来控制每次从哪个数开始深搜,用k来控制深搜的个数(k个数的组合)。

代码实现:

class Solution:
    def combine(self, n: int, k: int):
        def backtrack(i,k,li):
            if k == 0:
                res.append(li)
                return
            for i in range(i,n+1):
                backtrack(i+1, k-1, li+[i])

        res = []
        backtrack(1,k,[])
        return res

39. 组合总和

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取。

说明:

所有数字(包括 target)都是正整数。
解集不能包含重复的组合。 

示例 1:

输入: candidates = [2,3,6,7], target = 7,
所求解集为:
[
  [7],
  [2,2,3]
]

示例 2:

输入: candidates = [2,3,5], target = 8,
所求解集为:
[
  [2,2,2,2],
  [2,3,3],
  [3,5]
]

思路: 利用回溯,每个数可以重复使用任意次,对于数组中每个数nums[i],在递归时就有选与不选两种情况:1.如果选nums[i],递归到下一层时,由于nums[i]可以重复使用,因此下一层的起始下标还是i,但temp和target发生变化;2.如果不选nums[i],递归到下一层时,由于在这一层递归的时候什么都不做,因此temp和target不用变化,但i需要+1。

代码实现:

class Solution:
    def combinationSum(self, candidates, target: int):
        if (not candidates):
            return []
        n = len(candidates)
        res = []
        candidates.sort()   # 排序是为了后面判断target是否小于候选数,以便剪枝

        def helper(i, tmp, target):
            if (target == 0):
                res.append(tmp)
                return
            if (i == n or target < candidates[i]):
                return
            helper(i, tmp + [candidates[i]], target - candidates[i])
            helper(i + 1, tmp, target)

        helper(0, [], target)
        return res

40. 组合总和 II

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次。

说明:

所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。 

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]

示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
  [1,2,2],
  [5]
]

思路: 利用回溯,重点在于避免重复:

例1               1
                 / \
                2   2  这种情况不会发生 但是却允许了不同层级之间的重复即:
               /     \
              5       5
               
例2               1
                 /
                2      这种情况确是允许的
               /
              2  

首先 i-1 == i 是用于判定当前元素是否和之前元素相同的语句。这个语句就能砍掉例1。可是问题来了,如果把所有当前与之前一个元素相同的都砍掉,那么例二的情况也会消失。 因为当第二个2出现的时候,他就和前一个2相同了。

那么如何保留例2呢?就用 i > begin 来避免这种情况,你发现例1中的两个2是处在同一个层级上的,例2的两个2是处在不同层级上的。在一个for循环中,所有被遍历到的数都是属于一个层级的。我们要让一个层级中,必须出现且只出现一个2,那么就放过第一个出现重复的2,但不放过后面出现的2。第一个出现的2的特点就是 i == begin. 第二个出现的2 特点是 i > begin。

代码实现:

class Solution:
    def combinationSum2(self, candidates, target: int):
        def backtrack(begin, li, target):
            if target == 0:
                res.append(li[:])   # li[:]是对li作浅拷贝,与li地址不同,对li的pop并不影响li[:]
                return
            for i in range(begin,len(candidates)):
                if target < candidates[i]:
                    break
                if i > begin and candidates[i] == candidates[i-1]:
                    continue
                li.append(candidates[i])
                backtrack(i+1, li, target-candidates[i])
                li.pop()

        if not candidates:
            return []
        candidates.sort()
        res = []
        backtrack(0, [], target)
        return res

216. 组合总和 III

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:

所有数字都是正整数。
解集不能包含重复的组合。 

示例 1:

输入: k = 3, n = 7
输出: [[1,2,4]]

示例 2:

输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]

思路: 利用回溯,count表示当前已经使用的数字数,index表示当前访问的数字,li记录当前中间结果,target表示下一步的目标和。

代码实现:

class Solution:
    def combinationSum3(self, k: int, n: int):
        def backtrack(count, index, li, target):
            if count == k:
                if target == 0:
                    res.append(li)
                    return
            for j in range(index,10):
                if j > target:
                    break
                backtrack(count+1, j+1, li+[j], target-j)

        res = []
        backtrack(0, 1, [], n)
        return res

78. 子集

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

输入: nums = [1,2,3]
输出:
[
  [3],
  [1],
  [2],
  [1,2,3],
  [1,3],
  [2,3],
  [1,2],
  []
]

思路: 利用回溯,不用符合某个条件再把 li 添加到 res 中,每次循环都 append 当前 li 到 res 中。

代码实现:

class Solution:
    def subsets(self, nums):
        def backtrack(i,li):
            res.append(li)
            for j in range(i,len(nums)):
                backtrack(j+1, li+[nums[j]])

        res = []
        backtrack(0, [])
        return res

90. 子集 II

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

输入: [1,2,2]
输出:
[
  [2],
  [1],
  [1,2,2],
  [2,2],
  [1,2],
  []
]

思路: 为了去除重复的子集,第一时间想到排序。利用回溯,其余的处理和78题与40题类似。

代码实现:

class Solution:
    def subsetsWithDup(self, nums):
        def backtrack(index,li):
            res.append(li)
            for j in range(index,len(nums)):
                if j > index and nums[j] == nums[j-1]:
                    continue
                backtrack(j+1,li+[nums[j]])

        res = []
        nums.sort()
        backtrack(0, [])
        return res

131. 分割回文串

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。

返回 s 所有可能的分割方案。

示例:

输入: "aab"
输出:
[
  ["aa","b"],
  ["a","a","b"]
]

思路: 利用回溯,每次遍历检查当前字符串是否是回文串。

代码实现:

class Solution:
    def partition(self, s: str):
        def backtrack(s, li):
            if not s:
                res.append(li)
                return
            for i in range(1,len(s)+1):
                if s[:i] == s[:i][::-1]:
                    backtrack(s[i:], li+[s[:i]])

        res = []
        backtrack(s, [])
        return res

37. 解数独

编写一个程序,通过已填充的空格来解决数独问题。

一个数独的解法需遵循如下规则:

数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。

空白格用 ‘.’ 表示。
LeetCode刷题笔记(算法思想 三)_第5张图片
一个数独。
LeetCode刷题笔记(算法思想 三)_第6张图片
答案被标成红色。

Note:

给定的数独序列只包含数字 1-9 和字符 '.' 。
你可以假设给定的数独只有唯一解。
给定数独永远是 9x9 形式的。

思路: 一个回溯函数backtrack对board[i][j] 进行穷举尝试,一个isValid函数判断要填入的数字是否合法(符合数独规则)。

代码实现:

class Solution:
    def solveSudoku(self, board) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        # 对 board[i][j] 进行穷举尝试
        def backtrack(board, i, j):
            m, n = 9, 9
            if j == n:   # 走到9才越界,进入下一行
                return backtrack(board, i+1, 0)
            if i == m:   # 走到最后一行,找到一个可行解
                return True
            if board[i][j] != '.':   # 当前是预设数字,直接跳到下一个
                return backtrack(board, i, j+1)

            ch_list = ['1','2','3','4','5','6','7','8','9']
            for ch in ch_list:
                if not isValid(board, i, j, ch):   # 如果遇到不合法的数字,则跳过
                    continue

                board[i][j] = ch   # 做选择
                if backtrack(board, i, j+1):  # 如果找到一个可行解,立即结束
                    return True
                board[i][j] = '.'   # 撤销选择
            # 穷举完 1~9,依然没有找到可行解,此路不通
            return False

        # 判断 board[i][j] 是否可以填入 n
        def isValid(board, r, c, n):
            for i in range(9):
                # 判断行是否存在重复
                if board[r][i] == n: return False
                # 判断列是否存在重复
                if board[i][c] == n: return False
                # 判断 3 * 3 方框是否存在重复
                if board[(r//3)*3 + i//3][(c//3)*3 + i%3] == n:
                    return False
            return True

        backtrack(board, 0, 0)

51. N皇后

n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
LeetCode刷题笔记(算法思想 三)_第7张图片
上图为 8 皇后问题的一种解法。

给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。

每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。

示例:

输入: 4
输出: [
 [".Q..",  // 解法 1
  "...Q",
  "Q...",
  "..Q."],

 ["..Q.",  // 解法 2
  "Q...",
  "...Q",
  ".Q.."]
]
解释: 4 皇后问题存在两个不同的解法。

提示: 皇后,是国际象棋中的棋子,意味着国王的妻子。皇后只做一件事,那就是“吃子”。当她遇见可以吃的棋子时,就迅速冲上去吃掉棋子。当然,她横、竖、斜都可走一到七步,可进可退。(引用自 百度百科 - 皇后 )

思路: 本题与37数独题不相同的地方在于如果已经在某行某列放置了皇后,那么该行就不可能再放置皇后了,直接进入下一行。并且要注意触发结束条件时,把所有结果放在二维列表返回。由于是一行一行搜索,当前行上面是已经放置皇后的,下面还是空棋盘,在判断当前位置是否合法时,只用查看同列以及左斜上方和右斜上方是否有皇后互相冲突即可。

代码实现:

class Solution:
    def solveNQueens(self, n: int):
        board = [['.' for _ in range(n)] for _ in range(n)]  # 初始化空棋盘
        # 路径:board中小于row的那些行都已经成功放置了皇后
        # 选择列表:第row行的左右列都是放置皇后的选择
        # 结束条件:row超过board的最后一行
        def backtrack(board, row):
            if row == n:  # 触发结束条件
                li = []
                for row in board:
                    li.append("".join(row))
                res.append(li)
                return

            for col in range(n):
                if not isValid(board,row,col):   # 排除不合法选择
                    continue

                board[row][col] = 'Q'   # 做选择
                backtrack(board, row+1)   # 进入下一行
                board[row][col] = '.'   # 撤销选择

        # 判断 board[row][col] 是否可以填入 'Q'
        def isValid(board, row, col):
            for i in range(n):
                if board[i][col] == 'Q':   # 检查列是否有皇后互相冲突
                    return False

            i, j = row-1, col+1
            while i >= 0 and j < n:   # 检查右斜上方是否有皇后互相冲突
                if board[i][j] == 'Q':
                    return False
                i -= 1
                j += 1

            i, j = row-1, col-1
            while i >= 0 and j >= 0:   # 检查左斜上方是否有皇后互相冲突
                if board[i][j] == 'Q':
                    return False
                i -= 1
                j -= 1
            return True

        res = []
        backtrack(board, 0)
        return res

================================================================
以上为个人学习笔记总结,供学习参考交流,未经允许禁止转载或商用。

  • 题目全部出自LeetCode官网:https://leetcode-cn.com/
  • 题目分类参考这篇文章:https://github.com/CyC2018/CS-Notes/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3%20-%20%E7%9B%AE%E5%BD%95.md

你可能感兴趣的:(算法,python)