240318 Leetcode相关题
递归:LeetCode70、112、509
分治:LeetCode23、169、240
单调栈:LeetCode84、85、739、503
并查集:LeetCode547、200、684
滑动窗口:LeetCode209、3、1004、1208
前缀和:LeetCode724、560、437、1248
差分:LeetCode1094、121、122
拓扑排序:LeetCode210
字符串:LeetCode5、20、43、93
二分查找:LeetCode33、34 BFS:LeetCode127、139、130、529、815
DFS&回溯::LeetCode934、685、1102、531、533、113、332、337
动态规划:LeetCode213、123、62、63、361、1230
贪心算法:LeetCode55、435、621、452
字典树:LeetCode820、208、648
这么多吗;
class solution 隐去的输入处理部分
【面试算法题总结01】输入输出处理基础(Java) - TickTick123
【刷题总结与梳理】近期做题补充&二刷&输入输出整理(c++)
没找到相关资料▲
def函数参数
调用函数时 可使用的正式参数类型:必需参数,关键字参数,默认参数,不定长参数(无用知识点)
LeetCode70
假设你正在爬 n 阶楼梯,每次可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
思路:
要记得先初始化 dp = [0] * (n + 1) # 因为dp[0]无用
dp[n]=dp[n-1]+dp[n-2]
dp[1]=dp[0]=1
优化:
这种2阶的差分,优化时会有交替存储的现象:
for i in range(2, n + 1):
a, b = b, a + b
法2:记忆化递归
噢,原来前面看到的HJxx的解法是记忆化递归,def里面有个def× 帮忙处理好了输入而已,非ACM模式
看了很久 感觉还是别扭,模仿不来,这个复杂度不知道怎么样▲
class Solution:
def climbStairs(self, n:int) -> int:
def dfs(i:int, memo) -> int: # i:阶梯数;memo: memory
if i == 0 or i == 1:
return 1
if memo[i] == -1: # 看起来还是dp,从低到高更新dp,
memo[i] = dfs(i - 1, memo) + dfs(i - 2, memo)
return memo[i]
return dfs( n, [-1]*(n+1) ) # -1 需表示没有计算过,n 为最大索引,因此数组大小为 n+1
LeetCode112 树的递归(看还有没有类似题练一下)
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum。
· 叶子节点 指没有子节点的节点。
思路:树的操作不熟练
法2:递归
转化为小问题:是否存在从当前节点的子节点到叶子的路径,其路径和为 sum - val。
# 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 hasPathSum(self, root: TreeNode, sum: int) -> bool:
if not root: # 这是啥▲ 噢 若该二叉树连根都没有
return False
if not root.left and not root.right: # 若已递归到叶子结点
return sum == root.val
return self.hasPathSum(root.left, sum - root.val) or self.hasPathSum(root.right, sum - root.val) # 写在传参里,就不用path记录了
感觉有点抽象,还是不太熟
法1:广度优先搜索 即队列
★该写法要熟悉
★.deque
class Solution:
def hasPathSum(self, root: TreeNode, sum: int) -> bool:
if not root:
return False
que_node = collections.deque([root])
que_val = collections.deque([root.val])
while que_node:
now = que_node.popleft() # 移除并返回
temp = que_val.popleft()
if not now.left and not now.right:
if temp == sum:
return True
continue
if now.left:
que_node.append(now.left)
que_val.append(now.left.val + temp)
if now.right:
que_node.append(now.right)
que_val.append(now.right.val + temp)
return False
deque
Python标准库的collections模块提供了双端队列/deque,支持从任一端增删元素.
其方法:
deque(初始值):创建一个双端队列
append(x): 在双端队列的右侧添加元素x。
appendleft(x): 在双端队列的左侧添加元素x。
pop(): 移除并返回双端队列右侧的元素。
popleft(): 移除并返回双端队列左侧的元素。
extend(iterable): 在双端队列的右侧添加可迭代对象中的所有元素。
extendleft(iterable): 在双端队列的左侧添加可迭代对象中的所有元素。
clear(): 移除所有元素,使deque变为一个空的双端队列。
rotate(n): 将所有元素向右循环移动n步(如果n是负数,则向左循环移动)。
LeetCode113
给你二叉树的根节点 root 和一个整数目标和 targetSum,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
怎么和112题一样的,只是要找所有的
法2:广度优先搜索
怎么会这么长▲ 这要考肯定都忘了
class Solution:
def pathSum(self, root: TreeNode, targetSum: int) -> List[List[int]]:
ret = list()
parent = collections.defaultdict(lambda: None)
def getPath(node: TreeNode):
tmp = list()
while node:
tmp.append(node.val)
node = parent[node]
ret.append(tmp[::-1])
if not root:
return ret
que_node = collections.deque([root])
que_total = collections.deque([0])
while que_node:
node = que_node.popleft()
rec = que_total.popleft() + node.val
if not node.left and not node.right:
if rec == targetSum:
getPath(node)
else:
if node.left:
parent[node.left] = node
que_node.append(node.left)
que_total.append(rec)
if node.right:
parent[node.right] = node
que_node.append(node.right)
que_total.append(rec)
return ret
法1:深度优先搜索
class Solution:
def pathSum(self, root: TreeNode, targetSum: int) -> List[List[int]]:
ret = list()
path = list() # 为什么设俩
def dfs(root: TreeNode, targetSum: int):
if not root:
return
path.append(root.val) #
targetSum -= root.val
if not root.left and not root.right and targetSum == 0:
ret.append(path[:]) # 怎么没看到在哪有修改 奇奇怪怪的 喔 心不在焉 认知能力下降
dfs(root.left, targetSum)
dfs(root.right, targetSum)
path.pop()
dfs(root, targetSum)
return ret
LeetCode509 斐波那契数
LeetCode23 合并 K 个升序链表
给你一个链表数组,每个链表都已经按升序排列。请将所有链表合并到一个升序链表中,返回合并后的链表。
思路:输入两个list,返回一个list
★class
法1:分治
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
cur = dummy = ListNode() # 哨兵节点dummy简化代码逻辑 cur为新list
while list1 and list2: # 二合一绝对是做过的,但见到代码并未立即
if list1.val < list2.val:
cur.next = list1 # 把 list1 加到新链表中
list1 = list1.next
else: # =时加哪个节点都可以
cur.next = list2 # 把 list2 加到新链表中
list2 = list2.next
cur = cur.next # 指向下一节点
cur.next = list1 if list1 else list2 # 拼接剩余链表
return dummy.next
def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
m = len(lists)
if m == 0: return None # 注意输入的 lists 可能是空的
if m == 1: return lists[0] # 无需合并,直接返回
left = self.mergeKLists(lists[:m // 2]) # 合并左半部分
right = self.mergeKLists(lists[m // 2:]) # 合并右半部分
return self.mergeTwoLists(left, right) # 最后把左半和右半合并
这个合并过程中还用了二分,学一下该写法,易读
分治与递归
分治是一种算法思想,递归是实现这种思想的一种手段
法1:最小堆
初始把所有链表的头节点入堆,然后不断弹出堆中最小节点 x 如果 x.next 不为空就加入堆中。循环直到堆为空。把弹出的节点按顺序拼接起来
▲
Leetcode169 为什么是分治 应该不是这题
给定一个大小为 n 的数组 nums ,返回其中的多数元素,即出现次数>⌊n/2⌋ 的元素。
· 保证数组非空,且总是存在多数元素。
思路:感觉做过类似题,+-还是怎么着,k=0,=0时遇任何元素+1,非当前元素就-1
LCR 169. 招式拆解 II
某套连招动作 记作仅由小写字母组成的序列 arr,字符arr[i] 为第 i 个招式。请返回第一个只出现一次的字符,如不存在返回空格。
思路:这个前面也做过吧,不过忘了,可能创建一个list,if i in list吧
这里用的标记;之前也用的标记吗
class Solution:
def dismantlingAction(self, arr: str) -> str:
hmap = {}
for c in arr:
hmap[c] = not c in hmap # 字典,键值对,c的值由无变1变0
for c in arr:
if hmap[c]: return c
return ' '
Leetcode240 搜索二维矩阵 II 为什么是分治
编写一个高效的算法来搜索 mxn矩阵matrix 中的一个目标值。该矩阵有以下特性:
每行元素从左到右升序排列;每列元素从上到下升序排列。
思路:dp吗× 靠数学逻辑
以右上角顶点为起始点,若当前值<目标值,行序加1即下移;若>目标值,列序减1即左移,直到找到或越界(非 i<r and j>=0
)
从左下角开始增也是可以的
该法含义大概是,当行/列序移动,相当于排除了该行/列;若从右上角剔除行/列,剩余矩阵能具有相同的升序性质;而每次剔除的都能保证不是,因此O(n)次内能得到结果
按这么说,从任意固定点开始,好像都可以▲ 右上角优在什么地方
Leetcode84 柱状图中最大的矩形 单调栈 代码忘了已经
思路:做过 用的啥来着 数学
以某种方式计算得的值,往某个方向推后 之前的不必再计算
法3:想一下,最大的矩形,也就是两个端点,固定一端如左端后,右端s更大的,在左端右移后还可能会被替换吗,不知道× 管它替不替换,两个for遍历并用tmp比较就可以了,时O(n²)
为什么是栈呢
那会了该题后是不是能用于任意f
法2:单调栈
函数定义里老把类型声明看成某个新变量
★list.pop(index=-1)方法:默认=-1,移除末尾
class Solution:
def largestRectangleArea(self, heights:List[int]) -> int:
stack = [] # 用来暂存数组子串的下标
heights = [0] + heights + [0]
res = 0
for i in range(len(heights)):
#print(stack)
while stack and heights[stack[-1]] > heights[i]: # 当出现比目前右端更低的柱形,将右端出栈,计算一下该柱形对应的最大面积;而且是while不断判断
tmp = stack.pop()
res = max(res, (i - stack[-1] - 1) * heights[tmp])
stack.append(i) # 每次都会入栈的
return res
这看完怎么像滑动窗口 看不懂▲ 看评论区懂了一点;来看第二篇题解:
考虑以第i根柱子为最矮柱子所能延伸的最大面积,然后遍历i.
为此,我们需要:
左边看一下,看最多能向左延伸多长,找到≥当前高度的最左边元素的下标;
右边看一下,看最多能向右延伸多长;找到≥当前高度的最右边元素的下标。
from typing import List
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
res = 0
for i in range(len(heights)):
cur_height = heights[i]
left = i
while left > 0 and heights[left - 1] >= cur_height:
left -= 1
right = i
while right < size - 1 and heights[right + 1] >= cur_height:
right += 1
res = max(res, (right-left+1) * cur_height)
return res
目前由于是for+while,时O(n²).
继续考虑以第i根柱子为最矮柱子所能延伸的最大面积:
通过一些实例,我们发现,只要是遇到了当前柱形的高度比它上一柱形的高度严格小的时候,一定可以确定它之前的某些柱形的最大宽度.
由此得出,当我们设法确定以当前柱形为右端对应的左边界时,找的是第一个严格小于当前柱形的下标。此时中间的柱形可以当做不存在。
这说明什么呢?这说明我们可以从最高的柱形开始考虑其最大面积× 又不是只有单凸的形状
这说明当我们遍历1次得知各柱形高度后,每次计算各高度对应最大宽度,不需要遍历,而是可以通过数学计算的,
具体是通过,将柱形下标依次入栈,如果递增就保留;
当我们遇到比目前右端更低的柱形,将右端出栈,计算一下该柱形对应的最大面积;不断判断(最后剩下的形状就是递增吧,毕竟有凸的必然在某个时刻出栈了)
实现:
该方法在实现中要处理两种特殊情况:
- 弹栈的时候,栈为空,这时没法取stack[-1]
- 遍历完成以后,栈中还有元素,那要单独算他们各自的矩形面积,比较麻烦
为此,
我们在输入数组后两端加上两个高度为 0 (<1即可)的柱形,可以回避上面这两种分类讨论。
有了这两个0:
左边的0,比任一元素小,不会出栈,因此栈一定不会为空;
右边的0,比任一元素小,最后通过while会使剩余元素出栈,最后剩下俩0
这俩0的柱形即哨兵(Sentinel);这里栈元素对应的柱形高,(发现在变动中)呈单调增加不减的形态,因此称为单调栈(Monotone Stack),它是暴力解法的优化。计算每个柱形对应的最大矩形的顺序,由出栈顺序决定。
另外,有人还进行了一点优化:
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
stack = [-1]
heights.append(0)
mxarea = 0
for i, v in enumerate(heights):
while heights[stack[-1]] > v: # 当出现比目前右端即[stack[-1]更低的柱形
mid = stack.pop()
area = heights[mid] * (i - 1 - stack[-1])
if area > mxarea: mxarea = area
stack.append(i)
return mxarea
其优化说明:不想用[0] + heights,因为在数组前部追加的速度很慢,由于python没有优化,复杂度能有O(N);那么这么处理,一方面栈里初始放一个-1,另一方面在数组尾部添一个0。这样当数组遍历完后,heights[stack[-1]]=heights[-1]=0,也能使得剩余元素都出栈(前面的是通过=heights[0]=0).
不对啊 按这个说法,其优化的地方是在栈里面先放一个东西,以优化速度;另外他发现,放0放-1都行.本题暂完.
Leetcode85
给定一个仅包含 0 和 1 、大小 rows x cols 的二维二进制矩阵,找出只含 1 的最大矩形,并返回其面积。
思路:for i for j 如果递减 啊不是;得到每行/列最长的连续子串,设法拼起来×不一定都是最长子串拼起来,但每个最长子串都可以算得一个矩形
法1:84题的扩展。题解没那么好理解;图解一下就理解了,以第n行下底作为x轴,观察前n行,便是柱状图;计算完前1行的最大矩形面积后,下一行的heights可由前1行推出,便计算以第2行为底的最大矩形面积;最后max{}即可
用python的话代码短很多
class Solution:
def maximalRectangle(self, matrix: List[List[str]]) -> int:
if not matrix:return 0
m,n=len(matrix),len(matrix[0]) # m行n列
pre=[0]*(n+1) # heights,其上方有几个连续1;注意这里末尾补了0了
res=0
for i in range(m):
for j in range(n):
pre[j]=pre[j]+1 if matrix[i][j]=="1" else 0 # 自然地翻译 它说是前缀和
stack=[-1] # 这个-1可能一直不会动
for k,num in enumerate(pre):
while stack and pre[stack[-1]]>num: # 当栈尚为单调栈
index=stack.pop()
res=max(res,pre[index]*(k-stack[-1]-1))
stack.append(k)
return res
Leetcode739
给定一个整数数组 temperatures 表示每天温度,请返回数组,answer[i] 指对于第 i 天,下一个更高温度出现在几天后。若无则为0。
思路:就是问如何优化,感觉可以数组储存相邻的差值,但怎么判断±呢,<0则入栈 不行;噢好像想到了,用delta[]存储目前累积变化总和,出栈时将其保存到tmp里,每次可用出栈元素-tmp得到temperature队首的元素
法1:从后往前遍历并判断,遇到比栈顶大的数,栈顶就可以出栈并计算该点答案了;遇到比栈顶小的数,栈顶也已经可以计算该点答案,但并不出栈;也就是说,遇到一个数如5时,其右侧已经从出栈的都是≤5的数,不可能再被考虑了;而≥5的数不出栈的原因,是往前还可能遇到≥5但<该数的数。因此,栈内温度从后往前应当是递减的》
class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
n = len(temperatures)
ans = [0] * n
st = [] # 栈仍是存的下标
for i in range(n - 1, -1, -1):
t = temperatures[i]
while st and t >= temperatures[st[-1]]: #
# 哎呀 while、if、append顺序不能换 前面说错了
st.pop()
if st: # 若栈不空(这种特殊情况说明▲),说明t≤栈顶,符合题意,注意下标已知,可得该点答案
ans[i] = st[-1] - i
st.append(i) # 注意入栈是都入的,只是出栈的时候一次性把较t小的栈顶数都出了,然后接下来的一个或几个for会通过if算得这些点的答案
return ans
时O(n²),虽然有for+while,但每个元素只出入栈一次;空O(min{n,U})),U为temp最大差值(因为栈内不会存在相同元素)
法2:从前往后,原理一样的,一旦发现比栈顶高的,将栈顶就出栈,同时while输出相关答案,这个感觉好理解一点,代码也易读,t一旦<栈顶,意味着<栈内的所有数,即无法更新栈内元素
两个方法的共同点实际是:及时去掉后面无用的数据,保证栈中数据有序;计算内容涉及上一/下一更大/小的元素;题目要计算什么,while里就写什么.
class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
n = len(temperatures)
ans = [0] * n
st = []
for i, t in enumerate(temperatures):
while st and t > temperatures[st[-1]]:
j = st.pop()
ans[j] = i - j
st.append(i)
return ans
Leetcode42 接雨水
接雨水有前后缀、相向双指针做法,竖着算;也有单调栈做法,横着算每个小矩形
从最低的坑开始算,找稍高的坑,不断填坑
★enumerate的使用 看起来会偶尔用到
class Solution:
def trap(self, height: List[int]) -> int:
ans = 0
st = []
for i, h in enumerate(height):
while st and h >= height[st[-1]]: # 当目前的右端足够高,一定有某些矩形是可以算了的;就可以依次将栈顶作为左端 不断出栈并计算相应的横着的小矩形.
bottom_h = height[st.pop()] # 目前坑的最低高度
if not st: # len(st) == 0
break
left = st[-1] # 为什么呢
dh = min(height[left], h) - bottom_h # 面积的高
ans += dh * (i - left - 1)
st.append(i)
return ans
法1:前后缀分解 对于每个竖着的柱形i及其h,其装水后左端右端高度可以分别通过递推算得(左/右端没有更高的时左/右端高度记为h),最后对每个竖着的柱形,减去其h算得面积
class Solution:
def trap(self, height: List[int]) -> int:
n = len(height)
pre_max = [0] * n # pre_max[i]:从height[0] 到 height[i] 的最大值
pre_max[0] = height[0]
for i in range(1, n):
pre_max[i] = max(pre_max[i - 1], height[i]) # 装水后的高度是前缀pre中最高的那个高度
suf_max = [0] * n # suf_max[i]:从 height[i] 到 height[n-1] 的最大值
suf_max[-1] = height[-1]
for i in range(n - 2, -1, -1):
suf_max[i] = max(suf_max[i + 1], height[i])
ans = 0
for h, pre, suf in zip(height, pre_max, suf_max):
ans += min(pre, suf) - h # 累加每个水桶能接多少水
return ans
注:后两个 for 循环可以合并成一个循环,为了阅读没有合并
法2:相向双指针:在法2上进行优化,这里的pre_max感觉是指左柱形的右端,好像也不是 为什么可以直接-h;
回看ppt:还有些性质没用:有些suf_max的信息是多余的,不必求 写法:若从左往右的pre[i]>h[i],则pre的值可以延展到pre[i+1];同样地,若从右往左的suf>h,则...
而且不延展的时候,pre[i+1]就=h[i+1],故数组pre[i]可以O(1)的空间求得,时间仍O(n)
class Solution:
def trap(self, height: List[int]) -> int:
ans = left = pre_max = suf_max = 0
right = len(height) - 1
while left < right:
pre_max = max(pre_max, height[left])
suf_max = max(suf_max, height[right])
if pre_max < suf_max: # 这个if else什么意思呢 这是数学:如果前缀高度<后缀高度,那么无论形状如何,该点柱形高度都是前缀高度,同时前缀可以向右延展,变成下一柱形的前缀;后缀同理.
ans += pre_max - height[left]
left += 1 # 把前缀向右扩展
else:
ans += suf_max - height[right]
right -= 1
return ans
Leetcode46 全排列 回溯算法
它是for n[i] 下 n[0:i]+n[i+1:] 的全排列.那么已经选择的数字在 当前 要选择的数字中不能出现
★基本 >>
每个位向右移动,等于除以2
&
按位运算符
from typing import List
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
def dfs(nums, size, depth, path, state, res):
if depth == size:
res.append(path)
return
for i in range(size):
if ((state >> i) & 1) == 0:
dfs(nums, size, depth+1, path+[nums[i]], state^(1 << i), res)
# 通过传新的Path
size = len(nums)
if size == 0:
return []
state = 0
res = []
dfs(nums, size, 0, [], state, res)
return res
from typing import List
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
def dfs(nums, size, depth, path, used, res):
if depth == size: # 判定return顺序要在子问题前
res.append(path[:]) # 需要append一个新的,否则遍历完成后path为[]
return
for i in range(size):
if not used[i]:
used[i] = True
path.append(nums[i])
dfs(nums, size, depth+1, path, used, res)
used[i] = False
path.pop()
size = len(nums)
if len(nums) == 0: # 考虑特例
return []
used = [False for _ in range(size)]
res = []
dfs(nums, size, 0, [], used, res)
return res
if __name__ == '__main__':
nums = [1, 2, 3]
solution = Solution()
res = solution.permute(nums)
print(res)
较短的写法,将信息都放在了参数里,是吗▲
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
res= []
def f(a):
if len(a)==len(nums):
res.append(a)
for i in range(len(nums)):
if nums[i] not in a:
f(a+[nums[i]])
f([])
return res
Leetcode503 下一个更大元素 II
给定一个循环数组,返回 nums 中每个元素的 下一个更大元素 。若不存在,输出 -1
LeetCode547 基本的dfs,bfs题
有 n 个城市,其中一些彼此相连,另一些没有相连,isConnected[i][j] = 1 表示城市 i,j 直接相连,= 0 表示不直接相连。
间接相连:若城市 a 与 b 直接相连,b 与 c 直接相连,那么 a 与 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。返回矩阵中 省份 的数量。
思路:挺有意思 要想不重不漏的话怎么办呢,一个朴素的办法是取出来就置-1;
必然是深度/广度优先搜索,但怎么写呢
法1:深度优先
这里只用了一个参数,应该是先写的两个再优化成了一个×
这是个对称矩阵,不用两个参数好像;
就反复查[指定的i][j]就行了,写法:先判断合法并path记录,再递归
class Solution:
# 看这个数组的类型声明 为List[List[int]]
def findCircleNum(self, isConnected: List[List[int]]) -> int:
def dfs(i: int):
for j in range(cities): # 依次查[指定的i][j]
if isConnected[i][j] == 1 and j not in visited:
visited.add(j) # 写法:先判断合法并path记录,再递归
dfs(j) #
cities = len(isConnected)
visited = set() # 注意看.add(j)就行了 因为该列必定是同省的
provinces = 0
for i in range(cities):
if i not in visited:
dfs(i)
provinces += 1
return provinces
法2:广度优先
★当然是用队列+forfor实现,还得熟练
★还是用的deque
set()
这题用广度优先好易读啊,写法:先path记录,然后for,没有递归
class Solution:
def findCircleNum(self, isConnected: List[List[int]]) -> int:
cities = len(isConnected)
visited = set()
provinces = 0
for i in range(cities):
if i not in visited:
Q = collections.deque([i])
while Q: # while Q
j = Q.popleft() # 因为pop()了 所以要将相关元素 即相关行入列
visited.add(j) # 写法:这里是先path记录,然后for,没有递归
for k in range(cities):
if isConnected[j][k] == 1 and k not in visited:
Q.append(k)
provinces += 1
return provinces
法3:并查集 把不同连通分量合并
实现是通过一维list存信息,找parent,改parent,有文章说是哈希表
感觉有点难写
class Solution:
def findCircleNum(self, isConnected: List[List[int]]) -> int:
def find(index: int) -> int:
if parent[index] != index:
parent[index] = find(parent[index])
return parent[index]
def union(index1: int, index2: int): # union操作:二合一
parent[find(index1)] = find(index2) # 递归找到某城市parent,修改它为另一城市parent
cities = len(isConnected)
parent = list(range(cities) # 所有城市的parent先设为本身
for i in range(cities):
for j in range(i + 1, cities): # 下三角
if isConnected[i][j] == 1:
union(i, j)
provinces = sum(parent[i] == i for i in range(cities))
return provinces
LeetCode200 岛屿数量
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿:总是被水包围,且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
· 可假设网格四边被水包围
思路:老早见过这道,当时好像想用dfs,应该也是可以的,就是麻烦一点
那就是并查,岛屿扩充不断二合一吧,结构感觉也差不多,不同的是不是只有find和union呢×
不是,除了找parent、改parent,还有判定什么时候改parent;上一题是因为能容易地判定parent;这里find和union反而完全没变
法1:
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
def find(x) :
if p[x] != x :
p[x] = find(p[x])
return p[x]
def union(x, y) : # 改parent
px, py = find(x), find(y)
if px == py :
return
p[px] = py
leny = len(grid) # 取矩阵行列长度
lenx = len(grid[0])
p = list(range(lenx * leny)) # 设parent也和上题一样
for i, row in enumerate(grid) :
for j, v in enumerate(row) :
if v == '1' :
for a, b in [[0, -1], [-1, 0]] : # 相当于两个for 或四个if 美观一点
x, y = i + a, j + b
if 0 <= x < leny and 0 <= y < lenx and grid[x][y] == '1':
union(i * lenx + j, x * lenx + y)
ans = 0
havesee = dict()
for i in range(lenx * leny) :
if find(i) not in havesee and grid[i // lenx][i % lenx] != '0':
havesee[p[i]] = 1 # 连最后这个for都没怎么变
ans += 1
return ans
法2:dfs
★for x,y in [[0,1],[1,0],[0,-1],[-1,0]]:
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
row = len(grid)
col = len(grid[0])
ans = 0
def DFS(i,j):
grid[i][j] = "0" # path记录 遍历过就置0
for x,y in [[0,1],[1,0],[0,-1],[-1,0]]: # 这样写不错
tmp_i = i + x
tmp_j = j + y
if 0 <= tmp_i < row and 0 <= tmp_j < col and grid[tmp_i][tmp_j] == "1":
DFS(tmp_i,tmp_j) # 结构大概一样的 path记录完再递归
for i in range(row):
for j in range(col):
if grid[i][j] == "1": # path初始置1
DFS(i,j)
ans += 1
return ans
LeetCode684
将树看作一个连通且无环 的无向图,给定一个 往一棵节点值 1~n 的树中添一条边后 的图。请找出一条可以删去的边,删后可使剩余部分是一个有着 n 节点的树。若多个答案,返回数组 edges 中最后出现的那个。
· 添加的边的两个顶点在 1 到 n 中间,且这条边不为已存在的边。
· 图的信息记录于长度为 n 的二维(×✓ [[]]确实是二维)数组 edges ,edges[i] = [ai, bi] 表示 ai 和 bi 之间有边。
示例:
edges = [[1,2], [2,3], [3,4], [1,4], [1,5]]
>>> [1,4]
思路:不是二叉树;要返回具体答案的;
几个概念
1.集合树:以代表节点为父节点的所有节点构成的多叉树,好像就是指多叉树
2.节点的代表节点:可理解为节点的父节点
3.集合的代表节点:可理解为根节点
vector[2] = 2,意味着2这个节点所在集合的代表节点就是2
该解释有点复杂,不继续看了
法1:
- 初始时,每个节点都属于不同的连通分量
- 遍历每一条 边,判断这条边连接的两个顶点是否属于相同的连通分量。
2.a. 如果两个顶点属于不同的连通分量,则说明在遍历当前边之前,这两个顶点间不连通,因此该边不会导致环出现,即可合并这两个 顶点的连通分量。
2.b. 如果两个顶点属于相同的连通分量,则说明在遍历当前边之前,这两个顶点之间已经连通,因此当前的边导致环出现,为附加的边,即答案,将其返回。
find和union还是一样的
★再次,for node1, node2 in edges:
class Solution:
def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
n = len(edges)
parent = list(range(n + 1))
def find(index: int) -> int:
if parent[index] != index:
parent[index] = find(parent[index])
return parent[index]
def union(index1: int, index2: int):
parent[find(index1)] = find(index2)
for node1, node2 in edges:
if find(node1) != find(node2):
union(node1, node2)
else:
return [node1, node2] # 即为答案
return [] # 说明未找到
Leetcode209
找出数组中满足总和≥ target 的长度最小的 连续子数组,并返回其长度。若不存在返回0.
思路:已知考滑动数组,可能需要个minLen保存当前最小长度
由数学,在找到首个满足条件的切片后,这些最小的连续子数组可以由滑动窗口得出,先滑出,再滑入至恰好≥
★enumerate
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
n = len(nums)
ans = n + 1 # 也可以写 inf
s = left = 0
for right, x in enumerate(nums): # 枚举子数组右端点 一个标位置 一个
s += x
while s - nums[left] >= target: # 若剪完满足target,左端点便右移;这里有点绕,最后得到的是固定右端点时满足target的最短切片,然后便执行下面的if
s -= nums[left]
left += 1 #
if s >= target:
ans = min(ans, right-left+1)
return ans if ans <= n else 0
里面也可以改成 且看起来更易读:
for right, x in enumerate(nums):
s += x
while s >= target:
ans = min(ans, right - left + 1)
s -= nums[left]
left += 1
两个for吗×
for
fruits = ["apple", "banana", "orange"]
prices = [1.0, 2.0, 3.0]
fruits_dict = {}
若希望将这两个列表中的数据填充到字典中,就可以用
for fruit, price in zip(fruits, prices):
fruits_dict[fruit] = price
复习:zip()函数返回一个迭代器,该迭代器是从每个可迭代对象中聚合元素
这种for方式等同于
for i in range(len(l):
L1[i] = L2[i]
enumerate
可作用于任何可迭代对象,生成一个带索引的 键-值对格式的可迭代对象
语法: enumerate(iterable, start)
示例:
List = ["hello", "tutorialspoint", "python", "codes"]
enumerate_obj = enumerate(List)
print(enumerate_obj)
print(list(enumerate_obj))
>>> <enumerate object at 0x7f4e56b553c0>
>>> [(0, 'hello'), (1, 'tutorialspoint'), (2, 'python'), (3, 'codes')]
本题先到这里,耗了很多时间
Leetcode3
给定一个字符串s,请找出其中不含有重复字符的 最长子串的长度
思路:这个前面做过,用切片比,然后maxLen只增不减,for左端点、for右端点(左+maxLen)
# Step 4 - 情况1
# 如果题目的窗口长度固定:用一个if语句判断一下当前窗口长度是否达到了限定长度
# 如果达到了,左指针移一个单位,保证右指针右移时,窗口长度不变,
# 左指针移动之前, 更新Step 1的维护变量
# Step 4 - 情况2
# 如果窗口长度可变: 一般涉及到窗口是否合法的问题
# 如果当前窗口不合法, 用一个while去不断移动左指针
# 在左指针移动之前,更新Step 1的维护变量
×题不一样,那题是找最长重复子串,这题是最长不重复子串
不,这题就是慢慢地移动左指针,不断移动右指针去记录频率并判断合法性,不行左指针就下一个
★又用到字典.get(,) 参数2是键找不到的时候吗 对的,字典.get(key, default)
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
# Step 1: 维护变量max_len, 涉及去重故需要哈希表
max_len, hashmap = 0, {}
# Step 2: start慢慢移
start = 0
for end in range(len(s)):
# Step 3: 更新维护的 hashmap, max_len
# 把窗口末端元素加入哈希表,使其频率加1,并且更新最大长度
hashmap[s[end]] = hashmap.get(s[end], 0) + 1
if end - start + 1 == len(hashmap): # 这个if的顺序可和while交换
max_len = max(max_len, end - start + 1)
# Step 4: 非法就不断移动左指针, 剔除直到窗口再次合法
# ★while 窗口长度>哈希表长度,说明存在重复元素,不合法,左指针开始右移
while end - start + 1 > len(hashmap):
head = s[start]
hashmap[head] -= 1
if hashmap[head] == 0:
del hashmap[head]
start += 1
return max_len
Leetcode1004
给定一个二进制数组 nums 和一个整数 k,如果可以翻转最多 k 个 0 ,返回连续 1 的最大个数
思路:若已知用滑动窗口,那就能想到这是个可变窗口,合法窗口 即 可含k个0,那很简单了
Leetcode1208 尽可能使字符串相等
规定字符a变b 需要ASCII码差值绝对值的开销,用于变更字符串的最大预算是 maxCost.
请返回将 s的子串 转化为 t中对应的子串时,可以转化的最大长度。
示例:
s = "abcd", t = "cdef", maxCost = 3
>>> 1
说明:s中任一字符 变成t中对应字符 的开销都是 2。因此最大长度为 1。
思路:那就是要确定四个点呗,即是确定两个点,两个端点变动时,结果的变化完全来自于边界的变化
LeetCode724
定义 数组的中心下标:其左侧所有元素的和 = 右侧所有元素的和。
· 若位于最左侧,其左侧数之和视为 0
请计算数组的中心下标,若有多个,返回最左侧的;若无返回 -1.
思路:感觉是简化版的窗口类型
★sum(数组)函数