1. 程式人生 > >約瑟夫環的數學推導、數學方法求最後出圈的數字、迴圈單鏈表求所有出圈數字順序

約瑟夫環的數學推導、數學方法求最後出圈的數字、迴圈單鏈表求所有出圈數字順序

約瑟夫環是一個數學的應用問題:已知n個人(以編號1,2,3…n分別表示)圍坐在一張圓桌周圍;從編號為k的人開始報數,數到m的那個人出列;他的下一個人又從1開始報數,數到m的那個人又出列;依此規律重複下去,直到圓桌周圍的人全部出列。

前幾天,在一篇文章中得知了約瑟夫環的問題。然後,就涉及瞭解決辦法。這個問題,在許多計算機或者關於資料結構的書中都有提及,而其中的解決辦法便是使用迴圈連結串列——無論這個迴圈連結串列是使用指標還是陣列實現,模擬約瑟夫環的進行,最後得到解決方案。具體方法肯定早有某人披露。但是,令人深感奇妙的還是這個問題的數學解決。在不統計解決過程,即不統計每次都需要出列哪個序號時,就可以應用數學遞推公式,直接得出最後剩下的那個人的序號。

先總結一下約瑟夫環的遞推公式:

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

公式得到的是以0~n-1標註的最終序號;下面我們就推導公式。

給出一個序列,從0~n-1編號。其中,k代表出列的序號的下一個,即k-1出列。

a 0, 1, …, k-1, k, k+1, …, n-1

那麼,出列的序號是(m-1)%n,k=m%n。出列k-1後,序列變為

b 0, 1, …, k-2, k, k+1, …, n-1

然後,我們繼續從n-1後延長這個序列,可以得到

c` 0, 1, …, k-2, k, k+1, …, n-1, n, n+1, …, n+k-2

我們取從k開始直到n+k-2這段序列。其實這段序列可以看作將序列b的0~k-2段移到了b序列的後面。這樣,得到一個新的序列

c k, k+1, …, n-1, n, n+1, …, n+k-2

好了,整個序列c都減除一個k,得到

0, 1, …, n-2

c序列中的n-1, n, n+1都減除個k是什麼?這個不需要關心,反正c序列是連續的,我們知道了頭和尾,就能知道d序列是什麼樣的。

這樣你看,從序列a到序列d,就是一個n序列到n-1序列的變化,約瑟夫環可以通過遞推來獲得最終結果。ok,繼續向下。

剩下的就是根據n-1序列遞推到n序列。假設在n-1序列中,也就是序列d中,我們知道了最終剩下的一個序號是x,那麼如果知道了x轉換到序列a中的編號x`,不就是知道了最終的結果了麼?

下面我們就開始推匯出序列a中x的序號是什麼。

d->c,這個變換很容易,就是x+k;

c->b,這個變換是網上大家都一帶而過的,也是令我鬱悶的一個關鍵點。從b->c,其實就是0~k-2這段序列轉換為n~n+k-2這段序列,那麼再翻轉回去,簡單的就是%n,即(x+k)%n。%n以後,k~n-1這段序列值不會發生變化,而n~n+k-2這段序列則變成了0~k-2;這兩段序列合起來,就是序列b。

於是乎,我們就知道了,x`=(x+k)%n。並且,k=m%n,所以x`=(x+m%n)%n=(x+m)%n。公式1就出來了:f[i]=(f[i-1]+m)%i。當然,i=1就是特殊情況了,f[1]=0。這裡還有一個小問題。也許你會迷惑為什麼x`=(x+m%n)%n=(x+m)%n中的%n變成公式中f[i]=(f[i-1]+m)%i中的%i?其實這個稍微想想就能明瞭。我們%n就是為了從序列c轉換到序列b——這是在n-1序列轉換成n序列時%n;那麼從n-2轉換到n-1呢?不是要%(n-1)了嗎?所以這個值是變數,不是常量。

好了,這個最後需要注意的就是從一開始,我們將n序列從0~n-1編號,所以依據公式得出的序號是基於0開始的。下面給出程式碼:

    #include<iostream>  
    #include<cstdio>  
    using namespace std;  
      
    int main()  
    {  
        int ncase, n, m, ans;  
        scanf("%d", &ncase);  
        while(ncase--)  
        {  
            scanf("%d%d", &n, &m);  
            ans = 0;  
            for(int i = 2; i <= n; ++i)  
            {  
                ans = ( ans + m ) % i;  
            }  
            printf("%d\n", ans + 1); //易錯點,編號從1開始,所以結果+1
        }  
        return 0;  
    }  

迴圈單鏈表解法:

#include<stdio.h>   
#include <stdlib.h>  

typedef struct node//節點存放一個數據和指向下一個節點的指標  
{  
    int data;  
    struct node* pnext;  
} Node;  

Node *link_create(int n)//建立n個節點的迴圈連結串列  
{  
    //先建立第1個節點  
    Node *p,*q,*head;  
    int i ;  
    p = (Node *)malloc(sizeof(Node));  
    head = p;  
    p->data = 1;  

    for(i = 2;i<=n;i++)//再建立剩餘節點  
    {  
        q = (Node *)malloc(sizeof(Node));  
        q->data = i;  
        p->pnext = q;  
        p = q;  
    }  
    p->pnext = head;//最後一個節點指向頭部,形成迴圈連結串列  
    return head;  
}  

void link_process(Node *head,int k,int m)//從編號為k(1<=k<=n)的人開始報數,數到m的那個人出列;  
{  
    int i;  
    Node *p = head,*tmp1;  
    while(p->data != k)//p指向的節點data為k 
    {
        tmp1 = p;
        p = p->pnext;  
    }

    while(p->pnext != p)  
    {  
        for(i=1;i<m;i++)  
        {  
            tmp1 = p;  
            p = p->pnext;  
        }  
        printf("%d ",p->data);  
        tmp1->pnext= p->pnext;  
        free(p);  
        p = tmp1->pnext;  

    }  
    printf("%d ",p->data);  
    free(p);  
}  

int main()  
{  
    Node *head = link_create(5);      
    link_process(head,3,1);  
    system("pause");  
    return 0;  
}