利用递推公式求解约瑟夫问题——谈POJ 1012

试题链接:POJ 1012 Joseph

一、使用循环链表进行模拟(超时)

首先创建一个长度为 2 k 2k 2k的首尾相接的循环链表,每个结点从 1 1 1 2 k 2k 2k进行标号。对于每个满足 k + 1 ≤ m % 2 k ≤ 2 k k+1 \le m\%2k \le 2k k+1m%2k2k m m m,从 1 1 1开始,每数到第 m m m号,检查他是否为坏人(即此人的编号 k + 1 ≤ n ≤ 2 k k+1 \le n \le 2k k+1n2k),若是则删除之;否则表明这个 m m m的取值不对,尝试下一个 m m m

使用循环链表的超时代码:

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

struct Node
{
    int number;
    Node *next;
};

// create a cyclic list of 2k length, numbered from 1 to 2k
void create(Node *&head, Node *&tail, int k)
{
    Node *previous, *current;

    head = (Node *)malloc(sizeof(Node));
    head->number = 1;
    head->next = head;
    tail = head;

    previous = head;
    for (int i = 2; i <= 2*k; ++i)
    {
        current = (Node *) malloc(sizeof(Node));
        current->number = i;
        current->next = head;
        previous->next = current;
        previous = current;
        tail = current;
    }
}

// free list
void destroy(Node *&head, Node *&tail)
{
    Node *previous = tail, *current = head, *temp;

    while (current->next != current)
    {
        temp = current;
        previous->next = current->next;
        current = current->next;
        free(temp);
    }

    free(current);
    head = tail = NULL;
}

// kill m-th people
// 1..k is good, k+1..2k is bad
bool search(Node *&head, Node *&tail, int m, int k)
{
    Node *current = head, *previous = tail, *temp;
    int cnt = 0;
    int dead = 0;   // number of dead people

    while (current->next != current)
    {
        ++cnt;
        if (cnt == m)
        {
            if (1 <= current->number && current->number <= k)   // kill good guy
                return false;

            if (current == head)
                head = head->next;
            if (current == tail)
                tail = previous;
            temp = current;
            previous->next = current->next;
            current = current->next;
            free(temp);

            cnt = 0;
            ++dead;
            if (dead == k)      // all bad guys are killed
                return true;
        }
        else
        {
            previous = current;
            current = current->next;
        }
    }

    return false;
}

int main()
{
    int k;

    while (scanf("%d", &k) && k > 0)
    {
        bool found = false;
        for (int i = 1; !found; ++i)
        {
            for (int m = i*k+1; m <= i*k+k; ++m)
            {
                Node *head = NULL, *tail = NULL;
                create(head, tail, k);
                if (search(head, tail, m, k))
                {
                    printf("%d\n", m);
                    found = true;
                    break;
                }
                destroy(head, tail);
            }
        }
    }

    return 0;
}

二、利用递推公式求解

设现有 n n n个人围成一圈,从1到 n n n编号,每轮报到 m m m的人被枪决,记 f ( n , m ) f(n, m) f(n,m)为此轮被枪决的人的编号。

可以这样考虑,当有 n n n个人时,第 m m m个人被枪决(先简单考虑 m ≤ n m \le n mn的情形),则下一轮只有 n − 1 n-1 n1个人时,从第 m + 1 m+1 m+1号人开始报数,那么我们可以将第 m + 1 m+1 m+1号人编号为 1 1 1,这样就从规模为 n n n的问题变成了 n − 1 n-1 n1的问题。原问题和子问题下的人员编号如下表所示:

( n , k ) (n, k) (n,k)问题 ( n − 1 , k ) (n-1, k) (n1,k)问题
1 1 1 n − m + 1 n-m+1 nm+1
2 2 2 n − m + 2 n-m+2 nm+2
3 3 3 n − m + 3 n-m+3 nm+3
⋮ \vdots ⋮ \vdots
m − 1 m-1 m1 n − 1 n-1 n1
m m mkilled
m + 1 m+1 m+1 1 1 1(start next)
m + 2 m+2 m+2 2 2 2
⋮ \vdots ⋮ \vdots
n − 1 n-1 n1 n − 1 − m n-1-m n1m
n n n n − m n-m nm

但是这样我们是无法推出 f ( n , m ) f(n, m) f(n,m) f ( n − 1 , m ) f(n-1, m) f(n1,m)之间的关系式的。仔细琢磨一些题目,编号 1 1 1 k k k的为好人,编号 ( k + 1 ) (k+1) (k+1) 2 k 2k 2k的为坏人,而且我们要找到合适的 m m m使得前 k k k轮被杀的都是坏人(即 k + 1 ≤ f ( i , m ) ≤ 2 k , 2 k ≥ i ≥ k + 1 k+1 \le f(i, m) \le 2k, 2k \ge i \ge k+1 k+1f(i,m)2k,2kik+1)。因此我们可以这样思考:无需纠结于好人与坏人编号的精确值,只要保证前 k k k人是好人即可,即每一轮杀掉一个坏人后将其后的所有坏人编号全部减一(如果好人不被杀,好人的编号永远不会变)

例如,对于 k = 3 , m = 5 k = 3, m = 5 k=3,m=5,开始时,大家的编号如下:
1 2 3 4 5 6
第一轮从1号报数,杀掉5号,则其后的6号先前填充,编号变为:
1 2 3 4 5
第二轮从5号(实际上是最初的6号)报数,杀掉4号,则其后的5号先前填充,编号变为:
1 2 3 4
第三轮从4号(实际上是最初的6号)报数,杀掉4号,剩下的都是好人,所以这个 m m m的取值是正确的。在整个过程中,好人1、2、3的编号从未变化。

那么,在这种条件下,原问题和子问题下的人员编号如下表所示:

( n , k ) (n, k) (n,k)问题 ( n − 1 , k ) (n-1, k) (n1,k)问题
1 1 1 1 1 1
2 2 2 2 2 2
3 3 3 3 3 3
⋮ \vdots ⋮ \vdots
m − 1 m-1 m1 m − 1 m-1 m1
m m mkilled
m + 1 m+1 m+1 m m m
m + 2 m+2 m+2 m + 1 m+1 m+1(start next)
⋮ \vdots ⋮ \vdots
n − 1 n-1 n1 n − 2 n-2 n2
n n n n − 1 n-1 n1

我们可以推出
f ( i − 1 , m ) = ( f ( i , m ) − 2 + m ) % ( i − 1 ) + 1 f ( n , m ) = ( m − 1 ) % n + 1 \begin{aligned} f(i-1, m) &= (f(i, m)-2+m) \% (i-1) + 1 \\ f(n, m) &= (m-1) \% n + 1 \end{aligned} f(i1,m)f(n,m)=(f(i,m)2+m)%(i1)+1=(m1)%n+1

我们只要一一尝试 { m ∣ i k + 1 ≤ m ≤ i k + k , i = 1 , 2 , ⋯   } \{ m | ik+1 \le m \le ik+k, i=1, 2, \cdots \} {mik+1mik+k,i=1,2,}中的取值,找到使得 k + 1 ≤ f ( i , m ) ≤ 2 k , 2 k ≥ i ≥ k + 1 k+1 \le f(i, m) \le 2k, 2k \ge i \ge k+1 k+1f(i,m)2k,2kik+1得以满足的最小的 m m m

代码如下(为了避免超时,这里打表记录结果):

#include <stdio.h>

int result[15] = { 0 };

bool check(int m, int k)
{
    int killed = (m - 1) % (2*k) + 1;
    if (killed <= k)
        return false;
    for (int i = 2*k-1; i > k; --i)
    {
        killed = (killed + m - 2) % i + 1;
        if (killed <= k)
            return false;
    }
    return true;
}

int main()
{
    int k;
    bool found;

    while (scanf("%d", &k) && k > 0)
    {
        if (result[k])
        {
            printf("%d\n", result[k]);
            continue;
        }
        found = false;
        for (int i = 1; !found; ++i)
        {
            for (int m = i*k+1; m <= i*k+k; ++m)
            {
                if (check(m, k))
                {
                    printf("%d\n", m);
                    result[k] = m;
                    found = true;
                    break;
                }
            }
        }
    }

    return 0;
}

打表的结果为:

result[1] = 2;
result[2] = 7;
result[3] = 5;
result[4] = 30;
result[5] = 169;
result[6] = 441;
result[7] = 1872;
result[8] = 7632;
result[9] = 1740;
result[10] = 93313;
result[11] = 459901;
result[12] = 1358657;
result[13] = 2504881;
result[14] = 13482720;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值