約瑟夫環問題

約瑟夫環是一個數學的應用問題:已知n個人(以編號1,2,3...n分別表示)圍坐在一張圓桌周圍。從編號爲k的人開始報數,數到m的那個人出列;他的下一個人又從1開始報數,數到m的那個人又出列;依此規律重複下去,直到圓桌周圍的人全部出列。

C代碼如下(joseph.cpp):

#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>

typedef struct _node
{
	struct _node* next;
	int number;
}node,*linklist;

linklist create(int n);
void joseph(linklist head, int k, int m);

int main()
{
	linklist head;
	int m, n, k;
	printf("please input n:");
	scanf("%d",&n);
	printf("please input m:");
	scanf("%d",&m);
	printf("please input k:");
	scanf("%d",&k);
	head = create(n);
	printf("the sequences of leaving the list are:");
	joseph(head,k,m);
	return 0;
}
linklist create(int n)
{
	linklist head = (linklist)malloc(sizeof(node));
	node *tail;
	int i;
	head->next = head;
	head->number = 1;
	tail = head;
	for(i=2;i<=n;i++)
	{
		node *p = (node*)malloc(sizeof(node));
		p->number = i;
		p->next = tail->next;
		tail->next = p;
		tail = p;
	}
	return head;
}

void joseph(linklist head, int k, int m)
{
	int j;
	node *p;
	node *q;
	if(m == 1 && k == 1)
	{
		p = head;
		while(p->next != head)
		{
			printf("%d ",p->number);
			q = p->next;
			free(p);
			p = q;
		}
		printf("%d\n",p->number);
	}
	else if(m == 1 && k != 1)
	{
		p = head;
		for(j=1; j<k-1; j++)
			p = p->next;
		while(head->next != head)
		{
			q = p->next;
			p->next = q->next;
			printf("%d ",q->number);
			if(q == head)
				head = q->next;
			free(q);
		}
		printf("%d\n",head->number);
	}
	else
	{
		p = head;
		for(j=1; j<k; j++)
			p = p->next;
		while(head->next != head)
		{
			for(j=1; j<m-1; j++)
				p = p->next;
			q = p->next;
			p->next = q->next;
			printf("%d ",q->number);
			if(q == head)
				head = q->next;
			free(q);
			p = p->next;
		}
		printf("%d\n",head->number);
	}
}

需要特別注意m和k的值是否等於1。


幾組測試用例結果如下:

1、m != 1,k != 1



2、m != 1,k == 1


3、m == 1,k != 1


4、m == 1,k == 1



上面程序中,之所以要分別討論m==1和k==1的情況,是因爲在單向循環鏈表中要想刪除某一個結點,必須先找到該結點的前驅結點,然後更改相關指針域,使循環鏈表不斷鏈,而m=1,k=1時,要想使循環鏈表不斷鏈,必須先找到鏈表的尾結點,所以要分不同情況討論。

鑑於此,想到使用雙向循環鏈表,要想刪除某一個結點,不需要找前驅結點,即使是刪除第一個結點,也不需要找尾結點。

C代碼如下所示(joseph2.cpp),可以看到代碼邏輯簡潔了不少:

#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>

typedef struct _node
{
	struct _node* prev;
	struct _node* next;
	int number;
}node,*linklist;

linklist create(int n);
void joseph(linklist head, int k, int m);

int main()
{
	linklist head;
	int m, n, k;
	printf("please input n:");
	scanf("%d",&n);
	printf("please input m:");
	scanf("%d",&m);
	printf("please input k:");
	scanf("%d",&k);
	head = create(n);
	printf("the sequences of leaving the list are:");
	joseph(head,k,m);
	return 0;
}
linklist create(int n)
{
	linklist head = (linklist)malloc(sizeof(node));
	node *tail;
	int i;
	head->next = head;
	head->prev = head;
	head->number = 1;
	tail = head;
	for(i=2;i<=n;i++)
	{
		node *p = (node*)malloc(sizeof(node));
		p->number = i;
		p->next = tail->next;
		p->prev = tail;
		tail->next = p;
		tail = p;
		head->prev = tail;
	}
	return head;
}

