算法之贪心算法

贪心算法

  • 贪心算法
    • 核心思想
    • 常见应用场景
    • 典型案例
      • 案例一:找零问题
      • 案例二:活动选择问题
      • 案例三:货仓选址问题
    • 贪心算法的应用详解
      • 霍夫曼编码
      • 最小生成树
      • Dijkstra最短路径算法
    • 总结

贪心算法

核心思想

贪心算法(Greedy Algorithm)是一种在每一步选择中都采取在当前状态下最优的选择,从而希望以这种方式能够达到全局最优解的一种算法思想。贪心算法并不总是能得到全局最优解,但在一些问题中,它能产生出足够好的解,而且相对简单高效。

贪心算法的基本思想是通过一系列局部最优的选择,希望最终达到全局最优。在每一步,贪心算法会选择当前状态下的最优解,而不考虑当前选择对以后的影响。这与动态规划的思想不同,动态规划考虑到每一步的选择对于整体解的影响,而做出相应的决策。

贪心算法通常适用于满足两个条件的问题:

  • 最优子结构性质: 当一个问题的最优解包含其子问题的最优解时,称该问题具有最优子结构性质。
  • 贪心选择性质: 通过局部最优选择,期望能够达到全局最优。

一旦一个问题可以通过贪心算法求解,那么贪心算法一般会比其他算法更加高效。然而,需要注意的是,并非所有问题都满足贪心选择性质和最优子结构性质。

常见应用场景

  • 霍夫曼编码:用于数据压缩,为高频字符分配短编码、低频字符分配长编码,减少整体数据量。
  • 活动选择问题:选择一组互不相容的活动,使得参与的活动数最大。
  • 最小生成树:如Kruskal和Prim算法,用于在加权连通图中找到权值之和最小的生成树。
  • Dijkstra最短路径算法:求解单源最短路径问题,从起始点开始,每次遍历到距离最近且未访问过的顶点的邻接节点。
  • 货仓选址问题:在一维数轴上为多个商店选择货仓位置,使得总距离最小。

在使用贪心算法时,必须确保贪心选择的局部最优解能够推导出全局最优解,否则可能得不到最优解。

典型案例

案例一:找零问题

问题描述:假设你是柜台售货员,需要给客户找零n元钱。你拥有无限数量的1元、2元、5元、10元、20元、50元面额的硬币/纸币。如何用最少数量的硬币/纸币来找零?

贪心策略:每次尽量用面额最大的硬币来找零。

示例

假设要找零的金额为 n = 36 元。

可用的硬币面额为 1、2、5、10、20 和 50。

  1. 首先,找零最大的面额,即 50 元。但由于 50 元大于 36 元,所以不能使用。
  2. 接下来,尽量使用次大面额的硬币,即 20 元。36 元中能用一个 20 元,剩下 16 元。
  3. 然后使用次次大面额的硬币,即 10 元。16 元中能用一个 10 元,剩下 6 元。
  4. 继续使用 5 元的硬币,6 元中能用一张 5 元,剩下 1 元。
  5. 最后,用 1 元的硬币找零 1 元。

这样,用 20 元、10 元、5 元和 1 元的硬币一共找零 36 元,共需要 4 枚硬币,是最少数量的硬币。

在这个案例中,贪心算法的贪心策略是每次都选择当前情况下的最优解,即选择当前可用的最大面额的硬币。这种策略在这个特定问题中得到了最优解。但需要指出的是,对于不同的面额组合和要求找零的金额,贪心算法并不总是能够得到最优解。

代码实现

#include <stdio.h>
void findChange(int amount) {int money[] = { 50, 20, 10, 5, 2, 1 };int num = sizeof(money) / sizeof(money[0]);printf("找零 %d :\n", amount);for (int i = 0; i < num; ++i) {int current = money[i];int numNotes = amount / current;if (numNotes > 0) {printf("%d 张 %d 块\n", numNotes, current);amount = amount % current;}}
}
int main() {int amount = 46;findChange(amount);return 0;
}

