1. 贪心法(Greedy Algorithm)
1.1 贪心法的基本思想
贪心法总是做出在当前看来是最好的选择。也就是说,贪心法并不从整体最优上加以考虑,它所做出的选择只是在某种意义上的局部最优解。
贪心法的特点:
- 贪心策略一旦做出,不可回溯
- 贪心法求解的问题需要满足贪心选择性质和最优子结构性质
- 贪心法通常自顶向下进行求解
贪心选择性质:所求问题的整体最优解可以通过一系列局部最优的选择来达到。
1.2 实例:部分背包问题
问题描述:给定 $n$ 种物品和一个容量为 $W$ 的背包,物品 $i$ 的重量为 $w_i$,价值为 $v_i$。与0-1背包问题不同的是,在部分背包问题中,可以选择物品的一部分装入背包。
贪心策略:
按照单位重量价值($v_i/w_i$)从大到小的顺序选取物品,直到背包装满。
C++ 代码实现:
#include <stdio.h>
#define N 5 // 物品数量
#define W 10 // 背包容量
int v_temp[N + 1], w_temp[N + 1]; // 物品价值数组 和 物品重量数组的临时数组
double vw_temp[N + 1]; // 物品单位重量价值数组的临时数组
double answer[N + 1]; // 解方案数组
// 归并排序
void merge_sort(int v[], int w[], double vw[], int l, int r) {
if (l >= r) return;
int mid = l + r >> 1;
merge_sort(v, w, vw, l, mid), merge_sort(v, w, vw, mid + 1, r);
int i = l, j = mid + 1, k = 1;
while (i <= mid && j <= r) {
if (vw[i] >= vw[j]) { // 按照 物品单位重量价值数组 从大到小的顺序排序
vw_temp[k] = vw[i];
v_temp[k] = v[i];
w_temp[k] = w[i];
k++, i++;
} else {
vw_temp[k] = vw[j];
v_temp[k] = v[j];
w_temp[k] = w[j];
k++, j++;
}
}
while (i <= mid) {
vw_temp[k] = vw[i];
v_temp[k] = v[i];
w_temp[k] = w[i];
k++, i++;
}
while (j <= r) {
vw_temp[k] = vw[j];
v_temp[k] = v[j];
w_temp[k] = w[j];
k++, j++;
}
for (i = l, j = 1; i <= r; i++, j++) {
vw[i] = vw_temp[j];
v[i] = v_temp[j];
w[i] = w_temp[j];
}
}
// 显示物品价值、重量、单位重量价值数组
void show(int v[], int w[], double vw[]) {
int i;
printf("物品价值数组:");
for (i = 1; i <= N; i++) printf("%d ", v[i]);
printf("\n");
printf("物品重量数组:");
for (i = 1; i <= N; i++) printf("%d ", w[i]);
printf("\n");
printf("物品单位重量价值数组:");
for (i = 1; i <= N; i++) printf("%.1lf ", vw[i]);
printf("\n");
}
// 求解部分背包问题最优解
double Max_Value(int v[], int w[], double vw[]) {
double result = 0.0;
int i;
int W_temp = W;
for (i = 1; i <= N; i++) {
if (W_temp >= w[i]) { // 当前背包容量 大于等于 物品重量 就直接全部装入到背包中
answer[i] = 1.0;
result = result + v[i];
W_temp = W_temp - w[i];
} else { // 当前背包容量 小于 物品重量 就应该将该物品的一部分装入到背包中
break;
}
}
if (W_temp > 0 && i <= N) { // 当前背包还有剩余容量 并且 还有可选的物品
answer[i] = (double) W_temp / w[i];
result = result + W_temp * vw[i];
}
return result;
}
int main() {
int v[] = {0, 6, 3, 5, 4, 6}; // 物品价值数组
int w[] = {0, 2, 2, 6, 5, 4}; // 物品重量数组
double vw[N + 1]; // 物品单位重量价值数组
int i;
// 初始化 物品单位重量价值数组
for (i = 1; i <= N; i++) vw[i] = (double) v[i] / w[i];
printf("排序前:\n");
show(v, w, vw);
merge_sort(v, w, vw, 1, N);
printf("排序后:\n");
show(v, w, vw);
double result = Max_Value(v, w, vw);
printf("\nresult = %.2lf\n", result);
printf("\n");
printf("解方案结果:");
for (i = 1; i <= N; i++) printf("%.1lf ", answer[i]);
return 0;
}
1.3 贪心法的其他应用
| 问题 | 贪心策略 |
|---|---|
| 活动选择问题 | 每次选择结束时间最早的活动 |
| 哈夫曼编码 | 每次选择频率最小的两个节点合并 |
| 最小生成树(Prim/Kruskal) | 每次选择权重最小的边 |
| 单源最短路径(Dijkstra) | 每次选择距离源点最近的顶点 |
2. 回溯法(Backtracking)
2.1 回溯法的算法框架
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择。
回溯法的特点:
- 采用深度优先搜索策略
- 在搜索过程中用剪枝函数避免无效搜索
- 适用于求解组合数较大的问题
回溯法的基本步骤:
- 针对所给问题,定义问题的解空间
- 确定易于搜索的解空间结构(通常是树形结构)
- 以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索
2.2 实例:N皇后问题
问题描述:给定一个 $N \times N$ 的棋盘,要在棋盘上摆放 $N$ 个皇后,并且满足 $N$ 个皇后中任意两个皇后都不处于同一行、同一列、同一斜线上(正斜线、反斜线)。
2.2.1 非递归方式
#include <math.h>
#include <stdio.h>
#define N 10
int q[N + 1]; // 存储皇后的列号
int check(int j) { // 检查第 j 个皇后的位置是否合法
int i;
for (i = 1; i < j; i++) {
if (q[i] == q[j] || abs(i - j) == abs(q[i] - q[j])) { // 判断是否在同一列或同一斜线
return 0;
}
}
return 1;
}
void queen() { // 求解 N 皇后 方案
int i;
for (i = 1; i <= N; i++) {
q[i] = 0;
}
int answer = 0; // 方案数
int j = 1; // 表示正在摆放第 j 个皇后
while (j >= 1) {
q[j] = q[j] + 1; // 让第 j 个皇后向后一列摆放
while (q[j] <= N && !check(j)) { // 判断第 j 个皇后的位置是否合法
q[j] = q[j] + 1; // 不合法就往后一个位置摆放
}
if (q[j] <= N) { // 表示第 j 个皇后的找到一个合法的摆放位置
if (j == N) { // 找到了 N 皇后的一组解
answer = answer + 1;
printf("方案%d:", answer);
for (i = 1; i <= N; i++) {
printf("%d ", q[i]);
}
printf("\n");
} else {
j = j + 1; // 继续摆放下一个皇后
}
} else { // 表示第 j 个皇后找不到一个合法的摆放位置
q[j] = 0; // 还原第 j 个皇后的位置
j = j - 1; // 回溯
}
}
}
int main() {
queen();
return 0;
}
2.2.2 递归方式
#include <math.h>
#include <stdio.h>
#define N 10
int answer = 0;
int q[N + 1]; // 存储皇后的列号
int check(int j) { // 检查第 j 个皇后的位置是否合法
int i;
for (i = 1; i < j; i++) {
if (q[i] == q[j] || abs(i - j) == abs(q[i] - q[j])) { // 判断是否在同一列或同一斜线
return 0;
}
}
return 1;
}
void queen(int j) {
int i;
for (i = 1; i <= N; i++) {
q[j] = i;
if (check(j)) { // 当摆放的皇后位置为合法时
if (j == N) { // 找到了 N 皇后的一组解
answer = answer + 1;
printf("方案%d:", answer);
for (i = 1; i <= N; i++) {
printf("%d ", q[i]);
}
printf("\n");
} else {
queen(j + 1); // 递归摆放下一个皇后的位置
}
}
}
}
int main() {
queen(1);
return 0;
}
3. 贪心法与回溯法的比较
| 特性 | 贪心法 | 回溯法 |
|---|---|---|
| 决策方式 | 每步做出局部最优选择,不可回溯 | 试探性选择,可回溯 |
| 搜索策略 | 自顶向下,一次决策 | 深度优先搜索 |
| 解的性质 | 不一定得到全局最优解 | 可以得到所有可行解或最优解 |
| 效率 | 效率高,时间复杂度低 | 效率较低,可能需要遍历大量节点 |
| 适用场景 | 具有贪心选择性质的问题 | 组合优化、约束满足问题 |
| 典型问题 | 部分背包、最小生成树、最短路径 | N皇后、子集和、图着色、旅行商 |
4. 四种算法设计策略总结
| 算法策略 | 核心思想 | 典型问题 | 时间复杂度特点 |
|---|---|---|---|
| 分治法 | 分解 → 解决 → 合并 | 归并排序、快速排序、最大子段和 | 通常 $O(n \log n)$ |
| 动态规划 | 填表法,保存子问题解 | 0-1背包、最长公共子序列 | 通常多项式时间 |
| 贪心法 | 局部最优选择 | 部分背包、最小生成树 | 通常线性或对数时间 |
| 回溯法 | 深度优先搜索 + 剪枝 | N皇后、子集和 | 指数级,但可剪枝优化 |
评论