約瑟夫問題(Josephus Problem)的兩種快速遞歸算法



約瑟夫問題(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;
}

運行過程如下:

cJosephus

程序分析

這段代碼主要使用了循環鏈表的數據特性和結構特性,非常適合用來進行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 Githubhttps://github.com/haoyuanliu
個人博客 個人博客http://haoyuanliu.github.io/

個人站點,歡迎訪問,歡迎評論!


免責聲明!

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



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