lc刷题记录

Leetcode 刷题摘录

0. 常用操作模板

(1)I/O模板

Python 基本I/O
import sys
n = int(input()) # 读取单个整数n,input以空白符(如空格)结尾
for line in sys.stdin: # 读取一整行
arr = line.split() # 如读取一行整数数组
# 或者
while True:
line = sys.stdin.readline().strip()
values = list(map(int, line.split())) # 读取一行整数作为int列表
if line == '':
break
# or
while True:
try:
nums = list(map(int, input().split(' ')))
print(sum(nums))
except:
break

(2)工具函数

常用 python api
## 基础
# 1. python变量名存储的均为地址值。
# 2. nonlocal声明的变量不是局部变量也不是全局变量,而必须是嵌套函数外部(上一级函数内)已有的变量,允许对其修改。但嵌套函数内可以直接访问并修改上层函数的复杂类型变量(如列表)。
# 3. global关键字用来在函数/嵌套函数或其他局部作用域中标识该变量是全局变量,如果不修改全局变量也可以不使用global。
# 4. 当你在函数中使用未限定的变量名时,Python将根据LEGB规则查找作用域并在第一次找到该变量名的地方停下来:首先是局部作用域(L),其次是外一层的def或lambda的局部作用域(E),之后是全局作用域(G),最后是内置作用域(B)。如果在这一过程中没有被查找到,就会报错。
## 集合
# set(iterable) or {...} 有效去除重复元素,支持集合运算:.add(), .remove(), & | -,
# frozenset({...}) 不可变集合,仅允许查询,而不能修改
## map reduce
from functools import reduce
# map返回迭代器,对list_of_inputs每个元素应用函数function
map(function, list_of_inputs)
# 首先对前两个元素应用函数function,得到结果再与下一个元素应用函数,直到最后一个元素,输出最终结果
reduce(function, iterable[, initializer])
## collections模块常用类型有:计数器(Counter),默认字典(defaultdict),双向队列(deque),有序字典(OrderedDict),可命名元组(namedtuple)
## 计数器 类,dict字典的子类,一种能对重复元素计数的集合,且允许 计数值count 为 0 或者负值。
from collections import Counter
cnt_e = Counter(tuple(sorted(e)) for e in edges) # 统计相同边出现的次数,返回Counter字典对象
# cnt_e['key1'] # 表示访问元素key1的计数,也可作为左值对其进行更新,若key1不存在则默认计数为0
# cnt_e.elements() # 返回元素的迭代器,将具有正计数值的元素按其次数输出,0或负值的元素不输出。
# cnt_e.most_common(n) # 返回一个出现次数从大到小的前 n 个元素的列表。
# cnt_e.subtract(iterable_or_mapping) # 可以将两个 Counter 对象中的元素对应的计数相减,可能出现0或负数。
# c1 + c2 # 两个计数器相加
# c1 - c2 # 相减,区别是仅保留正计数值的元素
# c1 & c2 # 两个字典求交,相同元素计数值取最小
# c1 | c2 # 两个字典求并,相同元素计数值取最大
## 默认字典 defaultdict 可作为计数器使用
from collections import defaultdict
d = defaultdict(int) # 默认int值为0,还可以是list/set/str等
##################################################
## 数据结构 实现
# 线性表 = 顺序表(List) + 链表
# 栈 stack: 常用List实现,入栈.append(),出栈.pop(),栈顶s[-1]
# 堆 :
# 队列:多线程适应
from queue import Queue
q = Queue() # put(), get(), empty()
# 双向队列 deque:以双向链接列表的形式实现,操作类似List
from collections import deque
dq = deque() # append(), pop(), appendleft(), popleft()
# 有序列表,始终保持有序,很多操作复杂度O(lgN)
from sortedcontainers import SortedList
prefix_sum_list = SortedList([...], key=None) # add(x), remove(x)/discard(x), pop(), count(x), bisect_left(x) etc.

(3)排序函数

Python 中内置 2 个排序函数

一个是 list 的 sort() 方法,另一个是全局的 sorted() 方法:

  1. list.sort(key=None, reverse=False),将list自身进行排序,返回None,默认升序;reverse=True降序、False升序。
  2. sorted(iterable [,cmp=None [,key=None [,reverse=True]]]),返回排好序的新列表,不改变对象本身,默认升序;reverse=True降序、False升序。对所有可迭代的对象均有效。
    • (python2限定)cmp指定一个定制的比较函数cmp(x,y),这个函数接收两个参数(x, y, iterable的元素)。如果 x < y 返回 -1, 如果 x == y 返回 0, 如果 x > y 返回 1。更具体些,-1在排序中代表不改变x,y位置,1代表变成y,x位置。一个简单实现:return x-y
    • python3不支持cmp参数,而key函数并不直接比较任意两个原始元素,它把原始元素转换成一个新的可比较的关键字对象,代替原始元素去参与比较。如:key=lambda x: x.element
    • 多字段排序时,如排序元素为tuple,默认按第一、第二元素依次排序。对于复杂数据结构,可以采用函数functools.cmp_to_key(cmp)可以将自定义的比较函数cmp转化为关键字函数key,与接受key函数的api一同使用(如 sorted(), min(), max(), heapq.nlargest(), itertools.groupby())。

