【自用】LeetCode刷题记录(python版)

一、N叉树的后序遍历(590)

在这里插入图片描述

1、递归

"""
# Definition for a Node.
class Node:
    def __init__(self, val=None, children=None):
        self.val = val
        self.children = children
"""

class Solution:
    def postorder(self, root: 'Node') -> List[int]:
        ans = []
        def dfs(node:'Node'):
            if node is None:
                return
            for ch in node.children:
                dfs(ch)
            ans.append(node.val)
        dfs(root)
        return ans        
  • 时间复杂度:O(m),其中 m 为 N 叉树的节点。每个节点恰好被遍历一次。
  • 空间复杂度:O(m),递归过程中需要调用栈的开销,平均情况下为 O(log⁡m),最坏情况下树的深度为 m−1,需要的空间为 O(m−1),因此空间复杂度为 O(m)。

2、迭代

"""
# Definition for a Node.
class Node:
    def __init__(self, val=None, children=None):
        self.val = val
        self.children = children
"""

class Solution:
    def postorder(self, root: 'Node') -> List[int]:
        if root is None:
            return []
        ans = []
        st = []
        nextIndex = defaultdict(int)
        node = root
        while st or node:
            while node:
                st.append(node)
                if not node.children:
                    break
                nextIndex[node] = 1
                node = node.children[0]
            node = st[-1]
            i = nextIndex[node]
            if i < len(node.children):
                nextIndex[node] = i + 1
                node = node.children[i]
            else:
                ans.append(node.val)
                st.pop()
                del nextIndex[node]
                node = None
        return ans        
  • 时间复杂度:O(m),其中 m为 N 叉树的节点。每个节点恰好被访问一次。
  • 空间复杂度:O(m),其中 m 为 N 叉树的节点。如果 N 叉树的深度为 1 则此时栈和哈希表的空间为 O(1),如果 N 叉树的深度为 m−1, 则此时栈和哈希表的空间为 O(m−1),平均情况下栈和哈希表的空间为 O(log⁡m),因此空间复杂度为 O(m)。

二、树上的操作(1993)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

class LockingTree:

    def __init__(self, parent: List[int]):
        n = len(parent)
        self.parent = parent
        self.nodeLockUser = [-1] * n
        self.children = [[] for _ in range(n)]
        for node,p in enumerate(parent):
            if p != -1:
                self.children[p].append(node)


    def lock(self, num: int, user: int) -> bool:
        if self.nodeLockUser[num] == -1:
            self.nodeLockUser[num] = user
            return True
        else:
            return False


    def unlock(self, num: int, user: int) -> bool:
        if self.nodeLockUser[num] == user:
            self.nodeLockUser[num] = -1
            return True
        else:
            return False


    def upgrade(self, num: int, user: int) -> bool:
        res = self.nodeLockUser[num] == -1 and not self.hasLockedAncestor(num) and self.checkAndUnlockDescendant(num)
        if res:
            self.nodeLockUser[num] = user
        return res
    
    def hasLockedAncestor(self,num:int)->bool:
        num = self.parent[num]
        while num != -1:
            if self.nodeLockUser[num] != -1:
                return True
            else:
                num = self.parent[num]
        return False
    
    def checkAndUnlockDescendant(self,num:int)->bool:
        res = self.nodeLockUser[num] != -1
        self.nodeLockUser[num] = -1
        for child in self.children[num]:
            res |= self.checkAndUnlockDescendant(child)
        return res
  • 时间复杂度:初始化:构建 children 消耗 O(n),Lock和 Unlock都消耗 O(1),Upgrade消耗 O(n)。
  • 空间复杂度:初始化消耗 O(n),Lock和 Unlock都消耗 O(1),Upgrade消耗 O(n)。

三、从前序与中序遍历序列构造二叉树(105)

在这里插入图片描述

1、递归

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        def myBuildTree(preorder_left: int, preorder_right: int, inorder_left: int, inorder_right: int):
            if preorder_left > preorder_right:
                return None
            
            # 前序遍历中的第一个节点就是根节点
            preorder_root = preorder_left
            # 在中序遍历中定位根节点
            inorder_root = index[preorder[preorder_root]]
            
            # 先把根节点建立出来
            root = TreeNode(preorder[preorder_root])
            # 得到左子树中的节点数目
            size_left_subtree = inorder_root - inorder_left
            # 递归地构造左子树,并连接到根节点
            # 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素
            root.left = myBuildTree(preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1)
            # 递归地构造右子树,并连接到根节点
            # 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素
            root.right = myBuildTree(preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right)
            return root
        
        n = len(preorder)
        # 构造哈希映射,帮助我们快速定位根节点
        index = {element: i for i, element in enumerate(inorder)}
        return myBuildTree(0, n - 1, 0, n - 1)
  • 时间复杂度:O(n),其中 n 是树中的节点个数。
  • 空间复杂度:O(n),除去返回的答案需要的 O(n)空间之外,我们还需要使用 O(n)的空间存储哈希映射,以及 O(h)(其中 h 是树的高度)的空间表示递归时栈空间。这里 h<n,所以总空间复杂度为 O(n)。

四、连通网络的操作次数(1319)

在这里插入图片描述
在这里插入图片描述

1、知识前瞻——并查集

http://t.csdnimg.cn/GzQuW
*并查集通用模板:

//并查集类
class DisJointSetUnion
{
private:
//所有根节点相同的节点位于同一个集合中
	vector<int> parent;  //双亲节点数组,记录该节点的双亲节点,用于查找该节点的根节点
	vector<int> rank;  //秩数组,记录以该节点为根节点的树的深度,主要用于优化,在合并两个集合的时候,rank大的集合合并rank小的集合

public:
	DisJointSetUnion(int n)
	{
		for (int i = 0;i < n;i++)
		{
			parent.push_back(i); //此时各自为王,自己就是一个集合
			rank.push_back(1);//rank=1,此时每个节点自己就是一棵深度为1的树
		}
	}
	//查找根节点
	int find(int x)
	{
		if(x == parent[x])
			return x;
		else
		{
			parent[x] = find(parent[x]); //路径压缩,遍历过程中的所有双亲节点直至指向根节点,减少后续查找次数
			return parent[x];
		}
	}
void merge(int x,int y){
	int rx = find(x); //查找x的根节点,即x所在集合的代表元素
	int ry = find(y);
	if(rx != ry){ //如果不是同一个集合
		if(rank[rx] < rank[ry]) //rank大的集合合并rank小的集合
		{
			swap(rx,ry);//这里进行交换是为了保证rx的rank大于ry的rank,方便下面合并
		}
		parent[ry] = rx;
		if(rank[rx] == rank[ry])
			rank[rx] +=1;
		}
	}
};

2、解法

# 并查集模板
class UnionFind:
    def __init__(self, n: int):
        self.parent = list(range(n))
        self.size = [1] * n
        self.n = n
        # 当前连通分量数目
        self.setCount = n
    
    def findset(self, x: int) -> int:
        if self.parent[x] == x:
            return x
        self.parent[x] = self.findset(self.parent[x])
        return self.parent[x]
    
    def unite(self, x: int, y: int) -> bool:
        x, y = self.findset(x), self.findset(y)
        if x == y:
            return False
        if self.size[x] < self.size[y]:
            x, y = y, x
        self.parent[y] = x
        self.size[x] += self.size[y]
        self.setCount -= 1
        return True
    
    def connected(self, x: int, y: int) -> bool:
        x, y = self.findset(x), self.findset(y)
        return x == y

