PKU1012解題

PKU1012解題

PKU1012題,讓我費了幾天的時間,才終於算是理出個頭緒,真是杯具。看來以後的算法路還是相當曲折的,一道題弄個幾天,那麼多題,到我畢業時,也做了幾百道了。

首先來翻譯一下題目,鍛鍊下英語翻譯能力:
Description
問題描述
The Joseph's problem is notoriously known. For those who are not familiar with the original problem: from among n people, numbered 1, 2, . . ., n, standing in circle every mth is going to be executed and only the life of the last remaining person will be saved. Joseph was smart enough to choose the position of the last remaining person, thus saving his life to give us the message about the incident. For example when n = 6 and m = 5 then the people will be executed in the order 5, 4, 6, 2, 3 and 1 will be saved.
約瑟夫問題真是臭名昭著,notorious是臭名遠揚的意思。我們先來熟悉一下規則:編號爲1,2,3,.....n順序排列的n個人,依次站成一個圈,提供一個數m,從第一個人開始報數,報到m的人要自殺,接下來從第m+1個人開始,從1開始繼續報數,報到第m的人自殺,依次類推.....,直到還剩最後一個人,那這個人就可以存活下來了。約瑟夫太聰明瞭,他在報數前選了一個位置,結果最後只有他守到了最好,存活了下來。舉個例子來說,假設n=6,m=5,自殺的順序是4,6,2,3,而第一個人將會存活下來。

Suppose that there are k good guys and k bad guys. In the circle the first k are good guys and the last k bad guys. You have to determine such minimal m that all the bad guys will be executed before the first good guy.
假定這羣人裏有k個好人,有k個壞人,在這個圓圈裏前k個是好人,後k個是壞人,要求你找到最小的m值,使得後k個人壞人自殺完了,纔會輪到好人。

Input
The input file consists of separate lines containing k. The last line in the input file contains 0. You can suppose that 0 < k < 14.
輸入包括單獨的行,包含k值,最後一行是0,你可以假定k是在0到14之間的數。

Output
The output file will consist of separate lines containing m corresponding to k in the input file.
輸出對應的m值

Sample Input
3
4
0

Sample Output
5
30

下一步就是解題了,這個題有多種方法求解,如鏈表法,數組法,遞推法。我的程序是用鏈表法做的,先構建一個包含n個節點的循環鏈表,每個節點包括其序號,指向下個節點的指針,還有一個布爾變量,指示該節點是否已經被剔除;之後從當前節點(初始爲頭節點)開始搜索,跳過已被剔除的節點,找到第m-1個節點,如果第m-1個節點的序號在1~k間,將其布爾指示變量設爲真,將第m個節點賦給當前節點,用剛纔所述的方法繼續搜索,而如果其序號大於k,那說明m值選取不當,將m加1,將所有爲的布爾指示變量設爲false,重新按上述方法進行搜索。

剛剛完全是用編程序的角度來描述解題思路,可能有些費解,如果更通俗一點講,那就是找到一個報m的節點,那就判斷m值是否可取,不可取,將m加1,重來,可取,繼續搜,同時把報m的節點刪除。我的實現代碼如下:
#include<iostream>
#include<stdlib.h>
using namespace std;

typedef struct node
{
    int no;
    struct node* next;
}Node;

Node* head,*current;//頭節點

int guys,mth,count_exec;//guys:好人或壞人的數目,count_exec:已經弄死了多少人,mth:隔多少人自殺一個

//建立鏈表
void build_list()
{
    Node *s,*r;
    head=(Node *)malloc(sizeof(Node));
    head->no=1;
    head->next=NULL;
   
    r=head;

    for(int i=2;i<=2*guys;i++)
    {
        s=(Node *)malloc(sizeof(Node));
        s->no=i;
        r->next=s;
        r=s;
    }
    r->next=head;

    total=2*guys;
    current=head;
    count_exec=0;
}

//刪除自殺節點
int delete_node()
{
    int temp;
    Node *p,*s;
    p=current;
    for(int i=1;i<=mth-2;i++)
    {
        p=p->next;
    }
    s=p->next;
    current=s->next;
    temp=s->no;
    p->next=current;
    delete s;
    total--;
    return temp;
}   

//釋放內存
void del_list()
{
    Node *p,*s;
    p=current;
    s=p;
    for(int i=1;i<=total;i++)
    {
        s=p->next;   
        delete p;
        p=s;           
    }
}

//處理主程序
int deal_list()
{
    while(true)
    {
        int temp=delete_node();
        if(temp<=guys)
        {
            mth++;
            del_list();
            build_list();
            count_exec=0;
            continue;
        }
        else
        {
            count_exec++;
        }

        if(count_exec==guys)
            return mth;
    }
}

//打印結果
void print_result()
{
    cout<<mth<<endl;
}

int main()
{
    while(cin>>guys,guys>0&&guys<14)   
    {
        mth=guys+1;
        build_list();
        deal_list();
        print_result();
        del_list();
    }
    return 0;
}


數組法和鏈表法類似,爲每一個數組元素增加一指示變量,循環求解,不難實現,不多說了。

說一下遞推法,這是求解約瑟夫問題的數學解決方法,其簡單而又複雜,簡單是因其實現起來只有短短的幾行,複雜是指它的理解起來很困難,我花了很長時間才弄明白是怎麼回事,或許我的腦袋太笨了吧,嘿嘿,說一下大致思路,以設有n個人,以m報數:

