POJ读书笔记6.1 - 约瑟夫问题 2746

http://blog.csdn.net/pipisorry/article/details/39271139

问题描述:

约瑟夫生死问题的描述有三:

【其一】据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领乔塔帕特后,39个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus和他的朋友并不想遵从,Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

【其二】17世纪的法国数学家加斯帕在《数目的游戏问题》中讲了这样一个故事:15个教徒和15个非教徒在深海上遇险,必须将一半的人投入海中,其余的人才能幸免于难,于是想了一个办法:30个人围成一圆圈,从第一个人开始依次报数,每数到第九个人就将他扔入大海,如此循环进行直到仅余15个人为止。问怎样排法,才能使每次投入大海的都是非教徒。

【其三】个人,编号,从0开始报数,报到的退出,通常取。剩下的人继续从0开始报数,报到的退出,如此往复。求最终胜利者的编号。

解决算法:

循环链表算法原理:

题目中个人围成一圈,因而启发我们用一个循环的链来表示。可以使用结构数组来构成一个循环链。结构中有两个成员,其一为指向下一个节点的指针,以构成环形的链;其二为该节点在最初环中的序号标记。从第一个节点处开始计数,每数到时,将当前节点删除,表示该人已被扔下海了。这样循环计数直到有最后一个节点为止。

顺序表算法原理:

为了节省空间复杂度,采用线性表来实现。以动态数组元素代替循环链表节点的算法实现。考虑:动态数组的申请、使用、回收三原则。用个元素数组来存放结果,初始化全为1,如果这个人被丢到海里了,就把对应位置的元素置为0;用一个变量依次对数组里不为0的元素计数,数到则把对应位置的数组元素置0。循环控制可以用取余预算实现。

低复杂度算法原理:

无论是用链表实现还是用数组实现都有一个共同点:要模拟整个游戏过程,不仅程序写起来比较烦,而且时间复杂度高达O(mn)。我们注意到原问题仅仅是要求出最后的胜利者的序号,而不是要读者模拟整个过程。稍微改变问题描述:n个人,编号0~n-1,从0开始报数,报到m-1的退出,剩下的人继续从0开始报数。求胜利者的编号。

我们知道第一个人,编号一定是(m-1)%n,出列之后,剩下的n - 1个人组成了一个新的约瑟夫环,以编号为k  = m%n的人开始:

并且从开始报0。把编号做一下转换:

变换后就完完全全成为了n - 1个人报数的子问题,那么根据上面这个表把这个变回去则刚好就是n个人情况的解。(如果一个人在n-1时的报数为x,则他在n时的报数为x’)

如何知道n个人报数的问题的解?只要知道n-1个人的解就行了。n-1个人的解?当然是先求n-2的情况。这显然就是一个倒推问题。

递推公式:令表示个人玩游戏报退出最后胜利者的编号,最后的结果自然是f[n]。

递推公式

code:

/*	数组算法(有删除元素)	O(mn)	*/
static int jonseArray(int n, int m) {
	int *jones = new int[n];
	for(int i=0; i<n; i++)
		jones[i]=i+1;
	int t=0;
	for(int left=n; left>=1; left--) {			//剩余人数
		t=(t+m-1)%left;							//将要除去的编号
		//printf("%d ", jones[t]);
		for(int j=t+1; j<=left-1; j++)
			jones[j-1]=jones[j];
	}
	return jones[0];
}
/*	数组算法(无删除元素)	O(mn)	*/
static int jonseArray2(int n, int m){
	int *a = (int *)malloc(sizeof(int) * n);
	for(int i = 0; i < n; i++)
		a[i] = 1;

	int current = 0;
	for(int lose_cnt = 0; lose_cnt < n - 1; lose_cnt++){
		int call_num = 1;										//重新报数
		while(call_num < m){
			current = (current + 1) % n;
			while(a[current] == 0)								//跳过已划出的
				current = (current + 1) % n;
			call_num++;
		}
		a[current] = 0;											//划出一个
		current = (current + 1) % n;							//current指向下一个
		while(a[current] == 0)
			current = (current + 1) % n;
	}

	for(int i = 0; i < n; i++){
		if(a[i] == 1)
			return i + 1;
	}
}
/*	数组算法(无删除元素)	O(mn)	*/
static int jonseArray3(int n,int m){
	int a[300];
	for(int i=0;i<n;i++)
		a[i]=1;

	int i = 0, j = 0, k = 0;
	while(k!=n-1){
		if(a[i]==1){
			j=j+1;
			if(j==m){
				a[i]=0;
				k++;
				j=0;
			}
		}
		i = (i + 1) % n;
	}
	for(int i=0;i<n;i++){
		if(a[i]==1)
			return i + 1;
	}
}
/*	链表算法(双循环链表)	O(mn)	*/
typedef struct jonseNode{
	int code;									//编号(从0开始)
	struct jonseNode *next;
	struct jonseNode *pre;
}jonseNode;
static int jonseList(int n, int m){
	jonseNode *jonseMaxNum;						//最大code值的点作为头结点
	assert(jonseMaxNum = (jonseNode *)malloc(sizeof(jonseNode)));
	jonseMaxNum->code = n - 1;
	jonseMaxNum->next = jonseMaxNum;
	jonseMaxNum->pre = jonseMaxNum;

	for(int i = n - 2; i >= 0; i--){			//头插法插入其它结点
		jonseNode * jonseI;
		assert(jonseI = (jonseNode *)malloc(sizeof(jonseNode)));
		jonseI->code = i;
		jonseI->next = jonseMaxNum->next;
		jonseMaxNum->next = jonseI;
	}

	jonseNode *current_pre = jonseMaxNum;
	jonseNode *current;
	for(int i = 1; i <= n - 1; i++){			//每次除去一个,共除去n-1个
		int call_num = 1;
		current = current_pre->next;
		while(call_num++ < m){
			current_pre = current;
			current = current->next;
		}
		current_pre->next = current->next;
		//printf("%d\n", current->code);
		free(current);
	}

	return current_pre->code + 1;
}
/*	最优算法(低复杂度算法)	*/
static int jonseOptimal(int n, int m){
	int final = 0;
	for(int total = 2; total <= n; total++)	//total个人时的winner为final
		final = (final + m) % total;
	return final + 1;
}