class Solution:
    def makeConnected(self, n: int, connections: List[List[int]]) -> int:
        if len(connections) < n - 1:
            return -1
        
        uf = UnionFind(n)
        for x, y in connections:
            uf.unite(x, y)
        
        return uf.setCount - 1

五、两个字符串的删除操作(583)

在这里插入图片描述

1、最长公共子序列

  • 时间复杂度:O(mn),其中 m 和 n 分别是字符串word1和word2的长度。二维数组 dp 有 m+1 行和 n+1 列,需要对 dp 中的每个元素进行计算。

  • 空间复杂度:O(mn),其中 m 和 n 分别是字符串 word1和 word2 的长度。创建了 m+1 行 n+1 列的二维数组 dp。

2、动态规划

dp[i][j]表示word1[0:i]和word2[0:j]相同的最少删除操作次数。
(1)动态规划的边界:

  • 当i=0时,word1[0:i]为空,空字符串和任何字符串变成相同,只有将另一个字符串的字符全部删除,因此对任意0≤j≤n,有dp[0][j]=j;
  • 当j=0时,word2[0:j]为空,同理对任意0≤i≤m,有d[i][0] = i。
    (2)当i>0且j>0时,考虑dp[i][j]的计算:
  • 当word1[i-1] == word2[j-1]时,将这两个相同的字符称为公共字符,考虑使word1[0:i-1]和word2[0:j-1]相同的最少删除操作次数,增加一个公共字符后,最少删除操作次数不变,因此dp[i][j] = dp[i-1][j-1]。
  • 当word1[i-1] ≠ word2[j-1]时,考虑以下两项:
    • 使word1[0:i-1]和word2[0:j]相同的最少删除操作次数,加上删除word1[i-1]的一次操作;
    • 使word1[0:i]和word2[0:j-1]相同的最少删除操作次数,加上删除word2[j-1]的依次操作。
      要得到使word1[0:i]和word2[0:j相同的最少删除操作次数,应取两项中较小的一项,因此dp[i][j] = min(dp[i-1][j] + 1,dp[i][j-1]+1)=min(dp[i-1][j],dp[i][j-1])+1。
class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        m,n = len(word1),len(word2)
        dp = [[0] * (n + 1) for _ in range(m + 1)]
        for i in range(1,m + 1):
            for j in range(1,n + 1):
                dp[i][0] = i
                dp[0][j] = j
                if word1[i - 1] == word2[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1]
                else:
                    dp[i][j] = min(dp[i - 1][j],dp[i][j - 1]) + 1
        return dp[m][n]     
  • 时间复杂度:O(mn),其中 m和 n 分别是字符串 word1和word2的长度。二维数组 dp 有 m+1 行和 n+1 列,需要对 dp 中的每个元素进行计算。
  • 空间复杂度:O(mn),其中 m 和 n 分别是字符串 word1word2的长度。创建了 m+1 行 n+1 列的二维数组 dp。

六、3的幂(326)

在这里插入图片描述

class Solution:
    def isPowerOfThree(self, n: int) -> bool:
        while n and n % 3 == 0:
            n //= 3
        return n == 1

七、区间和的个数(327)

在这里插入图片描述

1、前缀和+二分查找

class Solution:
    def countRangeSum(self, nums: List[int], lower: int, upper: int) -> int:
        res,pre,now = 0,[0],0
        for n in nums:
            now += n
            res += bisect.bisect_right(pre,now-lower) - bisect.bisect_left(pre,now-upper)
            bisect.insort(pre,now)
        return res
  • 时间复杂度:O(nlogn),其中n为数组的长度, logn为三次二分查找位置的时间。

  • 空间复杂度:O(n),前缀和数组。

八、奇偶链表(328)

在这里插入图片描述

1、分离节点后合并

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def oddEvenList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if not head:
            return head
        evenHead = head.next
        odd, even = head, evenHead
        while even and even.next:
            odd.next = even.next
            odd = odd.next
            even.next = odd.next
            even = even.next
        odd.next = evenHead
        return head
  • 时间复杂度:O(n),其中 n 是链表的节点数。需要遍历链表中的每个节点,并更新指针。

  • 空间复杂度:O(1)。只需要维护有限的指针。

九、矩阵中的最长递增路径(329)

在这里插入图片描述

1、记忆化深度优先遍历

@lru_cache(None)的作用是将被装饰的函数的调用结果缓存起来,不限制缓存的大小。这意味着函数的调用结果都将被缓存,不会随着时间或调用次数的增加而被清除。

class Solution:
    
    DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]

    def longestIncreasingPath(self, matrix: List[List[int]]) -> int:
        if not matrix:
            return 0
        
        @lru_cache(None)
        def dfs(row: int, column: int) -> int:
            best = 1
            for dx, dy in Solution.DIRS:
                newRow, newColumn = row + dx, column + dy
                if 0 <= newRow < rows and 0 <= newColumn < columns and matrix[newRow][newColumn] > matrix[row][column]:
                    best = max(best, dfs(newRow, newColumn) + 1)
            return best

        ans = 0
        rows, columns = len(matrix), len(matrix[0])
        for i in range(rows):
            for j in range(columns):
                ans = max(ans, dfs(i, j))
        return ans
  • 时间复杂度:O(mn),其中 m 和 n 分别是矩阵的行数和列数。深度优先搜索的时间复杂度是 O(V+E),其中 V是节点数,E是边数。在矩阵中,O(V)=O(mn),O(E)≈O(4mn)=O(mn)。

  • 空间复杂度:O(mn),其中 m 和 n 分别是矩阵的行数和列数。空间复杂度主要取决于缓存和递归调用深度,缓存的空间复杂度是 O(mn),递归调用深度不会超过 mn。

十、验证二叉树的前序序列化(331)

在这里插入图片描述

1、栈

很喜欢这个解答方式!https://leetcode.cn/problems/verify-preorder-serialization-of-a-binary-tree/solutions/651132/pai-an-jiao-jue-de-liang-chong-jie-fa-zh-66nt

class Solution:
    def isValidSerialization(self, preorder: str) -> bool:
        stack = []
        for node in preorder.split(','):
            stack.append(node)
            while len(stack) >= 3 and stack[-1] == stack[-2] == '#' and stack[-3] != '#':
                stack.pop(),stack.pop(),stack.pop()
                stack.append('#')
        return len(stack) == 1 and stack.pop() == '#'

十一、猜数字大小 II(375)

在这里插入图片描述

1、动态规划

class Solution:
    def getMoneyAmount(self, n: int) -> int:
        dp = [[0] * (n + 1) for _ in range(n + 1)]
        for i in range(n - 1,0,-1):
            for j in range(i + 1,n + 1):
                dp[i][j] = j + dp[i][j - 1]
                for k in range(i,j):
                    dp[i][j] = min(dp[i][j], k + max(dp[i][k - 1],dp[k + 1][j]))
        return dp[1][n]

十二、赎金信(383)

在这里插入图片描述

1、字符统计

collections.Counter表示一个集合中各元素的出现次数。

class Solution:
    def canConstruct(self, ransomNote: str, magazine: str) -> bool:
        if len(ransomNote) > len(magazine):
            return False
        return not collections.Counter(ransomNote) - collections.Counter(magazine)

十三、最小面积矩形 ||(963)

在这里插入图片描述

十四、二叉搜索树的范围和(938)

在这里插入图片描述