void joseph(linklist head, int k, int m)
{
	int i;
	node *p;
	node *q;
	p = head;
	for(i=1; i<k; i++)//獲取開始計數的結點
			p = p->next;
	while(head->next != head)
	{
		for(i=1; i<m; i++)
			p = p->next;//獲取每輪計數的第m個結點,即待刪除結點
		q = p->next;
		q->prev = p->prev;
		p->prev->next = q;
		printf("%d ",p->number);
		if(p == head)//如果刪除的是第一個結點,則需要重新設置head指針
			head = q;
		free(p);
		p = q;//刪除一個結點之後,從該結點的下一個結點重新開始計數
	}
	printf("%d\n",head->number);
}

可以得到與第一種代碼相同的結果:






如果能使用C++標準庫中的list來模擬循環鏈表,那麼邏輯更清晰,代碼更簡潔。

C++代碼如下(joseph3.cpp):

#include<iostream>
#include<list>
using namespace std;

void joseph(int n, int m, int k);

int main()
{
	int n,m,k;
	cout<<"please input n:";
	cin>>n;
	cout<<"please input m:";
	cin>>m;
	cout<<"please inpur k:";
	cin>>k;
	cout<<"the sequences of leaving the list are:";
	joseph(n,m,k);
	return 0;
}

void joseph(int n, int m, int k)
{
	list<int> numbers;
	int i,j;
	for(i=1; i<=n; i++)
		numbers.push_back(i);
	list<int>::iterator current = numbers.begin();
	list<int>::iterator next;
	for(i=1; i<k; i++)
	{
		++current;
		if(current == numbers.end())
			current = numbers.begin();
	}
	while(numbers.size()>1)
	{
		for(i=1; i<m; i++)
		{
			++current;
			if(current == numbers.end())
				current = numbers.begin();
			/*
			由於list本身並不是一個循環鏈表,所以每當到達
			最後一個元素的下一個位置時,需要修改迭代器指向第一個元素
			*/
		}
		next = ++current;
		if(next == numbers.end())
			next = numbers.begin();
		--current;
		cout<<*current<<" ";
		numbers.erase(current);
		current = next;
	}
	cout<<*current<<endl;
}

可以得到與上面兩種代碼相同的結果。


