算法经典“可怜的奶牛”问题 使用堆高效组织数据 C语言描述
算法经典“可怜的奶牛”问题 使用堆高效组织数据 C语言描述
题目
问题描述
农夫John有n(n≤10000)头奶牛。可是由于它们产的奶太少,农夫对它们很不满意,决定每天把产奶最少的一头做成牛肉干吃掉。但还是有一点舍不得,John打算,如果有不止一头奶牛产奶最少,当天就大发慈悲,放过所有的牛。
由于John的奶牛产奶是周期性的,John在一开始就能了解所有牛的最终命运。不过他的数学很差,所以请你帮帮忙,算算最后有多少头奶牛可以幸免于难。
每头奶牛的产奶周期Ti可能不同,但不会超过10,也不会小于1。在每个周期中,奶牛每天产奶量不超过100。
基本要求
本题核心是每次取一个最小元素,不过,由于元素有周期性变化,所以不能把它们直接组织成一个堆。由于周期也不相同,只知道周期最大不超过10天,所以模拟的天数可能需要至少2520天(这是1、2、……、10的最小公倍数)。
- 生成奶牛参数表,包括每头牛的周期数以及它们在这些周期内每天的产奶量。考虑到数量比较大,要求用随机函数生成。
- 以天为单位,模拟一个合理的时间段(例如2520)天,显示每天被屠宰的牛编号或者因为有相同的牛产奶最少而大赦牛棚。
输入输出
输入:牛的总数cows,需要模拟的天数days。
输出:将随机生成的牛参数输出到一个文本文件中,要求格式为
周期为1的奶牛:编号 第1天(产奶量,下同) 45 34 97 56 …… ……
周期为2的奶牛:编号 第1天 第2天 243 7 39 651 75 37 …… …… ……
然后输出从第1天开始的模拟结果(也需要输出到另一个文本文件中):
第1天,产奶最少的牛编号是:…… x号牛被屠宰或没有牛被屠宰
第2天,产奶最少的牛编号是:…… y号牛被屠宰或没有牛被屠宰
……
整体思路
此题描述十分复杂,但整理一下大概是这么一个意思:有一群奶牛,每天要将产奶量最小的牛给杀了,除非这一天有一头以上的牛产奶量一样少就不杀,每头牛的产奶量变化是有自己的周期规律的。其他的描述就是关于牛、产奶量、周期的定义和描述了,不再赘述。
通过以上整理,可以得出一个简单的算法思路:将每头牛的产奶量信息存储起来,按照周期变化;每天取牛中产奶量最小者屠宰,并记录;更新牛群和每头牛的产奶量,继续屠宰,直至模拟的时间停止。
进一步,可以得出整个需要实现的过程:生成牛,存储牛,杀牛、更新牛和记录输出循环,主程序如下:
int main(void)
{ int cowAmount = MAX_INDEX + 1; int dayAmount = MIN_DAY; // 以上为默认值,宏定义详见后文 getPara(&cowAmount, &dayAmount); // 通过用户输入获取必要的参数 Cow *cows = initCow(cowAmount); // 生成牛的信息 killCow(dayAmount, cows, cowAmount); // 杀牛的过程 return 0;
}
这其中关键点和难点在于:如何存储牛和根据周期更新牛数据?如何高效寻找到产奶量最小者?
牛的存储与更新
根据题目描述,牛的数据主要有三项:编号、周期、产奶量。产奶量按照周期变化,故需要记录周期中每一个节点的值。因此,笔者选用广义表,产奶量以数组的数据结构记录。具体到C语言中,使用int
型的指针,记录数组的地址:
typedef struct
{ int index; int* milk; int cycle;
} Cow;
易得,产奶量数组的长度就是产奶量变化的周期数,这样就可以用日期对牛的周期数取余,并以之为索引获取数组中的值。
cow.milk[(day - 1) % cow.cycle]
需要注意的是,天数是从1开始算的,数组的下标是以0开始的,天数需要-1。
使用堆获取最小值
原理
堆这种数据结构在排序中比较常用,堆排序是和快速排序有着一样的时间与空间复杂度的算法。然而,本题中并不需要完美的排序,只需要取到最小值就可以了,所以只需构造堆,利用堆子节点必比父节点小(大)的特点,在整理好堆过后,对比前若干个节点是否有相等的即可。
整理堆有递归和非递归的两种写法,本解法中获取牛当前产量的表达式略微复杂,若使用非递归的方式实现,则会让本就复杂难懂的代码雪上加霜,因此使用递归方式实现。
实现
/** * @description: 交换两个牛的信息 * @param {Cow} *cowA 牛A的地址 * @param {Cow} *cowB 牛B的地址 */
void swapCow(Cow *cowA, Cow *cowB)
{ Cow temp = *cowA; *cowA = *cowB; *cowB = temp;
} /** * @description: 在堆中递归调整root节点 * @param {Cow} list[] 存放牛的列表地址 * @param {int} root 子堆顶在列表中的位置 * @param {int} length 牛列表的长(总长) * @param {int} day 当前天数 */
void adjustCowHeap(Cow list[], int root, int length, int day)
{ Cow temp, *rootCow = &list[root]; if ((root + 1) * 2 < length) // has right child { Cow *rChild = &list[(root + 1) * 2]; Cow *lChild = &list[(root + 1) * 2 - 1]; if (lChild->milk[(day - 1) % lChild->cycle] < rChild->milk[(day - 1) % rChild->cycle]) { if (lChild->milk[(day - 1) % lChild->cycle] < rootCow->milk[(day - 1) % rootCow->cycle]) { swapCow(rootCow, lChild); adjustCowHeap(list, (root + 1) * 2 - 1, length, day); } } else { if (rChild->milk[(day - 1) % rChild->cycle] < rootCow->milk[(day - 1) % rootCow->cycle]) { swapCow(rootCow, rChild); adjustCowHeap(list, (root + 1) * 2, length, day); } } } else if ((root + 1) * 2 == length) { Cow *lChild = &list[(root + 1) * 2 - 1]; if (lChild->milk[(day - 1) % lChild->cycle] < rootCow->milk[(day - 1) % rootCow->cycle]) { swapCow(rootCow, lChild); } }
} /** * @description: 初始化堆 * @param {Cow} list[] 牛列表 * @param {int} amount 列表的长 * @param {int} day 当前天数 */
void createCowHeap(Cow list[], int amount, int day)
{ for (int i = (amount - 2) / 2; i >= 0; i--) { adjustCowHeap(list, i, amount, day); }
}
细节实现
如何判定当天是否杀牛
杀牛的条件是只有一头牛产量最少,因为使用堆组织数据已经让牛数组在一定程度上是有序的了,故可以通过比较堆顶附近是否有和堆顶牛产量相同的牛即可判定。为此,笔者专门编写了一个isKillable()
函数:
/** * @description: 计算当前最小产奶量牛的个数 * @param {Cow} list[] 已整理成堆的牛列表 * @param {int} day 当前天数 * @return {int} 当前最小产奶量牛的个数 */
int isKillable(Cow list[], int day)
{ Cow head = list[0]; int i = 1; while (1) { Cow temp = list[i]; if (head.milk[0] == MILK_OF_KILLED) { return 0; } if (head.milk[(day - 1) % head.cycle] != temp.milk[(day - 1) % temp.cycle]) { return i; } i++; }
}
这个函数的功能是返回有几个产量最少者。在判断前,函数会先判断堆顶的牛是否已经被杀死,如果是,则说明牛已经杀绝了,无需继续比较。
如何杀牛
根据牛的存储方式和数据结构,以及使用堆来组织的特点,杀牛掉的牛不设特殊的标记,而是将周期改为1,产奶量为大于题目要求合法范围的数(最大值100,此题死牛设置为999,详见宏定义)。通过这样处理,死牛将会在取最小值整理堆的时候,自动的从堆顶“沉到”堆底,无需另外的删除操作和占用额外的标记空间。
if (isKillable == 1) { Cow *killedCow = &list[0]; fprintf( file, "产奶最少的牛编号是: %d\t\t%d号牛被屠宰\n", killedCow->index, killedCow->index); killedCowAmount++; free(killedCow->milk); killedCow->milk = (int *)malloc(sizeof(int)); killedCow->milk[0] = MILK_OF_KILLED; killedCow->cycle = 1; }
读取输入
使用占位符获取scanf()
函数返回的值,消除编译器警告。使用goto
语句实现用户输入非法内容时自动重试。支持随机随机,便于演示:
int _ = 0; // 占位符,消除scanf的警告
/** * @description: 获取模拟牛的数量和天数 * @param {int} *cow 存放牛数量的地址 * @param {int} *day 存放天数的地址 */
void getPara(int *cow, int *day)
{ int temp = 0; srand((unsigned)time(NULL)); system("chcp 936 > NUL"); // 确保不同设备正常显示 GETCOW: puts( "请输入牛的总数,建议大于200,不得大于10000。" "可以输入0以随机生成数量:"); _ = scanf("%d", &temp); if (temp == 0) { *cow = randint((MAX_INDEX + 1) / 5, MAX_INDEX + 1); } else if (temp <= 10000) { *cow = temp; } else { puts("您的输入有误,请重试!"); goto GETCOW; } GETDAY: puts( "请输入需要模拟的天数,不得小于2520。" "可以输入0以随机生成天数:"); _ = scanf("%d", &temp); if (temp == 0) { *day = randint(MIN_DAY, 5 * MIN_DAY); } else if (temp >= 2520) { *day = temp; } else { puts("您的输入有误,请重试!"); goto GETDAY; }
}
宏定义
#include <stdio.h>
#include <stdlib.h>
#include <time.h> #define MIN_INDEX 0 // 牛从0开始编号
#define MAX_INDEX 9999
#define MIN_CYCLE 1
#define MAX_CYCLE 10
#define MIN_MILK 0
#define MAX_MILK 100
#define MILK_OF_KILLED 999 // 牛死后产奶量设置为999,便于堆整理
#define MIN_DAY 2520
#define COWS_FILE "cows.txt"
#define DAYS_FILE "days.txt"
其他模块
/** * @description: 返回含指定下限与上限之间的随机整数 * @param {int} min 随机数大小下限 * @param {int} max 随机数大小上限 * @return {int} randomNum 给定范围内的伪随机数 */
int randint(int min, int max)
{ int randomNum = 0; randomNum = rand() % (1 + max - min) + min; return randomNum;
} /** * @description: 初始化牛的列表并格式化输出 * @param {int} amount 牛的数量 * @return {Cow*} 牛列表的数组的地址 */
Cow *initCow(int amount)
{ Cow *list = (Cow *)malloc(sizeof(Cow) * amount); for (int i = 0; i < amount; i++) { Cow *cow = &list[i]; cow->index = i; cow->cycle = randint(MIN_CYCLE, MAX_CYCLE); cow->milk = (int *)malloc(sizeof(int) * cow->cycle); for (int j = 0; j < cow->cycle; j++) { cow->milk[j] = randint(MIN_MILK, MAX_MILK); } } FILE *file = fopen(COWS_FILE, "w"); for (int i = 1; i <= MAX_CYCLE; i++) { fprintf(file, "周期为%d的奶牛:编号 ", i); for (int j = 1; j <= i; j++) { fprintf(file, "第%d天\t", j); } fprintf(file, "\n"); for (int j = 0; j < amount; j++) { Cow cow = list[j]; if (cow.cycle == i) { fprintf(file, "\t\t\t\t%d\t", cow.index); for (int k = 0; k < i; k++) { fprintf(file, "\t%d", cow.milk[k]); } fprintf(file, "\n"); } } } fclose(file); return list;
} /** * @description: 杀牛并格式化输出 * @param {int} days 模拟的总天数 * @param {Cow} list[] 牛列表 * @param {int} amount 牛的个数(含死牛) */
void killCow(int days, Cow list[], int amount)
{ FILE *file = fopen(DAYS_FILE, "w"); int killedCowAmount = 0, noKillDays = 0; for (int day = 1; day <= days; day++) { createCowHeap(list, amount, day); fprintf(file, "第%d天,", day); int minCow = isKillable(list, day); if (minCow == 1) { Cow *killedCow = &list[0]; fprintf( file, "产奶最少的牛编号是: %d\t\t%d号牛被屠宰\n", killedCow->index, killedCow->index); killedCowAmount++; free(killedCow->milk); killedCow->milk = (int *)malloc(sizeof(int)); killedCow->milk[0] = MILK_OF_KILLED; killedCow->cycle = 1; } else { fprintf(file, "产奶最少的牛编号是:"); for (int i = 0; i < minCow; i++) { fprintf(file, " %d", list[i].index); } fprintf(file, "\t\t没有牛被屠宰\n"); noKillDays++; } } fprintf(file, "统计:总共被宰掉%d头奶牛,到模拟结束,总共有%d天没有发生奶牛被屠宰事件。", killedCowAmount, noKillDays); fclose(file);
}
算法经典“可怜的奶牛”问题 使用堆高效组织数据 C语言描述相关推荐
- 算法学习 (门徒计划)2-2 堆(Heap)与优先队列 学习笔记
算法学习 (门徒计划)2-2 堆(Heap)与优先队列 学习笔记 前言 堆(Heap)的概念和基础操作 基础概念 基础操作 入堆与上滤 出堆与下滤 自定义类实现堆 优先队列 经典例题,堆的基础应用 l ...
- 阿里架构师强烈推荐《数据结构与算法经典问题解析》(PDF文档)
前言: 小编整理了一份数据结构与算法经典问题解析核心知识点.覆盖递归和回溯.链表.栈.队列.树.优先队列和堆.队列.优先队列和堆.并查集ADT.排序.选择算法(中位数).散列.算法设计技术.分治算法. ...
- 车道线检测算法经典编程
车道线检测算法经典编程 车道线曲线拟合算法编程 计算经过(50,50),(90,120),(70,200)三点的Catmull_Rom样条曲线. IplImage* img = cvCreateIma ...
- 【LeetCode-面试算法经典-Java实现】【015-3 Sum(三个数的和)】
[015-3 Sum(三个数的和)] [LeetCode-面试算法经典-Java实现][全部题目文件夹索引] 原题 Given an array S of n integers, are there ...
- 【LeetCode-面试算法经典-Java实现】【109-Convert Sorted List to Binary Search Tree(排序链表转换成二叉排序树)】...
[109-Convert Sorted List to Binary Search Tree(排序链表转换成二叉排序树)] [LeetCode-面试算法经典-Java实现][全部题目文件夹索引] 原题 ...
- Algorithm之PrA:PrA之nLP非线性规划算法经典案例剖析+Matlab编程实现
Algorithm之PrA:PrA之nLP整数规划算法经典案例剖析+Matlab编程实现 目录 有约束非线性规划案例分析 1.投资决策问题 2.利用Matlab实现求解下列非线性规划 无约束极值问题 ...
- 【LeetCode-面试算法经典-Java实现】【054-Spiral Matrix(螺旋矩阵)】
[054-Spiral Matrix(螺旋矩阵)] [LeetCode-面试算法经典-Java实现][全部题目文件夹索引] 原题 Given a matrix of m x n elements (m ...
- 数据结构与算法--经典10大排序算法(动图演示)【建议收藏】
十大经典排序算法总结(动图演示) 算法分类 十大常见排序算法可分为两大类: 比较排序算法:通过比较来决定元素的位置,由于时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序 非比较类型排 ...
- 【LeetCode-面试算法经典-Java实现】【129-Sum Root to Leaf Numbers(全部根到叶子结点组组成的数字相加)】...
[129-Sum Root to Leaf Numbers(全部根到叶子结点组组成的数字相加)] [LeetCode-面试算法经典-Java实现][全部题目文件夹索引] 原题 Given a bina ...
最新文章
- Mysql 多表使用 Case when then 遇到的坑
- python web-python web入坑指南
- 7 天玩转 ASP.NET MVC — 第 3 天
- #!(sha-bang)--脚本的开始
- 企业实战案例02_Jenkins_连接远程GitLab拉取代码
- 【java笔记】接口的定义,接口的使用
- DIV向上滚动(类似新闻)
- 简单的网页制作期末作业
- 不会安装Lomboz?直接下载eclipse JEE吧。
- 计算机主板供电,台式机计算机主板供电电路.doc
- [PHP]关于GearmanClient的诡异事件
- 解决Edge及Chrome等浏览器主页被篡改2345导航页
- SpringBoot+Vue下载文件Excel、PDF下载后打不开
- 本土实力派陈旭东出任IBM大中华区总经理,意外还是惊喜?
- camunda数据库表结构介绍
- 【STM32 x ESP8266】连接阿里云 MQTT 服务器(报文连接)
- App Inventor 模拟器问题的解决
- return的作用,返回函数值和结束程序执行
- xampp linux 命令,centos 下XAMPP 常用命令
- 源生代码封装轮播效果
热门文章
- 阿里巴巴图标库全部下载
- 超市管理系统制定测试计划
- 运行Equinox控制台报错Could not find bundle: org.eclipse.equinox.console
- HDP2.5更换集群IP
- 商业地产拓展市场调查报告内容及格式
- 一个稳赚的野路子,价值超大
- 小熊U租冲击IPO:IT办公租赁起风了?
- android studio实现小吃商城,android课程设设计
- 演讲实录!谷得技术总监陈镇洪教你打造游戏研发流水线
- network-scripts目录下添加新网卡文件方法、nmcli修改NAME网卡名称和DEVICE一致