1、深度优先搜索

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def rangeSumBST(self, root: Optional[TreeNode], low: int, high: int) -> int:
        if root is None:
            return 0
        if root.val < low:
            return self.rangeSumBST(root.right,low,high)
        elif root.val > high:
            return self.rangeSumBST(root.left,low,high)
        else:
            return root.val + self.rangeSumBST(root.left,low,high) + self.rangeSumBST(root.right,low,high)
  • 时间复杂度:O(n),其中 n 是二叉搜索树的节点数。

  • 空间复杂度:O(n)。空间复杂度主要取决于栈空间的开销。

十五、统计树中的合法路径数目(2867)

在这里插入图片描述
在这里插入图片描述

1、埃氏筛+DFS

N = 1000001
is_prime = [True] * N
is_prime[1] = False
for i in range(2,N):
    if is_prime[i]:
        for j in range(i * i,N,i):
            is_prime[j] = False
class Solution:
    def countPaths(self, n: int, edges: List[List[int]]) -> int:
        G = [[] for _ in range(n + 1)]
        for i,j in edges:
            G[i].append(j)
            G[j].append(i)
        def dfs(i,pre):
            seen.append(i)
            for j in G[i]:
                if j != pre and not is_prime[j]:
                    dfs(j,i)
        res = 0
        count = [0] * (n + 1)
        for i in range(1,n + 1):
            if not is_prime[i]:
                continue
            cur = 0
            for j in G[i]:
                if is_prime[j]:
                    continue
                if count[j] == 0:
                    seen = []
                    dfs(j,0)
                    for k in seen:
                        count[k] = len(seen)
                res += count[j] * cur
                cur += count[j]
            res += cur
        return res

十六、三个无重叠子数组的最大和(689)

在这里插入图片描述

1、滑动窗口

要计算三个无重叠子数组的最大和,可以枚举第三个子数组的位置,同时维护前两个无重叠子数组的最大和及其位置。
要计算两个无重叠子数组的最大和,可以枚举第二个子数组的位置,同时维护第一个子数组的最大和及其位置。
因此首先解决单个子数组的最大和问题,再解决两个无重叠子数组的最大和问题,最后解决三个无重叠子数组的最大和问题。

(1)单个子数组的最大和

s u m 1 sum_1 sum1为大小为k的窗口的元素和,当窗口从 [ i − k + 1 , i ] [i-k+1,i] [ik+1,i]向右滑动1个元素后,sum1增加了 n u m s [ i + 1 ] nums[i+1] nums[i+1],减少了 n u m s [ i − k + 1 ] nums[i-k+1] nums[ik+1]。因此可以O(1)地计算出向右滑动1个元素后的窗口的元素和。
[ 0 , k − 1 ] [0,k - 1] [0,k1]开始,不断向右滑动窗口,直至窗口右端点到达数组末尾时停止。统计这一过程中的 s u m 1 sum_1 sum1的最大值(记作 m a x S u m 1 maxSum_1 maxSum1)及其对应位置。

class Solution:
    def maxSumOfOneSubarray(self, nums: List[int], k: int) -> List[int]:
        ans = []
        sum1,maxSum1 = 0,0
        for i,num in enumerate(nums):
            sum1 += num
            if i >= k - 1:
                if sum1 > maxSum1:
                    maxSum1 = sum1
                    ans = [i - k + 1]
                sum1 -= nums[i - k + 1]
        return ans

(2)两个无重叠子数组的最大和

使用两个大小为k的滑动窗口。设 s u m 1 sum_1 sum1为第一个滑动窗口的元素和,该滑动窗口从 [ 0 , k − 1 ] [0,k-1] [0,k1]开始; s u m 2 sum_2 sum2为第二个滑动窗口的元素和,该滑动窗口从 [ k , 2 k − 1 ] [k,2k-1] [k,2k1]开始。
同时向右滑动这两个窗口,并维护 s u m 1 sum_1 sum1的最大值 m a x S u m 1 maxSum_1 maxSum1及其对应位置。每次滑动时,计算当前 m a x S u m 1 maxSum_1 maxSum1 s u m 2 sum_2 sum2之和。统计这一过程的 m a x S u m 1 + s u m 2 maxSum_1+sum_2 maxSum1+sum2的最大值(记作 m a x S u m 12 maxSum_{12} maxSum12)及其对应位置。

class Solution:
    def maxSumOfTwoSubarrays(self, nums: List[int], k: int) -> List[int]:
       ans = []
       sum1,maxSum1,maxSum1Index = 0,0,0
       sum2,maxSum12 = 0,0
       for i in range(k,len(nums)):
           sum1 += nums[i - k]
           sum2 += nums[i]
           if i >= 2 * k - 1:
               if sum1 > maxSum1:
                   maxSum1 = sum1
                   maxSum1Index = i- 2 * k + 1
                if maxSum1 + sum2 > maxSum12:
                    maxSum12 = maxSum1 + sum2
                    ans = [maxSum1Index,i - k + 1]
                sum1 -= nums[i - 2 * k + 1]
                sum2 -= nums[i - k + 1]
        return ans

(3)本题

使用三个大小为k的滑动窗口。设 s u m 1 sum_1 sum1为第一个滑动窗口的元素和,该窗口从 [ 0 , k − 1 ] [0,k - 1] [0,k1]开始; s u m 2 sum_2 sum2为第二个滑动窗口的元素和,该窗口从 [ k , 2 k − 1 ] [k,2k-1] [k,2k1]开始; s u m 3 sum_3 sum3为第三个滑动窗口自的元素和,该滑动窗口从 [ 2 k , 3 k − 1 ] [2k,3k-1] [2k,3k1]开始。
同时向右滑动这三个窗口,按照(2)的方法维护 m a x S u m 12 maxSum_{12} maxSum12及其对应位置。每次滑动时,计算当前 m a x S u m 12 maxSum_{12} maxSum12 s u m 3 sum_3 sum3之和。统计这一过程的 m a x S u m 12 + s u m 3 maxSum_{12}+sum_3 maxSum12+sum3的最大值及其对应位置。

class Solution:
    def maxSumOfThreeSubarrays(self, nums: List[int], k: int) -> List[int]:
        ans = []
        sum1,maxSum1,maxSum1Index = 0,0,0
        sum2,maxSum12,maxSum12Index = 0,0,()
        sum3,max_Total = 0,0
        for i in range(2 * k,len(nums)):
            sum1 += nums[i - 2 * k]
            sum2 += nums[i - k]
            sum3 += nums[i]
            if i >= 3 * k - 1:
                if sum1 > maxSum1:
                    maxSum1 = sum1
                    maxSum1Index = i - 3 * k + 1
                if maxSum1 + sum2 > maxSum12:
                    maxSum12 = maxSum1 + sum2
                    maxSum12Index = (maxSum1Index,i - 2 * k + 1)
                if maxSum12 + sum3 > max_Total:
                    max_Total = maxSum12 + sum3
                    ans = [*maxSum12Index,i - k + 1]
                sum1 -= nums[i - 3 * k + 1]
                sum2 -= nums[i - 2 * k + 1]
                sum3 -= nums[i - k + 1]
        return ans

十七、求一个整数的惩罚数(2698)

在这里插入图片描述

1、回溯

class Solution:
    def punishmentNumber(self, n: int) -> int:
        def dfs(s:str,pos:int,tot:int,target:int)->int:
            if pos == len(s):
                return tot == target
            sum = 0
            for i in range(pos,len(s)):
                sum = sum * 10 + int(s[i])
                if tot + sum > target:
                    break
                if dfs(s,i + 1,tot + sum,target):
                    return True
            return False
        res = 0
        for i in range(1,n + 1):
            if dfs(str(i * i),0,0,i):
                res += i * i
        return res

