2026-01-10-算法分析部分编程题整理-期末复习

  1. 欧几里得算法求最大公约数
    • 一句话: 给定两个正整数 m、n,利用“两个数的最大公约数等于较大数与较小数取余后的最大公约数”的性质,反复取余,最终得到最大公约数(GCD)。
    1. 描述:给定两个正整数 m、n,利用“两个数的最大公约数等于较大数与较小数取余后的最大公约数”的性质,反复取余,最终得到最大公约数(GCD)。
    2. 思路:循环执行 m mod n,用余数替换较大数:令 t = m mod n,接着 m = n,n = t;当 n 变为 0 时,当前的 m 就是最大公约数。
Euclid(m,n)
    while n != 0 do
        t = m mod n
        m = n
        n = t
    return m
  1. 连续整数检测算法求最大公约数
    • 一句话: 从较小的数开始,依次尝试每个可能的公约数(从大到小),找到第一个同时整除 m 和 n 的整数,该整数就是最大公约数。
    1. 描述:从较小的数开始,依次尝试每个可能的公约数(从大到小),找到第一个同时整除 m 和 n 的整数,该整数就是最大公约数。
    2. 思路:令 t = min(m,n),表示最大公约数不可能超过较小者;从 t 开始逐步递减,检查 m mod t == 0 且 n mod t == 0,首次满足条件时返回 t。
Consecutive_integer_gcd(m,n)
    t=min(m,n)
    while t>=1 do
        if m mod t == 0 and n mod t == 0
            return t
        t = t - 1
    return 1
  1. 素数筛算法
    • 一句话: 给定整数 n,找出 2 到 n 范围内的所有素数。
    1. 描述:给定整数 n,找出 2 到 n 范围内的所有素数。素数筛(埃拉托色尼筛)通过“标记合数”的方式一次性筛掉每个素数的倍数。
    2. 思路:先把 2..n 的数放入数组 A;从 p=2 扫到 sqrt(n),若 A[p] 未被标记为 0,则从 p*p 开始把 p 的所有倍数标记为 0(更小的倍数已由更小素数筛过);最后把 A 中未被标记的数收集起来即为素数表。
function(n)
    for p = 2 to n do
        A[p] = p
    for p = 2 to sqrt(n) do
        if A[p] != 0 then
            j = p*p
            while j <= n do
                A[j] = 0
                j = j + p
    i = 0
    for p = 2 to n do
        if A[p] != 0 
            L[i] = A[p]
            ++i
    return L
  1. 矩阵乘积
    • 一句话: 计算两个 n×n 矩阵 A 和 B 的乘积矩阵 C,其中 C[i][j] 等于 A 的第 i 行与 B 的第 j 列对应元素乘积之和。
    1. 描述:计算两个 n×n 矩阵 A 和 B 的乘积矩阵 C,其中 C[i][j] 等于 A 的第 i 行与 B 的第 j 列对应元素乘积之和。
    2. 思路:用三重循环实现定义式:外层枚举行 i、中层枚举行 j,先把 C[i][j] 初始化为 0;内层用 k 从 0..n-1 累加 A[i][k] * B[k][j],得到 C[i][j]。
function(A[0..n-1][0..n-1],B[0..n-1][0..n-1])
    for i = 0 to n-1 do
        for j = 0 to n-1 do 
            C[i][j] = 0
            for k = 0 to n-1 do
                C[i][j] += A[i][k]*B[k][j]
    return C
  1. 汉诺塔(A->c)
    • 一句话: 在三根柱子 A、B、C 上,将 A 上的 n 个大小不同的盘子全部移动到 C 上,每次只能移动一个盘子,并且任何时刻大盘不能放在小盘之上。
    1. 描述:在三根柱子 A、B、C 上,将 A 上的 n 个大小不同的盘子全部移动到 C 上,每次只能移动一个盘子,并且任何时刻大盘不能放在小盘之上。
    2. 思路:要将 n 个盘子从 A 移到 C,先把上面的 n−1 个盘子从 A 移到 B(借助 C),再把最大的盘子从 A 移到 C,最后将 n−1 个盘子从 B 移到 C(借助 A)。
Hanoi(n,A,B,C)
    if n == 1 then
        move A to C
    else
        Hanoi(n-1,A,C,B)
        move A to C
        Hanoi(n-1,B,A,C)
  1. 选择排序
    • 一句话: 对一个无序序列,从左到右逐个位置进行处理,每次在未排序的部分中选出最小的元素,与当前位置的元素交换,直到所有元素有序。
    1. 描述:对一个无序序列,从左到右逐个位置进行处理,每次在未排序的部分中选出最小的元素,与当前位置的元素交换,直到所有元素有序。
    2. 思路:从第一个位置开始,假设它是最小值,然后在后面的元素中寻找真正的最小值,若找到更小的就更新最小值位置,最后将最小值与当前起始位置交换;接着对后续位置重复该过程。
Select_sort(A[0..n-1],n)
    for i = 0 to n-2 do
        min_idx = i
        for j = i + 1 to n -1 do
            if A[j] < A[min_idx] 
                min_idx = j
        swap(A[i],A[min_idx])
  1. 冒泡排序
    • 一句话: 对一个无序序列,反复比较相邻的两个元素,如果顺序不正确就交换,使较大的元素逐步“冒泡”到序列的末尾,直到整个序列有序。
    1. 描述:对一个无序序列,反复比较相邻的两个元素,如果顺序不正确就交换,使较大的元素逐步“冒泡”到序列的末尾,直到整个序列有序。
    2. 思路:每一趟从序列开头开始,依次比较相邻元素,将较大的元素向后交换;一趟结束后,当前最大的元素一定被放到了末尾,重复该过程,逐步缩小未排序的范围。
  • 常规
bubble_sort(A[0..n-1],n)
    for i = 0 to n -2 do
        for j = 0 to n-2-i do
            if A[j] >  A[j+1] then
                swap(A[j+1],A[j])         
  • 设置标记提前截止
bubble_sort(A[0..n-1],n)
    for i = 0 to n -2 do
        flag = false
        for j = 0 to n-2-i do
            if A[j] > A[j+1] then
                swap(A[j+1],A[j])
                flag = true
        if flag == false then
            break
  1. 字符串蛮力匹配
    • 一句话: 在主串(文本)中从左到右依次选择一个起始位置,把模式串逐字符与主串对应位置比较;
    1. 描述:在主串(文本)中从左到右依次选择一个起始位置,把模式串逐字符与主串对应位置比较;如果某一位不相等,就把模式串整体右移一位,重新从模式串第一个字符开始比较,直到匹配成功或主串剩余长度不够为止。
    2. 思路:把模式串当作一个“窗口”在主串上滑动:从位置 i=0 开始,对齐后用 j 从模式串第 0 个字符逐个比对;若 j 比到末尾说明匹配成功并返回起始位置 i;若中途出现不等,则把 i 加 1(窗口右移一格),j 重新置 0,再来一轮,直到 i 超过 n-m(主串长度 n,模式串长度 m)。
// i:模式串在主串中的“起始对齐位置”
// j:当前正在比较的“第几个字符”
// T: 基准
Brute_force_match(T[0..n],P[0..m])
    for i = 0 to n - m do
        j = 0
        while j < m and T[i + j] == P[j] do
            ++j
        if j == m then
            return i
    return -1
  1. 蛮力平面距离最近两点
  • 一句话: 给定平面上 n 个点,要求找出距离最近的两个点。
    1. 描述:给定平面上 n 个点,要求找出距离最近的两个点。蛮力法就是把所有点对都拿出来算一遍距离,记录其中最小的那个距离以及对应的点对。
    2. 思路:用两层循环枚举所有不同的点对 (i, j)(其中 j>i 防止重复和自比),对每一对计算欧氏距离(或比较平方距离以避免开根号),维护当前最小距离 best 和对应点对;循环结束后输出最小距离及最近点对。
Brute_closest_pair(P[0..n-1])
    bestDist2 = infinity
    bestPair = (null,null)
    for i = 0 to n - 2 do
        for j = i + 1 to n-1 do
            dx = P[i].x - P[j].x
            dy = P[i].y - P[j].y
            dist2 = dx*dx + dy*dy //用平方距离比较,省去开根号
            if dist2 < bestDist2 then
                bestDist2 = dist2
                bestPair = {i,j}
    return bestPair,sqrt(bestDist2)
  1. 深度优先搜索遍历
  • 一句话: 深度优先搜索是一种图(或树)的遍历方法,从某个起始顶点出发,沿着一条路径不断向“更深处”访问相邻的未访问顶点,直到走不下去为止,再回退到上一个顶点,继续探索其他未访问的分支,直到所有可达顶点都被访问。
    1. 描述:深度优先搜索是一种图(或树)的遍历方法,从某个起始顶点出发,沿着一条路径不断向“更深处”访问相邻的未访问顶点,直到走不下去为止,再回退到上一个顶点,继续探索其他未访问的分支,直到所有可达顶点都被访问。
    2. 思路:从起点开始,先访问当前顶点并做标记,然后依次检查它的相邻顶点;只要发现还没访问过的,就立刻递归进入该顶点继续搜索;当某个顶点的所有相邻顶点都访问完后,递归返回,回到上一个顶点,继续检查剩余的邻居。
DFS(G)
    for each vertex v in V do
        mark v with 0

    count = 0
    for each vertex v in V do
        if mark[v] == 0 then 
            count = count + 1
            dfs(v)
dfs(v)
    mark v with count for each vertex w in Adj[v] do 
        if mark[w] == 0 then dfs(w)
DFS(G,V)
    visited[v] = true
    visit(v)

    for each u in Adj[v] do
        if visited[u] == false then
            DFS(G,u)

