约瑟夫环问题

约瑟夫环问题

百度百科中写道:"约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。"
其可以理解成有一个[0..N-1]的数组,从下标0开始,每次删掉第m个数,下一轮从被删掉的下一个数字开始,直至只剩下最后一个,那么最后剩下的一个是哪个数字?
我们以[0, 1, 2, 3, 4],m = 3为例。
从下标0开始,删掉第3个数,就是下标为m-1=2,那么删掉后就是[0, 1, 3, 4],下一次从3(下标为2)开始。
[0, 1, 3, 4]从3(下标2)开始就等价于[3, 4, 0, 1]从下标0开始,删掉第3个:0。
以此类推,下一个数组就是[1, 3, 4],删掉4。
再下一个数组就是[1, 3],删掉第三个,由于数组只有两个,所以删掉下标为 (2%2 = 0),也就是删掉1。
最后只剩下了数字3。

使用链表解决约瑟夫环问题

一个很自然的想法就是用循环链表来模拟删除的过程,看最后剩下的是哪个数字。
循环链表模拟约瑟夫环
我们将双向循环链表(定义为双向循环链表是为了方便删除当前节点)定义为:

struct bList {
    bList(int val, std::shared_ptr<bList> backward = nullptr, std::shared_ptr<bList> forward = nullptr)
        : val(val), forward(forward), backward(backward)
    {}
    int val;
    std::shared_ptr<bList> forward;
    std::shared_ptr<bList> backward;
};

使用双向链表模拟删除的过程:

int process(int m) {
    auto head = std::make_shared<bList>(0);
    auto cur = head;
    for (int idx = 1; idx < 5; ++idx) {
        cur->forward = std::make_shared<bList>(idx, cur);
        cur = cur->forward;
    }
    cur->forward = head;
    head->backward = cur;

    auto ls = head;
    int idx = 0;
    while (ls->forward != ls) {
        if (idx == m-1) { 
            // 下标从0开始,当下标为m-1 = 3-1 = 2时删除当前节点
            std::cout << "removed: " << ls->val << '\n';
            ls->backward->forward = ls->forward;
            ls->forward->backward = ls->backward;
            ls = ls->forward;
            idx = 1;
        } else {
            ls = ls->forward;
            ++idx;
        }
    }
    ls->backward = ls->forward = nullptr;
    return ls->val;
}


int main() {               
    std::cout << process(3) << '\n';             
}


// Output:
// removed: 2
// removed: 0
// removed: 4
// removed: 1
// 3

使用递推公式解决约瑟夫环问题

上面的解法虽然很简单,只需要模拟即可,但时间消耗却非常高,每查找一个数我们都需要遍历m次,即便我们可以将m对链表size取模减小,但时间复杂度仍是非常高的。于是我们试图找到一种更加高效的求解约瑟夫环的方法。
我们再回看我们手动解决例题的过程,其数组变化过程如下:

  • [0, 1, 2, 3, 4]
  • [3, 4, 0, 1]
  • [1, 3, 4]
  • [1, 3]
  • [3]

每次都删除第m个节点(下标为(m-1)%n,n为当前数组长度)。
可以确定地是最后幸存地数字在最后一个数组中的下标为0,而且此时数组的长度为1。
如果在我们知道第i+1个数组中最后幸存者的下标和数组长度的情况下,能够求出第i个数组中幸存者的下标和数组长度,我们是不是就可以从最后一个数组来倒推呢?第i个数组长度很明显是第i+1个数组长度+1,如何推导第i个数组中幸存者的下标呢?
首先我们假设第i+1个数组长度为L, 第i个数组长度为L+1;第i+1个数组中幸存者的下标为IdxN(意为idx next),第i个数组中幸存者的下标为IdxP(意为idx previous)。
递推公式示意图
i+1个数组中中幸存者的下标为N,那么必然有一个下表为0的节点。而该下标为0的节点在第i个数组中下标必然是m%(L+1)(因为它的上一个节点被删除了,而被删除的节点的下标必然是(m-1)%(L+1),那么被删除节点的下一个节点下标就是m%(L+1))。
我们就有如下等式:

IdxP - m = IdxN - 0 (mod L+1)
   Idxp  = IdxN + m (mod L+1)

(吐槽一下,博客园竟然不能显示公式。)

$$
\begin{equation}
\begin{split}
IdxP - m &= IdxN - 0\ (mod\ L+1) \\
IdxP &= IdxN + m\ (mod\ L+1)
\end{split}
\end{equation}
$$

