约瑟夫环O(N)和O(M*N)算法详解

问题描述:
已知n个人(以编号1,2,3…n分别表示)围坐在一张圆桌周围。从编号为1的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列,求最后一个出列人的编号。

该问题可以用数组或者循环链表模拟,因为都是直接模拟,需要循环n次,每次数m次,所以时间复杂度都是O(n*m)。虽然复杂度比O(n)大,但是可以求出出圈的编号顺序。也正是因为该方法将每次出圈的人都找了出来,所以做了很多无用操作,复杂度才会高达O(n*m)。
下面是数组和链表方法的代码:
题意:n个人,从第k个人开始数1,数到m的人剔除,输出剔除的顺序。

#include <iostream>
#include<cstdio>
#include<ctime>
#include<algorithm>
#include<cstdlib>
using namespace std;
#define MAXN 10000
int a[MAXN];
int N,k,m;
bool InCircle[MAXN];
typedef struct _node{
    struct _node* prev;
    struct _node* next;
    int number;
}node;
node* MakeDLL(int n){
    node *head = new node;/*Memory allocated to the head node*/
    node *tail;
    int i;
    head->next = head;
    head->prev = head;
    head->number = 1;
    tail = head;
    for(i=2;i<=n;i++){
        node *p = new node;/*Memory allocated to a new node*/
        p->number = i;
        p->next = tail->next;
        p->prev = tail;
        tail->next = p;
        tail = p;
        head->prev = tail;
    }
    return head;
}

void JosephDLL(node* head, int k, int m){
    int i;
    node *NodeToDelete,*q;
    NodeToDelete = head;
    for(i=1; i<k; i++)/*Get the element numbered k*/
            NodeToDelete = NodeToDelete->next;
    while(head->next != head){
        for(i=1; i<m; i++){
            NodeToDelete = NodeToDelete->next;/*Count m times*/
        }
        /*Delete operation,begin*/
        q = NodeToDelete->next;
        q->prev = NodeToDelete->prev;
        NodeToDelete->prev->next = q;
        printf("%d ",NodeToDelete->number);
        if(NodeToDelete == head){/*If the member to be deleted is the head, redefine the head.*/
            head = q;
        }
        free(NodeToDelete);
        /*Delete operation,end*/
        NodeToDelete = q;/*Count from the next node*/
    }
    printf("%d\n",head->number);
}
void JosephArr(){
    int tmpN=N,i=k;
    while(tmpN--){
        int tmpM=m;
        for(;tmpM;i++){/*Count m times*/
            if(InCircle[i%(N+1)])/*If the member is in the circle, count on it(That is the operation tmpM--) */
                tmpM--;
        }
        i--;
        cout<<i%(N+1)<<" ";
        InCircle[i%(N+1)]=0;/*delete the member i%(N+1) from the circle*/
    }
}
int main(){
    freopen("input.txt","r",stdin);
    freopen("output.txt","w",stdout);
    cout<<"Input N,k,m:"<<endl;
    cin>>N>>k>>m;
    for(int i=1;i<=N;i++){
        a[i]=i;
        InCircle[i]=1;
    }
    cout<<"Base on the array : ";
    JosephArr();/*Base on the array*/
    for(int i=1;i<=N;i++){
        InCircle[i]=1;
    }
    node* head;
    head = MakeDLL(N);
    cout<<endl<<"Base on the doubly link list : ";
    JosephDLL(head,k,m);/*Base on the doubly linked lists*/
    return 0;
}

O(n)方法运用动态规划,或者说递推,或者说找规律,都行吧。虽然复杂度低,但是不能求出出圈的顺序。如果需要求出出圈的顺序,复杂度依然是O(n*m)。
该方法递推公式网上很多都能找到,但是却极少人给出递推过程。
我们发现,当从圈出来一个人之后,后一个人需要从1开始重新计数直到计到m。例如有n个人(1,2,3,… ,n-2,n-1),第m个人出来之后,需要从第m+1开计数。如果从m+1个人重新编号(即m+1个人编号为0,m+1为2,… …),那么对剩下的n-1个人的操作跟开始有n个人的时候操作就是一样的。
这就是可以运用动态规划的原因:n的情况跟n-1的情况存在着某种关系。而我们只需用动态方程将这种关系表达出来,问题就可以解决。
下面显示了如何进行重新编号:

《约瑟夫环O(N)和O(M*N)算法详解》
设n个人围成一个圈的时候出列是第X’号,n-1个人围成一个圈的时候出列的是X号,并假设X已经求出来,那么根据上面的递推公式就可以反推求X’: 因为X=(X’*n+n-m)%n=X’*n%n+n%n-m%n=X’-m%n
所以X’=X+m%n=(X+m)%n
如果定义:dp[n]表示圈里有n个人的时候最后剩下那个人的编号,答案就是dp[n]。
那么就有dp[n]=(dp[n-1]+m)%n
但是dp[n-1]并不知道,所以dp[n]也没法求出。当然dp[n-1]也需要通过动态方程求出:
dp[n-1]=(dp[n-2]+m)%n。
同样dp[n-2],dp[n-3]….分别需要用dp[n-3],dp[n-4]求出。
最后dp[2]需要dp[1]求出,然而dp[1]=0是已知的。
所以从dp[2]开始依次可以求出dp[2],dp[3],dp[4]…dp[n]。
那么问题就解决了。

#include <iostream>
#include<cstdio>
#include<ctime>
#include<algorithm>
#include<cstdlib>
using namespace std;
#define MAXN 10000
int dp[MAXN];
int joseph(int m, int n) {
    dp[1] = 0;
    for(int i = 2; i <= n; i++){
        dp[i] = (dp[i-1] + m) % i;
    }
    return dp[n];
}
int main(){
    freopen("input.txt","r",stdin);
    freopen("output.txt","w",stdout);
    int m, n;
    cin >> m >> n;
    cout << joseph(m, n) << endl;
}
    原文作者:约瑟夫环问题
    原文地址: https://blog.csdn.net/Dafang_Xu/article/details/49335583
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