测试:

/****************************************************************************/
/*	POJ读书笔记6.1 - 约瑟夫问题 2746	皮皮 2014-9-14		*/
/****************************************************************************/
#include <assert.h>
#include <stdio.h>
#include <malloc.h>
#include <string.h>

int main(){
	int n, m;

	//assert(freopen("simulation\\Jonse.in", "r", stdin));
	while(1){
		scanf("%d%d", &n, &m);
		if(n == 0 && m == 0)
			break;
		printf("%d\n", jonseArray(n, m));
	}
	printf("\n");

	//assert(freopen("simulation\\Jonse.in", "r", stdin));
	while(1){
		scanf("%d%d", &n, &m);
		if(n == 0 && m == 0)
			break;
		printf("%d\n", jonseArray2(n, m));
	}
	printf("\n");

	//assert(freopen("simulation\\Jonse.in", "r", stdin));
	while(1){
		scanf("%d%d", &n, &m);
		if(n == 0 && m == 0)
			break;
		printf("%d\n", jonseArray3(n, m));
	}
	printf("\n");

	//assert(freopen("simulation\\Jonse.in", "r", stdin));
	while(1){
		scanf("%d%d", &n, &m);
		if(n == 0 && m == 0)
			break;
		printf("%d\n", jonseList(n, m));
	}
	printf("\n");

	assert(freopen("simulation\\Jonse.in", "r", stdin));
	while(1){
		scanf("%d%d", &n, &m);
		if(n == 0 && m == 0)
			break;
		printf("%d\n", jonseOptimal(n, m));
	}
	printf("\n");

	fclose(stdin);
	return 0;
}

测试案例:

5 3

6 2

12 4

8 3

0 0

output:

4

5

1

7

from:

http://blog.csdn.net/pipisorry/article/details/39271139

时间: 2024-10-13 22:51:06

POJ读书笔记6.1 - 约瑟夫问题 2746的相关文章

POJ读书笔记2.1 —— 鸡兔笼带

http://blog.csdn.net/pipisorry/article/details/36433305 问题描写叙述 一个笼子里面关了鸡和兔子(鸡有2仅仅脚.兔子有4仅仅脚.没有例外). 已经知道了笼子里面脚的总数a.问笼子里面至少有多少仅仅动物,至多有多少仅仅动物. 输入 第1行是測试数据的组数n,后面跟着n行输入.每组測试数据占1行.每行一个正整数a (a < 32768) 输出 输出包括n行,每行相应一个输入,包括两个正整数.第一个是最少的动物数.第二个是最多的动物数,两个正整数用

程序设计导引及在线实践 读书笔记2.1 —— 鸡兔同笼

http://blog.csdn.net/pipisorry/article/details/36433305 问题描述 一个笼子里面关了鸡和兔子(鸡有2只脚,兔子有4只脚,没有例外).已经知道了笼子里面脚的总数a,问笼子里面至少有多少只动物,至多有多少只动物. 输入 第1行是测试数据的组数n,后面跟着n行输入.每组测试数据占1行,每行一个正整数a (a < 32768) 输出 输出包含n行,每行对应一个输入,包含两个正整数,第一个是最少的动物数,第二个是最多的动物数,两个正整数用一个空格分开

