一、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(logm),最坏情况下树的深度为 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(logm),因此空间复杂度为 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、栈
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]
[i−k+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[i−k+1]。因此可以O(1)地计算出向右滑动1个元素后的窗口的元素和。
从
[
0
,
k
−
1
]
[0,k - 1]
[0,k−1]开始,不断向右滑动窗口,直至窗口右端点到达数组末尾时停止。统计这一过程中的
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,k−1]开始;
s
u
m
2
sum_2
sum2为第二个滑动窗口的元素和,该滑动窗口从
[
k
,
2
k
−
1
]
[k,2k-1]
[k,2k−1]开始。
同时向右滑动这两个窗口,并维护
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,k−1]开始;
s
u
m
2
sum_2
sum2为第二个滑动窗口的元素和,该窗口从
[
k
,
2
k
−
1
]
[k,2k-1]
[k,2k−1]开始;
s
u
m
3
sum_3
sum3为第三个滑动窗口自的元素和,该滑动窗口从
[
2
k
,
3
k
−
1
]
[2k,3k-1]
[2k,3k−1]开始。
同时向右滑动这三个窗口,按照(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(i−1)+dfs(i−2)
递归边界: 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×logn),滑动窗口消耗 O(n)。
- 时间复杂度:O(n),新建一个去重后排序的数组消耗 O(n)。
三十一、HTML实体解析器(1410)
1、模拟
class Solution:
def entityParser(self, text: str) -> str:
symbolMap = {
'"':'"',
''':"'",
"&":'&',
">":'>',
"<":'<',
"⁄":'/',
}
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(nlogm),其中 m 和 n 分别为 mat 的行数和列数。需要二分 O(logm)次,每次二分需要 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)=B′−A′
我们要计算的原问题
d
f
s
(
0
,
4
)
=
A
−
B
dfs(0,4) = A - B
dfs(0,4)=A−B
由于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)=A−B=pt4+A′−B′=pt4−(B′−A′)=pt4−dfs(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