约瑟夫环问题(报数退出)的极致优化,从39行代码优化到10行

问题描述

有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;
}

果然,思考能够带来一些新的思路和新解决方案。
希望大家都能够感受到思考的乐趣~

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值