一、算法基础
算法是对特定问题求解步骤的描述,是指令的有限序列,具有输入、输出、有穷性、确定性和可行性五个性质。程序则是算法用某种编程语言的具体实现。优秀的算法应具备正确性、健壮性、可理解性、抽象分级和高效性,其中时间复杂度是衡量算法效率的重要标准。常用的时间复杂度符号包括 O(上界)、Ω(下界)和 Θ(紧确界)。
1.1 时间复杂度分析
非递归算法
以嵌套循环为例,分析以下代码的时间复杂度:
for (i = 1; i <= n; ++i)
for (j = 1; j <= i - 1; ++j)
++x;
外层循环执行 n 次,内层循环对每个 i 执行 i-1 次,总执行次数为:
[
\sum_{i=1}^n (i-1) = 0 + 1 + 2 + \dots + (n-1) = \frac{n(n-1)}{2}
]
因此,时间复杂度为 O(n²)。
再看一个非递归算法:
i = 1;
while (i <= n)
i = i * 3;
每次循环 i 乘以 3,设循环执行 k 次,则 ( 3^k \leq n < 3^{k+1} )。取对数得:
[
k \leq \log_3 n < k+1
]
因此,循环次数为 ( \lfloor \log_3 n \rfloor + 1 ),时间复杂度为 O(log n)。
递归算法
以阶乘函数为例:
int fact(int n) {
if (n == 0 || n == 1)
return 1;
else
return n * fact(n - 1);
}
递归方程为:
[
T(n) = T(n-1) + O(1)
]
迭代展开:
[
T(n) = T(n-1) + O(1) = T(n-2) + O(1) + O(1) = \dots = T(0) + n \cdot O(1)
]
因此,时间复杂度为 O(n)。
再以汉诺塔问题为例:
void Hanoi(int n, char A, char B, char C) {
if (n == 1)
printf("A->C");
else {
Hanoi(n-1, A, C, B);
printf("A->C");
Hanoi(n-1, B, A, C);
}
}
递归方程为:
[
T(n) = 2T(n-1) + O(1)
]
解得:
[
T(n) = 2T(n-1) + 1 = 2[2T(n-2) + 1] + 1 = \dots = 2^n T(0) + 2^{n-1} + \dots + 1 = 2^n - 1
]
时间复杂度为 O(2^n)。
二、蛮力法
2.1 设计思想
蛮力法通过遍历所有可能解,采用一定策略逐一处理问题元素,直至找到解。其设计步骤包括:确定枚举范围和约束条件。蛮力法具有通用性、启发性、实用性和准绳性,但效率较低,常用于简单问题或作为基准算法。
2.2 示例:百钱买百鸡
问题:用 100 元买 100 只鸡,公鸡 5 元/只,母鸡 3 元/只,小鸡 3 只 1 元,求解方案。
设公鸡、母鸡、小鸡分别为 a、b、c,则:
[
a + b + c = 100
]
[
5a + 3b + \frac{c}{3} = 100
]
[
c \mod 3 = 0
]
代码实现:
void Chicken_problem(int &k, int g[], int m[], int s[]) {
int a, b, c;
k = 0;
for (a = 0; a <= 20; a++) {
for (b = 0; b <= 33; b++) {
c = 100 - a - b;
if (c % 3 == 0 && 5 * a + 3 * b + c / 3 == 100) {
g[k] = a;
m[k] = b;
s[k] = c;
k++;
}
}
}
}
时间复杂度为 O(n²),其中 n 为枚举范围的上限。
2.3 示例:0/1 背包问题
问题:给定 n 种物品,物品 i 的重量为 ( w_i ),价值为 ( v_i ),背包容量为 C,求最大价值组合。
以 n=4,重量 ( {7, 3, 4, 5} ),价值 ( {42, 12, 40, 25} ),C=10 为例,蛮力法枚举所有子集,检查总重量是否不超过 C,记录最大价值。时间复杂度为 O(2^n)。
2.4 示例:最大子段和
问题:给定序列 ( (a_1, a_2, \dots, a_n) ),求 ( \sum_{k=i}^j a_k ) 的最大值,若全为负数则返回 0。
以序列 ( (-20, 11, -4, 13, -5, -2) ) 为例:
int MaxSum(int a[], int n) {
int sum = a[0], thissum, i, j;
for (i = 0; i < n; i++) {
thissum = 0;
for (j = i; j < n; j++) {
thissum += a[j];
if (thissum > sum)
sum = thissum;
}
}
return sum;
}
时间复杂度为 O(n²)。
三、分治法
3.1 设计思想
分治法将规模为 n 的问题分解为 k 个独立子问题,递归求解子问题后合并解。其步骤包括:划分、求解子问题、合并。
3.2 示例:最大子段和
分治法将序列分为两半,最大子段和可能出现在左半部分、右半部分或跨中点。代码实现:
int MaxSum(int a[], int left, int right) {
if (left == right) return a[left];
int center = (left + right) / 2;
int leftSum = MaxSum(a, left, center);
int rightSum = MaxSum(a, center + 1, right);
int s1 = 0, lefts = 0, s2 = 0, rights = 0, i, j;
for (i = center; i >= left; i--) {
lefts += a[i];
if (lefts > s1) s1 = lefts;
}
for (j = center + 1; j <= right; j++) {
rights += a[j];
if (rights > s2) s2 = rights;
}
int midSum = s1 + s2;
return max(max(leftSum, rightSum), midSum);
}
递归方程:
[
T(n) = 2T(n/2) + O(n)
]
时间复杂度为 O(n \log n)。
3.3 示例:快速排序
快速排序通过选择枢轴将序列分为两部分,递归排序。代码实现:
int Partition(int r[], int left, int right) {
int i = left, j = right, temp = r[i];
while (i < j) {
while (i < j && temp <= r[j]) j--;
if (i < j) { r[i] = r[j]; i++; }
while (i < j && temp >= r[i]) i++;
if (i < j) { r[j] = r[i]; j--; }
}
r[i] = temp;
return i;
}
void QuickSort(int r[], int left, int right) {
if (left < right) {
int pivot = Partition(r, left, right);
QuickSort(r, left, pivot - 1);
QuickSort(r, pivot + 1, right);
}
}
以序列 ( {17, 18, 60, 40, 7, 32, 73, 65, 85} ) 为例,排序过程如下:
- 第一趟:( {7, 17, 60, 40, 18, 32, 73, 65, 85} )
- 第二趟:( {7, 17, 32, 40, 18, 60, 73, 65, 85} )
- 第三趟:( {7, 17, 18, 32, 40, 60, 65, 73, 85} )
平均时间复杂度为 O(n \log n)。
四、动态规划法
4.1 设计思想
动态规划通过分解问题为重叠子问题,记录子问题解以避免重复计算。其基本要素是最优子结构和子问题重叠性,求解步骤包括划分子问题、确定动态规划函数、填写表格。
4.2 示例:0/1 背包问题
设 ( V(i, j) ) 为前 i 个物品装入容量 j 的最大价值,递推公式为:
[
V(i, j) = \begin{cases}
V(i-1, j) & \text{if } j < w_i \
\max { V(i-1, j), V(i-1, j-w_i) + v_i } & \text{if } j \geq w_i
\end{cases}
]
以 n=5,重量 ( {3, 2, 1, 4, 5} ),价值 ( {25, 20, 15, 40, 50} ),C=6 为例,动态规划表如下:
V 0 1 2 3 4 5 6
0 0 0 0 0 0 0 0
1 0 0 0 25 25 25 25
2 0 0 20 25 25 45 45
3 0 15 20 35 40 45 60
4 0 15 20 35 40 55 60
5 0 15 20 35 40 55 65
最大价值为 65。代码实现:
int KnapSack(int w[], int v[], int n, int C, int x[]) {
int V[n+1][C+1];
for (int j = 0; j <= C; j++) V[0][j] = 0;
for (int i = 0; i <= n; i++) V[i][0] = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= C; j++)
if (j < w[i-1]) V[i][j] = V[i-1][j];
else V[i][j] = max(V[i-1][j], V[i-1][j-w[i-1]] + v[i-1]);
for (int i = n, j = C; i > 0; i--)
if (V[i][j] > V[i-1][j]) { x[i-1] = 1; j -= w[i-1]; }
else x[i-1] = 0;
return V[n][C];
}
时间复杂度为 O(n \cdot C)。
4.3 示例:最长公共子序列
问题:给定序列 ( X = (a, b, c, b, d, b) ),( Y = (a, c, b, b, a, b, d, b, b) ),求最长公共子序列。
设 ( L(i, j) ) 为 ( X_i ) 和 ( Y_j ) 的最长公共子序列长度,递推公式为:
[
L(i, j) = \begin{cases}
L(i-1, j-1) + 1 & \text{if } x_i = y_j \
\max { L(i, j-1), L(i-1, j) } & \text{otherwise}
\end{cases}
]
代码实现:
int CommonOrder(char x[], int m, char y[], int n, char z[]) {
int L[m+1][n+1], S[m+1][n+1];
for (int j = 0; j <= n; j++) L[0][j] = 0;
for (int i = 0; i <= m; i++) L[i][0] = 0;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
if (x[i-1] == y[j-1]) { L[i][j] = L[i-1][j-1] + 1; S[i][j] = 1; }
else if (L[i][j-1] >= L[i-1][j]) { L[i][j] = L[i][j-1]; S[i][j] = 2; }
else { L[i][j] = L[i-1][j]; S[i][j] = 3; }
int i = m, j = n, k = L[m][n];
while (i > 0 && j > 0) {
if (S[i][j] == 1) { z[--k] = x[i-1]; i--; j--; }
else if (S[i][j] == 2) j--;
else i--;
}
return L[m][n];
}
时间复杂度为 O(m \cdot n)。
五、贪心法
5.1 设计思想
贪心法通过局部最优选择逐步逼近全局最优,适用于具有贪心选择性质和最优子结构的问题。
5.2 示例:分数背包问题
问题:给定 n 种物品,重量 ( w_i ),价值 ( v_i ),背包容量 C,物品可部分装入,求最大价值。
策略:按单位重量价值 ( v_i / w_i ) 降序排序,依次装入。代码实现:
int KnapSack(int w[], int v[], int n, int C) {
double x[n] = {0}, maxValue = 0;
// 假设已按 v[i]/w[i] 降序排序
for (int i = 0; i < n && C > 0; i++) {
if (w[i] <= C) {
x[i] = 1;
maxValue += v[i];
C -= w[i];
} else {
x[i] = (double)C / w[i];
maxValue += x[i] * v[i];
C = 0;
}
}
return maxValue;
}
时间复杂度为 O(n \log n)(排序)。
5.3 示例:Huffman 树
问题:对字符集 ( {A, B, C, D, E, F, G} ) 编码,频率为 ( {9, 11, 5, 7, 8, 2, 3} ),设计最优编码。
构造 Huffman 树:
- 初始化:取最小频率 2 和 3,合并为 5。
- 重复:合并 5 和 5,得 10;合并 7 和 8,得 15;依次合并直至形成根节点。
编码方案:
- A: 00
- B: 10
- C: 010
- D: 110
- E: 111
- F: 0110
- G: 0111
加权路径长度:
[
WPL = (2+3) \cdot 4 + (5+7+8) \cdot 3 + (9+11) \cdot 2 = 120
]
六、回溯法
6.1 设计思想
回溯法采用深度优先搜索,通过约束函数和限界函数剪枝,避免无效搜索。
6.2 示例:0/1 背包问题
以 n=3,重量 ( {3, 4, 4} ),价值 ( {15, 12, 8} ),C=9 为例,解空间为完全二叉树,左分支选(1),右分支不选(0)。通过约束(总重量≤C)和限界(上界估算)剪枝,求得最优解。
七、分支限界法
7.1 设计思想
分支限界法采用广度优先搜索,通过限界函数剪枝,常见扩展方式包括队列式和优先队列式。
7.2 示例:任务分配问题
问题:将 n 项任务分配给 n 个人,成本矩阵为:
[
C = \begin{bmatrix}
9 & 2 & 7 & 8 \
6 & 4 & 3 & 7 \
5 & 8 & 1 & 8 \
7 & 6 & 9 & 4
\end{bmatrix}
]
下界为每行最小值之和 ( 2+3+1+4=10 ),上界为贪心解 ( 2+3+5+4=14 )。限界函数为:
[
lb = v + \sum_{k=i+1}^n \min_k
]
通过广度优先搜索,找到最优分配。
7.3 示例:0/1 背包问题
以 n=4,重量 ( {2, 4, 3, 5} ),价值 ( {15, 12, 8, 10} ),C=10 为例,限界函数为:
[
ub = v + (W - w) \cdot \frac{v_{i+1}}{w_{i+1}}
]
目标函数界为 [35, 75],通过广度优先搜索求解。
八、总结
算法设计与分析涉及多种策略,蛮力法适合简单问题,分治法和动态规划适合分解问题,贪心法追求局部最优,回溯法和分支限界法通过剪枝提高效率。选择合适的算法需根据问题特性、时间复杂度和实际需求综合考虑。