十八、爬楼梯(70)

在这里插入图片描述

1、递归

解决从0爬到i,所以定义 d f s ( i ) dfs(i) dfs(i)表示从0爬到i有多少种不同的方法。
分类讨论:

  • 若最后一步爬了1个台阶,那么我们得先爬i-1,要解决的问题缩小成:从0爬到i-1有多少种不同的方法。
  • 若最后一步爬了2个台阶,那么我们得先爬i-2,要解决的问题缩小成:从0爬到i-2有多少种不同的方法。
    这两种方法是互相独立的,所以根据加法原理,从 000 爬到 iii 的方法数等于这两种方法数之和,即
    d f s ( i ) = d f s ( i − 1 ) + d f s ( i − 2 ) dfs(i) = dfs(i-1)+dfs(i-2) dfs(i)=dfs(i1)+dfs(i2)

递归边界: d f s ( 0 ) = 1 , d f s ( 1 ) = 1 dfs(0)=1,dfs(1)=1 dfs(0)=1,dfs(1)=1。从0爬到0有一种方法,即原地不动。从0爬到1有一种方法,即爬1个台阶。

class Solution:
    def climbStairs(self, n: int) -> int:
        def dfs(n:int)->int:
            if n <= 1:
                return 1
            return dfs(n - 1) + dfs(n - 2)
        return dfs(n)
  • 时间复杂度: O ( 2 n ) O(2^{n}) O(2n)。搜索树可以近似为一棵二叉树,树高为 O ( n ) O(n) O(n),所以节点个数为 O ( 2 n ) O(2^{n}) O(2n),遍历搜索树需要 O ( 2 n ) O(2^{n}) O(2n)的时间。
  • 空间复杂度: O ( n ) O(n) O(n)。递归需要 O ( n ) O(n) O(n)的栈空间。

2、递归+记录返回值=记忆化搜索

class Solution:
    def climbStairs(self, n: int) -> int:
        @cache
        def dfs(n:int)->int:
            if n <= 1:
                return 1
            return dfs(n - 1) + dfs(n - 2)
        return dfs(n)
  • 时间复杂度: O ( n ) O(n) O(n)。由于每个状态只会计算一次,动态规划的复杂度=状态个数 × \times ×单个状态的计算时间。本体状态个数为 O ( n ) O(n) O(n),单个状态的计算时间为 O ( 1 ) O(1) O(1),所以动态规划的时间复杂度为 O ( n ) O(n) O(n)
  • 空间复杂度: O ( n ) O(n) O(n)。有多少状态,memo数组的大小就是多少。

十九、使二叉树所有路径值相等的最小代价(2673)

在这里插入图片描述
在这里插入图片描述

1、贪心+自底向上

