约瑟夫环问题多种解法
整理自原文
经典问题:
问题描述:编号为 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 次。