// 对图的DFS(防止图不连通)
DFS_traversal(G)
    initialize visited[v] = false for all v in G
    for each vertex v in G do 
        if visited[v] == false then 
            DFS(G,v)
  1. 广度优先搜索遍历
  • 一句话: 广度优先搜索是一种图(或树)的遍历方法,从起始顶点出发,先访问所有与其直接相邻的顶点,再依次访问这些顶点的未访问邻居,按“由近到远、逐层扩展”的顺序遍历图中所有可达顶点。
    1. 描述:广度优先搜索是一种图(或树)的遍历方法,从起始顶点出发,先访问所有与其直接相邻的顶点,再依次访问这些顶点的未访问邻居,按“由近到远、逐层扩展”的顺序遍历图中所有可达顶点。
    2. 思路:从起点开始,先把起点放入队列并标记为已访问;然后反复从队列中取出队首顶点,访问它,并将所有尚未访问的邻接顶点按顺序加入队列;队列为空时,说明当前连通分量已遍历完,若图不连通,则对其他未访问顶点重复该过程。
BFS(G)
    count = 0
    for each vertex v in V do
        if v is marked with 0 then
            bfs(v)
bfs(v)
    count = count + 1
    mark v with count and init a queue with v
    while the queue is not empty do
    for each vertex w in V adjacent to the front vertex do
        if w is marked with 0 then 
            count = count + 1
            mark w with count
            add w to the queue
        remove the front vertex from the queue
BFS(G,s)
    create an empty queue Q
    visited[s] = true
    enqueue(Q,s)

    while Q is not empty do
        v = dequeue(Q)
        visit(v)
        for each u in Adj[v] do
            if visited[u] == false then
                visited[u] = true
                enqueue(Q,u)

BFS_traversal(G)
    for each vertex v in G do
        visited[v] = false

    for each vertex v in G do
        if visited[v] == false then
            BFS(G,v)
  1. 插入排序
  • 一句话: 插入排序是一种简单直观的排序方法,将序列分为“已排序区”和“未排序区”,每次从未排序区中取出一个元素,把它插入到已排序区中合适的位置,使已排序区始终保持有序,直到所有元素都插入完成。
    1. 描述:插入排序是一种简单直观的排序方法,将序列分为“已排序区”和“未排序区”,每次从未排序区中取出一个元素,把它插入到已排序区中合适的位置,使已排序区始终保持有序,直到所有元素都插入完成。
    2. 思路:从第二个元素开始,认为第一个元素已经有序;取当前元素作为待插入值,从已排序区的末尾向前逐个比较,凡是比待插入值大的元素就向后移动一个位置,直到找到合适的位置,将待插入值放入该位置。
//往前找小于的值,插在其后
Insertion_sort(A[0..n])
for i = 0 to n-1 do 
    key = A[i]
    j = i-1
    while j >= 0 and A[j] > v do
        A[j+1] = A[j]
        j = j - 1
    A[j+1] = key
  1. 拓扑排序
  • 一句话: 拓扑排序是针对有向无环图(DAG)的一种排序方法,把图中所有顶点排成一个线性序列,使得每一条有向边 $u \rightarrow v$ 中,顶点 u 都排在顶点 v 的前面。
    1. 描述:拓扑排序是针对有向无环图(DAG)的一种排序方法,把图中所有顶点排成一个线性序列,使得每一条有向边 $u \rightarrow v$ 中,顶点 u 都排在顶点 v 的前面。如果图中存在回路,则不存在拓扑排序。
    2. 思路:把问题理解成“先做没有前置条件的事”。
    先找出所有入度为 0 的顶点(没有任何前驱),把它们输出;然后从图中“删除”这些顶点及其发出的边,更新其他顶点的入度;再重复这个过程,直到所有顶点都被输出。如果中途再也找不到入度为 0 的顶点,但还有顶点没输出,说明图中存在回路。
Topological_sort(G)
for each vertex v in G do 
    indegree[v] = 入度(v)

    create an empty queue Q
    for each vertex v in G do
        if indegree[v] == 0 then
            enqueue(Q,v)

    while Q is not empty do
        v = dequeue(Q)
        visit(v)
        for each u in Adj[v] do
            indegree[u] = indegree[u] - 1
            if indefree[u] == 0 then
                enqueue(Q,u)
    
    if 输出的顶点数 < |V| then 
        report "图中存在回路,无法进行拓扑排序"