复杂度分析

  • 时间复杂度:O(1),因为面额种类有限。
  • 空间复杂度:O(1)。

注意:对于某些面额组合,贪心算法不一定能得到最优解。

案例二:活动选择问题

问题描述:有n个活动,每个活动有开始时间和结束时间,要求选择最多数量的互不重叠活动。

贪心策略:每次选择结束时间最早且不与已选活动冲突的活动。

伪代码

1. 按活动结束时间从小到大排序
2. 依次选择与已选活动不冲突的下一个活动

代码实现

#include <stdio.h>
#include <stdlib.h>// 定义活动结构体,包含开始时间和结束时间
typedef struct {int start;  // 活动开始时间int end;    // 活动结束时间int index;  // 活动编号(可选,用于标识活动)
} Activity;// 比较函数,用于按活动结束时间排序
int cmp(const void *a, const void *b) {return ((Activity*)a)->end - ((Activity*)b)->end;
}// 贪心算法选择活动函数
void selectActivities(Activity acts[], int n) {// 按活动结束时间从小到大排序qsort(acts, n, sizeof(Activity), cmp);// 选择第一个活动(结束时间最早的活动)int count = 0;  // 记录选择的活动数量int lastEnd = -1;  // 上一个选择的活动的结束时间,初始为-1printf("选择的活动:\n");for (int i = 0; i < n; ++i) {// 如果当前活动的开始时间大于等于上一个选择的活动的结束时间,则选择该活动if (acts[i].start >= lastEnd) {count++;printf("活动%d: [%d, %d]\n", acts[i].index, acts[i].start, acts[i].end);lastEnd = acts[i].end;  // 更新最后选择的活动的结束时间}}printf("共选择了 %d 个活动\n", count);
}// 主函数,演示活动选择问题
int main() {// 示例活动集合,每个活动有开始时间、结束时间和编号Activity activities[] = {{1, 4, 1},   // 活动1:开始时间1,结束时间4{3, 5, 2},   // 活动2:开始时间3,结束时间5{0, 6, 3},   // 活动3:开始时间0,结束时间6{5, 7, 4},   // 活动4:开始时间5,结束时间7{3, 9, 5},   // 活动5:开始时间3,结束时间9{5, 9, 6},   // 活动6:开始时间5,结束时间9{6, 10, 7},  // 活动7:开始时间6,结束时间10{8, 11, 8},  // 活动8:开始时间8,结束时间11{8, 12, 9},  // 活动9:开始时间8,结束时间12{2, 14, 10}, // 活动10:开始时间2,结束时间14{12, 16, 11} // 活动11:开始时间12,结束时间16};int n = sizeof(activities) / sizeof(activities[0]);printf("共有 %d 个活动\n\n", n);// 调用贪心算法选择活动selectActivities(activities, n);return 0;
}

复杂度分析

  • 时间复杂度:O(nlogn),主要是排序的时间复杂度。
  • 空间复杂度:O(1),除了存储活动的数组外,只需要常数级的额外空间。

贪心策略正确性证明

活动选择问题的贪心策略是选择结束时间最早的活动,这一策略的正确性可以通过反证法证明:

假设最优解集合为S,包含k个活动,按结束时间排序后为{a₁, a₂, …, aₖ}。如果a₁不是所有活动中结束时间最早的活动,那么存在活动b,其结束时间早于a₁。

我们可以用b替换S中的a₁,得到新的解集合S’ = {b, a₂, …, aₖ}。由于b的结束时间早于a₁,b不会与a₂, …, aₖ冲突,因此S’也是一个可行解,且|S’| = |S|。这说明选择结束时间最早的活动不会导致最优解的丢失。

通过归纳法,可以证明每次选择当前结束时间最早的、与已选活动不冲突的活动,最终能得到最优解。

应用场景

活动选择问题在实际中有广泛应用,例如:

  • 会议室安排:在有限的会议室中安排尽可能多的会议
  • 课程表设计:在固定教室中安排尽可能多的课程
  • 任务调度:在单处理器上安排尽可能多的任务