1. 数学 问题

1.1 位运算

  1. 从集合论到位运算:通过二进制表示集合,实现状态压缩。 灵茶山艾府: 位运算技巧分类总结
## python中整数以补码存储,但无32/64等位数限制:0为24字节、1-2^30-1为28字节,以后每2^30加4个字节
# ①正数的原码、反码、补码都是一样的,负数是以补码的二进制表示存储的,所以正/负数都可以视作以补码存储。
# ②原码求反码:符号位不变,剩余位取反;原码求补码:符号位不变,剩余位取反再加一
# ③补码求原码: 取反,+1 / -1,取反
# ④按位取反操作~ 计算结果:取反后为正数,直接视作原码求整数值;取反后为负数,视作补码先求原码再求整数值。
# ⑤符号位,1表示负数,0表示正数
## 基本位运算操作
a, b = 6, -5
a ^ b == c # a ^ c == b
(a ^ b) ^ z == a ^ (b ^ c)
a ^ b ^ b == a ^ 0 == a # a ^ 0xffffffff == 取低32位按位取反,不等于求反码
~a == -(a+1) # 对a的二进制表示(补码)按位取反(包括符号位),值为-(a+1)
bin(a) # 返回正数a二进制字符串表示:'0b110',bin(-5) -> '-0b101'
r = b & 0xffffffff # 返回负数b的低32位补码表示,还原:~(r ^ 0xffffffff) == b
int(bin(r), 2) # 转化为python整数,正负仅取决于'0b'前是否有'-'
a.bit_length() # 返回二进制位数 ('0b'后面的位数) -> 3
a.bit_count() # 返回二进制数中`1`的个数
lowbit = a & -a # 返回最低位的`1`对应的二进制数,如 8 = 1000 = 2^3 表示出现在位置i=3处(最低0)
【1-1】枚举 N 元集合的 k 元子集 Gosper’s Hack是一种生成 n元集合所有 k元子集的算法,它巧妙地利用了位运算
# 这个算法的思想是,假如把所有符合要求的二进制数排序后得到的数组A,希望的是,通过任意给定的 A_i,都能递推计算出 A_{i+1} 。
# 实现:只需要把当前二进制数的最后一个 `01` 变成 `10` ,然后把它右边的 1 全部集中到最右边即可。
def GospersHack(k:int, n:int):
# Require: 1 <= k <= n
cur = (1 << k) - 1
limit = 1 << n
while cur < limit:
# do something
lb = cur & -cur # lowbit
r = cur + lb # 01 -> 10
cur = (((r ^ cur) >> 2) // lb) | r # 把 1 移到最后
# 或:cur = ((r ^ cur) >> __builtin_ctz(lb) + 2) | r;

1.2 常用数学操作

AcWing常用代码模板4——数学知识

【1-2-1】求两个数的 最大公因数 GCD
@cache
def gcd(x:int, y:int) -> int:
# 辗转相除法(递归)
return x if y==0 else gcd(y, x%y)
# 二进制优化
# if y==0: return x
# elif x==0: return y
# cnt = 0
# while (x&1 == 0) and (y&1 == 0):
# x >>= 1
# y >>= 1
# cnt += 1
# while True:
# if x < y: # swap x and y
# x^=y
# y^=x
# x^=y
# x -= y
# if x==0: return y<<cnt
# while (x&1 == 0):
# x >>= 1
def gcd_multi(nums): # 求一组正整数的 最大公因数
res = nums[0]
for n in nums:
while n:
res, n = n, res % n
return res
【1-2-2】求两个数的 最小公倍数 LCM
@cache
def lcm(a:int, b:int) -> int:
# return a * b // gcd(a, b)
x, y = a, b
while b:
t, a = a, b
b = t % b
return x/a*y
【1-3】GrayCode 格雷码 模板
# 求小于n位的所有格雷码
def grayCode(n):
res = [0]
i = 0
while i < n: # 从 2 的 0 次方开始,
next_base = 1 << i
res_right = [x + next_base for x in res] # 加上高位
res.extend(list(reversed(res_right)))
i += 1
for x in res:
print(bin(x))
【1-4】快速幂 模板
# 求x的n次幂
def fastExpMod(x:float, n:int):
res, p = 1.0, abs(n)
while p:
if (p & 1):
res *= x
p >>= 1
x *= x
return res if n >= 0 else 1. / res
# 牛顿法求平方根
def newtomSqrt(self, x):
r = x + 1 # avoid dividing 0
while r*r > x:
r = int((r+x/r)/2) # newton's method
return r
【1-5】质数相关
  1. 质数筛选
# Eratosthenes筛法:输出n以内的素数个数
def countPrimes(n):
if n < 2:
return 0
isPrime = [True] * (n + 1)
isPrime[0] = isPrime[1] = False
i = 2
while i * i <= n:
if isPrime[i]:
for j in range(i*i, n+1, i):
isPrime[j] = False
i += 1
return sum(isPrime)
  1. 分解质因数
from math import isqrt
from collections import Counter
def divide(x: int):
primes = Counter() # 计数器,统计每个质因数,及其次数
for i in range(2, isqrt(x)+1):
while x % i == 0:
x /= i
primes[i] += 1
if x > 1:
primes[x] = 1
return primes
【1-6】大数运算 模拟
# 大数加法
def addStrings(self, num1: str, num2: str) -> str:
res = ""
i, j, carry = len(num1) - 1, len(num2) - 1, 0
while i >= 0 or j >= 0:
# 高位补零
n1 = int(num1[i]) if i >= 0 else 0
n2 = int(num2[j]) if j >= 0 else 0
tmp = n1 + n2 + carry
carry = tmp // 10
res = str(tmp % 10) + res
i, j = i - 1, j - 1
return "1" + res if carry else res
# 大数乘法
def multiply(num1, num2):
product = [0] * (len(num1) + len(num2)) # placeholder for multiplication ndigit by mdigit result in n+m digits
position = len(product) - 1 # position within the placeholder
for n1 in num1[::-1]:
tempPos = position
for n2 in num2[::-1]:
product[tempPos] += int(n1) * int(n2) # ading the results of single multiplication
product[tempPos - 1] += product[tempPos] // 10 # bring out carry number to the left array
product[tempPos] %= 10 # remove the carry out from the current array
tempPos -= 1 # first shifting the multplication to the end of the first integer
position -= 1 # then once first integer is exhausted shifting the second integer and starting
# once the second integer is exhausted we want to make sure we are not zero padding
pointer = 0 # pointer moves through the digit array and locate where the zero padding finishes
while pointer < len(product) - 1 and product[
pointer] == 0: # if we have zero before the numbers shift the pointer to the right
pointer += 1
return ''.join(map(str, product[pointer:])) # only report the digits to the right side of the pointer
【1-7】组合数取模

讲解:组合数取模

# 情况1:组合数规模小、模数规模大。如:a的规模为1e5, p的规模为1e9
def pow(a:int, b:int, p:int): # 快速幂 取模
r, x = 1, a
while b:
if b&1:
r = r * a % p
a = a * a % p
b >>= 1
return r
def comb(a:int, b:int, p:int): # 组合数 取模 (a>=b)
b = a-b if a-b < b else b
r = 1
for i in range(1, b+1):
x, y = (a-b+i), i%p
# y%p 的逆元为 y' = y^(p-2)%p , y的逆元的作用类似于 1/y
r = r * (x * pow(y, p-2, p) % p) % p # 逆元法进行除法求模:x/y%p -> x * y' % p
return r
## 若先预处理计算了N以内的阶乘(模p):
factorial = [1] * (N+1)
for i in range(2, N+1):
factorial[i] = factorial[i-1] * i % p
def comb(a:int, b:int, p:int): # 组合数 取模 (a>=b)
b = a-b if a-b < b else b
return factorial[a] * pow(factorial[b], p-2, p) % p * pow(factorial[a-b], p-2, p) % p
# 情况2:组合数规模大、模数规模小
def lucas(n:int, m:int, p:int): # 卢卡斯定理
return comb(n%p, m%p, p) * lucas(n//p, m//p, p) % p
【1-8】异或之和 模板
# 对数组nums中任意两数的异或结果求和
## 1. 传统做法,2层循环,O(n^2)
## 2. O(n)算法:统计n个数每个二进制位上1的个数,进而分别求(0/1)对的个数,乘以各个二进制位的基数再求和
bits = Counter()
for x in nums:
for i in range(32): # 假设 x 为32位整数
bits[i] += 1 if x&1 else 0
x >>= 1
S = 0
for i in range(32):
S += ((bits[i] * (N-bits[i])) << i) % MOD
【1-x】其他数学操作
# n*n的矩阵转置
for i in range(n):
for j in range(i, n):
matrix[j][i], matrix[i][j] = matrix[i][j], matrix[j][i]
# 卡特兰数:给定n个0和n个1,它们按照某种顺序排成长度为2n的序列,满足任意前缀中0的个数都不少于1的个数的序列的数量为:
Cat[n] = C(2*n, n) / (n + 1)
#

2. 线性表

2.1 数组

  • 循环数组:数组首尾相接,最后一个元素的后继为第一个元素。一般通过 % 求模运算 来模拟 next_idx = (idx + 1) % N。多数情况也采用将数组复制一遍再连接到末尾来模拟环形数组。
【2-0】 数组 基本算法
  1. 前缀和
# 一维前缀和: pre[i] 计算前i个数的和
## input: nums shape=[N]
pre = [0]*(N+1)
for i in range(N):
pre[i+1] = pre[i] + nums[i]
# 二维前缀和: pre[i][j] 计算前i行j列的矩阵的和
## input: nums shape=[N, M]
pre = [[0] * (M+1) for _ in range(N+1)]
for i in range(N+1):
for j in range(M+1):
pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + nums[i][j]
  1. 差分数组

差分可以看成前缀和的逆运算,我们始终保持:原数组a 是 差分数组d 的前缀和数组。

d[i]=a[0](i==0)   =a[i]a[i1](i>=1)

有如下性质:

  1. 从左到右累加 差分数组 d 中的元素,可以得到 原数组 a 。
  2. 如下两个操作是等价的:
    • 区间操作:将数组a的子区间 [i, j] 的元素都加上 x 。
    • 单点操作:将数组 d[i] 增加 x,d[j+1] 减少 x(对于j+1==N则无需减操作)。

利用上面的性质:可以O(1)复杂度完成 数组 a 上闭区间[l, r]的加减操作,然后通过 性质1 复原出数组a。

# 一维差分:d[0]=a[0], d[i]=a[i] - a[i-1] (i>0)
## input: a shape[N]
d = [0] * N
d[0] = a[0]
for i in range(1, N):
d[i] = a[i] - a[i-1]
# 为a[l, r] 区间每个值加c
d[l] += c # a[l:]全部加c
d[r+1] -= c # a[r+1:]全部减c,使得加c的区间为[l,r]

二维差分https://zhuanlan.zhihu.com/p/338258918

2.2 链表

2.3 栈

栈(stack) 是一种简单的线性数据结构,遵循后进先出的逻辑顺序,符合某些问题的特点,如 函数调用栈。

单调栈实际上就是栈,只是利用了一些巧妙的逻辑,使得每次新元素入栈后,栈内的元素都保持有序(单调递增或单调递减)。

2.4 堆

【2-1】单调栈 模板
#
def func(nums):
st = []
res = [0]*len(nums)
for i, x in enumerate(nums):
while st and nums[st[-1]] < x: # 单调递减栈
idx = st.pop()
res[idx] = i-idx
st.append(i)
return res
【2-2】并查集 模板
# 并查集
parent = list(range(N))
def find(x):
if x != parent[x]: # 路径完全压缩
parent[x] = find(parent[x])
return parent[x]
def union(x, y):
root1 = find(x)
root2 = find(y)
parent[root2] = root1
【2-3】 模板
#
【2-4】 模板
#
【2-5】 模板
#

3. 动态规划

3.1

  1. 最大连续子数组和

  2. 使字符串变为非递减串的最少修改次数参考

    • dp[i][j]: 前 i 个字符串为非递减 且 最后一个字符为 j 的最少修改次数;min(dp[i-1][k])表示前i-1个字符形成非递减且最大字符小于等于k的最少修改次数。
    • s[i] == j时即第i个字符恰好是j时。不需要修改s[i]dp[i][j] = min(dp[i-1][k]) 其中 k<=j (保证非递减);
    • s[i] != j时即第i个字符不是j时,需要将s[i]改成字符j再加上 min(dp[i-1][k])dp[i][j] = min(dp[i-1][k]) + 1 其中 k<=j (保证非递减)
【3-1】数位DP模板
# 数位dp
## i:由0开始,填充当前位置,统计个数
## pre:当前位置之前已填充的数字
## isLimit:表示前面填充的数字是否完全对应n的前几位
## isNum:之前是否填充或跳过,True表示前面已填充,当前可从0开始填充;False表示前面都跳过,当前可选(继续跳过 or 从1开始填充)
@cache
def f(i:int, pre:int, isLimit:bool, isNum:bool) -> int:
if i == len(s): # 填充完毕
return int(isNum)
res = 0
if not isNum: # 当前 选择跳过 的次数贡献
res += f(i+1, 0, False, False)
up = int(s[i]) if isLimit else 9 # 可填充的数字上限
for d in range(1-int(isNum), up+1):
cnt = f(i+1, pre*10+d, isLimit and d==up, True)
res += cnt
return res
# 模板2:找到 [num1,num2] 中数位和在[min_sum,max_sum]之间的整数
n = len(num2)
num1 = num1.zfill(n) # 补前导零,和 num2 对齐
@cache
def dfs(i: int, s: int, limit_low: bool, limit_high: bool) -> int:
if s > max_sum: # 非法
return 0
if i == n:
return s >= min_sum
lo = int(num1[i]) if limit_low else 0
hi = int(num2[i]) if limit_high else 9
res = 0
for d in range(lo, hi + 1): # 枚举当前数位填 d
res += dfs(i + 1, s + d, limit_low and d == lo, limit_high and d == hi)
return res
【3-2】最长xx子序列 模板 求数组的最长上升子序列 LIS (Longest increasing subsequence)
# 贪心+二分法 O(NlgN)
def lis_1(nums: List):
dp = [] # dp[i]: 表示长度为 i+1 的最长上升子序列的末尾元素的最小值
for i in range(len(nums)):
idx = bisect_left(dp, nums[i])
if idx == len(dp):
dp.append(nums[i]) # 序列变长
else:
dp[idx] = nums[i] # 让序列上升得尽可能慢
return len(dp)
# 动态规划方法 O(N^2)
def lis_2(nums: List):
dp = [0] * len(nums) # dp[i]: 表示以第 i 个数字结尾的最长上升子序列的长度
for i in range(len(nums)):
dp[i] = 1
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j]+1)
return max(dp)
# ---------------------------------------
# 求两字符串最长公共子序列 LCS
def longestCommonSubsequence(text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
# dp[i][j]: 表示text1的前i和text2的前j长度的子串的最大LCS长度
dp = [[0] * (n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[-1][-1]
【3-3】区间DP 模板
# 区间DP
def calc() -> int:
n = len()
dp = [[0] * n for _ in range(n+1)]
for L in range(1, n+1): # 区间长度,自下而上更新
for i in range(1, n+1): # 枚举起点
j = i + L - 1 # 区间终点
for k in range(i+1, j) # 枚举分割点,开始转移
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j] + w[i][j])
return dp[1][n]

4. 回溯法

回溯算法: ,与 DFS 算法非常类似,本质上就是一种暴力穷举算法。

1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择

5. 贪心法


6. 分治法

6.1 二分法

(1)二分查找

二分查找也称折半查找,就是每次查找去掉不符合条件的一半区间,直到找到答案(整数二分)或者和答案十分接近(浮点二分)。

python内置实现二分 (bisection) 算法模块

bisect,能够 保持序列sequence顺序不变 的情况下对其进行 二分查找和插入,须确保待操作对象是 有序序列,即元素已按 从大到小 / 从小到大 的顺序排列。模块包括函数如下:

  • bisect_left() 查找 目标元素左侧插入点。存在:第一个相等元素的索引,不存在:第一个更大元素的索引。
  • bisect_right() 查找 目标元素右侧插入点。存在:最后一个相等元素的索引+1,不存在:第一个更大元素的索引。
  • bisect() 同 bisect_right()
  • insort_left() 查找目标元素左侧插入点,并保序地 插入 元素
  • insort_right() 查找目标元素右侧插入点,并保序地 插入 元素
  • insort() 同 insort_right()

举例:bisect.bisect_left(a, x, [lo=0, hi=len(a), key=None])

  • 在序列 a 中二分查找适合元素 x 插入的位置,保证 a 仍为 有序序列。
    • 若序列 a 中存在与 x 相同的元素,则返回与 x 相同的第一个 (最左侧) 元素的位置索引,使得 x 若插入后能位于其 左侧;
    • 若序列 a 中不存在与 x 相同的元素,则返回与 x 右侧距离最近的元素位置索引,使得 x 若插入后能位于其 左侧。
  • lo(包含) 和 hi(不包含) 为可选参数,分别定义查找范围/返回索引的 上限和下限,缺省时默认对 整个 序列查找。
  • 3.10版本后加入自定义排序参数key支持。
  • bisect.bisectbisect.bisect_right返回大于x的第一个下标(相当于C++中的upper_bound),bisect.bisect_left返回大于等于x的第一个下标(相当于C++中的lower_bound)。

(2)二分答案

  • 待求解的最优答案在一个区间内。(一般情况下区间范围很大,暴力超时)
  • 最优答案满足的条件比较复杂,难以直接求解,但是容易判断区间某个答案是否可行。
  • 最优答案的条件对该区间具有单调性:区间中的值越大或越小,条件中某个量对应递增或减少。

问题举例 如:

  1. 最短距离最大化问题:条件为保证任意区间距离要比最短距离mid大或相等(这样,mid才是最短距离)即:区间的距离>=mid。目标为求最大值。
  2. 最长距离最小化问题:条件为保证任意区间距离要比最大距离mid小或相等(这样,mid才是最大距离)即:区间的距离<=mid。目标为求最小值。
【6-1】二分答案 模板

注意:可行域为 [left, right] 闭区间,最终答案为 leftright

# 二分答案-1:答案具有单调性,返回满足条件的最 小 答案
def binary_search1(left, right):
while left < right: # left==right 时结束
mid = (left + right) >> 1
if check(mid):
right = mid # mid可行,right为当前最小可行答案,往下找更小值
else:
left = mid + 1 # mid不可行(太小),left疑似可行
return left
# 二分答案-2:答案具有单调性,返回满足条件的最 大 答案
def binary_search2(left, right):
while left < right: # left==right 时结束
mid = (left + right + 1) >> 1
if check(mid):
left = mid # mid可行,left为当前最大可行答案,往上找更大值
else:
right = mid - 1 # mid不可行(太大),right疑似可行
return left
# 浮点二分:答案具有单调性,返回满足条件的最 大 答案
def binary_search2(left, right):
while right - left > 1e-6: # 满足精度要求 时结束
mid = (left + right) / 2
if check(mid):
left = mid
else:
right = mid
return left
【6-2】
#

7. 树 相关

二叉树遍历框架(一般指递归形式)

def traverse(root):
if root is None:
return
# 前序位置
traverse(root.left)
# 中序位置
traverse(root.right)
# 后序位置
# 所谓前序位置,就是刚进入一个节点(元素)的时候,后序位置就是即将离开一个节点(元素)的时候,而中序位置一般指离开左子节点、即将进入右子节点的时候。代码写在不同位置,执行的时机和效果也不同。

一些规律

  • 中序位置主要用在 二叉搜索树BST 场景中,可以把 BST 的中序遍历认为是遍历有序数组。
  • 前序位置的执行是自顶向下的:只能从函数参数中获取父节点传递来的数据,
  • 后序位置的执行是自底向上的:不仅可以获取参数数据,还可以获取到子树通过函数返回值传递回来的数据。
【7-4】字典树 Trie 实现模板
# 字典树 Trie
import collections
class TrieNode():
def __init__(self):
self.children = collections.defaultdict(TrieNode)
self.isEnd = False
class Trie():
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for w in word:
node = node.children[w]
node.isEnd = True
def search(self, word):
node = self.root
for w in word:
if w not in node.children:
return False
node = node.children[w]
return node.isEnd
def startsWith(self, word):
current = self.root
for w in word:
current = current.children.get(w)
if current is None:
return False
return True
【7-5】线段树 (查找/更新)实现模板
# 线段树 SegmentTree
class SegmentTree:
def __init__(self, nums):
self.seg = collections.defaultdict(int)
self.lazy = collections.defaultdict(int)
self.s = 0
self.e = len(nums)-1
def build(idx, s, e, cur_s, cur_e): # 不初始化则不需要
if cur_s > e or cur_e < s:
return
if cur_s == cur_e:
self.seg[idx] = nums[cur_e]
else:
mid = (cur_s + cur_e)//2
build(2*idx + 1, s, e, cur_s, mid)
build(2*idx + 2, s, e, mid + 1, cur_e)
self.seg[idx] = max(self.seg[2*idx + 1], self.seg[2*idx + 2])
build(0, self.s, self.e, self.s, self.e)
def updateLazy(self, idx, val, s, e):
self.seg[idx] = val
if s != e: # 把 lazy 的往下放放
self.lazy[2*idx + 1] = val
self.lazy[2*idx + 2] = val
self.lazy[idx] = 0
def updateRange(self, s, e, val):
def __updateRange(idx, s, e, val, cur_s, cur_e):
if cur_s > e or cur_e < s:
return
if self.lazy[idx]: # 把 lazy 的往下放放
self.updateLazy(idx, self.lazy[idx], cur_s, cur_e)
if s <= cur_s and cur_e <= e:
self.updateLazy(idx, val, cur_s, cur_e)
else:
mid = (cur_s + cur_e)//2
__updateRange(2*idx + 1, s, e, val, cur_s, mid)
__updateRange(2*idx + 2, s, e, val, mid + 1, cur_e)
self.seg[idx] = max(self.seg[2*idx + 1], self.seg[2*idx + 2])
return __updateRange(0, s, e, val, self.s, self.e)
def queryRange(self, s, e):
def __queryRange(idx, s, e, cur_s, cur_e):
if cur_s > e or cur_e < s:
return 0
if self.lazy[idx]: # 把 lazy 的往下放放
self.updateLazy(idx, self.lazy[idx], cur_s, cur_e)
if s <= cur_s and cur_e <= e:
return self.seg[idx]
else:
mid = (cur_s + cur_e)//2
left = __queryRange(2*idx + 1, s, e, cur_s, mid)
right = __queryRange(2*idx + 2, s, e, mid + 1, cur_e)
return max(left, right)
return __queryRange(0, s, e, self.s, self.e)
【7-6】树状数组 模板
# 树状数组 一种用于维护前缀信息的数据结构
class FenwickTree:
def __init__(self, n):
self.sums_ = [0] * (n + 1)
def update(self, i, delta):
while i < len(self.sums_):
self.sums_[i] += delta
i += self.lowbit(i)
def query(self, i):
sum_ = 0
while i > 0:
sum_ += self.sums_[i]
i -= self.lowbit(i)
return sum_
def lowbit(self, x):
return x & (-x)
【7-7】 模板
#

8. 图 相关

【8-0】图 基本算法
# 构图
## 输入:N 节点数目(编号0 ~ N-1),edge_list 边表(edge_list[i]=[u,v]表示节点u和节点v之间存在一条 边 [有向u->v / 无向u-v])
graph = defaultdict(list) # 邻接表
in_deg = [0] * N # 节点入度
for e in edge_list:
graph[e[0]].append(e[1])
in_deg[e[1]] += 1
# graph[e[0]].append(e[1]) # 无向图处理,添加反向边
# 拓扑排序
## deg[i]: 表示节点i的入度,graph[i]表示节点i的所有邻居
from queue import Queue
def topoSort():
q = Queue()
for i in range(N): # 先找出入度为0的节点
if not deg[i]:
q.put(i)
out = []
while not q.empty():
u = q.get()
out.append(u)
for v in graph[u]:
deg[v] -= 1
if deg[v]==0: q.put(v)
return out if len(out)==N else []

8.1 最小生成树

【8-1】最小生成树 算法
# Kruskal算法 (适用于稀疏图,用到并查集)
n = 4
edges = [[0, 1, 1], [0, 3, 3], [0, 2, 6], [2, 3, 2], [1, 2, 4]]
parent = list(range(n))
def kruskal():
cost = 0
for u, v, w in sorted(edges, key=lambda x: x[2]):
pu, pv = find(u), find(v)
if pu == pv:
continue
parent[pu] = pv # 等同于union
cost += w
return cost
# Prime算法:适用于稠密图,堆优化版本
def prime():
cost, q, seen = 0, [], set()
heappush(q, (0, 0)) # push a dummy node, (cost, node)
for _ in range(n):
w, u = heappop(q)
if u in seen:
continue
cost += w
seen.add(u)
for v, w in graph[u]:
if v in seen:
continue
heappush(q, (w, v))
return cost

8.2 最短路径算法

分类:

  • 单源最短路
    • 所有边权都是正数
      • 朴素的Dijkstra算法 O(n^2) 适合稠密图
      • 堆优化版的Dijkstra算法 O(mlog n)(m是图中节点的个数)适合稀疏图
    • 存在负权边
      • Bellman-Ford O(nm)
      • spfa 一般O(m), 最坏O(nm)
  • 多源汇最短路:Floyd算法 O(n^3)
【8-2】单源最短路径 模板
# 单源最短路径 dijkstra
import heapq
def dijkstra(adj, source):
n = len(adj)
dist = [float('inf')] * (n+1) # 节点与源节点的最短距离
prev = [-1] * (n+1) # 节点的最短路径前驱
visited = set()
dist[source] = dist[0] = 0
min_heap = [(0, source)] # (distance, vid)
while min_heap:
_, u = heapq.heappop(min_heap) # 取距离最小的节点
visited.add(u)
for v in range(n):
if v not in visited and adj[u][v] > 0: # 要求adj中不存在负边
new_dist = dist[u] + adj[u][v]
if dist[v] > new_dist: # 更新s-v最短路径 经过u
dist[v] = new_dist
prev[v] = u
heapq.heappush(min_heap, (dist[v], v))
return dist
【8-3】多源最短路径 模板
# Floyd-Warshall 多源最短路径算法
def floyd_warshall(dist, N):
'''
:param dist: 邻接矩阵 [N, N]
:param N: 节点数
:return: 修改过的邻接矩阵
'''
for k in range(N):
for i in range(N):
for j in range(N):
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
return dist

8.3 二分图

图论中二分图指的是我们可以将所有节点划分成两个集合,任一集合内部没有边,所有边的起点和终点均在不同集合。

图论中一个重要性质:一个图是二分图当且仅当图中不含奇数环 (环中的边的数量为奇数)。

【8-4】二分图
# 1. 染色法判定二分图
# 2. 匈牙利算法 -- 求二分图最大匹配

8.4 网络流

【8-5】网络流
# Edmond-Karp算法(BFS实现的Ford-Fulkerson算法) -- 求最大网络流
def EK(N, edges, S, T):
M = len(edges)
graph = [defaultdict(int) for i in range(N)] # graph[v]={dst_i:cap_i} 每条边的目标节点及容量
for edge in edges: # (src, dst, cap)
graph[edge[0]][edge[1]] = edge[2] # 添加有向边,构建邻接表
graph[edge[1]][edge[0]] = -1 # 添加反向边
flow, pre = [0] * N, [0] * N
def bfs():
pre = [-1] * len(pre)
q = deque()
q.append(S)
flow[S] = float('inf')
while not q.empty(): # BFS遍历
cur = q.popleft() # 队首取出一个节点
if cur == T: break # 若到达目标节点,则找到一条增广路径,退出
for to in graph[cur]: # 枚举所有出边
cap = graph[cur][to] # 获取这条边剩余容量
if cap > 0 and pre[to]==-1: # 若该边的剩余容量大于0,且邻居to尚未访问
pre[to] = cur
flow[to] = min(flow[cur], cap) # 更新可以到达节点to的最大流,木桶原理
q.append(to)
return pre[T] != -1 # 目标节点被访问过,即找到一条增广路径
maxflow = 0
while bfs():
maxflow += flow[T]
v = T
while v != S:
graph[v][pre[v]] -= flow[T]
graph[pre[v]][v] += flow[T]
v = pre[v]
return maxflow

现在每条边除了容量外,还有一个属性 单位费用。一条边上的费用等于 流量×单位费用。网络最大流往往可以用多种不同的方式达到,所以现在要求 在保持流最大的同时,找到总费用最少的一种。

只要建了反向边,无论增广的顺序是怎样,都能求出最大流。所以只需要保证任意时刻、对于当前流量flow始终选择最小的费用(求最短路径),这样不断增大流量直至最大,就能得到最小费用最大流。具体地,把EK算法里的BFS换成SPFA:随便找篇

# Minimum Cost Maximum Flow, MCMF, 最小费用最大流
def MCMF(N, edges, S, T):
M = len(edges)
graph = [defaultdict(int) for i in range(N)] # graph[v]={dst_i:cap_i} 每条边的目标节点及容量
for edge in edges: # (src, dst, cap, cost)
graph[edge[0]][edge[1]] = (edge[2], edge[3]) # 添加有向边,构建邻接表
graph[edge[1]][edge[0]] = -1 # 添加反向边
flow, pre, cost = [0]*N, [0]*N, [0]*N
def spfa():
q = deque()
q.append(S)
pre = [-1] * len(pre)
cost = [float('inf')] * len(cost)
cost[S] = 0
flow[S] = float('inf')
while not q.empty(): # BFS遍历
cur = q.popleft() # 队首取出一个节点
if cur == T: break # 若到达目标节点,则找到一条增广路径,退出
for to in graph[cur]: # 枚举所有出边
cap = graph[cur][to][0] # 获取这条边的剩余容量
cst = graph[cur][to][1] # 获取这条边的单位费用
pas = min(flow[cur], cap) # 该边当前可通过流量,木桶原理
if cap > 0 and cost[to] > cost[cur]+cst*pas: # 若该边的剩余容量大于0,且通过后费用更少
flow[to] = pas # 更新可以到达节点to的最大流
cost[to] = cost[cur] + flow[to] * cst
if pre[to]==-1 : # 邻居to尚未访问
pre[to] = cur
q.append(to)
return pre[T] != -1 # 目标节点被访问过,即找到一条增广路径
maxflow, mincost = 0, 0
while spfa():
maxflow += flow[T]
mincost += cost[T]
v = T
while v != S:
graph[v][pre[v]] -= flow[T]
graph[pre[v]][v] += flow[T]
v = pre[v]
return maxflow

8.5




[End]

posted @   awysl  阅读(34)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示