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) \\ \quad\ \ \ = a[i]-a[i-1] (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 @ 2023-07-20 18:13  awysl  阅读(40)  评论(0)    收藏  举报