问题描述
有n人围成一圈,顺序排号。从第1个人开始报数(从1到3报数),凡报到3的人退出圈子,问最后留下的是原来的第几号的那位。
解决方案
刚开始的时候我也是写的暴力算法。
整个程序的设计思路是这个样子:
首先,我们先寻找一种易于删除中间数据的数据结构,比如,循环链表。
我们新建一个链表将所有的人都放进去,然后开始删除结点,直到剩下一个就停止。
忘记代码来源了,程序代码如下:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int n,i=0,j,k,a,q=0,(*p)[2];
scanf("%d", &n);
p=(int (*)[2])calloc(n+1,sizeof(int (*)[2]));
for(i=0;i<n;i++)
{
*(*(p+i)+0)=1;
*(*(p+i)+1)=i+1;
}
*(*(p+n)+0)=0;
*(*(p+n)+1)=n+1;
for (;n!=1;)
{
for(j=i+2,q=0;j<n-q;j+=2,q++)
{
for(k=j;k<n;k++)
{
*(*(p+k)+0)=*(*(p+k+1)+0);
*(*(p+k)+1)=*(*(p+k+1)+1);
}
}
a=n+3-i;
if(a%3==1)
i=-1;
else if(a%3==2)i=-2;
else
i=0;
n=n-q;
}
printf("最后剩下的是%d号同学\n\n",*(*(p+0)+1));
return 0;
}
可是人总是不甘心使用复杂的方式描述一个简单的问题。
改进和优化
我总觉得这个问题有迹可循,所以我就跑出了当n从1-200的所有数据。
具体实现代码如下:
#include <stdio.h>
#include <stdlib.h>
#define FILE_PATH "D:/demo.txt" // 文件路径
int main()
{
FILE *fp; // 文件指针
// 判断文件是否能够正确创建/打开
if( (fp=fopen(FILE_PATH,"wt+")) == NULL ){
perror(FILE_PATH);
exit(1);
}
for(int f = 3; f<200; f++){
int n,i=0,j,k,a,q=0,(*p)[2];
n= f;
p=(int (*)[2])calloc(n+1,sizeof(int (*)[2]));
for(i=0;i<n;i++)
{
*(*(p+i)+0)=1;
*(*(p+i)+1)=i+1;
}
*(*(p+n)+0)=0;
*(*(p+n)+1)=n+1;
for (;n!=1;)
{
for(j=i+2,q=0;j<n-q;j+=2,q++)
{
for(k=j;k<n;k++)
{
*(*(p+k)+0)=*(*(p+k+1)+0);
*(*(p+k)+1)=*(*(p+k+1)+1);
}
}
a=n+3-i;
if(a%3==1)
i=-1;
else if(a%3==2)i=-2;
else
i=0;
n=n-q;
}
printf("最后剩下的是%d号同学\n\n",*(*(p+0)+1));
// 从控制台输入学生信息并写入文件
fprintf(fp,"%d\t%d\n", f, *(*(p+0)+1));
// 刷新缓冲区,将缓冲区的内容写入文件
fflush(fp);
// 重置文件内部位置指针,让位置指针指向文件开头
//rewind(fp);
}fclose(fp);
return 0;
}
然后,我就拿着数据去做了excel表格,分别以n和最终留下的号码为横纵坐标,然后得到了下图:
Woc,这绝对有规律!
然后我就生成数据和模拟这条折线,就是简单的y=ax+b.
并且生成了一组数据去和原数据进行比较,结果竟然成功了!
#include <stdio.h>
#include <stdlib.h>
#define FILE_PATH "D:/demo2.txt" // 文件路径
int main()
{
FILE *fp; // 文件指针
// 判断文件是否能够正确创建/打开
if( (fp=fopen(FILE_PATH,"wt+")) == NULL ){
perror(FILE_PATH);
exit(1);
}
int a[200] = {0};
a[1] = 1;
a[2] = 2;
for(int i = 3, j = 2; i<200; i++){
if( (j+3) > i)
j = j+3-i;
else
j = j+3;
a[i] = j;
}
for(int f = 3; f<200; f++){
// 从控制台输入学生信息并写入文件
fprintf(fp,"%d\t%d\n", f, a[f]);
// 刷新缓冲区,将缓冲区的内容写入文件
fflush(fp);
// 重置文件内部位置指针,让位置指针指向文件开头
//rewind(fp);
}fclose(fp);
return 0;
}
结果如下图所示,完全重合:
然后,我就得到了我们新的代码,经过测试,完全可以实现报数退出。
#include <stdio.h>
int main(void){
int n;
int a[200] = {0};
a[0] = 1;
a[1] = 2;
for(int i = 2, j = 2; i<200; i++){
j = (j+3)%(i+1);
a[i] = j;
}
scanf("%d",&n);
printf("%d\n",a[n]);
return 0;
}
是不是很精简呢? 而且也丢掉了链表等数据结构,而且算法的复杂度也降低到了O(n).
极致优化
可是,问题并没有结束,如果细心看代码的话,可能可以看出来我的程序是有弊端的,当n = 1 or 2 的时候,程序没有办法进行识别…
于是,我就找了一位朋友 (大佬),丢过去了数据,让给我推一个递推式,递推式如下:
X(n) = (m + X(n-1)) % n; //其中n为总数,m为退出的个数,当前为3.
最后,我又得到了一个更为精简的版本。
#include <stdio.h>
#define m 3
int search(int n){
if(n == 0) return 1;
return ( (m + search(n-1)) % n);
}
int main(void){
int n;
scanf("%d",&n);
printf("%d",search(n)+1);
return 0;
}
果然,思考能够带来一些新的思路和新解决方案。
希望大家都能够感受到思考的乐趣~