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

算法经典“可怜的奶牛”问题 使用堆高效组织数据 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);    
}
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值