用C++解决约瑟夫环的问题

犹太历史学家弗拉维奥·约瑟夫在他的日记中提到过这么一个问题:他和他的40个战友被罗马军队包围在洞中。它们讨论是自杀还是投降,最终决定采用抽签的方式决定自杀的顺序。算法是这样的:所有人站成一圈,依次报数从1到3,每报到3的则自杀,下一个再从1开始报数。这样,所有人会依次自杀,直到最后一个。那最后一个死不死又有谁知道呢?那么站的位置就显得很重要了,到底站在哪个位置才能最后一个死呢?这便衍生成了一个计算机中的算法问题。

为了更加突出的说明这一问题,请看下面的图,改图内圈数字表示站立下标,外圈数字表示死亡顺序。
在这里插入图片描述
可见当有10个人,报数为3的人死亡时,站在4号位的最后可以存活。这一问题可以模拟到计算机程序中。

解决思路:首先很容易想到的便是采用链表的方式来处理,有一个首位相连的链表,每个节点里面存储了自己的下标并指向下一个节点的地址,最后一个节点指向首节点。根据这一思路,我们可以先创建一个约瑟夫环如下代码:

struct Jospher{
    int index;
    Jospher* next;

    Jospher(int i){
        this->index = i;
    }
};  //先定义一个结构体(类)存储每个节点的信息

void creatRing(Jospher* head, int ringSize){  //传入首节点和需要创建的环的节点总数
    Jospher* pointer = head;
    for(int i = 1; i < ringSize; i++){
        Jospher* nextPerson = new Jospher(i + 1);
        pointer->next = nextPerson;  //先将新创建的存入上一个节点中,依此循环
        pointer = nextPerson;
    }
    pointer->next = head; //最后将首节点放入末尾构成环
}

以上代码成功的构建了一个约瑟夫环,现在将模拟死亡顺序,我们需要两个可移动的指针,用来依次杀掉报到3的人(删掉对应的节点),可以参考下一幅图:
在这里插入图片描述
我们知道,报数是1,2,3,1,2,3…依次循环的,因此第一轮的报数是直接从1开始加,如图中的步骤1号指针(红色)指向首节点,2号指针(蓝色)指向其下一个节点,1报完之后是2,2正常,因此,将蓝色指针向后移动一位,红色指针也向后移动一位。再到3号报数,同样地,先将蓝色指针向后移动一位(此时指向4),再将此时的蓝色指针存入红色指针的next里面,再删掉3节点,那么第一个删除就完成了。依此类推可以逐个删掉直至最后一名存活者。值得注意地是,当只剩最后一个存活者时,它的next指向的是它本身。因此用代码实现如下:

void killNode(Jospher* head, int killSize){  //参数killSize传入第几号数字杀死
    Jospher* first = head;  //刚开始1号指针指向首节点
    Jospher* second = head->next; //2号指针指向第二个节点
    int killIndex = 1;  //报数下标,从1开始因为1号指针已经指向了首节点
    while(first->next && first->next != first){ //进入循环,依次删掉一个节点,注意结束的条件,包含了指针不指向自己本身
        second = second->next; //先将2号指针向后移一位
        killIndex++; //2号指针后移一位后,代表多报数了一人,报数下标自增
        if(killIndex == killSize){  //当满足要求时开始杀人
            Jospher* kill = first->next; //先将要杀的人,即1号指针指向的下一个(此时2号指针已经后移了一位)保存在中间变量中
            first->next = second;  //重新连接链表
            killIndex = 0;  //将报数下标恢复至0,下次移动时从1开始重新报数
            delete kill;  //记住释放掉不需要的节点,否则会造成不用的内存无法释放。
        }
        else{
            first = first->next;  //如果不满足条件的话,将1号指针也向后移一位
        }
    }
    cout << "the victor is: " first->index - 1;  //循环结束后剩下的节点即为存活的节点,下标要减1
}

最后,稍微测试一下这个链表:

int main(){
    Jospher* head = new Jospher(1); //创建首节点
    int ringSize, killSize;
    cin >> ringSize >> killSize;
    creatRing(head, ringSize);
    killNode(head, killSize);
}

输出结果:
在这里插入图片描述
最后是下标为3即站在4号位的人存活。

以上便是采用链表解决这个问题的方法,以上代码的时间复杂度为O(MN),即N个人,死亡号码为M,当N足够大时便显现出了代码的不足。因此我们还可以采用公式法来递推。观察以下表格:
在这里插入图片描述
从上表可以看出4号位也就是下标为3的人存活。公式可以归纳如下:
f(n) = (f(n - 1) + m) % n,该公式表示,当剩余n(假设为10)个人时,此场景的存活下标为剩余n-1(即9)人时的存活下标加上报数循环长度(3)除以当前人数n(即10)的余。很明显,这是个递归公式,可以很简单的用程序写出来。问题是,这个公式是怎么来的?

可以想象,当已知n为10时,存活的下标肯定为3。当杀掉一个人之后(即剩9个人)这时,存活下标一定是当前下标往前推3(一个循环),即为0(不信可以用链表代码验证以下)。再死一个人时,再往前推3(即8, 7, 6),下标为6,依次类推。直至只剩1个人时的下标一定是0.因此可以用递归法反向推出。代码如下:

int jospherRing(int ringSize, int killIndex){
    if(ringSize == 1)
        return 0;
    else
        return (jospherRing(ringSize - 1, killIndex) + killIndex) % ringSize;
}

输出结果与链表法一致。

由此可见公式递归法比链表法来的要简单的多。但是它仍然有一个弊端,那就是该程序无法知道死亡的次序,而链表法则可以得出依次死亡的次序(只需在delete要删除的节点之前将其中的index减1即可)。如下面输出结果:
在这里插入图片描述
以上两种方法各有各的使用场景,选择最合适的即可!

猜你喜欢

转载自blog.csdn.net/WJ_SHI/article/details/104952941