贪心算法(最优装载问题)

发布于:2025-09-13 ⋅ 阅读:(20) ⋅ 点赞:(0)

贪心算法

贪心算法(Greedy Algorithm)是一种在每一步决策中都采取当前状态下最优选择的启发式策略,其核心思想是通过“局部最优”逐步逼近“全局最优”。它不追求对所有可能情况的枚举,而是基于某种贪心策略快速做出选择,最终希望等到问题的最优解。

核心思想

贪心算法的本质是“视短”的,它只关注当前步骤中最好的选择(局部最优),不考虑该选择对未来步骤的影响,也不回溯修改之前决策。其逻辑可概括为:

  1. 对问题进行分解,将其拆分为一系列可独立决策的子问题;
  2. 定义一个 “贪心策略”(如 “选最小”“选最早”“选单位价值最高” 等),用于在每个子问题中选择局部最优解;
  3. 逐步执行这些局部最优选择,累积结果,最终形成全局解。

使用条件

并非所有问题都能用贪心算法求解,它仅适用于满足以下两个条件的问题:

  1. 贪心选择性质
    全局最优解可以通过一系列局部最优选择(贪心选择)得到。即,存在一种策略,使得每次选择的局部最优解能最终组合成全局最优解。

  2. 最优子结构
    问题的全局最优解包含其子问题的最优解。即,若将问题拆分为子问题,子问题的最优解可用于构建原问题的最优解。

基本步骤

  1. 问题建模:将问题转化为 “多步选择” 的形式,明确每个步骤的可选方案和目标(如 “最大化收益”“最小化成本”)。

  2. 确定贪心策略:设计一个规则,用于在每一步从可选方案中选择 “局部最优” 的选项(如 “选结束时间最早的活动”“选单位重量价值最高的物品”)。

  3. 证明策略有效性:验证该贪心策略是否满足 “贪心选择性质” 和 “最优子结构”(关键步骤,否则可能得到非最优解)。

  4. 逐步求解:按贪心策略依次选择,累积结果,得到最终解。

典型应用场景

贪心算法在许多经典问题中被证明有效,例如:

        1. 活动选择问题

        问题:有若干活动,每个活动有开始时间和结束时间,选择最多的不重叠活动。
        贪心策略:每次选择结束时间最早的活动,然后排除与其冲突的活动,重复此过程。
        原理:结束时间最早的活动能留下更多时间给后续活动,最终可得到最多不重叠活动。

        2. 哈夫曼编码(Huffman Coding)

        问题:为字符设计前缀码(无歧义编码),使总编码长度最短(压缩最优)。
        贪心策略:每次选择频率最低的两个字符合并为一个新节点,重复此过程构建哈夫曼树,树的路径即为编码。
        原理:频率低的字符用较长编码,频率高的用较短编码,总长度最优。

        3. 最小生成树(Prim/Kruskal 算法)

        问题:在连通图中选择 n-1 条边,使所有节点连通且总权重最小。

        贪心策略

                 Prim 算法:从某节点出发,每次选择连接树内外的最小权重边

                 Kruskal 算法:按边权重从小到大排序,每次选择不形成环的边

                原理:通过局部选择最小权重边,最终形成全局权重最小的生成树。

        4. 单源最短路径(Dijkstra 算法)

        问题:从起点到其他所有节点的最短路径(边权重非负)。
        贪心策略:每次选择当前已知最短路径的节点,并用其更新相邻节点的路径长度。
        原理:由于边权重非负,一旦某节点被选中,其最短路径即确定,无需回溯。

贪心算法的框架

例子:最优装载问题

要求:物品不可分割,要求装在最多货物。

思路:

(1)贪心策略:在所有货船里面每次选择重量最小的。

(2)局部最优解:根据贪心策略,一步一步地得到局部最优解,

例如:第一次选择一个重量最小的古董,记位置a1,第二次再从剩下的再选最小的记为a2,一次类推....

   (3) 把所有的局部最优解合为原来问题的一个最优解(a1、a2......)

   (4)设计古董重量的排序。(冒泡排序,快速排序都行)如图:

代码:

#include<stdio.h>

// 函数功能:用贪心算法选择最多数量的古董(优先选轻的)
// 参数:N-古董总数,W-最大载重,q-古董重量数组
int loadAntiques(int N, double W, double q[])
{
	int ans = 0;        // 已装载的古董数量
	double totalWeight = 0.0;  // 已装载的总重量
	
	// 1. 冒泡排序:将古董按重量从小到大排序(贪心策略基础)
	for (int i = 0; i < N - 1; i++)
	{
		for (int j = 0; j < N - i - 1; j++)
		{
			// 若前一个比后一个重,则交换(注意用double类型临时变量)
			if (q[j] > q[j + 1])
			{
				double temp = q[j];  // 修正:temp应为double类型(原代码用int错误)
				q[j] = q[j + 1];
				q[j + 1] = temp;
			}
		}
	}
	
	// 2. 贪心选择:从最轻的开始装,直到超重
	for (int i = 0; i < N; i++)
	{
		// 修正:先判断加上当前古董是否超重,不超重才装载(原代码逻辑反了)
		if (totalWeight + q[i] <= W)
		{
			totalWeight += q[i];
			ans++;
		}
		else
		{
			break;  // 超重则停止装载
		}
	}
	
	// 输出结果
	printf("最大古董数量为:%d个\n", ans);
	printf("船上总重量为:%lf吨\n", totalWeight);
	
	return 0;
}

int main()
{
	int N = 8;          // 古董总数
	double W = 30.0;    // 船的最大载重
	double q[8];        // 存储每个古董的重量
	
	// 修正:循环读取8个古董的重量(原代码只读取了1个)
	printf("请输入8个古董的重量(吨):\n");
	for (int i = 0; i < N; i++)
	{
		scanf("%lf", &q[i]);  // 注意取地址符&
	}
	
	// 调用函数计算最大装载量
	loadAntiques(N, W, q);
	
	return 0;
}

结果: