zoukankan      html  css  js  c++  java
  • 约瑟夫环问题的两种解法(循环链表和公式法)

    问题描述

    这里是数据结构课堂上的描述:

    • N people form a circle, eliminate a person every k people, who is the final survior?
    • Label each person with 0, 1, 2, ..., n - 1, denote(表示,指代) J(n, k) the labels of surviors when there are n people.(J(n, k) 表示了当有 n 个人时幸存者的标号)
    • First eliminate the person labeled k - 1, relabel the rest, starting with 0 for the one originally labeled k.
      0 1 2 3 ... k-2 k-1 k k+1 ... n-1
                  ... k-2       0 1     ...
      Dynamic programming
      J(n, k) = J(J(n - 1, k) + k) % n, if n > 1,
      J(1, k) = 0

    用中文的方式简单翻译一下就是 (吐槽:为啥课上不直接用中文呢?淦!) 有 n 个人围成一圈,从第一个人开始,从 1 开始报数,报 k 的人就将被杀死,然后从下一个人开始重新从 1 开始报数,往后还是报 k 的人被杀掉,杀到最后只剩一个人时,其人就为幸存者。(上面的英文是从 0 开始的,是因为我们写程序时使用了数组,所以下标从 0 开始)

    解决方案

    循环链表方法

    算法思路很简单,我们这里使用了循环链表模拟了这个过程:节点 1 指向节点 2,节点 2 指向节点 3,...,然后节点 N 再指向节点 1,这样就形成了一个圆环。如图所示,n 取 12,k 取 3,从 1 开始报数,然后依次删除 3, 6, 9, 12:

    20201015005811

    #include<stdio.h>
    #include<stdlib.h>
    
    typedef struct Node // 节点存放一个数据和指向下一个节点的指针
    {
        int data;
        struct Node *next;
    } *NList; // NList为指向 Node 节点的指针
    
    // 创建一个节点数为 n 的循环链表
    NList createList(int n)
    {
        // 先创建一个节点
        NList p, tmp, head;
        p = (NList)malloc(sizeof(struct Node));
        head = p; // 保存头节点
        p->data = 1; // 第一个节点
    
        for (int i = 2; i <=n ; i++)
        {
            tmp = (NList)malloc(sizeof(struct Node));
            tmp->data = i;
            p->next = tmp;
            p = tmp;
        }
        p->next = head; // 最后一个节点指回开头
        return head;
    }
    
    // 从 编号为 1 的人开始报数,报到 k 的人出列,被杀掉
    void processList(NList head, int k)
    {
        if (!head) return;
        NList p = head;
        NList tmp;
    
        while (p->next != p)
        {
            for (int i = 0; i < k - 1; i++)
            {
                tmp = p;
                p = p->next;
            }
            printf("%d 号被杀死
    ", p->data);
            tmp->next = p->next;
            free(p);
            p = NULL; // 防止产生野指针,下同
            p = tmp->next;
        }
    
        printf("幸存者为 %d 号", p->data);
        free(p);
        p = NULL;
    }
    
    
    int main()
    {
        NList head = createList(11);
        processList(head, 3);
    
        return 0;
    }
    

    测试结果:

    20201015141259

    易知,这个算法的时间复杂度为 (O(nk)),显然,这不是一个好的算法。

    公式法

    在问题描述里,我们就有提到公式:
    J(n, k) = J(J(n - 1, k) + k) % n, if n > 1,
    J(1, k) = 0

    下面,我们就来简单证明一下这个算法。

    举例,我们用数字表示每一个人:

    [1,2.3,4,5,6,7,8,9,10,11 ]

    一共 11 个人,他们排成一排,假设报到 3 的人被杀掉。

    • 刚开始时,头一个人编号是1,从他开始报数,第一轮被杀掉的是编号3的人。
    • 编号4的人从1开始重新报数,这时候我们可以认为编号4这个人是队伍的头。第二轮被杀掉的是编号6的人。
    • 编号7的人开始重新报数,这时候我们可以认为编号7这个人是队伍的头。第三轮被杀掉的是编号9的人。
    • ……
    • 第九轮时,编号2的人开始重新报数,这时候我们可以认为编号2这个人是队伍的头。这轮被杀掉的是编号8的人。
    • 下一个人还是编号为2的人,他从1开始报数,不幸的是他在这轮被杀掉了。
    • 最后的胜利者是编号为7的人。

    表格演示(表头代表数组的下标):

    0 1 2 3 4 5 6 7 8 9 10
    1 2 3 4 5 6 7 8 9 10 11
    4 5 6 7 8 9 10 11 1 2
    7 8 9 10 11 1 2 4 5
    10 11 1 2 4 5 7 8
    2 4 5 7 8 10 11
    7 8 10 11 2 4
    11 2 4 7 8
    7 8 11 2
    2 7 8
    2 7
    7

    我们用上面的数据验证一下公式的正确性,其中,J(n, k) 表示的是幸存者在这一轮的下标位置:

    • (J(1, 3) = 0);只有一个人,此人是最后的幸存者,其在数组中的下标为 0
    • (J(2, 3) = 1 = (J(1, 3) + 3) % 2);还剩 2 个人时
    • (J(3, 3) = 1 = (J(2, 3) + 3) % 3);还剩 3 个人时
    • (J(4, 3) = 0 = (J(3, 3) + 3) % 4)
    • (J(5, 3) = 3 = (J(4, 3) + 3) % 5)
    • ...
    • (J(11, 3) = 6 = (J(10, 3) + 3) % 11);最终计算出待求的情况

    我们通过实例只是验证了这一种情况是成立的,这能够很好地辅助我们理解,但是,我们还需要这个公式的具体推导,下面,就以问答的方式来推导这个公式。

    问题1:假设我们已经知道 11 个人时,幸存者的下标位置为 6,那么下一轮 10 个人时,幸存者下标的位置是多少?
    :我们在第 1 轮杀掉第 3 个人时,后面的人都往前面移动了 3 位,幸存者也往前移动了 3 位,所以他的下标由 6 变成了 3。

    问题2:假设我们呢已经知道 10 个人时,幸存者的下标位置为 3,那么上一轮 11 个人时,幸存者下标的位置是多少?
    :这可以看成是上一个问题的逆过程,所以由 10 变成 11 个人时,所有人都往后移动 3 位,所以 (J(11, 3) = J(10, 3) + 3),不过数组可能会越界,我们可以想象成数组的首尾是相接的环,那么越界的元素就要重新回到开头,所以这个式子还要模上当前的人数(注意,这里是当前数组,在这里就是人数为 11 的这个数组):(J(11,3) = (J(10, 3) + 3) % 11)

    问题3:推及到一般情况,人数为 n,报到 k 时,把那个人杀掉,那么数组又是怎么移动的?
    :由上面的推导,我们应该很容易就能得出,若已知 n - 1 个人时,幸存者的下标位置为 (J(n - 1, k)),那么 n 个人的时候,就是往后移动 k 位,同样的,因为可能数组越界,所以式子要变成:(J(n, k) = (J(n - 1, k) + 3) % n)

    C语言代码实现:

    #include <stdio.h>
    
    int JoseCir(int n, int k)
    {
        int p = 0; // 只剩一个人时,数组下标为 0
        for (int i = 2; i <= n; i++)
        {
            p = (p + k) % i; // i 是当前的人数,即数组的规模
        }
        return p + 1; // 因为数组是从 0 开始,所以返回的幸存者编号需要加 1
    }
    
    int main()
    {
        int n = 11, k = 3;
        int res;
        res = JoseCir(n, k);
    
        printf("幸存者的编号为:%d", res);
        return 0;
    }
    

    测试结果:

    20201015134533

    参考:
    https://blog.csdn.net/u011500062/article/details/72855826
    https://blog.csdn.net/wenhai_zh/article/details/9620847

  • 相关阅读:
    greybox关闭/刷新父窗口
    C# 获取文件编码
    框架页,URL中文参数乱码
    用来代替SQLSERVERAGENT的VBS脚本。
    jQuery的radio,checkbox,select操作
    mssql 的sp_help好难看
    如何判断网通、电信、铁通IP地址分配段
    IE8取不到 select 的option值
    如何识别当前的 SQL Server 版本号以及对应的产品级别
    控诉我的电脑
  • 原文地址:https://www.cnblogs.com/fanlumaster/p/13820296.html
Copyright © 2011-2022 走看看