先选没前驱的点,删掉它,再重复。

  1. 生成排列算法
  • 一句话: 给定 n 个元素(比如数组 A[0.
    1. 描述:给定 n 个元素(比如数组 A[0..n-1]),生成它们所有可能的排列(即把元素按不同顺序排出来的所有结果),每个排列恰好出现一次,共有 n! 种。
    2. 思路:用回溯(DFS)来“逐位填数”:从第 0 个位置开始,依次决定当前位置放哪个元素。做法是把某个候选元素放到当前位置,然后递归去填下一个位置;当填到第 n 个位置时,说明得到一个完整排列就输出。为了避免重复,通常用“交换法”:在第 k 位上,把 A[k] 与后面任意 A[i] 交换,相当于选择 A[i] 作为第 k 位,然后递归生成剩余部分,递归返回时再交换回来恢复现场。
// 采用回溯法实现
Permute(A,k,n)
if k == n then
    output A[0..n-1]
    return

for i = k to n - 1 do
    swap(A[k],A[i]) //选 A[i]放到第k 位
    Permute(A,k+1,n) //递归生成后面的排列
    swap(A[k],A[i]) //回溯:恢复现场

// 调用
// Permute(A,0,n)
  1. 反射格雷码
  • 一句话: 排出所有 n 位二进制数,并保证前后两个数只改动 1 位。
    1. 描述:排出所有 n 位二进制数,并保证前后两个数只改动 1 位。
    2. 思路:反射格雷码的核心思想是“复制 + 反转 + 加前缀”,也就是“反射”二字的来源。
    • 由 n−1 位格雷码生成 n 位格雷码:
    • 先把已有的 n-1 位格雷码原样复制一份
    • 再把这份格雷码倒序复制一份(反射)
    • 给第一部分每个码前面加 0
    • 给第二部分每个码前面加 1
    • 合并两部分,得到 n 位反射格雷码
BRG(n)
if n = 1 then 
    表L包含位串 0 和 1
else
    调用 BRG(n-1) 生成长度 n-1 位串列表L1
    把 L1 倒序后复制给表L2
    把 0 加到表 L1 中每个位串前面
    把 1 加到表 L2 中每个位串后面
    把表 L2 添加到表 L1 后面得到表L

return L
GrayCode(n):
    if n == 1 then
        return ["0","1"]

    prev = GrayCode(n-1)
    result = empty list

    // 第一部分:前面加 0
    for each code in prev do 
        result.append("0"+code)

    // 第二部分:反射后前面加 1
    for each code in reverse(prev) do
        result.append("1"+code)

    return result
  1. 折半查找
  • 一句话: 有序序列中查找指定元素。
    1. 描述:有序序列中查找指定元素。每次将查找区间一分为二,通过比较目标值与中间元素的大小,决定下一步只在左半部分或右半部分继续查找,从而不断缩小查找范围,直到找到目标元素或区间为空为止。
    2. 思路:设定查找区间的左右边界 low 和 high,初始时覆盖整个数组;在区间非空的情况下,计算中间位置 mid,将目标值与 A[mid] 比较:若相等则查找成功;若目标值小于 A[mid],则目标只可能在左半区间,更新 high = mid - 1;否则目标只可能在右半区间,更新 low = mid + 1。不断重复上述过程,直到找到目标或区间为空。

def Binary_search(A[0..n-1],key):
    low = 0
    high = n - 1

    while low <= high:
        mid = floor((low+high)/2)
        if A[mid] == key:
            return mid
        else if key < A[mid]:
            high = mid - 1
        else:
            low = mid + 1

    return -1
  1. 俄式乘法
  • 一句话: 俄式乘法是一种不用“逐位乘法”的整数乘法方法,通过不断把一个数减半、另一个数加倍,并在过程中只在特定条件下累加结果,最终得到两个整数的乘积。
    1. 描述:俄式乘法是一种不用“逐位乘法”的整数乘法方法,通过不断把一个数减半、另一个数加倍,并在过程中只在特定条件下累加结果,最终得到两个整数的乘积。
    它本质上是把乘法转化为加法 + 位运算(折半)。
    2. 思路:设要计算 $a \times b$。重复执行下面的过程,直到 a = 0:
    • 如果 a 是奇数,就把当前的 b 加到结果中
    • 把 a 整除 2(向下取整)
    • 把 b 乘以 2
      之所以可行,是因为任意整数都可以表示成若干个 2 的幂之和(二进制思想),而“奇偶判断 + 折半”正是在逐位处理 a 的二进制表示。
      1767631066021.png
def Russian_multiply(a,b):
    result = 0
    while a > 0:
        if a is odd: # if (a & 1) == 1:
            result = result + b
        a = a >> 1 # a折半等价于 a = a // 2
        b = b << 1  # b加倍等价于 b = b * 2
    return result
  1. 约瑟夫环问题
  • 一句话: 一圈人轮流报数,报到固定数字就出局,最后活下来的是谁?
    1. 描述:一圈人轮流报数,报到固定数字就出局,最后活下来的是谁?
    2. 思路:把问题看成一个“缩小规模的循环问题”。
      当第一个人被淘汰后,剩下的 n-1 个人仍然构成一个新的约瑟夫环,只是编号发生了平移。利用这个性质,可以用递推(或递归)来解决。
      核心递推思想是:
    • 设 $f(n, m)$ 表示 n 个人、报数到 m 时出局 的情况下,最后留下来的人的编号(从 0 开始编号)
    • 当只有 1 个人时,显然存活的是他自己:$f(1, m) = 0$
    • 当从 n-1 人增加到 n 人时,编号会整体右移 m 位:$f(n, m) = (f(n-1, m) + m) \bmod n$

最终如果题目要求 从 1 开始编号,只需在结果上加 1。
对 n 向左做一次循环移位;

# 1.递推求解
Josephus(n,m):
    ans = 0 # f(1,m) = 0
    for i = 2 to n:
        ans = (ans + m) mod i
    return ans + 1 #转化为1开始编号
# 2.循环队列方式求解
Josephus_queue(n,m):
    Q = empty queue
    for i = 1 to n:
        enqueue(Q,i)

    while size(Q) > 1:
        for t = 1 to m -1:
            x = dequeue(Q)
            enqueue(Q,x)
        dequeue(Q) #淘汰第 m 个
    return front(Q)
# 3.循环移位
Josephus_shift_optimized(A[0..n-1],n,m):
    start = 0

    while n > 1:
        # 1.起点左移m 位
        start = (start + m) mod n

        # 2.删除 A[start]
        for i = start to n -2:
            A[i] = A[i+1]
        n = n -1 

        # 3.start 不变,指向下一个起点
        if start == n:
                start = 0
    return A[0]
  1. 三重查找
  • 一句话: 在有序数组中查找元素的方法(或在单峰函数上找极值的方法)。
    1. 描述:在有序数组中查找元素的方法(或在单峰函数上找极值的方法)。这里先说最常见的“在有序数组中查找”:
    每次把当前区间分成 三段,通过比较目标值与两个分割点的值,判断目标可能落在哪一段,然后把搜索范围缩小到那一段,直到找到目标或区间为空。
    2. 思路:设搜索区间为 [l, r],取两个分割点:
    • m1 = l + (r-l)//3
    • m2 = r - (r-l)//3
      比较 key 与 A[m1]、A[m2]:
      •若 key == A[m1] 或 key == A[m2]:找到
      •若 key < A[m1]:只可能在左段 [l, m1-1]
      •若 key > A[m2]:只可能在右段 [m2+1, r]
      •否则:在中段 [m1+1, m2-1]

不断重复,直到 l > r。


def ternary_search(A,key):
    l = 0
    r = len(A) - 1
    while l <= r:
        m1 = l+(r-l)//3
        m2 = r-(r-l)//3

        if A[m1] = key:
            return m1
        if A[m2] = key:
            return m2
        
        if key < A[m1]:
            r = m1 - 1
        elif key > A[m2]:
            l = m2 + 1
        else:
            l = m1 + 1
            r = m2 - 1
    return -1
  1. Lomuto划分
  • 一句话: Lomuto 划分是快速排序中的一种划分方法。
    1. 描述:Lomuto 划分是快速排序中的一种划分方法。它选取区间末尾元素作为枢轴(pivot),通过一次线性扫描,把数组重排为三段结构:
      小于等于枢轴 | 枢轴 | 大于枢轴。
      划分完成后,枢轴被放到它在最终有序数组中的正确位置。
    2. 思路:设当前处理区间为 [l, r],选择 A[r] 作为枢轴 pivot。
      用一个指针 i 表示“小于等于 pivot 的最后位置”,初始为 l-1;再用指针 j 从 l 扫到 r-1:
      • 若 A[j] <= pivot:
      说明它应该被放到左侧,先将 i 右移一位,再交换 A[i] 与 A[j]
      • 扫描结束后,把枢轴 A[r] 与 A[i+1] 交换
      枢轴就落在了正确位置 i+1

这样一次扫描就完成了划分。

LomutoPartition(A[l..r])
    p = A[l]
    s = l
    for i = l+1 to r do
        if A[i] < p:
            s = s+1
            swap(A[s],A[i])
    swap(A[l],A[s])
    return s

def Lomuto_partition(A,l,r):
    pivot = A[r]
    i = l -1
    for j = l to r - 1:
        if A[j] <= pivot:
            ++i
            swap(A[i],A[j])
    swap(A[i+1],A[r])
    return i + 1 #枢纽最终位置
  1. Hoare 划分
  • 一句话: 左右两个指针,向中间夹逼,发现“站错队的”就交换。
    1. 描述:左右两个指针,向中间夹逼,发现“站错队的”就交换。
    2. 思路:选 第一个元素 作为 pivot
    • 左指针找 ≥ pivot 的
    • 右指针找 ≤ pivot 的
    • 找到就交换
    • 指针相遇就结束
def Hoare_partition(A,l,r):
    pivot = A[l]
    i = l - 1
    j = r + 1

    while True:
        repeat:
            i = i + 1
        until A[i] >= pivot

        repeat:
            j = j - 1
        until A[j] <= pivot

        if i >= j:
            retun j

        swap(A[i],A[j])
  1. 快速选择
  • 一句话: 快速选择是一种在无序数组中查找第 k 小(或第 k 大)元素的算法。
    1. 描述:快速选择是一种在无序数组中查找第 k 小(或第 k 大)元素的算法。
    它的目标不是把整个数组排好序,而是只定位一个元素的最终位置,因此通常比完整排序更快。
    1. 思路:快速选择借鉴了快速排序的“划分”思想(通常用 Lomuto 或 Hoare 划分):
    1. 在当前区间选择一个枢轴(pivot),对数组进行一次划分
      • 左边都是 ≤ pivot
      • 右边都是 ≥ pivot
      • pivot 被放在一个确定的位置 p
    2. 比较 p 和目标下标 k-1:
      • 如果 p == k-1:找到了第 k 小元素,直接返回
      • 如果 p > k-1:第 k 小元素一定在左半区间,只递归左边
      • 如果 p < k-1:第 k 小元素一定在右半区间,只递归右边
    3. 不断缩小区间,直到命中目标位置
      关键点在于:
      每一轮只递归“一边”,而不是两边。
      这正是它比快速排序更快的原因。
Quickselect(A[l..r],k)
    s = LomutoPartition(A[l..r])
    if s = l + k - 1 then 
        return A[s]
    else if s < l + k -1 then
        Quickselect(A[l..s-1],k)
    else 
        Quickselect(A[s+1,r],l+k-1-s)

def Qucik_select(A,l,r,k):
    if l == r:
        return A[l]
    p = Lomuto_Partition(A,l,r)

    if p == k -1:
        return A[p]
    else if p > k -1:
        return Quick_select(A,l,p-1,k)
    else:
        return Quick_select(A,p+1,r,k)

#调用,找第k 小
# Quick_select(A,0,n-1,k)
  1. 快速排序
  • 一句话: 选择一个基准元素(pivot),将待排序序列划分为两部分:。
    1. 描述:选择一个基准元素(pivot),将待排序序列划分为两部分:
    一部分元素小于(或等于)基准,另一部分元素大于(或等于)基准,
    然后对这两部分分别递归地进行排序,最终得到有序序列。
    2. 思路:
    1. 选择基准元素(pivot)
      从序列中选取一个元素作为基准(常见做法是选第一个、最后一个或随机一个)。
    2. 划分(Partition)
      通过一次扫描,将序列重新排列,使得:
      • 基准左边的元素都不大于基准
      • 基准右边的元素都不小于基准
      此时基准元素处在其最终正确位置上。
    3. 递归排序
      对基准左、右两部分子序列分别重复上述过程,直到子序列长度为 0 或 1。
QuickSort(A[l..r])
    if l < r then
        s = LomutoPartition(A[l..r])
        QuickSort(A[l..s-1])
        QuickSort(A[s+1,r])
def Quick_sort(A,l,r):
    if l < r:
        p = partition(A,l,r)
        Quick_sort(A,low,p-1)
        Quick_sort(A,p+1,r)
  1. 归并排序
  • 一句话: 待排序序列不断划分为若干个规模更小的子序列,直到每个子序列只包含一个元素(天然有序),。
    1. 描述:待排序序列不断划分为若干个规模更小的子序列,直到每个子序列只包含一个元素(天然有序),
    然后再将这些有序子序列逐步合并成一个完整的有序序列。
    先拆到不能再拆,再把有序的小块合并成大块。
    2. 思路:
    1. 分解(Divide)
      将原序列从中间划分为左右两个子序列,并递归地对左右子序列进行归并排序。
    2. 合并(Conquer / Merge)
      将两个已经排好序的子序列,按照大小顺序合并成一个新的有序序列。
      当子序列长度为 1 时,递归结束。
MergeSort(A[0..n-1])
    if n > 1 then
        copy A[0..n/2-1] to B[0..n/2-1]
        copy A[n/2..n-1] to C[0..n/2-1]
        MergeSort(B[0..n/2-1])
        MergeSort(C[0..n/2-1])
        Merge(B,C,A)

Merge(B[0..p-1],C[0..q-1],A[0..p+q-1])
    i = 0
    j = 0
    k = 0
    while i < p and j < q do
        if B[i] <= C[j] then
            A[k] = B[i]
            ++i
        else
            A[k] = C[j]
            ++j
        ++k
    if i == p
        copy C[j..q-1] to A[k..p+q-1]
    else 
        copy B[i..p-1] to A[k..p+q-1]
  1. 归并排序Merge
  • 一句话: 将两个已经有序的子序列合并为一个新的有序序列。
    1. 描述:将两个已经有序的子序列合并为一个新的有序序列。
    Merge 操作直接决定了归并排序的正确性和效率。
    2. 思路:
    1. 分别用两个指针指向两个有序子序列的起始位置
    2. 比较两个指针所指的元素
    3. 将较小的元素放入辅助数组中,并移动对应指针
    4. 重复上述过程,直到其中一个子序列遍历完成
    5. 将另一个子序列中剩余的元素依次复制到辅助数组中
      最后将辅助数组中的结果拷贝回原序列。
def Merge_sort(A,l,r):
    if l < r:
        mid = (l+r)//2
        Merge_sort(A,l,mid)
        Merge_sort(A,mid+1,r)
        Merge(A,l,m,r)

def Merge(A,l,m,r):
    create array temp
    i = l
    j = mid + 1
    k = 0

    while i <= mid and j <= right:
        if A[i] <= A[j]:
            temp[k] = A[i]
            ++i
        else:
            temp[k] = A[j]
            ++j
        ++k
    while i <= mid:
        temp[k] = A[i]
        ++i
        ++k
    
    while j <= r:
        temp[k] = A[j]
        ++j
        ++k

    for t = 0 to k -1:
        A[left + t] = temp[t]
  1. 二叉树遍历
  • 一句话: 二叉树遍历是指按照一定的顺序,依次访问二叉树中所有结点,使得每个结点被访问且只被访问一次的过程。
    1. 描述:二叉树遍历是指按照一定的顺序,依次访问二叉树中所有结点,使得每个结点被访问且只被访问一次的过程。
    根据访问根结点的先后顺序不同,二叉树的遍历方式主要包括:
    • 先序遍历(根 → 左 → 右)
    • 中序遍历(左 → 根 → 右)
    • 后序遍历(左 → 右 → 根)
    此外,还可以按层次从上到下、从左到右访问结点,称为层序遍历。
    2. 思路:
    • 先/中/后序:递归实现
    • 层序遍历:队列实现
    • 中序遍历二叉搜索树:得到有序序列
// 1.先序遍历
peroder(BTNode *p)
    if p != null
        printf(p->data)
        preorder(p->lchild)
        prorder(p-rchild)
// 2.后序遍历
postorder(BTNode *p)
    if p == NULL then 
        return
    else
        postorder(p.lchild)
        postorder(p.rchid)
        visit()
// 3.中序遍历
inorder
# 结点
Node:
    data
    left
    right

# 1.先序遍历:根,左,右
def Pre_order(T):
    if T != NULL:
        visit(T)
        Pre_order(T.left)
        Pre_order(T.right)

# 2.中序遍历:左,根,右
def In_order(T):
    if T != NULL:
        In_order(T.left)
        visit(T)
        In_order(T.right)

# 3.后序遍历:左,右,根
def Post_order(T):
    if T != NULL:
        Post_order(T.left)
        Post_order(T.right)
        visit(T)

# 4.层序遍历
def Level_order(T):
    if T = NULL:
        return
    
    create empty queue Q
    enqueue(Q,T)

    while Q is not empty:
        p = dequeue(Q)
        visit(p)

        if p.left != NULL:
            enqueue(Q,p.left)

        if p.right != NULL:
            enqueue(Q,p.right)
  1. 高斯消去法
  • 一句话: 高斯消去法(Gaussian Elimination) 是一种求解线性方程组的经典方法。
    1. 描述:高斯消去法(Gaussian Elimination) 是一种求解线性方程组的经典方法。
    它通过一系列初等行变换,将线性方程组对应的增广矩阵逐步化为上三角矩阵(或行阶梯形矩阵),
    再通过回代过程求出未知量的解。
    2. 思路:高斯消去法的基本思路可以概括为两个阶段:
    (1)消元阶段(前向消去)
    • 从第一行开始,选择一个主元
    • 通过行变换,将主元下方同一列的元素消为 0
    • 依次向下处理每一列
    • 最终得到一个上三角矩阵
    (2)回代阶段(Back Substitution)
    • 从最后一个方程开始
    • 逐步向上代入,依次求解各个未知数
    • 最终得到整个线性方程组的解
function(A[1..n][1..n],b[1..n])
    for i = 1 to n do 
        A[i,n+1] = b[i]
    for i = 1 to n -1 do
        for j = i + 1 to n do 
            for k = n + 1 downto i do
                A[j,k]=A[j,k]-A[i,k]*A[j,i]/A[i,i]

不选主元

def Gaussian_elimination(A,b,n):
    # 1.消元阶段
    for k = 1 to n -1:
        for i = k + 1 to n:
            if A[k][k] == 0:
                error "Zero pivot"
            factor = A[i][k] / A[k][k]
            for j = k to n:
                A[i][j] = A[i][j] - factor*A[k][j]
            b[i] = b[i] - factor*b[k]
        
    # 2.回代阶段
    x[n] = b[n] / A[n][n]
    for i = n -1 down to 1:
        sum = b[i]
        for j = i + 1 to n:
            sum = summ - A[i][j] * x[j]
        x[i] = sum / A[i][i]
    return x

选主元

GAUSSIAN-ELIMINATION-PIVOT(A, b, n):
    // 消元阶段(带选主元)
    for k = 1 to n - 1:
        // 选主元
        maxRow = k
        for i = k + 1 to n:
            if abs(A[i][k]) > abs(A[maxRow][k]):
                maxRow = i

        if A[maxRow][k] == 0:
            error "No unique solution"

        // 交换行
        swap A[k] and A[maxRow]
        swap b[k] and b[maxRow]

        // 消元
        for i = k + 1 to n:
            factor = A[i][k] / A[k][k]
            for j = k to n:
                A[i][j] = A[i][j] - factor * A[k][j]
            b[i] = b[i] - factor * b[k]

    // 回代阶段
    x[n] = b[n] / A[n][n]
    for i = n - 1 down to 1:
        sum = b[i]
        for j = i + 1 to n:
            sum = sum - A[i][j] * x[j]
        x[i] = sum / A[i][i]

    return x
  1. 构造堆
  • 一句话: 构造堆是指将一个无序的线性序列,通过一定的方法,调整为满足堆性质的完全二叉树的过程。
    1. 描述:构造堆是指将一个无序的线性序列,通过一定的方法,调整为满足堆性质的完全二叉树的过程。
    根据堆中结点关键字的大小规律不同,堆可分为:
    • 大根堆(最大堆):任一结点的关键字 ≥ 其左右孩子
    • 小根堆(最小堆):任一结点的关键字 ≤ 其左右孩子
    构造堆是堆排序和优先队列实现中的基础操作。
    2. 思路:构造堆通常采用自底向上调整(Heapify)的方法,基本思路如下:
    1. 将待处理序列看作一棵完全二叉树
    2. 从最后一个非叶子结点开始,向前逐个对结点进行下沉调整
    3. 对每个结点,将其与左右孩子中更符合堆性质的那个进行比较
    4. 若不满足堆性质,则交换,并继续向下调整
    5. 重复上述过程,直到根结点调整完成
      最终,整个序列就满足堆的性质。
function(H[1..n])
    for i = n/2 downto 1 do
    k = i
    v = H[k]
    heap = false
    while not heap and 2*k<=n do
        j = 2*k
        if j < n then
            if H[j]<H[j+1] then
                ++j
        if v >= H[j] then
            heap = true
        else
            H[k] = H[j]
            k = j
    H[k] = v
# 大根堆
def Heapify(A,i,n):
    largest = i
    left = 2*i
    right = 2*i+1

    if left <= n and A[left] > A[largest]:
        largest =left

    if right <= n and A[right] > A[largest]:
        largest = right
    
    if largest != i:
        swap(A[i],A[largest])
        Heapify(A,largest,n)

# 构造堆
def Build_heap(A,n):
    for i = n // 2 downto 1:
        Heapify(A,i,n)
  1. 霍纳法则
  • 一句话: 霍纳法则是一种高效计算多项式值的方法。
    1. 描述:霍纳法则是一种高效计算多项式值的方法。
    它通过把多项式改写成“嵌套乘法”的形式,显著减少乘法和加法的次数,从而更快、更稳定地计算多项式在某个给定点上的值。
    把多项式“从里往外算”,
    每一步只做一次乘法、一次加法。
    2. 思路:假设有一个多项式:
    $$
    P(x) = a_n x^n + a_{n-1} x^{n-1} + \cdots + a_1 x + a_0
    $$
    直接算的话,需要很多次幂运算和乘法。
    霍纳法则的做法是把它改写成:
    $$
    P(x) = (\cdots((a_n x + a_{n-1})x + a_{n-2})x + \cdots + a_0)
    $$
    这样计算时:

    1. 先从最高次系数 $a_n$ 开始
    2. 每一步:
      • 当前结果乘以 $x$
      • 再加上下一个系数
    3. 重复,直到常数项

    整个过程只需要:

    • $n$ 次乘法
    • $n$ 次加法
Horner(P[0..n],x)
p = P[n]
for i = n-1 downto 0 do
    p=x*p+P[i]
return p
def Horner(a[0..n],x):
    // a[0]是常数项,a[n]是x^n的系数
    result = a[n]
    for i = n-1 downto 0:
        result = result * x + a[i]
    return result
  1. 从左至右二进制幂
  • 一句话: 从左至右二进制幂是一种高效计算 $a^e$ 的方法。
    1. 描述:从左至右二进制幂是一种高效计算 $a^e$ 的方法。
      它把指数 $e$ 写成二进制,从最高位到最低位依次处理,通过“平方”和在必要时“乘一次底数”,在 $O(\log e)$ 的时间内完成幂运算。
      先把指数写成二进制,
      从最高位开始,每走一位先平方,
      遇到 1 再乘一次底数。

    2. 思路:设指数 $e$ 的二进制表示为 $b_{k-1}b_{k-2}\cdots b_0$。
      算法从最高位 $b_{k-1}$ 开始:

      1. 初始化 result = 1
      2. 对每一位(二进制位)从左到右:
        - 先做一次:result = result * result(平方,表示“向下一位推进”)
        - 如果当前位是 1:再做一次 result = result * a
      3. 所有位处理完后,result 就是 $a^e$

      这样做的本质是:逐步“构造”指数对应的幂,而不是一次性算幂。

LRBE(a,b(n))
product = a
for i = I - 1 downto 0 do
    product=product*product
    if bi = 1 then 
        product = product * a
return product
def Binary_power_L2R(a,e):
    result = 1
    for bit in binary_representation of e from MSB to LSB:
        retult= result*result
        if bit == 1:
            result = result * a
    return result
  1. 从右至左二进制幂
  • 一句话: 从指数 $e$ 的最低位开始处理,通过反复“判断奇偶 + 平方 + 折半”,在 $O(\log e)$ 时间内完成幂运算,是实际编程中最常用的快速幂写法。
    1. 描述:从指数 $e$ 的最低位开始处理,通过反复“判断奇偶 + 平方 + 折半”,在 $O(\log e)$ 时间内完成幂运算,是实际编程中最常用的快速幂写法。
    2. 思路:设要计算 $a^e$,维护两个变量:
    • result:当前结果,初始为 1
    • base:当前“有效底数”,初始为 $a$
      每一轮循环:
    1. e 是奇数(最低位是 1),则
      result = result * base
    2. base = base * base(为处理下一位做准备)
    3. e = e // 2(右移一位,处理下一位)
      重复,直到 e == 0
      本质含义是:

    逐位“吃掉”指数的二进制表示,
    每吃一位,就把 base 的权重翻倍。

RLBE(a,b(n))
term = a
if b0 = 1 then
    product = a
else
    product = 1
for i = 1 to I do
    term = term * term
    if b1 = 1 product = product * term

return product
def Binary_power_R2L(a,e):
    result = 1
    base = a
    while e > 0:
        if e is odd: # if(e&1):
            result = result * base
        base = base * base
        e = e // 2 # e >>= 1
    return result
  1. 比较计数排序
  • 一句话: 统计每个元素“比多少个元素大”,从而直接确定该元素在有序序列中的最终位置。
    1. 描述:统计每个元素“比多少个元素大”,从而直接确定该元素在有序序列中的最终位置。
    不急着排队,
    先数清楚“我前面应该站多少人”,
    位置自然就定了。
    1. 思路:给定数组 A[0..n-1],再准备一个计数数组 C[0..n-1]

    1. 初始化 C[i] = 0,表示 A[i] 前面应有 0 个元素**
    2. 对每一对元素 (A[i], A[j])i < j)进行比较:
      • 如果 A[i] < A[j],说明 A[j] 前面应该多一个元素:
        C[j]++
      • 否则(A[i] >= A[j]),说明 A[i] 前面应该多一个元素:
        C[i]++
    3. 比较完成后,C[i] 就表示 A[i] 在有序数组中的位置
    4. C[i] 的值,把 A[i] 放到结果数组对应位置

    核心思想是:
    “位置 = 比我小的元素个数”

Function(A[0..n-1])
for i = 0 to n - 1 do
    Count[i] = 0 
for i = 0 to n - 2 do
    for j = i + 1 to n-1 do
        if A[i] < A[j] then
            ++Count[j]
        else
            ++Count[i]
for i = 0 to n-1 do
    S[Count[i]] = A[i]

return S
def ComparisonCounting_sort(A[0..n-1]):
    C[0..n-1] = 0
    B[0..n-1] #结果数组

    for i = 0 to n-2:
        for j = i+1 to n-1:
            if A[i] < A[j]:
                ++C[j]
            else:
                ++C[i]
    
    for i = 0 to n-1:
        B[C[i]] = A[i]

    return B
  1. 分布计数排序
  • 一句话: 分布计数排序是一种不基于比较的排序算法,适用于待排序元素是整数且取值范围不大的情况。
    1. 描述:分布计数排序是一种不基于比较的排序算法,适用于待排序元素是整数且取值范围不大的情况。
    它通过统计每个取值出现的次数,再把元素“按值分布”到正确的位置上,从而完成排序。
    不比大小,
    先数每个数有多少个,
    再按数值顺序一次铺开。
    1. 思路:假设待排序数组为 A[0..n-1],元素的取值范围是 [0..k](或能映射到这个范围)。
    排序过程分三步:
    1. 计数
      用计数数组 C[0..k],统计每个值出现的次数:C[v] = 值 v 在 A 中出现的次数
    2. 前缀和(定位)
      C 改造成前缀和数组,使得:C[v] = 小于等于 v 的元素个数
      这样就能知道“值为 v 的元素,在结果数组中应该放到哪里结束”。
    3. 回填(分布)
      从右向左扫描原数组 A,把每个元素放入结果数组 B 的正确位置,同时更新计数
      (从右向左是为了保证稳定性)。
Function(A[0..n-1],l,u)
for j = 0 to u-l do
    D[j] = 0
for i = 0 to n-1 do
    ++D[A[i]-l]
for j = 1 to u-l do
    D[j] = D[j-1]+D[j]
for i = n-1 downto 0 do
    j = A[i] - l
    S[D[j]-1] = A[i]
    --D[j]
return S
def Counting_sort(A[0..n-1],k):
    C[0..k] = 0
    B[0..n-1] #结果数组

    # 1.计数
    for i = 0 to n-1:
        ++C[A[i]]
    # 2.前缀和
    for v = 1 to k:
        C[v] = C[v] + C[v-1]
    # 3.分布(从右向左,保证稳定)
    for i = n-1 downto 0:
        v = A[i]
        B[C[v] - 1] = v
        --C[v]
    return B
  1. 填充移动表
  • 一句话: 填充移动表是为字符串匹配提前准备的一张表,用来记录:。
    1. 描述:填充移动表是为字符串匹配提前准备的一张表,用来记录:
    当模式串某个字符与文本不匹配时,模式串应该向右移动多少位。
    这张表通常称为 坏字符表(bad-character table)。
    提前记住:
    每个字符如果“撞坏了”,
    模式串该往右挪几步。
    1. 思路:设模式串为 P,长度为 m

    核心思想只有三步:

    1. 先假设最坏情况
      • 如果某个字符在模式串里根本不存在
      • 那就直接把模式串整体右移 m
    2. 再修正模式串中出现过的字符
      • 对于模式串中每个字符 P[i]
      • 记录:

        当它在位置 i 发生不匹配时,
        模式串至少要移动 m - 1 - i

    3. 取“最右出现位置”
      • 同一个字符如果出现多次
      • 只保留最靠右那次(移动最小,最安全)
ShiftTable(P[0..m-1])
for c = 0 to size-1 do 
    Table[c] = m
for j = 0 to m - 2 do
    Table[P[j]] = m-1-j
return Table
def BuildShiftTable(P[0..m-1]):
    for each character c in alphabet:
        Shift[c] = m #默认不在模式串中
    for i = 0 to m-2:
        shift[P[i]] = m - 1 - i

    return shift
  1. Horspool字符串匹配算法
  • 一句话: Horspool 字符串匹配算法是一种高效的字符串匹配算法。
    1. 描述:Horspool 字符串匹配算法是一种高效的字符串匹配算法。
    它在匹配失败时,不是一位一位地移动模式串,而是根据预先构建的移动表,一次性向右跳多位,从而减少不必要的比较次数。
    从右往左比,
    一旦不匹配,就按字符“一口气右跳”。
    1. 思路:Horspool 的核心思想可以拆成 三步

(1)预处理:构造移动表(坏字符表)

  • 模式串长度为 m
  • 对每个字符 c
    • 默认移动 m 位(表示字符不在模式串中)
    • c 在模式串中出现,则记录它最右出现位置到末尾的距离
      “填充移动表” 那题。

(2)匹配时:从右向左比较

  • 把模式串与文本对齐
  • 从模式串最后一个字符开始比较
  • 若字符全部匹配 → 匹配成功
  • 若在某一位失败 → 进入下一步

(3)失败时:只看文本中“对齐的最后一个字符”

  • 取当前窗口中 文本里与模式串最后一位对齐的字符
  • 根据移动表,决定模式串向右移动多少位
  • 文本指针永不回退
    这是 Horspool 跳得快的关键。
HorspoolMatching(P[0..m-1],T[0..n-1])
ShiftTable(P[0..m-1]) //生成移动表
i = m -1
while i <= n-1 do
    k = 0
    while k <= m-1 and P[m-1-k] == T[i-k] do 
        ++k
    if k == m then
        return i-m+1
    else
        i = i + Table[T[i]]
return -1
def HorspoolMathch(T[0..n01],P[0..m-1]):
    shift = BuildShiftTable(p)
    i = m -1 # 模式串末尾对齐的位置

    while i < n:
        k = 0
        while k < m and P[m-1-k] == T[i-k]:
            ++k
        if k == m:
            return i - m + 1 #匹配成功,返回起始位置
        else:
            i = i + shift[T[i]] #按坏字符表跳转

    return -1
  1. 币值最大化问题
  • 一句话: 给定一排硬币,每个硬币都有一个正的价值。
    1. 描述:给定一排硬币,每个硬币都有一个正的价值。规则是:不能同时选择相邻的两枚硬币。
    目标是在遵守规则的前提下,选出若干枚硬币,使得总价值最大。
    一排硬币随你拿,但不能连着拿,
    怎么拿,钱最多?
    1. 思路:这是一个典型的动态规划问题,核心在于“选或不选”的抉择。

    • F[i] 表示:只考虑前 i 枚硬币时,能得到的最大总价值

    对第 i 枚硬币,有两种选择:

    1. 不选第 i 枚
      • 那最优值就是前 i-1 枚的最优解
      • 价值:F[i-1]
    2. 选第 i 枚
      • 那第 i-1 枚不能选
      • 价值:F[i-2] + value[i]

    取两种情况中的较大值:

    $$
    F[i] = \max(F[i-1],; F[i-2] + value[i])
    $$

    初始条件:

    • F[0] = 0(没有硬币)
    • F[1] = value[1](只有一枚,只能选它)
CoinRow(C[1..n])
F[0] = 0
F[1] = C[1]
for i = 2 to n do
    F[i] = max(C[i] + F[i-2],F[i-1])
return F[n]
def CoinRow(value[1..n]):
    F[0] = 0
    F[1] = value[1]

    for i = 2 to n:
        F[i] = max(F[i-1],F[i-2]+value[i])
    
    return F[n]
  1. 找零问题
  • 一句话: 给定若干种不同面值的硬币,以及一个需要找零的金额 V。
    1. 描述:给定若干种不同面值的硬币,以及一个需要找零的金额 V。
    在每种硬币数量不限的情况下,要求用这些硬币凑出金额 V,并且所用硬币数量最少(或判断是否能找开)。
    有很多面值的硬币,
    想凑出指定金额,
    怎么用得“最省枚数”?
    2. 思路:这是一个标准的 动态规划问题,核心思想是:

    大金额的最优解,可以由小金额的最优解推出来。


    (1)状态定义
    设:

    • dp[i]:表示 凑出金额 i 所需要的最少硬币数

    (2)状态转移

    对每个金额 i,尝试使用一枚面值为 coin 的硬币:

    • 如果 i >= coin,那么可以从金额 i - coin 转移过来:
      $$
      dp[i] = \min(dp[i],; dp[i - coin] + 1)
      $$

    (3)初始条件

    • dp[0] = 0(凑 0 元不需要硬币)
    • 其他 dp[i] 初始化为“无穷大”(表示暂时不可达)
ChangeMaking(D[1..m],n)
F[0] = 0
for i = 1 to n do
    temp = -infintity
    j = 1
    while j <= m and i >= D[j] do
        temp = min(F[i-D[j]],temp)
        ++j
    F[i] = temp + 1
return F[n]
def CoinChange(coins[1..k],V):
    dp[0] = 0
    for i = 1 to V:
        dp[i] = infintity

    for i = 1 to V:
        for each coin in coins:
            if i >= coin:
                dp[i] = min(dp[i],dp[i-coin] + 1)
    if dp[V] == infinity
        return -1 #无法找零
    else:
        return dp[V]
  1. 硬币收集问题
  • 一句话: 在一个 m × n 的网格中,每个格子里可能有一枚硬币(或一个非负价值)。
    1. 描述:在一个 m × n 的网格中,每个格子里可能有一枚硬币(或一个非负价值)。
    一个人从左上角出发,只能向右或向下移动,最终到达右下角。
    目标是在合法移动的前提下,收集到尽可能多的硬币。
    在棋盘上从左上走到右下,
    只能向右或向下,
    怎么走,捡到的硬币最多?
    2. 思路:典型的 二维动态规划 问题,核心思想仍然是:

    到达某个格子的最优结果,只取决于能到它的两个方向。


    (1)状态定义

    设:

    • dp[i][j]:表示 走到第 i 行、第 j 列时,最多能收集到的硬币数

    (2)状态转移

    要到达格子 (i, j),只有两种可能路径:

    • 上方 (i-1, j) 向下走
    • 左方 (i, j-1) 向右走

    因此状态转移方程为:

    $$
    dp[i][j] = \max(dp[i-1][j],\ dp[i][j-1]) + coin[i][j]
    $$


    (3)初始条件

    • dp[0][0] = coin[0][0]
    • 第一行:只能从左往右
    • 第一列:只能从上往下
RobotCoinCollection(C[1..n,1..m])
F[1,1] = C[1,1]
for j = 2 to m do
    F[1,j] = F[1,j-1]+C[1,j]
for i = 2 to n do
    F[i,1] = F[i-1]+C[i,1]
    for j = 2 to m do
        F[i,j] = max(F[i-1,j],F[i,j-1])+C[i,j]
return F[n,m]
def CoinCollect(coin[0..m-1][0..n-1]):
    dp[0][0] = coin[0][0]
    # 第一行
    for j = 1 to n-1:
        dp[0][j] = dp[0][j-1] + coin[0][j]
    # 第一列
    for i = 1 to m-1:
        dp[i][0] = dp[i-1][0] + coin[i][0]

    # 其余格子
    for i = 1 to m-1:
        for j = 1 to n-1:
            dp[i][j] = max(dp[i-1][j],dp[i][j-1]) + coin[i][j]

    return dp[m-1][n-1]
        
  1. 背包记忆化
  • 一句话: 背包记忆化是 0/1 背包问题 的一种解法,把递归(分治)和动态规划结合起来。
    1. 描述:背包记忆化是 0/1 背包问题 的一种解法,把递归(分治)和动态规划结合起来。
    它通过在递归过程中缓存已经算过的子问题结果,避免重复计算,从而把原本指数级的暴力递归,优化到多项式时间。
    递归地“选或不选”,
    算过的结果先存起来,
    下次直接用,不再重算。
    2. 思路:0/1 背包的核心决策永远是:

    对第 i 个物品:装,还是不装?


    (1)状态定义

    设:

    • dp[i][w]:表示 只考虑前 i 个物品、背包容量为 w 时,能得到的最大价值

    (2)递归关系(核心)

    对第 i 个物品(重量 weight[i],价值 value[i]):

    • 如果不选它:
      dp[i-1][w]
    • 如果选它(前提是 w >= weight[i]):
      dp[i-1][w - weight[i]] + value[i]

    取两者最大值:

    $$
    dp[i][w] = \begin{cases} \max(dp[i-1][w],\ dp[i-1][w-weight[i]] + value[i]) & w \ge weight[i] \ dp[i-1][w] & w < weight[i] \end{cases}
    $$


    (3)记忆化的关键

    • 用一个二维表 dp[i][w]
    • 初始值设为 -1(表示“还没算过”)
    • 每次递归前先检查:
      • 算过就直接返回
      • 没算过才继续递归
Function(i,j)
if F[i,j] < 0 then
    if j < Weights[i] then
        value = Function(i-1,j)
    else
        value = max(Function(i-1,j),Values[i]+Function(i-1,j-Weights[i]))
    F[i][j] = value

return F[i][j]
def Knapsack(i,w):
    if i == 0 or w == 0:
        return 0
    if dp[i][w] != -1:
        return dp[i][w]
    if weight[i] > w:
        dp[i][w] = Knapsack(i-1,w)
    else:
        dp[i][w] = max(
            Knapsack(i-1,w),
            Knapsack(i-1,w-weight[i])+value[i]
        )

    return dp[i][w]

# 调用方式
# 初始化 dp[][] = -1
# 答案 = Knapsack(n,W)
  1. 最优二叉查找树
  • 一句话: 最优二叉查找树问题是在已知各个关键字被查找概率的情况下,构造一棵二叉查找树(BST),使得平均查找代价最小。
    1. 描述:最优二叉查找树问题是在已知各个关键字被查找概率的情况下,构造一棵二叉查找树(BST),使得平均查找代价最小。
    这里的“代价”通常指:一次成功或失败查找时,比较次数的期望值。
    不是随便建 BST,
    而是把“常被查的键”放得更靠近根,
    让平均查找更省事。
    2. 思路:这是一个典型的区间型动态规划问题,核心思想是:
    选哪个关键字当根,会决定左右子树的结构和整体代价。
    (1)问题建模
    给定:
    • 有序关键字:
      $$
      K_1 < K_2 < \cdots < K_n
      $$
    • 成功查找概率:
      $$
      p_1, p_2, \ldots, p_n
      $$
    • 失败查找概率(虚拟键):
      $$
      q_0, q_1, \ldots, q_n
      $$
      (2)状态定义
      设:
    • e[i][j]:表示 由关键字 $K_i$ 到 $K_j$ 构成的最优 BST 的最小期望代价
    • w[i][j]:表示 该区间内所有查找概率之和
      (3)状态转移(核心)
      如果选择 K_ri ≤ r ≤ j)作为根:
    • 左子树:e[i][r-1]
    • 右子树:e[r+1][j]
    • 所有结点深度 +1(因此要加上 w[i][j]
      转移方程:
      $$
      e[i][j] = \min_{i \le r \le j} \left( e[i][r-1] + e[r+1][j] + w[i][j] \right)
      $$
      其中:
      $$
      w[i][j] = \sum_{k=i}^{j} p_k + \sum_{k=i-1}^{j} q_k
      $$
      (4)初始条件
    • 空树情况:
      $$
      e[i][i-1] = q_{i-1}
      $$
OptimalBST(P[1..n])
for i = 1 to n do
    C[i,i-1] = 0
    C[i,i] = P[i]
    R[i,i] = i
C[n+1][n] = 0
for d = 1 to n-1 do
    for i=1 to n-d do
        j= i+d
        minval = -infinity
        for k = i to j do
            if C[i,k-1]+C[k+1,j]
            kmin = k
        R[i,j]=kmin
        sum = P[i]
        for s = i + 1 to j do
            sum = sum + P[s]
            C[i,j] = minval+sum
return C[1,n],R
def OptimalBST(p[1..n],q[0..n]):
    for i = 1 to n+1:
        e[i][i-1] = q[i-1]
        w[i][i-1] = q[i-1]

    for length = 1 to n:
        for i = 1 to n-length+1:
            j = i + length - 1
            e[i][j] = infinity
            w[i][j] = w[i][j-1]+p[j]+q[j]

            for r = i to j:
                cost = e[i][r-1]+e[r+1][j]+w[i][j]
                if cost < e[i][j]:
                    e[i][j] = cost

    return e[1][n]
  1. Warshall算法
  • 一句话: Warshall 算法是一种用于有向图的算法,用来计算图的传递闭包。
    1. 描述:Warshall 算法是一种用于有向图的算法,用来计算图的传递闭包。
    图中任意两个顶点之间,是否“存在一条路径”?
    换句话说:
    给你一个有向图,Warshall 算法能算出一个矩阵,明确告诉你
    从 i 能不能走到 j。
    不关心走多远,只关心“能不能走到”。
    1. 思路:Warshall 算法的核心思想是一个逐步放开“中间点”的动态规划
    (1)状态表示
    用一个布尔矩阵 R 表示可达性:
    • R[i][j] = 1:从顶点 i 能到达顶点 j
    • R[i][j] = 0:不能到达
      初始时:
    • 如果图中有一条边 i → j,则 R[i][j] = 1
    • 通常也令 R[i][i] = 1(自己可达自己)
      (2)核心思想(非常重要)
      允许使用的“中间顶点”,一开始是空的,
      然后逐个把顶点 1、2、…、k 加进来。

      当考虑第 k 个顶点时,判断:
      ij
      是否可以 先到 k,再从 k 到 j
      如果可以,就说明 i → j 是可达的。
      (3)状态转移(关键公式)
      $$
      R[i][j] = R[i][j] ;\lor; (R[i][k] \land R[k][j])
      $$
      意思是:
    • 原来能到:保留
    • 原来到不了,但 能到 k 且 k 能到 j:现在也能到
Warshall(A[1..n,1..n])
R0 = A
for k = 1 to n do
    for i = 1 to n do
        fo j = 1 to n do
            Rk[i,j] = Rk-1[i,j] or (Rk-1[i,k] and Rk-1[k,j])

return Rn
def Warshall(R[1..n][1..n]):
    for k = 1 to n:
        for i = 1 to n:
            for j = 1 to n:
                R[i][j] = R[i][j] or (R[i][k] and R[k][j])
  1. Floyd算法
  • 一句话: Floyd 算法是一种用于带权图(可含负权,但不能有负权回路)的算法,用来求 任意两点之间的最短路径。
    1. 描述:Floyd 算法是一种用于带权图(可含负权,但不能有负权回路)的算法,用来求 任意两点之间的最短路径。
    它一次运行,就能得到图中 所有顶点对 (i, j) 的最短距离。
    一次性算清楚:
    任意两点之间,走哪条路最短。
    1. 思路:Floyd 算法的本质是一个三维动态规划思想,和刚学的 Warshall 算法一脉相承,只不过:
    • Warshall:关心 能不能到
    • Floyd:关心 走多远最短
      (1)状态定义
      用一个距离矩阵 D
    • D[i][j]:表示 从顶点 i 到顶点 j 的当前最短距离
      初始化:
    • 若存在边 i → j,则 D[i][j] = w(i, j)
    • 若不存在边,D[i][j] = +∞
    • D[i][i] = 0
      (2)核心思想(非常重要)
      逐步放宽“允许作为中转的顶点集合”。
      一开始:
    • 不允许任何中间点
    • D[i][j] 只表示“直接走”
      然后依次:
    • 允许顶点 1 作为中转
    • 允许顶点 1, 2 作为中转
    • ……
    • 最后允许所有顶点作为中转
      (3)状态转移方程(核心公式)
      当考虑顶点 k 作为中转点时:
      $$
      D[i][j] = \min\big(D[i][j],; D[i][k] + D[k][j]\big)
      $$
      含义是:
    • 原来 i → j 的路
    • 和 “i → k → j” 这条新路
      谁更短就用谁
Floyd(W[1..n,1..n])
D = W
for k = 1 to n do
    for i = 1 to n do
        for j = 1 to n do
            D[i,j] = min{D[i,j],D[i,k]+D[k,j]}
return D
def Floyd(D[1..n][1..n]):
    for k = 1 to n:
        for i = 1 to n:
            for j = 1 to n:
                if D[i][k] + D[k][j] < D[i][j]:
                    D[i][j] = D[i][k] + D[k][j]

return D
  1. 最小生成树Prim算法
  • 一句话: 从任意一个顶点出发,每一步选择一条“连接已选集合与未选集合的最小权边”,逐步把所有顶点连成一棵权值之和最小的树。
    1. 描述:从任意一个顶点出发,每一步选择一条“连接已选集合与未选集合的最小权边”,逐步把所有顶点连成一棵权值之和最小的树。
    从一个点起步,
    每次把“离我最近的那个新点”拉进来,
    直到所有点都连上。
    1. 思路:Prim 的核心是“扩展一棵树”,而不是“选边成森林”。
    维护两个集合:

    S:已经加入生成树的顶点

    V−S:还没加入的顶点

    初始:任选一个顶点放入 S

    重复执行直到 S 包含所有顶点:

    在所有一端在 S、另一端在 V−S 的边中

    选择权值最小的那一条

    把这条边和对应的新顶点加入 S

    这一步之所以正确,是因为 MST 的切分性质:

    任意切分中,跨越切分的最小权边一定属于某棵最小生成树。

Prim(G)
Vt = {V0}
Et = 空
for i = 1 to |V| - 1 do
    在所有的边(v,u)中,求权重最小的边e* = (v*,u*)
    使得v在Vt中,而u在V-Tt中
    Vt = VT ∪ {u*}
    Et = Et ∪ {e*}
return Et
def Prim(G):
    任选顶点 s
    S = {s}
    while |S| < |V|:
        从所有(u 属于 S,v 不属于 S)的边中选权值最小的边(u,v)
        S = S ∪ {v}
        记录边(u,v)
  1. 最小生成树Kruskal算法
  • 一句话: Kruskal 算法是一种在连通无向带权图中构造最小生成树(MST)的贪心算法。
    1. 描述:Kruskal 算法是一种在连通无向带权图中构造最小生成树(MST)的贪心算法。
    它的做法是:按边的权值从小到大排序,依次选择边加入生成树,只要这条边不会形成回路,就把它加入,直到选出 $n-1$ 条边为止。
    先把边按小到大排好,
    能连就连,
    一旦成环就跳过。
    2. 思路:Kruskal 的核心思想是“从边出发,逐步合并森林”。
    • 初始状态:
      • 每个顶点都是一棵独立的树(森林)
    • 把所有边按权值升序排序
    • 依次扫描排序后的边 (u, v)
      • 如果 uv 属于不同的连通分量
        加入这条边(不会形成回路)
      • 如果 uv 已在同一分量
        跳过(否则会形成回路)
    • 重复,直到选出 |V| - 1 条边
      判断“是否在同一连通分量”,通常用 并查集(Union–Find) 实现。
Kruskal(G)
按照边权重非递减顺序对集合E排序
Et = 空
ecounter = 0 //初始化已处理边数量
while ecounter < |V| - 1 do
    ++k
    if Et ∪ {eik} 无回路
        Et = Et ∪ {eik}
        ++ecounter
return Et
def Kruskal(G):
    MST = 空集
    对所有边按权值升序排序
    初始化并查集(每个顶点一个集合)

    for each edge(u,v) in 排序后的边:
        if Find(u) != Find(v):
            MST = MST ∪ {(u,v)}
            Union(u,v)
        if |MST| == |V| - 1:
            break
  1. 最短路径Dijkstra算法
  • 一句话: Dijkstra 算法是一种用于带权有向图或无向图的最短路径算法,用来求从某一个源点出发,到其他所有顶点的最短路径长度。
    1. 描述:Dijkstra 算法是一种用于带权有向图或无向图的最短路径算法,用来求从某一个源点出发,到其他所有顶点的最短路径长度。
    前提条件是:边权必须是非负数。
    从起点出发,
    每次确认“当前最近的那个点”,
    再用它去更新别人的距离。
    2. 思路:反复做三件事:
    • 选一个当前“离源点最近、还没确定”的顶点
    • 确定它的最短距离
    • 用它去更新相邻顶点的距离
    • 直到所有顶点都被确定。
DijStra(G,s)
Init(Q) // 顶点优先队列初始化为空
for V 中每个顶点
    dv = infinity
    pv = null
    Insert(Q,v,dv)//初始化优先队列顶点优先级

ds = 0
Decrease(Q,s,ds) //将s的优先级更新为ds
Vt = 空
for i = 0 to |V| - 1 do
    u* = DeleteMin(Q) //删除优先级最小的元素
    Vt = Vt ∪ {u*}
    for V - Vt 中每一个和u*相邻的顶点 v do
        if du* + W(u*,v) < dv then
            dv = du* + W(u*,v)
            pv = u*
            Decrease(Q,v,dv)
def Dijkstra(G,s):
    for each vertex v in G:
        dist[v] = infinity
        visited[v] = false
    dist[s] = 0

    for i = 1 to |V|:
        u = 未访问顶点中dist最小的
        visited[u] = true

        for each edge (u,v) with weight w:
            if not visited[v] and dist[u] + w < dist[v]:
                dist[v] = dist[u] + w 

    return dist
  1. 单源最短路径Bellman-Ford
  • 一句话: Bellman–Ford 算法是一种用于带权有向图或无向图的最短路径算法,用来求从某一个源点出发,到其他所有顶点的最短路径长度。
    1. 描述:Bellman–Ford 算法是一种用于带权有向图或无向图的最短路径算法,用来求从某一个源点出发,到其他所有顶点的最短路径长度。
    它允许存在负权边,并且还能检测图中是否存在负权回路。
    从起点出发,
    不断尝试用“多走一条边”的方式更新最短距离,
    最终得到所有点的最短路径;
    如果路径还能无限变短,则说明存在负权回路。
    2. 思路:反复做两件事:
    对所有边进行松弛(尝试更新距离)
    重复这个过程 |V|−1 次
    具体来说:
    第 1 轮:尝试用 1 条边 到达各点
    第 2 轮:尝试用 2 条边 到达各点
    ……
    第 |V|−1 轮:尝试用 最多 |V|−1 条边 到达各点
    如果在完成 |V|−1 轮之后,
    还能继续让某个距离变小,
    说明图中存在 负权回路,最短路径不存在。
def BellmanFord(G,s):
    for each vertex v:
        dist[v] = infinity
    dist[s] = 0

    # 重复 |v| - 1 轮
    for i = 1 to |V| - 1:
        for each edge (u,v,w) in G:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w

    # 检测负权回路
    for each edge (u,v,w) in G:
        if dist[u] + w < dist[v]:
            report "存在负权回路"
            return

    return dist
  1. 平分法求方程$x^3+x-1=0$的根
  • 一句话: 平分法(又叫二分法)是一种用来求连续函数方程根的数值方法。
    1. 描述:平分法(又叫二分法)是一种用来求连续函数方程根的数值方法。
    对方程 $x^3 + x - 1 = 0$,令 $f(x)=x^3+x-1$。如果在区间 $[a,b]$ 上满足 $f(a)$ 与 $f(b)$ 异号,就说明区间内至少有一个根。平分法每次取中点,把区间砍半,并保留仍然“夹着根”的那一半,直到精度足够。
    从一个“夹住根”的区间开始,
    每次看中点,
    不对就砍掉一半,
    区间越缩越小,根就被逼出来。
    2. 思路:
    • 设 $f(x)=x^3+x-1$,先找一个区间 $[a,b]$ 使得 $f(a)\cdot f(b)<0$。
      例如:
    • $f(0)= -1$
    • $f(1)= 1$
      所以根在 $[0,1]$ 里。
    • 循环执行:
      • 取中点 $m=\frac{a+b}{2}$
      • 如果 $f(m)=0$(或足够接近 0)就结束
      • 否则看哪一边仍然异号:
        • 若 $f(a)\cdot f(m)<0$,根在 $[a,m]$,令 $b=m$
        • 否则根在 $[m,b]$,令 $a=m$
    • 当区间长度 $(b-a)$ 小于给定误差 $\varepsilon$ 时,返回中点作为近似根。
do
    mid = (a+b)/2
    t3= f(mid)
    t1 = f(a)
    t2 = f(b)
    if t1*t3 > 0 then
        a = mid
    else
        b = mid
while fabs(t3) > le-2
return t3
def BisectionSolve(eps):
    f(x) = x^3 + x - 1
    a = 0,b = 1 #因为 f(0) < 0,f(1)>0

    while (b-a) > eps:
        m = (a+b) /2
        if f(m) == 0:
            return m
        if f(a) * f(m) < 0:
            b = m
        else;
            a = m
        
    return (a/b)/2
  1. 试位法求方程$x^3+x-1 = 0$的根
  • 一句话: 试位法(假位法)是一种求方程根的数值方法。
    1. 描述:试位法(假位法)是一种求方程根的数值方法。
    对 $f(x)=x^3+x-1$,先找一段区间 $[a,b]$ 使得 $f(a)$ 与 $f(b)$ 异号,从而“夹住”根。然后用 连接两端点 $(a,f(a))$、$(b,f(b))$ 的直线 与 x 轴的交点作为新的近似根,再用这个点把区间更新为仍然异号的那一半,重复直到精度足够。
    先把根夹在两点之间,
    然后用两点连线去“猜”根的位置(x 轴交点),
    再继续夹住根,越猜越准。

    1. 思路:

    1. 设 $f(x)=x^3+x-1$,先找异号区间。比如:$f(0)=-1$,$f(1)=1$ 根在 $[0,1]$。
    2. 用直线插值求“试位点” $c$:
      $$
      c=\frac{a,f(b)-b,f(a)}{f(b)-f(a)}
      $$

    (这就是直线过两点与 x 轴的交点)

    1. 计算 $f(c)$,并更新区间:
    • 若 $f(a)\cdot f(c)<0$,根在 $[a,c]$,令 $b=c$
    • 否则根在 $[c,b]$,令 $a=c$
    1. 重复直到满足精度条件(比如 $|f(c)|<\varepsilon$ 或 $|b-a|<\varepsilon$)。
do
    x = (a*f(b)-b*f(a) / f(b)-f(a))
    y = f(x)
    if y*fa > 0 then
        a= x
        fa = y
    else
        b = x
        fb = y
while fabs(y) > eps
return x
def FalsePositionSolve(eps):
    f(x) = x^3 + x - 1
    a = 0,b = 1 #f(a)<0,f(b) > 0

    while True:
        c = (a*f(b) - b*f(a)/(f(b)-f(a)))
        if abs(f(c)) < eps:
            return c

        if f(a) * f(c) < 0:
            b = c
        else:
            a = c
  1. 牛顿法求方程$x^3+x-1 = 0$的根
  • 一句话: 牛顿法是一种求方程根的数值方法。
    1. 描述:牛顿法是一种求方程根的数值方法。它从一个初始猜测 $x_0$ 出发,在曲线 $y=f(x)$ 上取点 $(x_k, f(x_k))$,作该点的切线,用切线与 x 轴的交点作为新的近似值 $x_{k+1}$。不断重复,就能快速逼近方程的根(通常收敛很快)。
    对本题令 $f(x)=x^3+x-1$,牛顿法就是:
    用切线来“预测”根的位置,越迭代越接近。
    先随便猜一个根,
    用切线往 x 轴上一投影得到新猜测,
    重复几次通常就很准。

    2. 思路:
    1. 先写出函数与导数:
      $$
      f(x)=x^3+x-1,\qquad f'(x)=3x^2+1
      $$
    2. 选择一个初值 $x_0$(比如在根所在区间 $[0,1]$ 内选 1 或 0.5 都行)。
    3. 迭代更新公式(牛顿法核心):
      $$
      x_{k+1} = x_k - \frac{f(x_k)}{f'(x_k)} = x_k - \frac{x_k3+x_k-1}{3x_k2+1}
      $$
    4. 停止条件(满足任意一个即可):
    • $|x_{k+1}-x_k|<\varepsilon$
    • 或 $|f(x_k)|<\varepsilon$
f1为原方程,f2为其导数
do 
    x = x0-f1(x0)/f2(x0)
    x0 = x
while fabs(f1(x)>eps)
return x
def NewtonSolve(eps):
    f(x) = x^3 + x - 1
    df(x) = 3*x^2 + 1

    x = 1.0 #初值,可选1或0.5
    while True:
        x_new = x - f(x) / df(x)
        if abs(x_new - x) < eps:
            return x_new
        x = x_new
  • 牛顿法很快,但更“挑初值”:初值太离谱可能不收敛。
  • 本题 $f'(x)=3x^2+1$ 永远 > 0,所以不用担心导数为 0(这点很好)。
  1. 贪心法求田忌赛马问题
  • 一句话: 田忌赛马问题中,田忌和齐王各有 n 匹马,每匹马都有不同的速度。
    1. 描述:田忌赛马问题中,田忌和齐王各有 n 匹马,每匹马都有不同的速度。比赛规则是:
      每一轮各出一匹马,速度快的一方获胜。田忌可以自由安排自己出马的顺序,而齐王的出马顺序已知(或等价于已排序)。目标是:通过合理安排出马顺序,使田忌获胜的场次数最多。
      不是拼总速度,
      而是拼“怎么对位”,
      用聪明的安排赢更多局。
    2. 思路:田忌赛马的经典解法是一个双指针贪心算法,核心原则是:
      能赢就赢,
      赢不了就用最慢的去“送”。
      具体做法:
      1. 双方马匹按速度从快到慢排序。
      2. 用两个指针分别指向田忌和齐王的:
        • 最快马
        • 最慢马
      3. 重复比较:
        • 如果田忌的最快马快于齐王的最快马:
          直接对决,田忌赢一局(最快对最快)
        • 否则(赢不了最快):
          用田忌的最慢马去对齐王的最快马
          这一局大概率输,但保留了中快马去赢别的局
      4. 直到所有马都比完。
def TianJiRace(T[1..n],Q[1..n]):
    sort T descending
    sort Q descending
    
    t_fast = 1,t_slow = n
    q_fast = 1,q_slow = n
    win_cnt = 0

    while t_fast <= t_slow:
        if T[t_fast] > Q[q_fast]:
            win += 1
            t_fast += 1
            q_fast += 1
        else:
            t_slow -= 1
            q_fast += 1

    return win_cmt
  1. 用来生成排列的Johnson-Trotter算法(相邻交换生成排列)
    1. 描述:Johnson–Trotter 算法是一种生成全排列的算法,保证相邻两个生成的排列只通过交换一对相邻元素得到(相邻交换)。它按一定顺序枚举所有 n! 个排列,适合需要逐步遍历或在线生成排列的场景。
    2. 思路:从初始排列 1,2,...,n 开始,给每个元素一个方向(向左或向右)。每一步选择当前所有“可移动元素”中数值最大的那个(即其箭头指向的位置存在且指向的元素比它小),把它与箭头指向的相邻元素交换;然后将所有比该元素大的元素的方向反转。重复上述过程直到不存在可移动元素。这样可以保证:
    3. 数据结构约定:perm[1..n]:当前排列(存 1..n)
    • dir[x]:元素 x 的方向
    • -1 表示向左(←)
    • +1 表示向右(→)
    • pos[x]:元素 x 在 perm 中的位置(下标)
将第一个排列初始化为带左方向的标志 1,2..n
while 存在一个可移动元素 do
    求最大移动元素 k
    把 k 和它箭头指向元素互换
    调转所有大于k 的元素的方向
    将新排列添加到列表中

// 辅助函数,判断元素是否可移动
IS_mobile(x,perm,ps,dir,n)
p = pos[x]
next = p + dir[x]
if next < 1 or next > n then
    return false
y = perm[next]
if y < x then
    return true
else
    return false
// 主算法
Johnson_trotter(n)
// 初始化排列 1..n,全部方向向左
for i = 1 to n do
    perm[i] = i
    pos[i] = i
    dir[i] = -1

output perm

while true do
    // 1.找到最大可移动元素 k
    k = 0
    for x = 1 to n do
    if Is_mobile(x,perm,pos,dir,n) and x > k then
        k = x
    if k == 0 then
        break
    
    // 2.让k 沿着箭头方向移动一步(与相邻元素交换)
    p = pos[k]
    q = p + dir[k] //k 要去的位置
    t = perm[q]  // 与 k 交换的那个元素(相邻元素)

    swap(perm[p],perm[q])

    // 交换后更新位置表
    pos[k] = q
    pos[t] = p

    // 3.反转所有大于 k 的元素方向
    for x = k + 1 to n do
        dir[x] = -dir[x]

    output perm

  1. 韩信点兵(中国剩余定理的经典模型)
  • 一句话: “韩信点兵”问题描述的是这样一类情形:。
    1. 描述:“韩信点兵”问题描述的是这样一类情形:
    一支军队人数不详,但按不同的数目分组时,余数是已知的。例如:
    三人一组余 2,五人一组余 3,七人一组余 2。
    问题是:这支军队最少有多少人?
    不知道总人数,
    只知道“怎么分都剩多少”,
    反过来把总人数算出来。
    2. 思路:韩信点兵的核心思想是:
    找一个数,使它同时满足多条“取余条件”。
    这正是数学中 中国剩余定理(CRT) 的应用场景。
    基本解题思路:
    3. 把问题写成“同余式”:
    • 若“三人一组余 2”,写成$x \equiv 2 \pmod{3}$
    • 若“五人一组余 3”,写成$x \equiv 3 \pmod{5}$
    • 若“七人一组余 2”,写成$x \equiv 2 \pmod{7}$
  1. 从满足其中一个条件的数开始,按步长逐个试
    • 先列出满足第一个条件的数
    • 再筛掉不满足第二个条件的
    • 最后筛掉不满足第三个条件的
  2. 第一个同时满足所有条件的正整数,就是“最少兵力”。
def HanXinOrder(m[1..k],r[1..k]):
    x = r[1]
    step = m[1]

    for i = 2 to k:
        while x % m[i] != r[i]:
            x = x + step
        step = step * m[i] #扩大步长,保持已满足条件

    return x
posted @ 2026-01-10 00:58  ttkqwe  阅读(9)  评论(0)    收藏  举报