約瑟夫斯環問題的幾種經典解法


經典的約瑟夫斯

問題描述:

有n個人圍成一圈,從1開始順序排號。從第一個人開始報數(從1~3報數),凡報到3的人退出圈子,問最后留下的是原來的第幾號?

數組循環模擬法

const int N = 1000;
int person[N]={0};

int getJosePhus(int n,int m)
{
	if (n <= 0 || m <= 0)	//檢查參數的有效值
		return -1;

	for(int j=0;j<n;j++)	//對下標為0~n-1賦值1~n
	{
		person[j] = j+1;
	}

	int leave = 0;	//離開的人
	int count = 0;	//計數1、2、3
	int index = 0;	//記錄下標,超過n要取模,形成環形

	while (leave<n-1)	//當離開的人數比n-1少(即未退出人數大於1時,執行循環)
	{
		if(person[index]!=0)	//i下標的人還活着,計數就+1
		{
			count++;
		}
		if(count==m)	//當數到下標為m時,下標值置為0,計數器置0,離開人數+1
		{
			person[index] = 0;
			count = 0;
			leave++;
		}
		index++;	//每次循環下標僅+1
		if(index==n)	//達到了數組的最大下標,取模回到開頭
		{
			index = index%n;
		}
	}
	index = 0;
	while (person[index]==0)	//循環找出剩下最后的值未清零的那個人的數組下標
	{
		index++;
	}	
	return person[index];
}

雙向鏈表模擬法

int getJosePhus(int n,int m)
{
	list<int> circle;
	for(int i=1;i<=n;i++)
	{
		circle.push_back(i);
	}
	list<int>::iterator cur = circle.begin();
	while (circle.size()>1)
	{
		for(int i=0;i<m-1;++i)
		{
			++cur;
			if(cur==circle.end())	//走到end,就是頭節點的前一個結點,要跳到第一個begin頭節點
			{
				cur = circle.begin();
			}
		}
		list<int>::iterator next = ++cur;	//刪除數到m的人要記錄下一個人的地址,如果下一個人是end(頭節點的前一個結點),要跳到第一個begin頭節點
		if(next==circle.end())
		{
			next = circle.begin();
		}
		--cur;
		circle.erase(cur);	//erase函數會析構此結點,在返回的時候,指向當前迭代器的下一個節點
		cur = next;
	}
	return circle.front();
}

數學推理法

無論是用鏈表實現還是用數組實現都有一個共同點:要模擬整個游戲過程,不僅程序寫起來比較煩,而且時間復雜度高,當n,m非常大(例如上百萬,上千萬)的時候,幾乎是沒有辦法在短時間內出結果的。

如果原問題僅僅是要求出最后的勝利者的序號,而不是要讀者模擬整個過程。因此如果要追求效率,就要打破常規,實施一點數學策略。

為了討論方便,先把問題稍微改變一下,並不影響原意:
問題描述:n個人(編號0~(n-1)),從0開始報數,報到(m-1)的退出,剩下的人繼續從0開始報數。求勝利者的編號。
我們知道第一個人(編號一定是m%n-1) 出列之后,剩下的n-1個人組成了一個新的約瑟夫環(以編號為k=m%n的人開始):
  k  k+1  k+2  ... n-2, n-1, 0, 1, 2, ... k-2並且從k開始報0。
現在我們把他們的編號做一下轉換:
k     --> 0
k+1   --> 1
k+2   --> 2
...
...
k-2   --> n-2
k-1   --> n-1
變換后就完完全全成為了(n-1)個人報數的子問題,假如我們知道這個子問題的解:例如x是最終的勝利者,那么根據上面這個表把這個x變回去不剛好就是n個人情況的解嗎?!!變回去的公式很簡單,相信大家都可以推出來:x'=(x+k)%n
如何知道(n-1)個人報數的問題的解?對,只要知道(n-2)個人的解就行了。(n-2)個人的解呢?當然是先求(n-3)的情況 ---- 這顯然就是一個倒推問題!好了,思路出來了,下面寫遞推公式:
令f[i]表示i個人玩游戲報m退出最后勝利者的編號,最后的結果自然是f[n]
遞推公式

f[1] = 0; (i = 1)
f[i] = (f[i - 1] + m) % i; (i > 1)

有了這個公式,我們要做的就是從1-n順序算出f[i]的數值,最后結果是f[n]。因為實際生活中編號總是從1開始,我們輸出f[n]+1.由於是逐級遞推,不需要保存每個f[i],程序也是異常簡單:

int getJosePhus(int n,int m)
{
	if(n<=0||m<=0)
	{
		return -1;
	}
	int last = 0;
	for(int i=2;i<=n;i++)
	{
		last = (last + m) % i;
	}
	return last + 1;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM