圓圈中最後剩餘的數字:約瑟夫環問題
0-n這n個數字排成一圈,從數字零開始每次從這個圓圈中刪除第m個數字,求出剩餘的最後一個數字
public class _Q45<T> {
// 低效的模擬而已
public int LastRemaining(int n, int m){
if(n < 0 || m < 0) return -1;
int circle[] = new int[n];
for(int i=0; i<n; i++){
circle[i] = i;
}
int count = 0;
int time = 0;
for (int i = 0; i < n; i++) {
if (circle[i] != -1) {
count++;
if (count == m) {
circle[i] = -1;
count = 0;
time++;
}
}
if ((i == n - 1) && (time != (n-1)))
i = -1;
}
int result = 0;
for(int i=0; i<n; i++){
if(circle[i] != -1){
result = circle[i];
break;
}
}
return result;
}
// 採用映射的方法:
/*
* 對於原始 0 - n數據,刪除第m個數,被刪除的數字爲(m-1)%n,將該數記爲k ,那麼剩餘數字爲
* 0 1 2 .. m-2 m m+1 ... n-1
* 新的計數起點爲 m.
*
* 將每次被刪除的數字記爲m 和 n的一個函數,那麼 k = f(n,m)
* 刪除一個數字之後,原序列如果不變的話,是不能繼續使用f的,因爲f中關於n的序列是從0-n
* 因此剩餘序列中刪除的下一個數字可以用g(n,m)來表示 -- 顯然已經破壞了遞歸的條件
*
* 爲了使刪除下一個數字時仍然可以使用f函數,可以將刪除一個數字之後的序列做一個映射,以刪除第一個數字之後的序列爲例
* m -> 0
* m+1 -> 1
* ...
* n-1 -> n-m-1
* 0 -> n-m
* 1 -> n-m+1
* ...
* m-2 -> n-2
* 映射函數記爲p,是關於x的一個函數p(x) = (x-m)%n 其中x取[0, m-2]並[m, n-1]
* 該函數最終的值是最後剩餘數字的映射後的編號,因此該函數的逆即爲最後剩餘數字實際編號
* 其逆爲: p`(x) = (x+m)%n
*
* g(n-1, m) = p`(f(n-1, m)) = (f(n-1, m) + m)%n = f(n, m)
* 注意該公式第一個等號成立的原因:
* 該等號左側函數代表在不是從0開始的序列中刪除第m個元素
* 右側函數代表從0開始的n-1個元素中刪除第m個元素,由於刪除前都對這些元素做了映射,因此對於最後結果求逆,得出最後剩餘元素的實際編號
* 根據實際經驗,無論是每次重新從0開始刪除還是每次從新的被刪除元素的下一元素開始刪除,最終結果是相等的 -- 其實還是存在嚴格的數學證明
*
* 第二個等號只是把參數帶入而已 x = f(n-1, m)
*
* 第三個等號成立的原因是 g(n-1, m) = f(n, m)
*
* 最終得到遞歸等式:
* f(n, m) = (f(n-1, m) + m) % n 其中變量爲n;而m其實可以看做是常量
* 當n 取爲1的時候就代表只剩下最後一個數字,即爲所求 -- 當然n == 1也就是遞歸的出口;不過不使用遞歸直接用循環也可以
*
*/
// 非遞歸版本
public int LastRemainingV2(int n, int m){
if(m < 1 || n < 1) return -1;
int last = 0;
for(int i=2; i<=n; i++){
last = (last + m) % i;
}
return last;
}
// 遞歸版本
public int LastRemainingV3(int n, int m){
if(m < 1 || n < 1) return -1;
if(n == 1) return 0; // 出口
return (LastRemainingV3(n-1, m) + m)%n;
}
}
測試代碼:
public class _Q45Test extends TestCase {
_Q45<?> remain = new _Q45();
public void test(){
int n1 = 5;
int m1 = 3;
int n2 = 6;
int m2 = 5;
System.out.println(remain.LastRemaining(n1, m1));
System.out.println(remain.LastRemainingV2(n1, m1));
System.out.println(remain.LastRemainingV3(n1, m1));
System.out.println(remain.LastRemaining(n2, m2));
System.out.println(remain.LastRemainingV2(n2, m2));
System.out.println(remain.LastRemainingV3(n2, m2));
}
}