案例三:货仓选址问题

问题描述:在一条数轴上有N家商店,它们的坐标分别为A1∼AN。现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。

贪心策略:将货仓建在所有商店坐标的中位数位置。

代码实现

#include <stdio.h>
#include <stdlib.h>// 比较函数,用于排序
int compare(const void* a, const void* b) {return (*(int*)a - *(int*)b);
}// 计算距离之和的函数
int sum(int warehouse, int* shops, int n) {int totalDistance = 0;for (int i = 0; i < n; ++i) {totalDistance += abs(warehouse - shops[i]);}return totalDistance;
}// 找到货仓位置的函数
int find(int* shops, int n) {// 将商店坐标排序qsort(shops, n, sizeof(int), compare);// 中位数位置int warehousePosition = shops[n / 2];return warehousePosition;
}int main() {int n;printf("输入商店的数量:");scanf("%d", &n);int* shops = (int*)malloc(n * sizeof(int));printf("输入商店的坐标:");for (int i = 0; i < n; ++i) {scanf("%d", &shops[i]);}int pos = find(shops, n);int total = sum(pos, shops, n);printf("把货仓建在坐标 %d 处,使得货仓到每家商店的距离之和最小,为:%d\n", pos, total);free(shops);return 0;
}

复杂度分析

  • 时间复杂度:O(nlogn),主要是排序的时间复杂度。
  • 空间复杂度:O(n),需要存储n个商店的坐标。

注意:这个问题的贪心策略是选择中位数位置,这是因为在一维数轴上,到各点距离之和最小的位置就是这些点的中位数。

贪心算法的应用详解

霍夫曼编码

霍夫曼编码是一种变长编码方式,它根据字符出现的频率来设计编码长度,频率高的字符使用较短的编码,频率低的字符使用较长的编码,从而达到压缩数据的目的。

贪心策略:每次选择两个频率最低的节点合并,构建一棵哈夫曼树,然后根据从根到叶子的路径确定编码。

最小生成树

最小生成树是连通无向图的一个生成树,其权值之和最小。常用的算法有:

Prim算法

  1. 从图中任选一个顶点作为起始点,加入到最小生成树中
  2. 在所有与当前最小生成树中顶点相邻的边中,选择权值最小的边,将其连接的未访问顶点加入到最小生成树中
  3. 重复步骤2,直到所有顶点都加入到最小生成树中

Kruskal算法

  1. 将图中所有边按权值从小到大排序
  2. 按照权值从小到大的顺序选择边,如果该边不会与已选择的边构成环路,则将其加入到最小生成树中
  3. 重复步骤2,直到选择了n-1条边(n为顶点数)

Dijkstra最短路径算法

Dijkstra算法用于求解单源最短路径问题,即从一个顶点到其余各顶点的最短路径。

贪心策略:每次选择当前未访问的距离起点最近的顶点,然后更新与该顶点相邻的其他顶点的距离。

总结

贪心算法通过每一步的局部最优选择,期望获得全局最优解。适用于满足最优子结构和贪心选择性质的问题。常见应用有找零、活动选择、最小生成树、最短路径、霍夫曼编码等。实际应用时需验证贪心策略的正确性,因为贪心算法并不总是能得到全局最优解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/77689.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

英码科技与泊川软件,携手加速AI与嵌入式系统融合创新

2025年4月15日&#xff0c;广州英码信息科技有限公司&#xff08;以下简称“英码科技”&#xff09;与广州泊川软件技术有限公司&#xff08;以下简称“泊川软件”&#xff09; 正式签署战略合作框架协议。此次合作将充分发挥双方在AI计算硬件与嵌入式操作系统领域的技术优势&a…

Flowable7.x学习笔记(九)部署 BPMN XML 流程