上面編寫的解約瑟夫環的程序模擬了整個報數的過程,程序運行時間還可以接受,很快就可以出計算結果。可是,當參與的總人數n及出列值m非常大時,其運算速度就慢下來。例如,當n的值有上百萬,m的值爲幾萬時,到最後雖然只剩2個人,也需要循環幾萬次(m的數量)才能確定2個人中下一個出列的序號。顯然,在這個程序的執行過程中,很多步驟都是進行重複無用的循環。那麼,能不能設計出更有效率的程序呢?
在約瑟夫環中,如果只是需要求出最後的一個出列者最初的序號,就沒有必要去模擬整個報數的過程。因此,爲了追求效率,可以考慮從數學角度進行推算,找出規律然後再編寫程序即可。
爲了討論方便,先根據原意將問題用數學語言進行描述。
問題:將編號爲1~n這n個人進行圓形排列,按順時針從1開始報數,報到m的人退出圓形隊列,剩下的人繼續從1開始報數,不斷重複。求最後出列者最初在圓形隊列中的編號。
下面首先列出0~n這n個人的原始編號如下:
1、2、3、……、m-2、m-1、m、m+1、m+2、……、n-2、n-1、n
第一個出列人的編號一定是m%n。例如,在41個人中,若報到3的人出列,則第一個出列人的編號一定是3%41=3,1人出列後的列表如下:
1、2、3、……、m-2、m-1、m+1、m+2、……、n-2、n-1、n
根據規則,當有人出列之後,下一個位置的人又從1開始報數,則以上列表可調整爲以下形式(即以m+1位置開始,n之後再接上0、1、2……,形成環狀):
m+1、m+2、……、n-2、n-1、n、1、2、3、……、m-2、m-1
按上面排列的順序重新進行編號,可得到下面的對應關係:
1、       2、        3、   ……、n-2、n-1
m+1、m+2、m+3、……、m-2、m-1
即,將出列1人後的數據重新組織成了1~n-1的列表,繼續求n–1個參與人員,按報數到m即出列,求解最後一個出列者最初在圓形隊列中的編號。
通過一次處理,將問題的規模縮小了。即,對於n個人報數的問題,可以分解爲先求解(n–1)個人報數的子問題;而對於(n–1)個人報數的子問題,又可分解爲先求[(n–1)–1]人個報數的子問題,……。
問題中的規模最小時是什麼情況?就是隻有1個人時(n=1),報數到m的人出列,這時最後出列的是誰?當然只有編號爲1這個人。因此,可設有以下函數:
F(1)= 1
那麼,當n=2,報數到m的人出列,最後出列的人是誰?應該是隻有一個人報數時得到的最後出列的序號加上m+1(因爲已經有1個人出了隊列,求F(n)時因爲已經有n-1個人出了隊列,所以需要加上n-1),可用公式表示爲以下形式:
F(2)= F(1)+ m + 1
通過上面的算式計算時,F(2)的結果可能會超過n值(人數的總數)。例如,設n=2,m=3(即2個人,報數到3時就出列),則按上式計算得到的值是:
F(2)= F(1)+ 3 + 1 = 1 + 3 + 1 = 5
一共只有2人蔘與,編號爲5的人顯然沒有。怎麼辦?由於是環狀報數,因此當兩個人報完數之後,又從編號爲1的人開始接着報數。根據這個原理,即可對求得的值與總人數n進行模運算,然後再加上1,因爲不是從0開始計數的,即:
F(2)= [F(1)+ m + 1] % n + 1 = [1 + 3 + 1]%2 + 1 = 2
即,n=2,m=3(即有2個人,報數到3的人出列)時,循環報數最後一個出列的人的編號爲2(編號從1開始)。
根據上面的推導過程,可以很容易推導出,當n=3時的公式:
F(3)= [F(2)+ m + 2]%3 + 1
同理,也可以推導出參與人數爲N時,最後出列人員編號的公式:
F(n)= [F(n-1)+ m + n - 1]%n + 1
其實,這就是一個遞推公式,公式包含以下兩個式子:
F(1)= 1;                                                     n=1
F(n)= [F(n-1)+ m + n - 1]%n + 1;     n>1 
有了這個遞推公式,再來設計程序就很簡單了。

使用遞歸方式的代碼如下(joseph4.cpp):

#include<stdio.h>
#include<stdlib.h>

int joseph(int n, int m);

int main()
{
	int n,m;
	printf("please input n:");
	scanf("%d",&n);
	printf("please input m:");
	scanf("%d",&m);
	printf("the last number is: %d\n", joseph(n,m));
	return 0;
}

int joseph(int n, int m)
{
	if(n == 1)
		return 1;
	else
		return (joseph(n-1,m)+m+n-1)%n + 1;
}
幾組測試用例結果如下:


使用遞歸函數會佔用計算機較多的內存,當遞歸層次太深時可能導致程序不能執行,因此,也可以將程序直接編寫爲以下的迭代形式。
joseph5.cpp:

#include<stdio.h>
#include<stdlib.h>

int joseph(int n, int m);

int main()
{
	int n,m;
	printf("please input n:");
	scanf("%d",&n);
	printf("please input m:");
	scanf("%d",&m);
	printf("the last number is: %d\n", joseph(n,m));
	return 0;
}

int joseph(int n, int m)
{
	int last = 1;//相當於F(1)
	int i;
	for(i=2; i<=n; i++)//一步一步求F(2)到F(n)
		last = (last + m + i - 1)%i + 1;
	return last;
}
也可以得到與上面相同的結果。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章