首页 > 技术文章 > 约瑟夫环问题多种解法

kingwz 2021-12-08 21:38 原文

整理自原文

经典问题:

问题描述:编号为 1-N 的 N 个士兵围坐在一起形成一个圆圈,从编号为 1 的士兵开始依次报数(1,2,3…这样依次报),数到 m 的 士兵会被杀死出列,之后的士兵再从 1 开始报数。直到最后剩下一士兵,求这个士兵的编号。

使用循环链表

步骤:
1、先创建一个环形链表来存放元素。
2、然后一边遍历链表一遍删除,直到链表只剩下一个节点。
效率:
时间复杂度为 O(n * m), 空间复杂度是 O(n)。
代码: 简单代码,就不写了(太长)

数学解法


效率: O(n)

推理过程:

程序过程模拟:
对于数组[0, 1, 2, 3, 4],每次杀死第三位。为了模拟循环,数组写两遍。

第一轮是 [0, 1, 2, 3, 4, 0, 1, 2, 3, 4 ] 从 0 开始,2 被删除了。
第二轮是 [3, 4, 0, 1,3, 4, 0, 1 ] ,从 3 开始, 0 删除了。
第三轮是 [1, 3, 4,1, 3, 4 ],从 1 开始,4 删除了。
第四轮是 [1, 3,1, 3 ],从 1 开始,1 删除了。
最后剩下的数字是 [3]。

可以看到:每次都是固定地向前移位 m 个位置。
求解推理过程:
我们从最后剩下的 3 ,反向推这个数字在之前每个轮次的位置。
最后剩下的 3 的下标一定是 0。

第四轮反推,补上 m 个位置,然后模上当时的数组大小 2,位置是(0 + 3) % 2 = 1。

第三轮反推,补上 m 个位置,然后模上当时的数组大小 3,位置是(1 + 3) % 3 = 1。

第二轮反推,补上 m 个位置,然后模上当时的数组大小 4,位置是(1 + 3) % 4 = 0。

第一轮反推,补上 m 个位置,然后模上当时的数组大小 5,位置是(0 + 3) % 5 = 3。

所以最终剩下的数字的下标就是3。因为数组是从0开始的,所以最终的答案就是3。

总结一下反推的过程,就是
上轮num的下标 = (当前index + m) % 上一轮剩余数字的个数。
index是此轮过后的num下标。

理解2

.最后只剩下一个元素,假设这个最后存活的元素为 num, 这个元素最终的的下标一定是0 (因为最后只剩这一个元素),
所以如果我们可以推出上一轮次中这个num的下标,然后根据上一轮num的下标推断出上上一轮num的下标,
直到推断出元素个数为n的那一轮num的下标,那我们就可以根据这个下标获取到最终的元素了。推断过程如下:

首先最后一轮中num的下标一定是0, 这个是已知的。
那上一轮应该是有两个元素,此轮次中 num 的下标为 (0 + m)%n = (0+3)%2 = 1; 说明这一轮删除之前num的下标为1;
再上一轮应该有3个元素,此轮次中 num 的下标为 (1+3)%3 = 1;说明这一轮某元素被删除之前num的下标为1;
再上一轮应该有4个元素,此轮次中 num 的下标为 (1+3)%4 = 0;说明这一轮某元素被删除之前num的下标为0;
再上一轮应该有5个元素,此轮次中 num 的下标为 (0+3)%5 = 3;说明这一轮某元素被删除之前num的下标为3;
....

因为我们要删除的序列为0 ~ n-1,
所以求得下标其实就是求得了最终的结果。比如当n为5的时候,num的初始下标为3,所以num就是3,

代码:

for循环

如果是从0开始

class Solution {
    public int lastRemaining(int n, int m) {
        int ans = 0;
        // 最后一轮剩下2个人,所以从2开始反推
        for (int i = 2; i <= n; i++) {
            ans = (ans + m) % i;
        }
        return ans;
    }
}

如果是从1开始

#include <bits/stdc++.h>
using namespace std;
int main()
{
   int n, m;
   cin>>n>>m;
    int ans=1;
    for(int i=2;i<=n;i++){
        ans=(ans+m-1)%i+1;
        //cout<<ans<<endl;
    }
    cout<<ans<<endl;
    return 0;
}

递归写法

int f1(int n, int m){
    if(n == 1)   return n;
    return (f(n - 1, m) + m - 1) % n + 1;
}
/************或者********/
int f2(int n, int m){
    return n == 1 ? n : (f(n - 1, m) + m - 1) % n + 1;
}

效率:
时间复杂度是 O(n),空间复杂度还是 O(n),因为会递归会调用 n 次。

推荐阅读