有趣的約瑟夫問題

本來就這題目而言沒必要寫博客的,不過這個故事挺有意思的,所以還是記錄一下。

據說著名猶太曆史學家 Josephus有過以下的故事:在羅馬人佔領喬塔帕特後,39 個猶太人與Josephus及他的朋友躲到一個洞中,39個猶太人決定寧願死也不要被敵人抓到,於是決定了一個自殺方式,41個人排成一個圓圈,由第1個人開始報數,每報數到第3人該人就必須自殺,然後再由下一個重新報數,直到所有人都自殺身亡爲止。然而Josephus 和他的朋友並不想遵從。首先從一個人開始,越過k-2個人(因爲第一個人已經被越過),並殺掉第k個人。接着,再越過k-1個人,並殺掉第k個人。這個過程沿着圓圈一直進行,直到最終只剩下一個人留下,這個人就可以繼續活着。問題是,給定了和,一開始要站在什麼地方纔能避免被處決?Josephus要他的朋友先假裝遵從,他將朋友與自己安排在第16個與第31個位置,於是逃過了這場死亡遊戲。

其一般形式爲:N個人圍成一圈,從第一個開始報數,第M個將被殺掉,最後剩下一個,其餘人都將被殺掉。

約瑟夫問題並不難,但求解的方法很多;題目的變化形式也很多。最容易想到的就是模擬殺人過程,代碼如下:

int Josephus(int n, int m)
{
	//vector<bool>a(n);
	bool*a = new bool[n]();
	int f = 0, t = 0, s = 0;
	do
	{
		if (t == n)
			t = 0;//數組模擬環狀,最後一個與第一個相連
		if (!a[t])
			s++;//第t個位置上有人則報數
		if (s == m)//當前報的數是m
		{
			s = 0;//計數器清零
			cout << setw(3)<<t+1 << ' ';//輸出被殺人編號(setw需要包含頭文件iomanip)
			a[t] = 1;//此處人已死,設置爲空
			f++;//死亡人數+1
			if (f % 5 == 0)cout <<endl;
		}
		++t;//逐個枚舉圈中的所有位置
	} while (f != n-1);//直到所有人都被殺死爲止
	for (int i = 0; i < n; i++)
	{
		if (a[i] == 0)t = i+1;
	}
	return t;
	
}

int main()
{
	int k = Josephus(41, 3);
	cout << "最後剩下:" << k;
}

不難發現,這其實是一個O(NM)時間複雜度的暴力解法,對於較大的MN來說效率極低。

想想,我們能否將其轉化爲遞歸問題呢?假設約瑟夫函數爲f(n,m),即n個人,每數m個人就殺掉一個人,返回值爲最後剩下的那個人。問題就在於我們能否通過f(n-1,m)推出f(n,m)呢。

假設f(n-1,m)=k,要推出f(n,m),我們之需要往f(n-1,m)的中再加一個人,那麼加在什麼位置呢?我們先假設加到n-1個人的後面,很明顯我們需要將最後加進去的這個人,調整爲第一次殺掉的那個人。那麼f(n,m)這個函數第一個殺掉的人就是第m個位置的人,此時這個人在最後的位置,因此我們將其+m之後對n取餘,與之對應的f(n-1,m)由k變爲了(k+m)%n。

也就是說:
最終剩下一個人時的安全位置肯定爲0,反推安全位置在人數爲n時的編號
人數爲1: 0
人數爲2: (0+m) % 2
人數爲3: ((0+m) % 2 + m) % 3
人數爲4: (((0+m) % 2 + m) % 3 + m) % 4

於是有了如下的遞推公式:

                                                   

遞歸寫法:

int f(int n, int m) {
    if (n == 1)
        return 0;
    int k = f(n - 1, m);
    return (m + k) % n;
}

時間複雜度O(N),空間複雜度O(N)

迭代寫法:

int f(int n, int m) {
    int k = 0;
    for (int i = 2; i != n + 1; ++i)
        k = (m + k) % i;
    return k;
}

時間複雜度O(N),空間複雜度O(1)

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