算法经典“可怜的奶牛”问题 使用堆高效组织数据 C语言描述

题目

问题描述

农夫John有n(n≤10000)头奶牛。可是由于它们产的奶太少,农夫对它们很不满意,决定每天把产奶最少的一头做成牛肉干吃掉。但还是有一点舍不得,John打算,如果有不止一头奶牛产奶最少,当天就大发慈悲,放过所有的牛。

由于John的奶牛产奶是周期性的,John在一开始就能了解所有牛的最终命运。不过他的数学很差,所以请你帮帮忙,算算最后有多少头奶牛可以幸免于难。

每头奶牛的产奶周期Ti可能不同,但不会超过10,也不会小于1。在每个周期中,奶牛每天产奶量不超过100。

基本要求

本题核心是每次取一个最小元素,不过,由于元素有周期性变化,所以不能把它们直接组织成一个堆。由于周期也不相同,只知道周期最大不超过10天,所以模拟的天数可能需要至少2520天(这是1、2、……、10的最小公倍数)。

  1. 生成奶牛参数表,包括每头牛的周期数以及它们在这些周期内每天的产奶量。考虑到数量比较大,要求用随机函数生成。
  2. 以天为单位,模拟一个合理的时间段(例如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语言描述相关推荐

  1. 算法学习 (门徒计划)2-2 堆(Heap)与优先队列 学习笔记

    算法学习 (门徒计划)2-2 堆(Heap)与优先队列 学习笔记 前言 堆(Heap)的概念和基础操作 基础概念 基础操作 入堆与上滤 出堆与下滤 自定义类实现堆 优先队列 经典例题,堆的基础应用 l ...

  2. 阿里架构师强烈推荐《数据结构与算法经典问题解析》(PDF文档)

    前言: 小编整理了一份数据结构与算法经典问题解析核心知识点.覆盖递归和回溯.链表.栈.队列.树.优先队列和堆.队列.优先队列和堆.并查集ADT.排序.选择算法(中位数).散列.算法设计技术.分治算法. ...

  3. 车道线检测算法经典编程

    车道线检测算法经典编程 车道线曲线拟合算法编程 计算经过(50,50),(90,120),(70,200)三点的Catmull_Rom样条曲线. IplImage* img = cvCreateIma ...

  4. 【LeetCode-面试算法经典-Java实现】【015-3 Sum(三个数的和)】

    [015-3 Sum(三个数的和)] [LeetCode-面试算法经典-Java实现][全部题目文件夹索引] 原题 Given an array S of n integers, are there ...

  5. 【LeetCode-面试算法经典-Java实现】【109-Convert Sorted List to Binary Search Tree(排序链表转换成二叉排序树)】...

    [109-Convert Sorted List to Binary Search Tree(排序链表转换成二叉排序树)] [LeetCode-面试算法经典-Java实现][全部题目文件夹索引] 原题 ...

  6. Algorithm之PrA:PrA之nLP非线性规划算法经典案例剖析+Matlab编程实现

    Algorithm之PrA:PrA之nLP整数规划算法经典案例剖析+Matlab编程实现 目录 有约束非线性规划案例分析 1.投资决策问题 2.利用Matlab实现求解下列非线性规划​ 无约束极值问题 ...

  7. 【LeetCode-面试算法经典-Java实现】【054-Spiral Matrix(螺旋矩阵)】

    [054-Spiral Matrix(螺旋矩阵)] [LeetCode-面试算法经典-Java实现][全部题目文件夹索引] 原题 Given a matrix of m x n elements (m ...

  8. 数据结构与算法--经典10大排序算法(动图演示)【建议收藏】

    十大经典排序算法总结(动图演示) 算法分类 十大常见排序算法可分为两大类: 比较排序算法:通过比较来决定元素的位置,由于时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序 非比较类型排 ...

  9. 【LeetCode-面试算法经典-Java实现】【129-Sum Root to Leaf Numbers(全部根到叶子结点组组成的数字相加)】...

    [129-Sum Root to Leaf Numbers(全部根到叶子结点组组成的数字相加)] [LeetCode-面试算法经典-Java实现][全部题目文件夹索引] 原题 Given a bina ...

最新文章

  1. Mysql 多表使用 Case when then 遇到的坑
  2. python web-python web入坑指南
  3. 7 天玩转 ASP.NET MVC — 第 3 天
  4. #!(sha-bang)--脚本的开始
  5. 企业实战案例02_Jenkins_连接远程GitLab拉取代码
  6. 【java笔记】接口的定义,接口的使用
  7. DIV向上滚动(类似新闻)
  8. 简单的网页制作期末作业
  9. 不会安装Lomboz?直接下载eclipse JEE吧。
  10. 计算机主板供电,台式机计算机主板供电电路.doc
  11. [PHP]关于GearmanClient的诡异事件
  12. 解决Edge及Chrome等浏览器主页被篡改2345导航页
  13. SpringBoot+Vue下载文件Excel、PDF下载后打不开
  14. 本土实力派陈旭东出任IBM大中华区总经理,意外还是惊喜?
  15. camunda数据库表结构介绍
  16. 【STM32 x ESP8266】连接阿里云 MQTT 服务器(报文连接)
  17. App Inventor 模拟器问题的解决
  18. return的作用,返回函数值和结束程序执行
  19. xampp linux 命令,centos 下XAMPP 常用命令
  20. 源生代码封装轮播效果

热门文章

  1. 阿里巴巴图标库全部下载
  2. 超市管理系统制定测试计划
  3. 运行Equinox控制台报错Could not find bundle: org.eclipse.equinox.console
  4. HDP2.5更换集群IP
  5. 商业地产拓展市场调查报告内容及格式
  6. 一个稳赚的野路子,价值超大
  7. 小熊U租冲击IPO:IT办公租赁起风了?
  8. android studio实现小吃商城,android课程设设计
  9. 演讲实录!谷得技术总监陈镇洪教你打造游戏研发流水线
  10. network-scripts目录下添加新网卡文件方法、nmcli修改NAME网卡名称和DEVICE一致