2026-01-10-算法分析部分编程题整理-期末复习
- 欧几里得算法求最大公约数
- 一句话: 给定两个正整数 m、n,利用“两个数的最大公约数等于较大数与较小数取余后的最大公约数”的性质,反复取余,最终得到最大公约数(GCD)。
- 描述:给定两个正整数 m、n,利用“两个数的最大公约数等于较大数与较小数取余后的最大公约数”的性质,反复取余,最终得到最大公约数(GCD)。
- 思路:循环执行 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
- 连续整数检测算法求最大公约数
- 一句话: 从较小的数开始,依次尝试每个可能的公约数(从大到小),找到第一个同时整除 m 和 n 的整数,该整数就是最大公约数。
- 描述:从较小的数开始,依次尝试每个可能的公约数(从大到小),找到第一个同时整除 m 和 n 的整数,该整数就是最大公约数。
- 思路:令 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
- 素数筛算法
- 一句话: 给定整数 n,找出 2 到 n 范围内的所有素数。
- 描述:给定整数 n,找出 2 到 n 范围内的所有素数。素数筛(埃拉托色尼筛)通过“标记合数”的方式一次性筛掉每个素数的倍数。
- 思路:先把 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
- 矩阵乘积
- 一句话: 计算两个 n×n 矩阵 A 和 B 的乘积矩阵 C,其中 C[i][j] 等于 A 的第 i 行与 B 的第 j 列对应元素乘积之和。
- 描述:计算两个 n×n 矩阵 A 和 B 的乘积矩阵 C,其中 C[i][j] 等于 A 的第 i 行与 B 的第 j 列对应元素乘积之和。
- 思路:用三重循环实现定义式:外层枚举行 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
- 汉诺塔(A->c)
- 一句话: 在三根柱子 A、B、C 上,将 A 上的 n 个大小不同的盘子全部移动到 C 上,每次只能移动一个盘子,并且任何时刻大盘不能放在小盘之上。
- 描述:在三根柱子 A、B、C 上,将 A 上的 n 个大小不同的盘子全部移动到 C 上,每次只能移动一个盘子,并且任何时刻大盘不能放在小盘之上。
- 思路:要将 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)
- 选择排序
- 一句话: 对一个无序序列,从左到右逐个位置进行处理,每次在未排序的部分中选出最小的元素,与当前位置的元素交换,直到所有元素有序。
- 描述:对一个无序序列,从左到右逐个位置进行处理,每次在未排序的部分中选出最小的元素,与当前位置的元素交换,直到所有元素有序。
- 思路:从第一个位置开始,假设它是最小值,然后在后面的元素中寻找真正的最小值,若找到更小的就更新最小值位置,最后将最小值与当前起始位置交换;接着对后续位置重复该过程。
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])
- 冒泡排序
- 一句话: 对一个无序序列,反复比较相邻的两个元素,如果顺序不正确就交换,使较大的元素逐步“冒泡”到序列的末尾,直到整个序列有序。
- 描述:对一个无序序列,反复比较相邻的两个元素,如果顺序不正确就交换,使较大的元素逐步“冒泡”到序列的末尾,直到整个序列有序。
- 思路:每一趟从序列开头开始,依次比较相邻元素,将较大的元素向后交换;一趟结束后,当前最大的元素一定被放到了末尾,重复该过程,逐步缩小未排序的范围。
- 常规
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
- 字符串蛮力匹配
- 一句话: 在主串(文本)中从左到右依次选择一个起始位置,把模式串逐字符与主串对应位置比较;
- 描述:在主串(文本)中从左到右依次选择一个起始位置,把模式串逐字符与主串对应位置比较;如果某一位不相等,就把模式串整体右移一位,重新从模式串第一个字符开始比较,直到匹配成功或主串剩余长度不够为止。
- 思路:把模式串当作一个“窗口”在主串上滑动:从位置 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
- 蛮力平面距离最近两点
- 一句话: 给定平面上 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. 描述:深度优先搜索是一种图(或树)的遍历方法,从某个起始顶点出发,沿着一条路径不断向“更深处”访问相邻的未访问顶点,直到走不下去为止,再回退到上一个顶点,继续探索其他未访问的分支,直到所有可达顶点都被访问。
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. 描述:广度优先搜索是一种图(或树)的遍历方法,从起始顶点出发,先访问所有与其直接相邻的顶点,再依次访问这些顶点的未访问邻居,按“由近到远、逐层扩展”的顺序遍历图中所有可达顶点。
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. 描述:插入排序是一种简单直观的排序方法,将序列分为“已排序区”和“未排序区”,每次从未排序区中取出一个元素,把它插入到已排序区中合适的位置,使已排序区始终保持有序,直到所有元素都插入完成。
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
- 拓扑排序
- 一句话: 拓扑排序是针对有向无环图(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 "图中存在回路,无法进行拓扑排序"
先选没前驱的点,删掉它,再重复。
- 生成排列算法
- 一句话: 给定 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)
- 反射格雷码
- 一句话: 排出所有 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. 描述:有序序列中查找指定元素。每次将查找区间一分为二,通过比较目标值与中间元素的大小,决定下一步只在左半部分或右半部分继续查找,从而不断缩小查找范围,直到找到目标元素或区间为空为止。
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. 描述:俄式乘法是一种不用“逐位乘法”的整数乘法方法,通过不断把一个数减半、另一个数加倍,并在过程中只在特定条件下累加结果,最终得到两个整数的乘积。
它本质上是把乘法转化为加法 + 位运算(折半)。
2. 思路:设要计算 $a \times b$。重复执行下面的过程,直到 a = 0:- 如果 a 是奇数,就把当前的 b 加到结果中
- 把 a 整除 2(向下取整)
- 把 b 乘以 2
之所以可行,是因为任意整数都可以表示成若干个 2 的幂之和(二进制思想),而“奇偶判断 + 折半”正是在逐位处理 a 的二进制表示。

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
- 约瑟夫环问题
- 一句话: 一圈人轮流报数,报到固定数字就出局,最后活下来的是谁?
- 描述:一圈人轮流报数,报到固定数字就出局,最后活下来的是谁?
- 思路:把问题看成一个“缩小规模的循环问题”。
当第一个人被淘汰后,剩下的 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. 描述:在有序数组中查找元素的方法(或在单峰函数上找极值的方法)。这里先说最常见的“在有序数组中查找”:
每次把当前区间分成 三段,通过比较目标值与两个分割点的值,判断目标可能落在哪一段,然后把搜索范围缩小到那一段,直到找到目标或区间为空。
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
- Lomuto划分
- 一句话: Lomuto 划分是快速排序中的一种划分方法。
- 描述:Lomuto 划分是快速排序中的一种划分方法。它选取区间末尾元素作为枢轴(pivot),通过一次线性扫描,把数组重排为三段结构:
小于等于枢轴 | 枢轴 | 大于枢轴。
划分完成后,枢轴被放到它在最终有序数组中的正确位置。 - 思路:设当前处理区间为 [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
- 描述:Lomuto 划分是快速排序中的一种划分方法。它选取区间末尾元素作为枢轴(pivot),通过一次线性扫描,把数组重排为三段结构:
这样一次扫描就完成了划分。
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 #枢纽最终位置
- 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])
- 快速选择
- 一句话: 快速选择是一种在无序数组中查找第 k 小(或第 k 大)元素的算法。
1. 描述:快速选择是一种在无序数组中查找第 k 小(或第 k 大)元素的算法。
它的目标不是把整个数组排好序,而是只定位一个元素的最终位置,因此通常比完整排序更快。
1. 思路:快速选择借鉴了快速排序的“划分”思想(通常用 Lomuto 或 Hoare 划分):- 在当前区间选择一个枢轴(pivot),对数组进行一次划分
• 左边都是 ≤ pivot
• 右边都是 ≥ pivot
• pivot 被放在一个确定的位置 p - 比较 p 和目标下标 k-1:
• 如果 p == k-1:找到了第 k 小元素,直接返回
• 如果 p > k-1:第 k 小元素一定在左半区间,只递归左边
• 如果 p < k-1:第 k 小元素一定在右半区间,只递归右边 - 不断缩小区间,直到命中目标位置
关键点在于:
每一轮只递归“一边”,而不是两边。
这正是它比快速排序更快的原因。
- 在当前区间选择一个枢轴(pivot),对数组进行一次划分
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)
- 快速排序
- 一句话: 选择一个基准元素(pivot),将待排序序列划分为两部分:。
1. 描述:选择一个基准元素(pivot),将待排序序列划分为两部分:
一部分元素小于(或等于)基准,另一部分元素大于(或等于)基准,
然后对这两部分分别递归地进行排序,最终得到有序序列。
2. 思路:- 选择基准元素(pivot)
从序列中选取一个元素作为基准(常见做法是选第一个、最后一个或随机一个)。 - 划分(Partition)
通过一次扫描,将序列重新排列,使得:
• 基准左边的元素都不大于基准
• 基准右边的元素都不小于基准
此时基准元素处在其最终正确位置上。 - 递归排序
对基准左、右两部分子序列分别重复上述过程,直到子序列长度为 0 或 1。
- 选择基准元素(pivot)
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. 描述:待排序序列不断划分为若干个规模更小的子序列,直到每个子序列只包含一个元素(天然有序),
然后再将这些有序子序列逐步合并成一个完整的有序序列。
先拆到不能再拆,再把有序的小块合并成大块。
2. 思路:- 分解(Divide)
将原序列从中间划分为左右两个子序列,并递归地对左右子序列进行归并排序。 - 合并(Conquer / Merge)
将两个已经排好序的子序列,按照大小顺序合并成一个新的有序序列。
当子序列长度为 1 时,递归结束。
- 分解(Divide)
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]
- 归并排序Merge
- 一句话: 将两个已经有序的子序列合并为一个新的有序序列。
1. 描述:将两个已经有序的子序列合并为一个新的有序序列。
Merge 操作直接决定了归并排序的正确性和效率。
2. 思路:- 分别用两个指针指向两个有序子序列的起始位置
- 比较两个指针所指的元素
- 将较小的元素放入辅助数组中,并移动对应指针
- 重复上述过程,直到其中一个子序列遍历完成
- 将另一个子序列中剩余的元素依次复制到辅助数组中
最后将辅助数组中的结果拷贝回原序列。
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. 描述:二叉树遍历是指按照一定的顺序,依次访问二叉树中所有结点,使得每个结点被访问且只被访问一次的过程。
根据访问根结点的先后顺序不同,二叉树的遍历方式主要包括:
• 先序遍历(根 → 左 → 右)
• 中序遍历(左 → 根 → 右)
• 后序遍历(左 → 右 → 根)
此外,还可以按层次从上到下、从左到右访问结点,称为层序遍历。
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)
- 高斯消去法
- 一句话: 高斯消去法(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. 描述:构造堆是指将一个无序的线性序列,通过一定的方法,调整为满足堆性质的完全二叉树的过程。
根据堆中结点关键字的大小规律不同,堆可分为:
• 大根堆(最大堆):任一结点的关键字 ≥ 其左右孩子
• 小根堆(最小堆):任一结点的关键字 ≤ 其左右孩子
构造堆是堆排序和优先队列实现中的基础操作。
2. 思路:构造堆通常采用自底向上调整(Heapify)的方法,基本思路如下:- 将待处理序列看作一棵完全二叉树
- 从最后一个非叶子结点开始,向前逐个对结点进行下沉调整
- 对每个结点,将其与左右孩子中更符合堆性质的那个进行比较
- 若不满足堆性质,则交换,并继续向下调整
- 重复上述过程,直到根结点调整完成
最终,整个序列就满足堆的性质。
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. 描述:霍纳法则是一种高效计算多项式值的方法。
它通过把多项式改写成“嵌套乘法”的形式,显著减少乘法和加法的次数,从而更快、更稳定地计算多项式在某个给定点上的值。
把多项式“从里往外算”,
每一步只做一次乘法、一次加法。
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)
$$
这样计算时:- 先从最高次系数 $a_n$ 开始
- 每一步:
- 当前结果乘以 $x$
- 再加上下一个系数
- 重复,直到常数项
整个过程只需要:
- $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
- 从左至右二进制幂
- 一句话: 从左至右二进制幂是一种高效计算 $a^e$ 的方法。
-
描述:从左至右二进制幂是一种高效计算 $a^e$ 的方法。
它把指数 $e$ 写成二进制,从最高位到最低位依次处理,通过“平方”和在必要时“乘一次底数”,在 $O(\log e)$ 的时间内完成幂运算。
先把指数写成二进制,
从最高位开始,每走一位先平方,
遇到 1 再乘一次底数。 -
思路:设指数 $e$ 的二进制表示为 $b_{k-1}b_{k-2}\cdots b_0$。
算法从最高位 $b_{k-1}$ 开始:- 初始化
result = 1 - 对每一位(二进制位)从左到右:
- 先做一次:result = result * result(平方,表示“向下一位推进”)
- 如果当前位是1:再做一次result = result * a - 所有位处理完后,
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
- 从右至左二进制幂
- 一句话: 从指数 $e$ 的最低位开始处理,通过反复“判断奇偶 + 平方 + 折半”,在 $O(\log e)$ 时间内完成幂运算,是实际编程中最常用的快速幂写法。
1. 描述:从指数 $e$ 的最低位开始处理,通过反复“判断奇偶 + 平方 + 折半”,在 $O(\log e)$ 时间内完成幂运算,是实际编程中最常用的快速幂写法。
2. 思路:设要计算 $a^e$,维护两个变量:result:当前结果,初始为 1base:当前“有效底数”,初始为 $a$
每一轮循环:
- 若
e是奇数(最低位是 1),则
result = result * base base = base * base(为处理下一位做准备)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. 思路:给定数组A[0..n-1],再准备一个计数数组C[0..n-1]:- 初始化
C[i] = 0,表示A[i]前面应有 0 个元素** - 对每一对元素
(A[i], A[j])(i < j)进行比较:- 如果
A[i] < A[j],说明A[j]前面应该多一个元素:
C[j]++ - 否则(
A[i] >= A[j]),说明A[i]前面应该多一个元素:
C[i]++
- 如果
- 比较完成后,
C[i]就表示A[i]在有序数组中的位置 - 按
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. 思路:假设待排序数组为A[0..n-1],元素的取值范围是[0..k](或能映射到这个范围)。
排序过程分三步:- 计数
用计数数组C[0..k],统计每个值出现的次数:C[v] = 值 v 在 A 中出现的次数 - 前缀和(定位)
把C改造成前缀和数组,使得:C[v] = 小于等于 v 的元素个数
这样就能知道“值为 v 的元素,在结果数组中应该放到哪里结束”。 - 回填(分布)
从右向左扫描原数组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. 描述:填充移动表是为字符串匹配提前准备的一张表,用来记录:
当模式串某个字符与文本不匹配时,模式串应该向右移动多少位。
这张表通常称为 坏字符表(bad-character table)。
提前记住:
每个字符如果“撞坏了”,
模式串该往右挪几步。
1. 思路:设模式串为P,长度为m。核心思想只有三步:
- 先假设最坏情况
- 如果某个字符在模式串里根本不存在
- 那就直接把模式串整体右移
m位
- 再修正模式串中出现过的字符
- 对于模式串中每个字符
P[i] - 记录:
当它在位置
i发生不匹配时,
模式串至少要移动m - 1 - i位
- 对于模式串中每个字符
- 取“最右出现位置”
- 同一个字符如果出现多次
- 只保留最靠右那次(移动最小,最安全)
- 先假设最坏情况
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
- 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. 思路:这是一个典型的动态规划问题,核心在于“选或不选”的抉择。F[i]表示:只考虑前 i 枚硬币时,能得到的最大总价值
对第
i枚硬币,有两种选择:- 不选第 i 枚
- 那最优值就是前
i-1枚的最优解 - 价值:
F[i-1]
- 那最优值就是前
- 选第 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]
- 找零问题
-
一句话: 给定若干种不同面值的硬币,以及一个需要找零的金额 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]
- 硬币收集问题
-
一句话: 在一个 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]
- 背包记忆化
-
一句话: 背包记忆化是 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)
- 最优二叉查找树
- 一句话: 最优二叉查找树问题是在已知各个关键字被查找概率的情况下,构造一棵二叉查找树(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_r(i ≤ 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]
- Warshall算法
- 一句话: Warshall 算法是一种用于有向图的算法,用来计算图的传递闭包。
1. 描述:Warshall 算法是一种用于有向图的算法,用来计算图的传递闭包。
图中任意两个顶点之间,是否“存在一条路径”?
换句话说:
给你一个有向图,Warshall 算法能算出一个矩阵,明确告诉你
从 i 能不能走到 j。
不关心走多远,只关心“能不能走到”。
1. 思路:Warshall 算法的核心思想是一个逐步放开“中间点”的动态规划。
(1)状态表示
用一个布尔矩阵R表示可达性:R[i][j] = 1:从顶点i能到达顶点jR[i][j] = 0:不能到达
初始时:- 如果图中有一条边
i → j,则R[i][j] = 1 - 通常也令
R[i][i] = 1(自己可达自己)
(2)核心思想(非常重要)
允许使用的“中间顶点”,一开始是空的,
然后逐个把顶点 1、2、…、k 加进来。
当考虑第k个顶点时,判断:
从i到j,
是否可以 先到 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])
- 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
- 最小生成树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)
- 最小生成树Kruskal算法
- 一句话: Kruskal 算法是一种在连通无向带权图中构造最小生成树(MST)的贪心算法。
1. 描述:Kruskal 算法是一种在连通无向带权图中构造最小生成树(MST)的贪心算法。
它的做法是:按边的权值从小到大排序,依次选择边加入生成树,只要这条边不会形成回路,就把它加入,直到选出 $n-1$ 条边为止。
先把边按小到大排好,
能连就连,
一旦成环就跳过。
2. 思路:Kruskal 的核心思想是“从边出发,逐步合并森林”。- 初始状态:
- 每个顶点都是一棵独立的树(森林)
- 把所有边按权值升序排序
- 依次扫描排序后的边
(u, v):- 如果
u和v属于不同的连通分量
加入这条边(不会形成回路) - 如果
u和v已在同一分量
跳过(否则会形成回路)
- 如果
- 重复,直到选出
|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
- 最短路径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
- 单源最短路径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
- 平分法求方程$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$ 时,返回中点作为近似根。
- 设 $f(x)=x^3+x-1$,先找一个区间 $[a,b]$ 使得 $f(a)\cdot f(b)<0$。
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
- 试位法求方程$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. 思路:- 设 $f(x)=x^3+x-1$,先找异号区间。比如:$f(0)=-1$,$f(1)=1$ 根在 $[0,1]$。
- 用直线插值求“试位点” $c$:
$$
c=\frac{a,f(b)-b,f(a)}{f(b)-f(a)}
$$
(这就是直线过两点与 x 轴的交点)
- 计算 $f(c)$,并更新区间:
- 若 $f(a)\cdot f(c)<0$,根在 $[a,c]$,令 $b=c$
- 否则根在 $[c,b]$,令 $a=c$
- 重复直到满足精度条件(比如 $|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
- 牛顿法求方程$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. 思路:- 先写出函数与导数:
$$
f(x)=x^3+x-1,\qquad f'(x)=3x^2+1
$$ - 选择一个初值 $x_0$(比如在根所在区间 $[0,1]$ 内选 1 或 0.5 都行)。
- 迭代更新公式(牛顿法核心):
$$
x_{k+1} = x_k - \frac{f(x_k)}{f'(x_k)} = x_k - \frac{x_k3+x_k-1}{3x_k2+1}
$$ - 停止条件(满足任意一个即可):
- $|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(这点很好)。
- 贪心法求田忌赛马问题
- 一句话: 田忌赛马问题中,田忌和齐王各有 n 匹马,每匹马都有不同的速度。
- 描述:田忌赛马问题中,田忌和齐王各有 n 匹马,每匹马都有不同的速度。比赛规则是:
每一轮各出一匹马,速度快的一方获胜。田忌可以自由安排自己出马的顺序,而齐王的出马顺序已知(或等价于已排序)。目标是:通过合理安排出马顺序,使田忌获胜的场次数最多。
不是拼总速度,
而是拼“怎么对位”,
用聪明的安排赢更多局。 - 思路:田忌赛马的经典解法是一个双指针贪心算法,核心原则是:
能赢就赢,
赢不了就用最慢的去“送”。
具体做法:- 双方马匹按速度从快到慢排序。
- 用两个指针分别指向田忌和齐王的:
- 最快马
- 最慢马
- 重复比较:
- 如果田忌的最快马快于齐王的最快马:
直接对决,田忌赢一局(最快对最快) - 否则(赢不了最快):
用田忌的最慢马去对齐王的最快马
这一局大概率输,但保留了中快马去赢别的局
- 如果田忌的最快马快于齐王的最快马:
- 直到所有马都比完。
- 描述:田忌赛马问题中,田忌和齐王各有 n 匹马,每匹马都有不同的速度。比赛规则是:
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
- 用来生成排列的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. 描述:“韩信点兵”问题描述的是这样一类情形:
一支军队人数不详,但按不同的数目分组时,余数是已知的。例如:
三人一组余 2,五人一组余 3,七人一组余 2。
问题是:这支军队最少有多少人?
不知道总人数,
只知道“怎么分都剩多少”,
反过来把总人数算出来。
2. 思路:韩信点兵的核心思想是:
找一个数,使它同时满足多条“取余条件”。
这正是数学中 中国剩余定理(CRT) 的应用场景。
基本解题思路:
3. 把问题写成“同余式”:- 若“三人一组余 2”,写成$x \equiv 2 \pmod{3}$
- 若“五人一组余 3”,写成$x \equiv 3 \pmod{5}$
- 若“七人一组余 2”,写成$x \equiv 2 \pmod{7}$
- 从满足其中一个条件的数开始,按步长逐个试:
- 先列出满足第一个条件的数
- 再筛掉不满足第二个条件的
- 最后筛掉不满足第三个条件的
- 第一个同时满足所有条件的正整数,就是“最少兵力”。
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

浙公网安备 33010602011771号