前言 到本篇为止&#xff0c;我们已经完成了流程定义以及其 BPMN XML 本身的查询和新增功能&#xff0c;那我们有有了XML之后就可以开始着手研究实现 Flowable7对流程的各种操作了&#xff0c;比如部署&#xff0c;挂起&#xff0c;发起等等。 首先第一步&#xff0c;我们本篇文…

electron 渲染进程按钮创建新window,报BrowserWindow is not a constructor错误;

在 Electron 中&#xff0c;有主进程和渲染进程 主进程&#xff1a;在Node.js环境中运行—意味着能够使用require模块并使用所有Node.js API 渲染进程&#xff1a;每个electron应用都会为每个打开的BrowserWindow&#xff08;与每个网页嵌入&#xff09;生成一个单独的渲染器进…

深入规划 Elasticsearch 索引:策略与实践

一、Elasticsearch 索引概述 &#xff08;一&#xff09;索引基本概念 Elasticsearch 是一个分布式、高性能的全文搜索引擎&#xff0c;其核心概念之一便是索引。索引本质上是一个存储文档的逻辑容器&#xff0c;它使得数据能够在高效的检索机制下被查询到。当我们对文档进行…

llamafactory的包安装

cuda版本12.1&#xff0c;python版本3.10&#xff0c;torch版本2.4.0&#xff0c;几个关键包版本如下&#xff1a; torch2.4.0cu121 transformers4.48.3 triton3.0.0 flash-attn2.7.1.post4 xformers0.0.27.post2 vllm0.6.3.post1 vllm-flash-attn2.6.1 unsloth2025.3.18 unsl…

Redis专题

前言 Redis的各种思想跟机组Cache和操作系统对进程的管理非常类似&#xff01; 一&#xff1a;看到你的简历上写了你的项目里面用到了redis&#xff0c;为啥用redis&#xff1f; 因为传统的关系型数据库如Mysql,已经不能适用所有的场景&#xff0c;比如秒杀的库存扣减&#xff…

【Rust 精进之路之第7篇-函数之道】定义、调用与参数传递:构建代码的基本单元

系列: Rust 精进之路:构建可靠、高效软件的底层逻辑 作者: 码觉客 发布日期: 2025-04-20 引言:封装逻辑,代码复用的基石 在之前的文章中,我们已经探索了 Rust 如何处理数据(变量、标量类型、复合类型)以及如何控制程序的执行流程(if/else、循环)。这些构成了编写简…

文件有几十个T,需要做rag,用ragFlow能否快速落地呢?

一、RAGFlow的优势 1、RAGFlow处理大规模数据性能&#xff1a; &#xff08;1&#xff09;、RAGFlow支持分布式索引构建&#xff0c;采用分片技术&#xff0c;能够处理TB级数据。 &#xff08;2&#xff09;、它结合向量搜索和关键词搜索&#xff0c;提高检索效率。 &#xf…

安卓的桌面 launcher是什么

安卓的桌面Launcher是一种安卓应用程序&#xff0c;它主要负责管理和展示手机主屏幕的界面以及相关功能&#xff0c;为用户提供与设备交互的主要入口。以下是其详细介绍&#xff1a; 功能 主屏幕管理&#xff1a;用户可以在主屏幕上添加、删除和排列各种应用程序图标、小部件…

【学习笔记】计算机网络(九)—— 无线网络和移动网络

第9章 无线网络和移动网络 文章目录 第9章 无线网络和移动网络9.1 无线局域网WLAN9.1.1 无线局域网的组成9.1.2 802.11局域网的物理层9.1.3 802.11局域网的MAC层协议CSMA 协议CSMA/CD 协议 - 总线型 - 半双工CSMA/CA 协议 9.1.4 802.11局域网的MAC帧 9.2 无线个人区域网WPAN9.3…

无线网络入侵检测系统实战 | 基于React+Python的可视化安全平台开发详解

随着无线网络的普及&#xff0c;网络攻击风险也日益严峻。本项目旨在构建一个实时监测、智能识别、高效防护的无线网络安全平台&#xff0c;通过结合前后端技术与安全算法&#xff0c;实现对常见攻击行为的有效监控和防御。 一、项目简介与功能目的 本系统是一款基于 React 前…