&#183;读书笔记」 具体数学

头脑一热,然后买了<具体数学>,差点还买了英文版后来觉得自己英文太烂,还是决定购买的中文版书是前几天到的,从2014.8.1号开始看,坚持每天看一点,学习一点.为了监督自己,决定在这里开一贴,记录我的学习路程. 时间:2014.8.1  地点:家里 今天阅读的是本书的第一章,讲的是递归问题,也是算法的基础,当然这里主要讲的是数学.但是没办法,我是一名ACMer,必须要和程序紧密联系在一起.我觉得明天还需要花一天时间来看这章,需要解决下后面的习题以及再加深点体会. 第一章 递归问题 1.1 汉诺

《C#图解教程》读书笔记之三:方法

本篇已收录至<C#图解教程>读书笔记目录贴,点击访问该目录可获取更多内容. 一.方法那些事儿 (1)方法的结构:方法头-指定方法的特征,方法体-可执行代码的语句序列: (2)方法的调用:参数.值参数.引用参数.输出参数.参数数组: ①参数: 形参-本地变量,声明在参数列表中:形参的值在代码开始之前被初始化: 实参-实参的值用于初始化形参: ②值参数: 为形参在栈上分配内存,将实参的值复制到形参: ③引用参数: 不为形参在栈上分配内存,形参的参数名作为实参变量的别名指向同一位置,必须使用ref关

《C#图解教程》读书笔记之五:委托和事件

本篇已收录至<C#图解教程>读书笔记目录贴,点击访问该目录可获取更多内容. 一.委托初窥:一个拥有方法的对象 (1)本质:持有一个或多个方法的对象:委托和典型的对象不同,执行委托实际上是执行它所"持有"的方法.如果从C++的角度来理解委托,可以将其理解为一个类型安全的.面向对象的函数指针. (2)如何使用委托? ①声明委托类型(delegate关键字) ②使用该委托类型声明一个委托变量 ③为委托类型增加方法 ④调用委托执行方法 (3)委托的恒定性: 组合委托.为委托+=增加

《Effective C++》读书笔记汇总

我之前边读<Effective C++>边写下每个条款的读书笔记,这一版是C++11之前的版本.这里我将每个条款令我印象深刻的点小结一下. 1.C++包括:Plain C(面向过程).OOP(面向对象).模板(泛型和模板元编程).STL(C++标准库). 2.用inline.enum.const代替#define.#define定义的宏,一旦复杂起来,高手都很难掌控.不要带入C的习惯. 3.灵活使用const前缀.不需要进行改变的数据加上const前缀.指针的const前缀有两种形式,cons

【读书笔记】《Linux内核设计与实现》内核同步介绍&内核同步方法

简要做个笔记,以备忘. 需同步的原因是,我们并发访问了共享资源.我们将访问或操作共享资源的代码段称"临界区",如果两个执行线程处于同一临界区中同时执行,称"竞争条件".这里术语执行线程指任何正在执行的代码实例,如一个在内核执行的进程.一个中断处理程序或一个内核线程. 举个简单例子,i++操作.该操作可以转换为下面的机器指令序列: 1.得到当前变量i的值,并保存到一个寄存器. 2.将寄存器的值加1. 3.将i的新值写回到内存中. 当两个线程同时进入这个临界区,若i初值

鸟哥的Linux私房菜 基础学习篇读书笔记(7):Linux文件与目录管理

这一章主要讲述的是如何操作与管理Linux系统中的文件和目录,主要包括目录间的切换,目录的创建与删除,文件的创建与删除,文件的查找,文件内容的查看等等. 前一章中已经讲过相对路径以及绝对路径,绝对路径就是指从根目录("/")开始写起的路径名,而相对路径绝不会由根目录开始写起,相反,相对路径是相对于当前工作目录的路径名.Linux操作系统中有几个特殊的目录: . 代表此层目录: .. 代表上一层目录: - 代表前一个工作目录: ~ 代表当前用户身份所在的主文件夹: ~account 代表

《30天自制操作系统》读书笔记(2)hello, world

让系统跑起来 要写一个操作系统,我们首先要有一个储存系统的介质,原版书似乎是06年出版的,可惜那时候没有电脑,没想到作者用的还是软盘,现在的电脑谁有软驱?不得已我使用一张128M的SD卡来代替,而事实上你用的是U盘还是软盘对我们的操作系统没有影响,缺点是你的U盘刷入系统后容量只能是1440 MB,即当年流行的3.5英寸软盘的大小,当然不用担心,再格式化一次(用DiskGeniu),就可以恢复. 我做事情的话,总是怕自己的努力的结果白费了,害怕辛辛苦苦看完这本书但是发现做出来的东西现在根本没法用,