- 博文鏈接:http://haoyuanliu.github.io/2016/04/18/Josephus/
- 對,我是來騙訪問量的!O(∩_∩)O~~
約瑟夫問題(Josephus Problem)也稱“丟手絹問題”,是一道非常經典的算法問題,其解法涉及了鏈表、遞歸等算法和數據結構,本文主要分為如下三個內容:
- 使用C語言定義循環鏈表,通過遍歷鏈表模擬事件處理過程;
- 使用數學方法,找出第
n - 1
步與第n
步的關系,通過遞歸解決問題;- 對第二種方法進行優化,加速遞歸過程,提高算法效率
循環鏈表(C語言)
代碼
#include <stdio.h>
#include <stdlib.h>
//定義循環鏈表
typedef struct node//定義node結構體
{
int data;
struct node* next;
}cLinkList;//typedef struct node* cLinkList;定義一個struct node類型的循環鏈表
//主函數
int main()
{
cLinkList *head, *p, *s, *temp;
int n, k;
int i = 1;
printf("Please enter the total number n:\n");
scanf("%d", &n);
printf("Please enter the key value:\n");
scanf("%d", &k);
k %= n;
head = (cLinkList *)malloc(sizeof(cLinkList));
p = head;
p->next = p;//這里要賦值為p,不能賦值為head,要保持head的位置不變
p->data = i;
for(i = 2; i <= n; i++)
{
s = (cLinkList *)malloc(sizeof(cLinkList));
s->data = i;
s->next = p->next;
p->next = s;
p = s;
}
p = head;
int total = n;
while(n--)
{
for(i = 1; i < k - 1; i++)
{
p = p->next;
}
printf("%d->", p->next->data);
temp = p->next;//temp為要刪除的元素
p->next = temp->next;//鏈表中跳過temp
free(temp);//釋放temp
p = p->next;//p向前移動繼續尋找
}
printf("Done!\n");
return 0;
}
運行過程如下:
程序分析
這段代碼主要使用了循環鏈表的數據特性和結構特性,非常適合用來進行Josephus問題的模擬,但是相對來說處理問題的復雜度較高,下面將介紹兩種更加高效的算法。
第一種遞歸
原理
令f[n]表示當有n個候選人時,最后當選者的編號。則:
f[1] = 0
f[n] = (f[n - 1] + K) mod n
方法證明
上述公式可以用數據歸納法簡單證明其正確性:
f[1] = 0
當只有一個候選人的時候,顯然結果應該是0
f[n] = (f[n - 1] + K) mod n
f[n - 1]
為第n - 1
次數到的id序列,則第n
次就是再往下數k
個,最后進行取模運算即可得到結果序列
這種算法的時間復雜度為O(N),空間復雜度為O(1),效率有所提高!
代碼
#include <iostream>
using namespace std;
int main()
{
int num, n, k;
cin >> num;
while(num--)
{
int ret = 0;
cin >> n >> k;
for(int i = 2; i <= n; ++i)
{
ret = (ret + k) % i;//ret記錄每一次數到的序列號
}
cout << ret << endl;//輸出最終序列結果
}
return 0;
}
第二種遞歸
原理
- 在每一輪報數過程中,都有
N/K
個人退出了隊伍,比如N = 10, K = 3
,第一輪有N / K = 3
三個人退出;- 上述第一種方法每次遞歸的步長為
1
,這里我們利用上述關系,建立一個步長為N / K
的遞歸過程;- 需要注意的是,當
N
減少到N = K
的時候就需要使用第一種遞歸進行計算;N > K
時的遞歸公式為:
ret < N mod K: ret = ret - (N mod K) + N
ret >= N mod K: ret = ret - (N mod K) + (ret - N mod K) / (K - 1)
代碼
#include <iostream>
using namespace std;
int josephus(int n, int k)
{
int ret;
if(n == 1)
return 0;
//n < k的時候使用第一種遞歸算法
if(n < k)
{
int ret = 0;
for(int i = 2; i <= n; ++i)
ret = (ret + k) % i;
return ret;
}
//執行遞歸過程
ret = josephus(n-n/k,k);
if(ret < n % k)
{
ret = ret - n % k + n;
}
else
{
ret = ret - n % k + (ret - n % k ) / (k - 1);
}
return ret;
}
int main()
{
int num;
cin >> num;
while(num--)
{
int n, k;
cin >> n >> k;
cout << josephus(n, k) << endl;
}
return 0;
}
代碼分析
這個算法加快了遞歸算法的迭代速度,當所求N
比較大K
比較小的時候比較適用,能夠以更快的速度進行求解。
Github: https://github.com/haoyuanliu
個人博客: http://haoyuanliu.github.io/
個人站點,歡迎訪問,歡迎評論!