题目要求
7-2 猴子选大王 (10分) 一群猴子要选新猴王。新猴王的选择方法是:让N只候选猴子围成一圈,从某位置起顺序编号为1~N号。从第1号开始报数,每轮从1报到3,凡报到3的猴子即退出圈子,接着又从紧邻的下一只猴子开始同样的报数。如此不断循环,最后剩下的一只猴子就选为猴王。请问是原来第几号猴子当选猴王?
输入格式:
输入在一行中给一个正整数N(≤1000)。
输出格式:
在一行中输出当选猴王的编号。
输入样例:
11
输出样例:
7
让我们先来看看输入样例中11个猴子的选举情况:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|
第一轮 | 1 | 2 | 3 | 1 | 2 | 3 | 1 | 2 | 3 | 1 | 2 |
第二轮 | 3 | 1 | 2 | 3 | 1 | 2 | 3 | 1 | |||
第三轮 | 2 | 3 | 1 | 2 | 3 | ||||||
第四轮 | 1 | 2 | 3 | ||||||||
第五轮 | 1 | 2 | |||||||||
第六轮 | 3 | 1 | |||||||||
第七轮 | 2 |
所以最后7号猴子被选为大王。
链表法
很容易想到循环链表,每次循环只需将报数为3的猴子代表的节点从链表中删除(即题目中的退出圈子),使用双向或单向链表皆可,这里我使用单向循环链表。
- 关于头结点,在链表中,使用头结点在操作上更方便,对于此题还有一个作用,可以将当前还在圈子的猴子个数存于头节点中,并以此作为是否继续遍历循环链表的依据。
节点内容以及循环链表示意如下图所示:
节点包含猴子的编号number、每次报数的号码data以及指向下一节点的next指针。
由于是单向链表,所以在找到data=3的节点再删除时,无法找到其前驱节点,所以当data=2时,就删除下一节点。
#include<stdio.h>
#include<malloc.h>
typedef struct Node{
int number,data;
struct Node *next;
} LinkList;
LinkList *create(int n){
int i,num = 0;
//定义头节点,普通节点,尾部节点;
LinkList *head, *node, *p;
//分配头节点地址
head = (LinkList*)malloc(sizeof(LinkList));
//若是空链表则头尾节点一样
p = head;
head -> data = n;
head -> number=0;
for (i = 0; i < n; i++) {
node = (LinkList*)malloc(sizeof(LinkList));
node -> number = i + 1;
node -> data = 0;
p -> next = node;
p = node;
}
//结束创建,构造循环链表
p -> next = head -> next;
return head;
}
int main(){
int i,n,num = 0;
scanf("%d",&n);
LinkList *p,*head = create(n);
p = head -> next;
int count = head -> data;
while(count != 1){
p->data = (num++) % 3 + 1;
printf("%d-%d\n",p->number,p->data);
if(p -> data == 2){
p -> next = p -> next -> next;
num+=1;
count--;
}
p = p -> next;
}
printf("%d",p -> number);
free(p);
free(head);
return 0;
}
数组法
如果不采用链表法也可以使用一个2维数组来解决,首先需要重新分析选出猴子大王过程。
分析:
在第一轮中淘汰3号、6号、9号,均为3的倍数,可以拓展思路,在第二轮报数中,表面上淘汰报数为3的猴子,实际上淘汰累计报数为3的倍数的猴子,下图更加直观地展现了这一过程。
图中括号中的数字代表累计报数,可以发现11只猴子中选出1个大王,淘汰的为累计报数为3的倍数的前10只猴子,即:3,6,9,12,15,18,21,24,27,30。
构造数组
这里二维数组的列数即为猴子个数,很好确定。但是行数因猴子个数的不同会有变化,具体行数可能可以通过数学公式计算,或者直接构造行数一个足够大的数组。数学方式确定行数的方法暂时我还没找到,读者有这方面的想法可以在评论区讨论;足够大的数组会造成不必要的空间浪费。所以秉持“环保”理念,这里只需构造两行的二维数组,每行都存储报数的数据,至于猴子本身的编号可根据下标确定,循环将后来的数据覆盖前面的数据即可。
数组初始化
为了与之后的判断相区别,这里将下标为0的列初始化为猴子编号,下标为1的列全部初始化为 -1。
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
1 | -1 | -1 | -1 | -1 | -1 | -1 | -1 | -1 | -1 | -1 | -1 |
- 交替插入:遍历插入第0行后,遍历插入第1行,然后再遍历插入第1行,以此类推,节约空间。
这种操作类似于有刷电机的原理,每次插入的行与之前插入的相反。代码中以 reset=!reset 来实现。
对于 第一轮 循环:向 下标为 1 的行插入数字,并判断 上一行 相同列数字是否为3的倍数,若是,则插入0,否则插入的数字按序增长。
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
1 | 12 | 13 | 0 | 14 | 15 | 0 | 16 | 17 | 0 | 18 | 19 |
对于 第二轮 循环:向 下标为 0 的行插入数字,并判断 上一行 相同列数字是否为3的倍数,若是,则插入0,否则插入的数字按序增长。
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 20 | 0 | 21 | 0 | 0 | 22 | 23 | 0 | 0 | 24 |
1 | 12 | 13 | 0 | 14 | 15 | 0 | 16 | 17 | 0 | 18 | 19 |
之后的循环以此类推。
- 循环的退出条件: 由题意可知,当最后圈内猴子个数剩余1个时,即选出了大王。
所以每当有新的3的倍数的累计报数产生,就相应地减少猴子个数。注意这里是新的3的倍数的累计报数,对于之间上一行已经是0的列来说,猴子个数count保持不变。
即:
if(当前插入数字为3x && 上一行该列数字不为0){ count - -; }
if(当前插入数字为3x && 上一行该列数字为0) { count保持不变; }
完整代码如下:
#include<stdio.h>
int main(){
int i,n;
scanf("%d",&n);
int king[2][n];
//二维数组初始化
for(i=0;i<n;i++){
king[0][i]=i+1;
king[1][i]=-1;
}
int count=n,last=n,reset=0,flag;
i=0;
while(count!=1){
for(i=0;i<n;i++){
if(king[reset][i]%3==0 && king[!reset][i]%3==0){
king[!reset][i]=0;
}else if(king[reset][i]%3==0 && king[!reset][i]%3!=0){
king[!reset][i]=0;
count--;
}else {
king[!reset][i]=++last;
flag=i+1;
}
}
//交替插入
reset=!reset;
}
printf("%d",flag);
return 0;
}
通过添加输出语句可以显示每一轮的选举以及完整的选举过程:
每一轮选举结果:
完整选举过程: