【算法设计与分析】实验三:贪心法 - 最短路径问题(TSP)
目录
- 一、实验目的与要求
- [1. 实验目的](#1. 实验目的)
- [2. 实验要求](#2. 实验要求)
- 二、实验内容
- 三、实验方法
- 四、详细的算法设计及运行结果
- 五、实验感想
- 贪心法的核心思想总结
- 贪心法的优缺点
- [贪心法 vs 动态规划 vs 回溯法](#贪心法 vs 动态规划 vs 回溯法)
- 贪心法的经典应用场景
一、实验目的与要求
1. 实验目的
- 理解并掌握贪心法的基本原理与核心思想,深刻认识"局部最优导向全局最优"的算法策略。
- 能够灵活运用贪心法解决最短路径及TSP旅行商等经典问题。
- 通过与动态规划、回溯法的对比,深入理解贪心法的适用条件与局限性。
2. 实验要求
- 准确理解最短路径问题和TSP问题的含义,掌握多种求解策略。
- 掌握贪心法的贪心选择性质 和最优子结构性质,判断问题是否适合使用贪心法。
- 能编写出正确实现贪心法解决最短路径问题的代码,并添加详细注释。
- 分析程序的运行结果,进行多组测试数据的对比验证。
- 对算法的时间复杂度和空间复杂度进行理论分析。
二、实验内容
在本实验中,需要理解并实现贪心法的基本原理,并将其运用在最短路径问题中。具体包括:
- TSP旅行商问题:使用贪心法的最近邻点策略,求解从某一城市出发,经过所有城市恰好一次,最后回到出发城市的最短路径。
- 算法分析:对贪心法的正确性、复杂度和局限性进行系统分析。
三、实验方法
- 学习并深入理解最短路径问题的基本概念和应用背景(物流规划、电路布线等)。
- 学习并理解贪心法的基本理论,掌握贪心法的两个核心性质:
- 贪心选择性质:每一步都做出当前最优的选择。
- 最优子结构性质:问题的最优解包含子问题的最优解。
- 按照贪心法的理论,用 C++ 编写实现贪心法解决 TSP 问题的代码。
- 运行程序,记录程序的运行结果,分析不同起点对结果的影响,并反思解决方案的效果以及可能存在的局限性。
四、详细的算法设计及运行结果
1. 贪心法基本原理
贪心算法(Greedy Algorithm)是一种在每一步选择中都采取当前状态下最优(即最有利)的选择,从而希望导致结果是全局最优的算法策略。
贪心法的核心步骤
| 步骤 | 描述 |
|---|---|
| ① 选择初始状态 | 确定算法的起点,初始化相关数据结构 |
| ② 应用贪心选择 | 在当前状态下选择一个局部最优的动作 |
| ③ 可行性检查 | 判断当前选择是否满足问题的约束条件 |
| ④ 更新状态 | 根据贪心选择的结果更新当前状态 |
| ⑤ 结束条件 | 检查是否达到算法的结束条件,若未结束则回到步骤② |
贪心法的适用条件
关键定理:一个问题能用贪心法求得最优解,必须同时满足以下两个性质:
- 贪心选择性质:问题的整体最优解可以通过一系列局部最优的选择来达到。
- 最优子结构性质:问题的最优解包含其子问题的最优解。
2. TSP问题的贪心法求解
问题描述
给定 n 个城市及它们之间的距离矩阵,从某个城市出发,经过每个城市恰好一次后返回出发城市,要求总路径长度最短。
TSP精确求解的时间复杂度为 O(n!),在城市数量较大时不可行。贪心法通过牺牲最优性来换取较低的计算复杂度。
贪心策略:最近邻点法
核心思想:从起点出发,每次选择距离当前城市最近且未被访问过的城市作为下一个访问目标,直到所有城市都被访问,最后返回起点。
算法流程详解
┌────────────────────────────────┐
│ 开始:选择起点s │
│ 标记s为已访问 │
│ 当前城市 u = s │
│ 总距离 sum = 0 │
└──────────────┬─────────────────┘
↓
┌──────────────────────────────────┐
│ 遍历所有城市j (j = 0 to n-1) │
│ 找到未访问且距离u最近的城市v │
│ min_dist = min(A[u][j]) │
└──────────────┬───────────────────┘
↓
┌───────┴───────┐
│ 是否找到城市v? │
└───────┬───────┘
是 ↓ 否 ↓
┌───────────────┐ ┌──────────────────┐
│ sum += min_dist│ │ 所有城市已访问 │
│ 标记v为已访问 │ │ sum += A[u][s] │
│ u = v │ │ 输出路径和总长度 │
│ 返回上方循环 │ │ 结束 │
└───────────────┘ └──────────────────┘
实验数据
使用 5 个城市的距离矩阵进行测试:
| 城市0 | 城市1 | 城市2 | 城市3 | 城市4 | |
|---|---|---|---|---|---|
| 城市0 | ∞ | 3 | 3 | 2 | 6 |
| 城市1 | 3 | ∞ | 7 | 3 | 2 |
| 城市2 | 3 | 7 | ∞ | 2 | 5 |
| 城市3 | 2 | 3 | 2 | ∞ | 3 |
| 城市4 | 6 | 2 | 5 | 3 | ∞ |
实验代码
cpp
#include <iostream>
using namespace std;
const int inf = 10000; // 用一个大数表示不可达(即无穷大)
const int n = 5; // 城市的数量
/**
算法思想:
1. 从起点出发,标记为已访问
2. 每次在未访问的城市中,选择距离当前城市最近的城市
3. 移动到该城市,标记为已访问
4. 重复步骤2-3,直到所有城市都被访问
5. 最后从最后一个城市返回起点
*/
void findTSP(int A[n][n], int start) {
bool visited[n] = {false}; // 记录每个城市是否被访问过
int sum = 0; // 记录路径总长度
int u = start; // 当前所在的城市
visited[u] = true; // 标记起点为已访问
cout << "路径:" << u;
// 需要再访问 n-1 个城市
for (int i = 0; i < n - 1; i++) {
int min = inf; // 记录到未访问城市的最短距离
int v = -1; // 记录最近的未访问城市编号
// 遍历所有城市,查找距离当前城市最近的未访问城市
for (int j = 0; j < n; j++) {
if (!visited[j] && A[u][j] < min) {
min = A[u][j]; // 更新最短距离
v = j; // 更新最近城市编号
}
}
// 贪心选择:移动到最近的未访问城市
sum += min; // 累加路径长度
visited[v] = true; // 标记该城市为已访问
cout << "->" << v;
u = v; // 更新当前城市
}
// 从最后一个城市返回起点,形成回路
sum += A[u][start];
cout << "->" << start << '\n';
cout << "总长度:" << sum << endl;
}
int main() {
// 5个城市的对称距离矩阵
int dist[n][n] = {
{inf, 3, 3, 2, 6}, // 城市0到各城市的距离
{3, inf, 7, 3, 2}, // 城市1到各城市的距离
{3, 7, inf, 2, 5}, // 城市2到各城市的距离
{2, 3, 2, inf, 3}, // 城市3到各城市的距离
{6, 2, 5, 3, inf} // 城市4到各城市的距离
};
for (int i = 0; i < n; i++) {
cout << "起点 " << i << ": ";
findTSP(dist, i);
cout << endl;
}
return 0;
}
运行结果
===== 不同起点的贪心法TSP结果 =====
起点 0: 路径:0->3->2->4->1->0
总长度:14
起点 1: 路径:1->4->3->2->0->1
总长度:14
起点 2: 路径:2->3->0->1->4->2
总长度:13
起点 3: 路径:3->0->1->4->2->3
总长度:14
起点 4: 路径:4->1->0->3->2->4
总长度:14
结果分析
| 起点 | 路径 | 总长度 | 是否最优 |
|---|---|---|---|
| 0 | 0→3→2→4→1→0 | 14 | 近似最优 |
| 1 | 1→4→3→2→0→1 | 14 | 近似最优 |
| 2 | 2→3→0→1→4→2 | 13 | ✅ 本组最优 |
| 3 | 3→0→1→4→2→3 | 14 | 近似最优 |
| 4 | 4→1→0→3→2→4 | 14 | 近似最优 |
⚠️ 关键发现 :不同的起点会导致不同的贪心路径和总长度。起点2得到的路径总长度为13,优于其他起点的14。这充分说明了贪心法得到的解依赖于初始条件,不一定能获得全局最优解。
3. 算法效率分析
时间复杂度分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 外层循环 | O(n) | 遍历 n-1 个城市 |
| 内层查找最近邻点 | O(n) | 在 n 个城市中查找最小距离 |
| 总时间复杂度 | O(n²) | 两层循环嵌套 |
📊 与精确求解 TSP 的 O(n!) 相比,贪心法的 O(n²) 复杂度有巨大优势。对于 n=20 的情况,精确算法需要约 2.4×10¹⁸ 次运算,而贪心法仅需 400 次。
空间复杂度分析
| 存储项 | 空间复杂度 | 说明 |
|---|---|---|
| 距离矩阵 A | O(n²) | 存储 n×n 的邻接矩阵 |
| 访问标记数组 visited | O(n) | 记录每个城市是否被访问 |
| 辅助变量 | O(1) | sum、u、v、min 等 |
| 总空间复杂度 | O(n²) | 主要由距离矩阵决定 |
4. 算法的特色与局限性
算法特色
- 思路直观:最近邻点策略模拟了人类直觉------"先去最近的地方",算法实现简单清晰。
- 效率极高:O(n²) 的时间复杂度使其可以处理大规模城市集合(数千个城市),而精确算法在几十个城市时就已不可行。
- 贪心选择明确:每一步的选择标准唯一且易于实现------选择距离最小的未访问城市。
- 可作为启发式初始解:贪心法的结果常作为其他优化算法(如模拟退火、遗传算法)的初始解。
算法局限性
- 不保证最优解:贪心法只关注局部最优,可能在前期选择了较短的边,导致后期被迫选择很长的边(尤其是返回起点的那条边)。
- 起点敏感性:不同起点会得到不同结果(如上表所示),需要遍历所有起点取最优。
- 无法应用于非对称TSP的改进:对于非对称距离矩阵(Aij ≠ Aji),贪心法的近似比可能更差。
📐 理论分析:对于满足三角不等式的对称 TSP 问题,最近邻点贪心法的近似比上界为 O(log n),即贪心解不超过最优解的 O(log n) 倍。
五、实验感想
贪心法的核心思想总结
贪心法是一种在每一步选择中都采取当前最优解的算法策略,通过局部最优期望获得全局最优或近似最优解。它的基本步骤可以归纳为:
- 建立数学模型:将问题抽象为可量化的形式,明确目标函数和约束条件。
- 确定贪心策略:找到一个局部最优选择的准则(如最近邻、最小权重、最早结束等)。
- 逐步构建解:按贪心策略逐步选择,每一步不可撤回。
- 验证可行性:确保每一步的选择满足约束条件。
贪心法的优缺点
| 维度 | 优点 | 缺点 |
|---|---|---|
| 简单性 | 算法思路直观,代码简洁 | 正确性证明往往较复杂 |
| 效率 | 时间复杂度通常较低(多项式级) | --- |
| 最优性 | 在满足贪心选择性质的问题中可得最优解 | 不满足条件时只能得到近似解 |
| 回溯 | 无需回溯,决策过程单向 | 一旦做出错误选择无法纠正 |
| 适用性 | 适用于活动选择、哈夫曼编码、Dijkstra等 | 不适用于0-1背包、TSP精确求解等 |
贪心法 vs 动态规划 vs 回溯法
| 特性 | 贪心法 | 动态规划 | 回溯法 |
|---|---|---|---|
| 决策序列 | 只产生一个决策序列 | 产生多个决策序列 | 产生多个决策序列 |
| 求解方向 | 自顶向下求解 | 自底向上求解 | 深度优先搜索 |
| 最优性保证 | 不一定全局最优 | ✅ 保证全局最优 | ✅ 保证全局最优 |
| 时间复杂度 | 通常最低 | 中等 | 通常最高 |
| 空间复杂度 | 通常最低 | 需要存储子问题的解 | 需要递归栈空间 |
| 是否回溯 | ❌ 不回溯 | ❌ 不回溯 | ✅ 需要回溯 |
贪心法的经典应用场景
| 问题 | 贪心策略 | 是否最优 |
|---|---|---|
| 活动选择问题 | 按结束时间排序,选不冲突的 | ✅ 最优 |
| 哈夫曼编码 | 每次合并频率最小的两个节点 | ✅ 最优 |
| Dijkstra最短路径 | 每次选距离最小的未确定节点 | ✅ 最优(非负权) |
| Kruskal最小生成树 | 每次选权重最小的不成环的边 | ✅ 最优 |
| Prim最小生成树 | 每次选权重最小的跨割边 | ✅ 最优 |
| TSP旅行商问题 | 最近邻点策略 | ❌ 近似解 |
| 0-1背包问题 | 按单位价值排序选择 | ❌ 不一定最优 |
提示 :贪心法不能"盲目"使用。在应用之前,必须分析问题是否满足贪心选择性质 和最优子结构性质。对于 TSP 问题,由于不满足贪心选择性质(局部最优不能保证全局最优),因此贪心法只能给出近似解。