速通FlinkCDC3.0

1.FlinkCDC概述 1.1FlinkCDC是什么&#xff1f; FlinkCDC&#xff08;Flink Change Data Capture&#xff09;是一个用于实时捕获数据库变更日志的工具&#xff0c;它可以将数据库的变更实时同步到ApacheFlink系统中。 1.2 FlinkCDC的三个版本&#xff1f; 1.x 这个版本的Fli…

B+树节点与插入操作

B树节点与插入操作 设计B树节点 在设计B树的数据结构时&#xff0c;我们首先需要定义节点的格式&#xff0c;这将帮助我们理解如何进行插入、删除以及分裂和合并操作。以下是对B树节点设计的详细说明。 节点格式概述 所有的B树节点大小相同&#xff0c;这是为了后续使用自由…

C# 检查字符串是否包含在另一个字符串中

string shopList "我是大浪,你的小狼"; this.ShopId"你的小狼"; bool existsShopId false; if (!string.IsNullOrEmpty(shopList)) {existsShopId shopList.Split(,).Any(part > part.Trim() this.ShopId); }检查 goodsIdSet 中的每个元素是否都在 …

珈和科技遥感赋能农业保险创新 入选省级卫星应用示范标杆

为促进空天信息与数字经济深度融合&#xff0c;拓展卫星数据应用场景价值&#xff0c;提升卫星数据应用效能和用户体验&#xff0c;加速卫星遥感技术向民生领域转化应用&#xff0c;近日&#xff0c;湖北省国防科工办组织开展了2024年湖北省卫星应用示范项目遴选工作。 经多渠…

深入理解 React 组件的生命周期:从创建到销毁的全过程

React 作为当今最流行的前端框架之一&#xff0c;其组件生命周期是每个 React 开发者必须掌握的核心概念。本文将全面剖析 React 组件的生命周期&#xff0c;包括类组件的各个生命周期方法和函数组件如何使用 Hooks 模拟生命周期行为&#xff0c;帮助开发者编写更高效、更健壮的…

缓存 --- Redis性能瓶颈和大Key问题

缓存 --- Redis性能瓶颈和大Key问题 内存瓶颈网络瓶颈CPU 瓶颈持久化瓶颈大key问题优化方案 Redis 是一个高性能的内存数据库&#xff0c;但在实际使用中&#xff0c;可能会在内存、网络、CPU、持久化、大键值对等方面遇到性能瓶颈。下面从这些方面详细分析 Redis 的性能瓶颈&a…

Python爬虫与代理IP:高效抓取数据的实战指南

目录 一、基础概念解析 1.1 爬虫的工作原理 1.2 代理IP的作用 二、环境搭建与工具选择 2.1 Python库准备 2.2 代理IP选择技巧 三、实战步骤分解 3.1 基础版&#xff1a;单线程免费代理 3.2 进阶版&#xff1a;多线程付费代理池 3.3 终极版&#xff1a;Scrapy框架自动…

Nginx HTTP 414 与“大面积”式洪水攻击联合防御实战

一、引言 在大规模分布式应用中&#xff0c;Nginx 常作为前端负载均衡和反向代理服务器。攻击者若结合超长 URI/头部攻击&#xff08;触发 HTTP 414&#xff09;与海量洪水攻击&#xff0c;可在网络层与应用层形成双重打击&#xff1a;一方面耗尽缓冲区和内存&#xff0c;另一…

【上位机——MFC】运行时类信息机制

运行时类信息机制的使用 类必须派生自CObject类内必须添加声明宏DECLARE_DYNAMIC(theClass)3.类外必须添加实现宏 IMPLEMENT_DYNAMIC(theClass,baseClass) 具备上述三个条件后&#xff0c;CObject::IsKindOf函数就可以正确判断对象是否属于某个类。 代码示例 #include <…