class Solution:
    def minIncrements(self, n: int, cost: List[int]) -> int:
        ans = 0
        for i in range(n-2,0,-2):
            ans += abs(cost[i + 1] - cost[i])
            cost[i//2] += max(cost[i+1],cost[i])
        return ans

二十、逃离火灾(2258)

纯纯折磨人的题==
在这里插入图片描述
在这里插入图片描述

1、BFS+二分查找

class Solution:
    def maximumMinutes(self, grid: List[List[int]]) -> int:
        def bfs():
            m, n = len(grid), len(grid[0])
            q = []
            for i in range(m):
                for j in range(n):
                    if grid[i][j] == 1:
                        q.append((i, j))
                        fireTime[i][j] = 0
            
            time = 1
            while len(q) > 0:
                tmp = q
                q = []
                for cx, cy in tmp:
                    for nx, ny in (cx, cy - 1), (cx, cy + 1), (cx - 1, cy), (cx + 1, cy):
                        if nx >= 0 and ny >= 0 and nx < m and ny < n:
                            if grid[nx][ny] == 2 or fireTime[nx][ny] != inf:
                                continue
                            q.append((nx, ny))
                            fireTime[nx][ny] = time
                time += 1

        def check(stayTime):
            print(stayTime)
            m, n = len(grid), len(grid[0])
            visit = set((0, 0))
            q = []
            q.append((0, 0, stayTime))
            while len(q) > 0:
                tmp = q
                q = []

                for cx, cy, time in tmp:
                    for nx, ny in (cx, cy - 1), (cx, cy + 1), (cx - 1, cy), (cx + 1, cy):
                        if nx >= 0 and ny >= 0 and nx < m and ny < n:
                            if (nx, ny) in visit or grid[nx][ny] == 2:
                                continue
                            # 到达安全屋
                            if nx == m - 1 and ny == n - 1:
                                return fireTime[nx][ny] >= time + 1
                            # 火未到达当前位置 
                            if fireTime[nx][ny] > time + 1:
                                q.append((nx, ny, time + 1))
                                visit.add((nx, ny))
            return False

        m, n = len(grid), len(grid[0])
        fireTime = [[inf] * n for _ in range(m)]
        # 通过 bfs 求出每个格子着火的时间
        bfs()
        # 二分查找找到最大停留时间
        ans = -1
        low, high = 0, m * n
        while low <= high:
            mid = low + (high - low) // 2
            if check(mid):
                ans = mid
                low = mid + 1
            else:
                high = mid - 1
        return ans if ans < m * n else 10**9

二十一、需要添加的硬币的最小数量(2952)

在这里插入图片描述

1、贪心

class Solution:
    def minimumAddedCoins(self, coins: List[int], target: int) -> int:
        coins.sort()
        ans,x = 0,1
        length,index = len(coins), 0

        while x <= target:
            if index < length and coins[index] <= x:
                x += coins[index]
                index += 1
            else:
                x = x * 2
                ans += 1
        return ans

二十二、确定两个字符串是否相近(1657)

在这里插入图片描述

1、计数

  • Counter(word1) 和 Counter(word2) 分别使用 collections.Counter 创建了两个字典,用于统计每个字符串中字符的出现次数
  • Counter(word1).keys() 和 Counter(word2).keys() 分别获取了 word1 和 word2 中所有字符的集合(即去除重复字符后的集合)。
  • Counter(word1).values() 和 Counter(word2).values() 分别获取了 word1 和 word2 中所有字符出现次数的集合
class Solution:
    def closeStrings(self, word1: str, word2: str) -> bool:
        return Counter(word1).keys() == Counter(word2).keys() and sorted(Counter(word1).values()) == sorted(Counter(word2).values())

二十三、不浪费原料的汉堡制作方法(1276)

在这里插入图片描述

1、方程

class Solution:
    def numOfBurgers(self, tomatoSlices: int, cheeseSlices: int) -> List[int]:
        if tomatoSlices % 2 != 0 or tomatoSlices < 2 * cheeseSlices or  4 * cheeseSlices < tomatoSlices:
            return []
        else:
            return [tomatoSlices // 2 -cheeseSlices,2 * cheeseSlices - tomatoSlices // 2]

二十四、掉落的方块(699)

在这里插入图片描述
在这里插入图片描述

二十五、所有可能的真二叉树(894)

在这里插入图片描述

(1)性质

由于真二叉树的每个节点恰好有0或2个子节点,若往一颗真二叉树上添加节点,最少要(在一个叶子下)添加2个节点。这意味着整棵树及每棵子树的节点个数一定是奇数1,3,5,…。
此外,由于每增加2个节点,真二叉树就会多1个叶子,所以有n个节点的真二叉树恰有 n + 1 2 \frac{n+1}{2} 2n+1个叶子结点。

(2)寻找子问题

对于示例1,n=7,有4个叶子。枚举根节点的左子树有多少个叶子:

  • 左子树有1个叶子,那么右子树有3个叶子
  • 左子树有2个叶子,那么右子树有2个叶子
  • 左子树有3个叶子,那么右子树有1个叶子
    对于每棵子树,我们同样需要生成所有真二叉树,这是一个和原问题相似的,规模更小的子问题。

(3)状态定义及转移

定义f[i]为有i 个叶子的所有真二叉树的列表。
枚举左子树有j = 1,2,…,i个叶子,那么右子树有i-j个叶子。
左子树的所有真二叉树列表为f[j],右子树的所有真二叉树列表为f[i-j]。从这两个列表中各选一棵真二叉树,作为根节点的左右子树,从而得到有i个叶子的真二叉树,这些真二叉树组成了f[i]。
初始值:f[1]为只包含一个节点的二叉树列表。

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
MX=11
f = [[] for _ in range(MX)]
f[1] = [TreeNode()]
for i in range(2,MX):
    f[i] = [TreeNode(0,left,right)
            for j in range(1,i)
            for left in f[j]
            for right in f[i-j]]
    
class Solution:
    def allPossibleFBT(self, n: int) -> List[Optional[TreeNode]]:
        return f[(n + 1) // 2] if n % 2 else []

在这里插入图片描述

二十六、找出克隆二叉树中的相同节点(1379)

在这里插入图片描述

1、DFS

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

class Solution:
    def getTargetCopy(self, original: TreeNode, cloned: TreeNode, target: TreeNode) -> TreeNode:
        if original is None:
            return None
        if original == target:
            return cloned
        left = self.getTargetCopy(original.left,cloned.left,target)
        if left is not None:
            return left
        return self.getTargetCopy(original.right,cloned.right,target)                   
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

二十七、合并后数组中的最大元素(2789)

在这里插入图片描述

1、贪心+倒序遍历数组

class Solution:
    def maxArrayValue(self, nums: List[int]) -> int:
        n = len(nums)
        i = n - 2
        while i >= 0:
            if nums[i] <= nums[i+1]:
               nums[i] += nums[i + 1]
            i -= 1        
        return nums[0]        
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

二十八、卖木头块(2312)

在这里插入图片描述

1、动态规划

(1)寻找子问题

在实例1中,对于一个高为3宽为5的木块,第一步一共有6种切割方案:

  • 竖着切开,有4种切法
  • 横着切开,有2种切法
    比如横着切开,第一步可以分成一个高为2宽为5的木块和一个高为1宽为5的木块。
    这俩都是更小的木块,可以分别处理,接着切割,这意味着我们要处理的问题都是「高为 i 宽为 j 的木块」。

(2)状态定义

定义f[i][j]表示切割一块高i宽j的木块,能够得到的最多钱数。
分类讨论:

  • 如果直接售卖,则收益为对应的price(如果存在的话)
  • 如果竖着切开,枚举切割位置(宽度)k,得到两个高为i,宽分别为k和j-k的木块,最大权益为:
    在这里插入图片描述
  • 如果横着切开,枚举切割位置(高度)k,得到两个宽为j,高分别为k和i-k的木块,最大收益为:
    在这里插入图片描述
    取上述三种情况的最大值,即为 f[i][j]。
class Solution:
    def sellingWood(self, m: int, n: int, prices: List[List[int]]) -> int:
        pr = {(h,w):p for h,w,p in prices}
        f = [[0] * (n + 1) for _ in range(m + 1)]
        for i in range(m + 1):
            for j in range(n + 1):
                f[i][j] = max(pr.get((i,j),0),
                            max((f[i][k] + f[i][j - k] for k in range(1,j)),default = 0),
                            max((f[k][j] + f[i - k][j] for k in range(1,i)),default = 0))
        return f[m][n]

二十九、王位继承顺序(1600)

1、多叉树的前序遍历

在这里插入图片描述

class ThroneInheritance:

    def __init__(self, kingName: str):
        self.edges = defaultdict(list)
        self.dead = set()
        self.king = kingName


    def birth(self, parentName: str, childName: str) -> None:
        self.edges[parentName].append(childName)


    def death(self, name: str) -> None:
        self.dead.add(name)


    def getInheritanceOrder(self) -> List[str]:
        ans = list()
        def preorder(name:str):
            if name not in self.dead:
                ans.append(name)
            if name in self.edges:
                for childName in self.edges[name]:
                    preorder(childName)
        preorder(self.king)
        return ans



# Your ThroneInheritance object will be instantiated and called as such:
# obj = ThroneInheritance(kingName)
# obj.birth(parentName,childName)
# obj.death(name)
# param_3 = obj.getInheritanceOrder()

三十、二叉树的锯齿形层序遍历

在这里插入图片描述

1、BFS

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def zigzagLevelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
        if not root:
            return []
        res = []
        q = collections.deque([root])
        while q:
            tmp = collections.deque()
            for _ in range(len(q)):
                node = q.popleft()
                if len(res) % 2 == 0: 
                    tmp.append(node.val)
                else:
                    tmp.appendleft(node.val)
                if node.left:
                    q.append(node.left)
                if node.right:
                    q.append(node.right)
            res.append(list(tmp))
        return res

三十一、使数组连续的最小操作数(2009)

在这里插入图片描述

1、去重+排序+滑动窗口

class Solution:
    def minOperations(self, nums: List[int]) -> int:
        n = len(nums)
        sortedUniqueNums = sorted((set(nums)))
        res = n
        j = 0
        for i,left in enumerate(sortedUniqueNums):
            right = left + n - 1
            while j < len(sortedUniqueNums) and sortedUniqueNums[j] <= right:
                res = min(res,n - (j - i + 1))
                j += 1
        return res
  • 时间复杂度:O(nlogn),其中n是数组 nums的长度。排序消耗 O(n×log⁡n),滑动窗口消耗 O(n)。
  • 时间复杂度:O(n),新建一个去重后排序的数组消耗 O(n)。

三十一、HTML实体解析器(1410)

在这里插入图片描述

1、模拟

class Solution:
    def entityParser(self, text: str) -> str:
        symbolMap = {
            '&quot;':'"',
            '&apos;':"'",
            "&amp;":'&',
            "&gt;":'>',
            "&lt;":'<',
            "&frasl;":'/',
        }
        i = 0
        n = len(text)
        res = []
        while i < n:
            flag = 0
            if text[i] == '&':
                for s in symbolMap:
                    if text[i:i + len(s)] == s:
                        res.append(symbolMap[s])
                        i += len(s)
                        flag = 1
                        break
            if flag == 0:
                res.append(text[i])
                i += 1
        return "".join(res)
  • 时间复杂度:考虑最坏情况,每个位置都是 &,那么探测的总时间代价和「实体字符」的总长度 k 相关,总的时间代价为 O(k×n)。
  • 空间复杂度:这里用了 entityList 作为辅助变量,故渐进空间复杂度为O(k)。

三十二、统计区间中的整数数目(2276)

在这里插入图片描述

1、线段树

class CountIntervals:
    __slots__ = 'l','r','left','right','cnt'

    def __init__(self,l = 1,r = 10**9):
        self.left = self.right = None
        self.l,self.r,self.cnt = l,r,0


    def add(self, l: int, r: int) -> None:
        if self.cnt == self.r - self.l + 1:
            return
        if l <= self.l and self.r <= r:
            self.cnt = self.r - self.l + 1
            return
        
        mid = (self.l + self.r) // 2
        if self.left is None:
            self.left = CountIntervals(self.l,mid)
        if self.right is None:
            self.right = CountIntervals(mid + 1,self.r)
        if l <= mid:
            self.left.add(l,r)
        if mid < r:
            self.right.add(l,r)
        self.cnt = self.left.cnt + self.right.cnt
    


    def count(self) -> int:
        return self.cnt



# Your CountIntervals object will be instantiated and called as such:
# obj = CountIntervals()
# obj.add(left,right)
# param_2 = obj.count()

三十三、找出叠涂元素(2661)

在这里插入图片描述

三十四、修改后的最大二进制字符串(1702)

在这里插入图片描述

1、贪心

(1)提示1

答案不会有连续的0。
**证明:**反证法。若答案包含00,可通过操作1转变为10,从而得到更大的答案,所以答案不会包含连续的0.

(2)提示2

答案至多包含一个0。
**证明:**反证法。假设至少有两个0,随意选择其中两个0,由提示1可知这两个0不相邻。例如10110,通过操作2将右边的0移动到第一个0的右边,即:10110->10101->10011,然后通过操作1转变为11011。由于左边更高位的0变成了1,所以我们得到了比10110更大的答案。一般地,在有多个0的情况下,总可以通过操作2让最高位的0的右侧也是0,然后通过操作1让最高位的0变成1,从而得到更大的答案,因此答案至多包含一个0。

(3)提示3

若binary全是1,直接返回binary即可。
若binary中有0,由于操作1和操作2的结果都包含0,所以无法吧所有0变成1。结合提示2,最终答案会恰好包含一个0。
此外,提示2相当于给出了一个让二进制更大的方案:只要还有两个0,那么用操作2把右边的0往左移,当出现00时就通过操作1把左边的0变成1,这会让二进制更大。
设binary从左到右第一个0的下标为i,为了得到更大的二进制,下标在[i,n-1]中的1会随着0的左移被挤到binary的末尾。例如:
在这里插入图片描述
一般的,设[i,n-1]中有cnt1个1,那么答案中唯一的0的下标为n-1-cnt1(从左往右)。

class Solution:
    def maximumBinaryString(self, binary: str) -> str:
        i = binary.find('0')
        if i < 0:
            return binary
        cnt1 = binary.count('1',i)
        return '1' * (len(binary) - cnt1 - 1) + '0' + '1' * cnt1

三十五、找出数组的第K大和

在这里插入图片描述

1、优先队列(最小堆)

首先找到最大的子序列和mx,即所有正数之和。
可以发现,其他子序列的和,都可以看成在这个最大子序列和直说,减去其他部分子序列之和得到的。因此,我们可以将问题转换为求第k小的子序列和。
只需要将所有数的绝对值升序排列,建立小根堆,存储二元组(s,i),表示当前和为s,下一个待选择的数字的小标为i的子序列。
每次取出堆顶,有两种情况:

  • 选择下一位
  • 选择下一位且不选择本位
    【解释:假如现在堆顶取出的是(nums[0] + nums[1],2),情况1是选择下一位,即:(nums[0] + nums[1] + nums[2],3);情况2是选择下一位且不选择本位:(nums[0] + nums[2],3)】
    由于数组是从小到大排序,这种方式能够不重不漏地按序遍历完所有的子序列和。
class Solution:
    def kSum(self, nums: List[int], k: int) -> int:
        mx = 0
        for i,x in enumerate(nums):
            if x > 0:
                mx += x
            else:
                nums[i] = -x
        nums.sort()
        h = [(0,0)]
        for _ in range(k - 1):
            s,i = heappop(h)
            if i < len(nums):
                heappush(h,(s + nums[i],i + 1))
                if i:
                    heappush(h,(s + nums[i] - nums[i - 1],i + 1))
        return mx - h[0][0]
  • 时间复杂度:O(nlogn + klogk)
  • 空间复杂度:O(k)

三十六、寻找峰值II(1901)

在这里插入图片描述
暴力求解很舒服,但是别忘了题目要求时间复杂度为O(mlogn)或O(nlogm)!!

1、二分查找

在这里插入图片描述
综上所述,我们可以二分包含峰顶的行号i:

  • 若mat[i]的最大值比它下面的相邻数字小,则存在一个峰顶,其行号大于i。缩小二分范围,更新二分区间左端点left。
  • 若mat[i]的最大值比它下面的相邻数字大,则存在一个峰顶,其行号小于等于i。缩小二分范围,更新二分区间右端点right。
class Solution:
    def findPeakGrid(self, mat: List[List[int]]) -> List[int]:
        left,right = 0,len(mat) - 2
        while left <= right:
            i = (left + right) // 2
            mx = max(mat[i])
            if mx > mat[i + 1][mat[i].index(mx)]:
                right = i - 1
            else:
                left = i + 1
        i = left
        return [i,mat[i].index(max(mat[i]))]
  • 时间复杂度:O(nlog⁡m),其中 m 和 n 分别为 mat 的行数和列数。需要二分 O(log⁡m)次,每次二分需要 O(n)的时间寻找 mat[i]最大值的下标。
  • 空间复杂度:O(1)。仅用到若干额外变量。

三十七、路径总和II(113)

在这里插入图片描述

1、DFS

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
        res = list()
        ans = list()
        def dfs(node:TreeNode,target:int):
            if node is None:
                return
            ans.append(node.val)
            target -= node.val
            if not node.left and not node.right and target == 0:
                res.append(ans[:])
            dfs(node.left,target)
            dfs(node.right,target)
            ans.pop()
        dfs(root,targetSum)
        return res
  • 时间复杂度:O(n),n 为二叉树的节点数,先序遍历需要遍历所有节点。
  • 空间复杂度: O(n) ,最差情况下,即树退化为链表时,ans存储所有树节点,使用 O(n)额外空间。

三十八、Nim游戏(292)

在这里插入图片描述

1、推理

若石头堆中只有一块、两块或三块,那么在你的回合,你就可以把全部石头取走,从而在游戏中获胜;若堆中恰好有四块石头,你就会失败,因为无论你怎么取,总会为对手留下几块,他可以将剩下所有石头取走,从而在游戏中打败你。因此,想要取胜,在你的回合中,必须避免石头堆中的石头数为4或4的倍数。

class Solution:
    def canWinNim(self, n: int) -> bool:
        return n % 4 != 0
  • 时间复杂度:O(1)
  • 空间复杂度:O(1)

三十九、互质树(1766)

在这里插入图片描述

1、DFS

MX = 51
coprime = [[j for j in range(1,MX) if gcd(i,j) == 1]
            for i in range(MX)]
class Solution:
    def getCoprimes(self, nums: List[int], edges: List[List[int]]) -> List[int]:
        n = len(nums)
        g = [[] for _ in range(n)]
        for x,y in edges:
            g[x].append(y)
            g[y].append(x)
        ans = [0] * n
        val_depth_id = [(-1,-1)] * MX
        def dfs(x:int,fa:int,depth:int)->None:
            val = nums[x]
            ans[x] = max(val_depth_id[j] for j in coprime[val])[1]
            tmp = val_depth_id[val]
            val_depth_id[val] = (depth,x)
            for y in g[x]:
                if y != fa:
                    dfs(y,x,depth + 1)
            val_depth_id[val] = tmp
        dfs(0,-1,0)
        return ans
  • 时间复杂度:O(nU,其中 n 为 nums的长度,U=max⁡(nums)=50。
  • 空间复杂度:O(n+U)。忽略预处理的时间和空间。

四十、拼车(1094)

在这里插入图片描述

1、差分数组

class Solution:
    def carPooling(self, trips: List[List[int]], capacity: int) -> bool:
        d = [0] * 1001
        for num,from_,to in trips:
            d[from_] += num
            d[to] -= num
        return all(s <= capacity for s in accumulate(d))
  • 时间复杂度:O(n+U),其中 n 为 trips的长度,U=max⁡( t o i to_i toi)
  • 空间复杂度:O(U)。

四十一、尽量减少恶意软件的传播(924)

在这里插入图片描述

1、DFS、状态机

一个大小为k的连通块内,若只有一个节点x被感染(x在initial)中,那么移除x后,这个连通块不会被感染,从而让M(initial)减少k。
而如果连接块中至少有两个节点被感染,无论移除哪个点,仍会导致连接块所有节点被感染,M(initial)不变。
因此我们要找的是只包含一个被感染节点的连通块,并且这个连通块越大越好。
算法如下:
(1)遍历initial中的节点x;
(2)若x没有被访问过,那么从x开始DFS,同时用一个vis数组标记访问过的节点;
(3)DFS过程中,统计连通块的大小size;
(4)DFS过程中,记录访问到的在initial中的节点;
(5)DFS结束后,若发现该连通块只有一个在initial中的节点,并且该连通块的大小比最大的连通块更大,那么更新最大连通块的大小,以及答案节点x。若一样大,则更新答案节点的最小值。
(6)最后若没找到符合要求的节点,返回min(initial),否则返回答案节点。
如何判断连通块内有一个或多个在initial中的节点?
可以使用状态机:
在这里插入图片描述

  • 初始状态为-1;
  • 若状态为-1,在找到被感染的节点x后,状态变为x;
  • 若状态为非负数x,在找到另一个被感染的节点后,状态变为-2。若状态已经为-2,则不变。
class Solution:
    def minMalwareSpread(self, graph: List[List[int]], initial: List[int]) -> int:
        st = set(initial)
        vis = [False] * len(graph)
        def dfs(x:int)->None:
            vis[x] = True
            nonlocal node_id,size
            size += 1
            if node_id != -2 and x in st:
                node_id = x if node_id == -1 else -2
            for y,conn in enumerate(graph[x]):
                if conn and not vis[y]:
                    dfs(y)
        ans = -1
        max_size = 0
        for x in initial:
            if vis[x]:
                continue
            node_id = -1
            size = 0
            dfs(x)
            if node_id >= 0 and (size > max_size or size == max_size and node_id < ans):
                ans = node_id
                max_size = size
        return min(initial) if ans < 0 else ans          
  • 时间复杂度:O(n²)
  • 空间复杂度:O(n)

四十二、尽量减少恶意软件的传播 II(928)

在这里插入图片描述

1、DFS、状态机

逆向思维,从不initial中的点v出发DFS,在不经过initial中的节点的前提下,看看v是否只能被一个点感染到,还是能被多个点感染到。若v只能被点x=initial[i]感染到,那么在本次DFS过程中访问到的其他节点,也只能被点x感染到。
算法如下:
(1)创建一个vis数组,标记在DFS中访问过的节点;
(2)枚举[0,n-1]中没有访问过的,且不在initial中的节点i;
(3)从i开始DFS;
(4)DFS过程中,只访问不在initial中的节点,统计访问到的节点个数size;
(5)DFS过程中,若发现了在initial中的节点,按照状态机更新变量node_id;
(6)DFS结束后,若node_id≥0,那么把node_id(作为key)和size(作为value)添加到一个哈希表或数组cnt中,其中相同的node_id要累加size;
(7)最后,若cnt为空,返回min(initial);否则返回cnt中size最大的node_id,若有多个size一样大,返回node_id的最小值。

class Solution:
    def minMalwareSpread(self, graph: List[List[int]], initial: List[int]) -> int:
        st = set(initial)
        vis = [False] * len(graph)
        def dfs(x:int)->None:
            vis[x] = True
            nonlocal node_id,size
            size += 1
            for y,conn in enumerate(graph[x]):
                if conn == 0:
                    continue
                if y in st:
                    if node_id != -2 and node_id != y:
                        node_id = y if node_id == -1 else -2
                elif not vis[y]:
                    dfs(y)
        
        cnt = Counter()
        for i,seen in enumerate(vis):
            if seen or i in st:
                continue
            node_id = -1
            size = 0
            dfs(i)
            if node_id >= 0:
                cnt[node_id] += size
        return min((-size,node_id) for node_id,size in cnt.items())[1] if cnt else min(initial)
  • 时间复杂度:O(n²)
  • 空间复杂度:O(n)

四十三、从双倍数组中还原原数组(2007)

在这里插入图片描述

1、哈希表、排序

class Solution:
    def findOriginalArray(self, changed: List[int]) -> List[int]:
        changed.sort()
        ans = []
        cnt = Counter()
        for x in changed:
            if x not in cnt:
                cnt[x * 2] += 1
                ans.append(x)
            else:
                cnt[x] -= 1
                if cnt[x] == 0:
                    del cnt[x]
        return [] if cnt else ans 
  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(n)

四十四、组合总和(39)

在这里插入图片描述

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        ans = []
        path = []
        candidates.sort()
        n = len(candidates)
        def dfs(i:int,target:int):
            if target == 0:
                ans.append(path[:])
                return
            if target < candidates[i]:
                return
            for j in range(i,n):
                path.append(candidates[j])
                dfs(j,target - candidates[j])
                path.pop()
        dfs(0,target)
        return ans

四十五、组合总和III(216)

在这里插入图片描述

1、DFS

class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        ans = []
        path = []
        def dfs(i:int,n:int):
            if n == 0 and len(path) == k:
                ans.append(path[:])
                return
            if n < i:
                return
            for j in range(i +1,10):
                path.append(j)
                dfs(j,n-j)
                path.pop()
        dfs(0,n)
        return ans

四十六、总行驶距离(2739)

在这里插入图片描述

class Solution:
    def __init__(self):
        self.count = 0
        self.res = 0
    def distanceTraveled(self, mainTank: int, additionalTank: int) -> int:      
        if mainTank > 0:
            self.res += 10
            self.count += 1
            mainTank -= 1
            if self.count % 5 == 0 and additionalTank > 0:
                return self.distanceTraveled(mainTank + 1,additionalTank - 1)
            else:
                return self.distanceTraveled(mainTank,additionalTank)
        else:
            return self.res

四十七、在带权树网络中统计可连接服务器对数目

在这里插入图片描述
在这里插入图片描述

1、DFS

class Solution:
    def countPairsOfConnectableServers(self, edges: List[List[int]], signalSpeed: int) -> List[int]:
        n = len(edges) + 1
        g = [[] for _ in range(n)]
        for x,y,wt in edges:
            g[x].append((y,wt))
            g[y].append((x,wt))
        
        def dfs(x:int,fa:int,s:int)->int:
            cnt = 0 if s % signalSpeed else 1
            for y,wt in g[x]:
                if y != fa:
                    cnt += dfs(y,x,s + wt)
            return cnt

        ans = [0] * n
        for i,gi in enumerate(g):
            if len(gi) == 1:
                continue
            s = 0
            for y,wt in gi:
                cnt = dfs(y,i,wt)
                ans[i] += cnt * s
                s += cnt
        return ans

四十八、救生艇(881)

在这里插入图片描述

1、贪心

class Solution:
    def numRescueBoats(self, people: List[int], limit: int) -> int:
        people.sort()
        n = len(people)
        left,right = 0,n - 1
        res = 0
        while left <= right:
            if people[left] + people[right] > limit:
                right -= 1
            else:
                left += 1
                right -= 1
            res += 1
        
        return res        
  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(logn)

四十九、石子游戏 VII

在这里插入图片描述

1、记忆化搜索

(1)寻找子问题

设爱丽丝的最终得分为A,鲍勃的最终得分为B,那么爱丽丝需要最大化A-B,鲍勃需要最小化A-B,或是说最大化B-A。
即每个玩家都需要最大化自己的得分减去对手的得分。
e.g.:
stones = [5,3,1,4,2]。枚举爱丽丝第一回合移除的石子:

  • 移除最左边的石子stones[0]=5,那么需要解决的问题是:剩余石子为stones’=[3,1,4,2],鲍勃(他现在是先手)的得分减去爱丽丝(她现在是后手)的得分最大是多少。
  • 移除最右边石子stones[4]=2,那么需要解决的问题是:剩余石子为stones’=[5,3,1,4],鲍勃(他现在是先手)的得分减去爱丽丝(她现在是后手)的得分最大是多少。
    我们需要解决的子问题,都是最大化先手的得分减去后手的得分,都是和原问题相似、规模更小的子问题,因此可以用递归解决。

(2)递归怎么写:状态定义与状态转移方程

因为要解决的问题都形如「对于stones中的一个连续子数组,计算先手得分减去后手得分的最大值」,所以定义 d f s ( i , j ) dfs(i,j) dfs(i,j)表示剩余石子从stones[i]到stones[j],先手得分减去后手得分的最大值。
例如stones=[5,3,1,4,2]。在第一回合中,若爱丽丝移除最右边的stones[4]=2,得到pt4 = 5 + 3 + 1 + 4 = 13分,那么问题变成:对于stones’=[5,3,1,4],鲍勃选择哪颗石子,可以最大化鲍勃的得分减去爱丽丝的得分,这里的得分是指在stones’上的得分。
对于stones’,设鲍勃最终得分为B’,爱丽丝最终得分为A’,则子问题
d f s ( 0 , 3 ) = B ′ − A ′ dfs(0,3) = B'-A' dfs(0,3)=BA
我们要计算的原问题
d f s ( 0 , 4 ) = A − B dfs(0,4) = A - B dfs(0,4)=AB
由于A=pt4 + A’,B = B’,则有
d f s ( 0 , 4 ) = A − B = p t 4 + A ′ − B ′ = p t 4 − ( B ′ − A ′ ) = p t 4 − d f s ( 0 , 3 ) dfs(0,4) = A - B = pt4 + A' - B' = pt4 - (B' - A') = pt4 - dfs(0,3) dfs(0,4)=AB=pt4+AB=pt4(BA)=pt4dfs(0,3)
这样就找到了原问题和子问题的关系。
一般的,若剩余石子从stones[i]到stones[j],枚举先手移除的石子:

  • 若移除的是最左边的石子stones[i],利用stones的前缀和s可以算出s[j + 1]-s[i + 1]分,这种情况下dfs(i,j)=s[j+1]-s[i+1]-dfs(i+1,j);
  • 若移除的是最右边的石子stones[j],得到s[j] - s[i]分,这种情况下dfs(i,j)=s[j]-s[i]-dfs(i,j-1)
class Solution:
    def stoneGameVII(self, stones: List[int]) -> int:
        s = list(accumulate(stones,initial=0))
        @cache
        def dfs(i:int,j:int)->int:
            if i == j:
                return 0
            return max(s[j+1]-s[i+1]-dfs(i+1,j),s[j]-s[i]-dfs(i,j-1))
        ans = dfs(0,len(stones)-1)
        dfs.cache_clear() 
        return ans
  • 时间复杂度: O ( n 2 ) O(n^2) O(n2),其中 n为 stones的长度。由于每个状态只会计算一次,动态规划的时间复杂度 = 状态个数 × 单个状态的计算时间。本题状态个数等于 O ( n 2 ) O(n^2) O(n2),单个状态的计算时间为 O(1),所以动态规划的时间复杂度为 O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( n 2 ) O(n^2) O(n2)。有多少个状态,memo数组的大小就是多少。

五十、甲板上的战舰(419)

在这里插入图片描述

1、枚举

class Solution:
    def countBattleships(self, board: List[List[str]]) -> int:
        m = len(board)
        n = len(board[0])
        ans = 0
        for i in range(m):
            for j in range(n):
                if board[i][j] == 'X':
                    if i == 0 and j == 0:
                        ans += 1
                    elif i == 0 and j != 0:
                        if board[i][j - 1] == '.':
                            ans += 1
                    elif i != 0 and j == 0:
                        if board[i - 1][j] == '.':
                            ans += 1
                    else:
                        if board[i - 1][j] == '.' and board[i][j - 1] == '.':
                            ans += 1
        return ans 

五十一、二叉树的堂兄弟节点(993)

在这里插入图片描述

1、DFS

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def isCousins(self, root: Optional[TreeNode], x: int, y: int) -> bool:
        x_parent,x_depth,x_found = None,None,False
        y_parent,y_depth,y_found = None,None,False
        def dfs(node:TreeNode,parent:TreeNode,depth:int)->bool:
            nonlocal x_parent,x_depth,x_found,y_parent,y_depth,y_found
            if not node:
                return
            if node.val == x:
                x_parent,x_depth,x_found = parent,depth,True
            elif node.val == y:
                y_parent,y_depth,y_found = parent,depth,True
            if x_found and y_found:
                return
            
            dfs(node.left,node,depth + 1)

            if x_found and y_found:
                return
            
            dfs(node.right,node,depth + 1)

        dfs(root,None,0)
        return x_depth == y_depth and x_parent != y_parent
  • 时间复杂度:O(n),其中 n 是树中的节点个数。在最坏情况下,我们需要遍历整棵树,时间复杂度为 O(n)。
  • 空间复杂度:O(n),即为深度优先搜索的过程中需要使用的栈空间。在最坏情况下,树呈现链状结构,递归的深度为 O(n)。

五十二、给小朋友分糖果I(2928)

在这里插入图片描述

1、容斥原理

要计算合法方案数(每个小朋友分到的糖果都不超过limit),可以先计算所有方案数(没有limit限制),再减去不合法的方案数(至少一个小朋友分到的糖果超过limit)

(1)所有方案数

相当于把 n个无区别的小球放入 3 个有区别的盒子,允许空盒的方案数。
隔板法:假设 n 个球和 2 个隔板放到 n+2 个位置,第一个隔板前的球放入第一个盒子,第一个隔板和第二个隔板之间的球放入第二个盒子,第二个隔板后的球放入第三个盒子。那么从 n+2个位置中选 2 个位置放隔板,有 C(n+2,2) 种放法。
隔板可以放在最左边或最右边,也可以连续放,对应着空盒的情况。例如第一个隔板放在最左边,意味着第一个盒子是空的;又例如第一个隔板和第二个隔板相邻,意味着第二个盒子是空的。

(2)至少一个小朋友分到的糖果超过limit

设三个小朋友分别叫A,B,C。
只关注A,若A分到的糖果超过limit

  • 30
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值