上面的公式中第一个公式表示是在两个数组中两个数字的下标之差(距离)是相同的,因为被删除的数字在i[m%(L+1)]的紧贴着的左侧,i[m%(L+1)]向右看必然先看到i[IdxP],再看到i[(m-1)%(L+1)];而且IdxN-0是必然小于L+1的,因为第i+1个数组总长度只有L。
将第一个公式左边的m移到右边就得到了第二个公式,也就是从第i+1个数组中幸存者的下标递推出第i个数组中幸存者的下标的递推公式,其是第i个数组长度L+1、往后数的个数m和第i个数组中幸存者下标的函数。

将上述的过程转化为代码即为:

#include <iterator>
#include <memory>
#include <iostream>
#include <list>

// 这就是第二个公式,只是cnt表示的是L+1
// next是IdxN,m即为m
int process4_aux(int m, int next, int cnt) {
    return (next + m)%cnt;
}

int process4(int m) {
    int cnt = 1;
    int next = 0;
    while (cnt != 5) {
        next = process4_aux(m, next, cnt+1);
        ++cnt;
        std::cout << "at cnt = " << cnt << ", idx: " << next << '\n';
    }
    return next;
}

int main() {
    std::cout << process4(3) << '\n';
}
// at cnt = 2, idx: 1
// at cnt = 3, idx: 1
// at cnt = 4, idx: 0
// at cnt = 5, idx: 3
// 3

关于网上的题解

我在百度上搜索约瑟夫环相关的问题时,LeetCode上的《约瑟夫环问题的三种解法讲解》这个帖子排名很高。但他的代码貌似有问题,其关于使用循环链表解题的前两个代码好像都错了,这里贴上我用C++写的(我认为)正确的版本:

#include <iterator>
#include <memory>
#include <iostream>
#include <list>

struct bList {
    bList(int val, std::shared_ptr<bList> backward = nullptr, std::shared_ptr<bList> forward = nullptr)
        : val(val), forward(forward), backward(backward)
    {}
    int val;
    std::shared_ptr<bList> forward;
    std::shared_ptr<bList> backward;
};

class Solution {
public:
    int process( std::shared_ptr<bList> ls,int m) {
        int idx = 1;
        while (ls->forward != ls) {
            if (idx == m) {
                std::cout << "removed: " << ls->val << '\n';
                ls->backward->forward = ls->forward;
                ls->forward->backward = ls->backward;
                auto tmp = ls;
                ls = ls->forward;
                tmp->forward = tmp->backward = nullptr;
                idx = 1;
            } else {
                ls = ls->forward;
                ++idx;
            }
        }
        ls->forward = ls->backward = nullptr;
        return ls->val;
    }
    int process2( std::shared_ptr<bList> ls,int m) {
        int idx = 1;
        auto cur = ls;
        int n = 1;
        while (cur ->forward != ls) {
            ++n;
            cur = cur->forward;
        }
        while (ls->forward != ls) {
            if (idx == ((m+n-1)%n+1)) {
                std::cout << "removed: " << ls->val << '\n';
                ls->backward->forward = ls->forward;
                ls->forward->backward = ls->backward;
                auto tmp = ls;
                ls = ls->forward;
                tmp->forward = tmp->backward = nullptr;
                idx = 1;
                --n;
            } else {
                ls = ls->forward;
                ++idx;
            }
        }
        ls->forward = ls->backward = nullptr;
        return ls->val;
    }

    int process3(int m) {
        std::list<int> list;
        for (int idx = 0; idx < 5; ++idx) {
            list.push_back(idx);
        }
        int idx = 0;
        while (list.size() > 1) {
            idx = (idx+m-1)%list.size();
            auto itr = list.cbegin();
            std::advance(itr, idx);
            std::cout << "remove: " << *itr << '\n';
            list.erase(itr);
        }
        return list.front();
    }

    int process4(int m) {
        int cnt = 1;
        int next = 0;
        while (cnt != 5) {
            next = process4_aux(m, next, cnt+1);
            ++cnt;
            std::cout << "at cnt = " << cnt << ", idx: " << next << '\n';
        }
        return next;
    }

private:
    int process4_aux(int m, int next, int cnt) {
        return (next + m)%cnt;
    }
};

int main() {
    auto head = std::make_shared<bList>(0);
    auto cur = head;
    for (int idx = 1; idx < 5; ++idx) {
        cur->forward = std::make_shared<bList>(idx, cur);
        cur = cur->forward;
    }
    cur->forward = head;
    head->backward = cur;
    Solution s ;
    std::cout << s.process2(head, 3) << '\n';
    std::cout << s.process3(3) << '\n';
    std::cout << s.process4(3) << '\n';
}
posted @ 2022-12-08 17:20  流云cpp  阅读(353)  评论(0)    收藏  举报