1. 题目来源
相关题目:[剑指-Offer] 62. 圆圈中最后剩下的数字(数学、环形链表、约瑟夫环、巧妙解法)
2. 题目解析
约瑟夫环问题。一般拿环形链表模拟可做,也是在数据结构一章中环形链表的经典应用,在竞赛中一时没想起来,直接数组暴力模拟也过了,数据范围太小了。但是写了一个很丑的代码,却好在 1a 了。
数组模拟思路:
- 一个大小为
n
的数组,用 0 1 标记该位置是否有效。总共循环n - 1
次,每次循环遍历k
个有效的位置,将其标记,代表其被枪毙。然后再从该位置寻找到下一个有效的位置将其作为起点即可。
递归思路:
- 记
f(n, k)
为n
个人,跳k
步的最后胜利者的编号,在此要求是从下标 0 开始的,即它的下标是0,1,2,3,...,n-1
。 - 考虑
f(n-1, k)
此时下标应该从k
开始,可写作k, k+1, k+2,...,0,1,2,...,k-2
,总共n-1
个人,现在从下标k
开始报数。 - 但是
f(n-1,k)
的定义是n-1
个人从下标 0 开始报数最后的幸存者,这两者并不等价,即f(n,k)!=f(n-1,k)
。 - 那么
f(n-1,k)
是从下标0,1,2,3,...,n-2
开始构成的一个约瑟夫环,环内n-1
个点,我们可以将这个环转k
下,让 0 对应到k
,后面的均将一一对应。 - 故考虑做下标映射,即:将
k, k+1, k+2,...,n-1,0,1,2,...,k-2
,总共n-1
个人,映射为0,1,2,3,...,n-2
,0 和k
一一对应,1 和k+1
一一对应,k-2
和n-2
一一对应。 - 此时
f(n-1,k)
就是下标0,1,2,3,...,n-2
得到的胜利者的编号了,但是我们需要将这个结果映射到k, k+1, k+2,...,n-1,0,1,2,...,k-2
中,观察可以发现每一项就是多加了个k
,为了保证下标是在合法范围内,再%n
,将下标再映射到[0,n-1]
。其实在此有个疑问就是为啥不是映射到%(n-1)
中呢,毕竟只有n-1
个数啊,数的范围就是[0,n-2]
啊。但是实际上,这里的映射%n
并不是针对0,1,2,...,n-2
的,而是针对f(n,k)
这n
个数作的映射,同理f(n-1,k)
就得%(n-1)
,这个模数是随着第一个参数递减的。 f(n-1,k)
做完下标映射之后,就是以下标为k
开始的n-1
个人中的最后胜利者,这就和f(n,k)
的胜利者完全等价了。故f(n,k)= (f(n-1,k)+k)%n
,这就是一个非常简洁的递推式了,可以递推、递归实现。- 边界情况就是当
n==1
的时候,返回下标 0 即可,即自己直接胜出。 - 最后注意题目要求是从下标 1 开始,结果需要加 1。
要注意一点,这个 %n
随着 f(n-1,k)
的不断递减,模数也会不断减少!这点在递归改迭代的过程中相当重要!
- 时间复杂度: O ( n ) O(n) O(n)。
- 空间复杂度: O ( 1 ) O(1) O(1)
代码:
自己写的繁琐的模拟代码
class Solution {
public:
int findTheWinner(int n, int k) {
vector<int> q(n);
int cnt = n, index = 0;
for (int i = 1; i < n; i ++ ) {
for(int i = index, cnt = 0; cnt < k; i ++ ) {
if (i == n) i = 0;
if (q[i] == 0) cnt ++;
index = i;
}
q[index] = 1;
for (int i = index; i <= n; i ++ ) {
if (i == n) {
i = 0;
}
if (q[i] == 0) {
index = i;
break;
}
}
}
for (int i = 0; i < n; i ++ )
if (q[i] == 0)
return i + 1;
return 0;
}
};
大佬简洁的模拟写法:
class Solution
{
public:
int findTheWinner(int n, int k)
{
vector<int> a(n);
for (int i = 0; i < n; ++i) { // 0~n-1 代表序号,1~n 对应它的数值
a[i] = i + 1; // 真是及其巧妙的数据组织!
}
int at = 0;
while (a.size() > 1) {
at = (at + k - 1) % a.size(); // 找到下标,取模防止越界处理
a.erase(a.begin() + at);
}
return a[0];
}
};
约瑟夫经典递归:
// 需要最后下标 +1,不方便写一行
class Solution {
public:
int f(int n, int k) {
if (n == 1) return 0;
return (f(n - 1, k) + k) % n;
}
int findTheWinner(int n, int k) {
return f(n, k) + 1;
}
};
约瑟夫迭代写法:
class Solution {
public:
int findTheWinner(int n, int k) {
int ans = 0; // i=1 的情况,ans=0,循环也可以从i=1开始,因为 %1=0,ans无变化
for (int i = 2; i <= n; i ++ ) ans = (ans + k) % i;
return ans + 1;
}
};