將n個人從爲開始排序:
0 1 2 3 4 5 ....... n-1
剔除第m%n-1個人(他報m,這一點需要注意,如果m%n==0的話,是指第n-1個人),將剩下的人列出來:
m%n m%n+1 m%n+2 .... n-1 0 1 ....(總共n-1個),好,將這幾個的序號依次變爲:
0 1 2 3 4 5 .....  n-2
呵呵,應該可以看得出來了吧,下一步我們就是從這n-1個人的序列裏找到要剔除的序號,顯示其序號是m%(n-1)-1個人(同樣需注意m%(n-1)==0的情況),將其剔除,將剩下的人列出:
m%(n-1) m%(n-1)+1 m%(n-1)+2 ... n-2 0 1 ...(總共n-2個),我們採用同樣的方法,變爲:
0 1 2 3 4 5 .... n-3
...............................
..........................
..................
依次類推,直到還剩一個人。用這種方法,我們不僅可以找到最後一個人的序號,還可以把自殺的序列找出來。試想一下:如果當前我們知道有i個人的自殺序列,我們只需要將依某種規則把它們對應到i+1個人的隊列中去,得到他們在i+1個人中序號,加上m%(i+1)-1,那不就是自殺序列了嗎? 關鍵一點是我們該如何對應呢?
設變換前的序列爲k k+1 k+2 k+3 ... i-1 0 1 2 ... 變換後的序列爲0 1 2 3 4 .... i-1
設x爲變換後的某一序號,它在變換前的序列中的序號爲(x+k)%(i+1), 其中k又可以通過m%(i+1)得到 你看是不是?

有了以上思路,我們就可以編寫算法程序,將自殺序列順序輸出了:
#include<iostream>
#define MAX 10000
int a[MAX];
using namespace std;

int main()
{
    int guys,mth,i,j,k;
    while(cin>>guys>>mth,guys)
    {
        a[0]=0;
        for(i=1;i<guys;i++)
        {
            if(mth%(i+1)==0)
                a[i]=i;
            else
                a[i]=mth%(i+1)-1;

            k=mth%(i+1);

            for(j=0;j<i;j++)
                a[j]=(a[j]+k)%(i+1);
        }

        for(i=guys-1;i>=0;i--)
            cout<<a[i]+1<<" ";
        cout<<endl;
    }
    return 0;
}

用以上的遞推方法,可以解決PKU1012題了,算法程序如下:
#include<iostream>
#define MAX 14
int a[2*MAX];
using namespace std;

int main()
{
    int guys,mth,i,j,k;
    bool stop;
    while(cin>>guys,guys>0&&guys<MAX)
    {
        mth=guys+1;
        while(true)
        {
            stop=false;
            a[0]=0;
            for(i=1;i<2*guys;i++)
            {
                if(mth%(i+1)==0)
                    a[i]=i;
                else
                    a[i]=mth%(i+1)-1;

                k=mth%(i+1);

                for(j=0;j<i;j++)
                    a[j]=(a[j]+k)%(i+1);
            }

            for(j=0;j<guys;j++)
                if(a[j]+1>guys)
                {
                    stop=true;
                    break;
                }

            if(stop)
            {
                mth++;
                continue;
            }
            else
            {
                cout<<mth<<endl;
                break;
            }
        }
    }

    return 0;
}

呵呵,弄了一大堆,費了一大堆工夫,編譯也通過了,將幾個測試用例輸入,也正確了,那應該行了吧,拿到PKU Judge on Line上,TLE!!!運行超時,杯具,又重新運行我的程序,輸入13,嘿嘿,半天也沒有出來,難怪(注:上述兩種方法都沒AC,都TLE了)。

在網上搜了一下,找到一個網頁http://www.cnblogs.com/lotus3x/articles/1240935.html,源程序如下:
#include <iostream>
#define MAXK 14
using namespace std;

bool check(int n, int k)
{
    int all = n * 2, w = k % all;

    for (int i = 1; i < n; i++)
    {
        all--;
        if (--w < 0)
            w = all;
        w = (w + k) % all;
        if (w <= n && w > 0)
            return false;
    }
    return true;
}
 
int main()
{
    int m[MAXK];
    int i, j, k;
   
    for (i = 1; i < MAXK; i++)
        for (j = i + 1; ; j += (j % i == 0) ? i + 1 : 1)
            if (check(i, j))
            {
                m[i] = j;
                break;
            }
    while (cin>>k,k>0&&k<MAXK)
    cout<<m[k]<<endl;
    return 0;
}

弄到PKU Judge on line上,AC了,對上述程序的Check函數百思不得其解,分析了好久,Check函數對於本題是行得能的,可對於一般情況下的約瑟夫問題就不行了,如果將j設爲0~i+1間,結果就是錯誤了,不知道那位網友的思路是不是僅僅針對此題的。

另外,發現一個很怪的現象,因爲覺得此題一下把所有的情況都計算了,覺得沒有必要,就小改動了下:
#include<iostream>
using namespace std;

bool check(int n,int k)
{
    int all=n*2,w=k%all;

    for(int i=1;i<n;i++)
    {
        all--;
        if(--w<0)
            w=all;
        w=(w+k)%all;
        if(w<=n&&w>0)
            return false;
    }
    return true;
}
int main()
{
    int guys,j;
    while(cin>>guys,guys>0&&guys<14)
    {
        for(j=guys+1;;j+=(j%guys==0)?guys+1:1)
            if(check(guys,j))
            {
                cout<<j<<endl;
                break;
            }
    }
    return 0;
}

按理說,應該可以通過的,可惜,又是TLE,我真的訝異了!

好了,下面不說PKU1012題了,我們看一下約瑟夫問題,約瑟夫問題有很多變種:諸如:

1)不從第一個開始,從第P個開始,那最後剩下的是哪一個?

2)輸入m,n,k,輸入第k個人是第幾個自殺的?

3)給定m和最後一個自殺的編號,問最小的報數n?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章