描述
人们站在一个等待被处决的圈子里。 计数从圆圈中的指定点开始,并沿指定方向围绕圆圈进行。 在跳过指定数量的人之后,处刑下一个人。 对剩下的人重复该过程,从下一个人开始,朝同一方向跳过相同数量的人,直到只剩下一个人,并被释放。
问题即,给定人数、起点、方向和要跳过的数字,选择初始圆圈中的位置以避免被处决。
注:跳过的数字,并不是间隔,间隔=跳过的数字-开始报数的人(从0开始 or 从1开始)
过程模拟
#include <iostream>
#include <cstdlib>
#include <cstdio>
using namespace std;
typedef struct _LinkNode {
int value;
struct _LinkNode *next;
} LinkNode, *LinkNodePtr;
// 创建一个循环链表
// int total 节点个数,value 从 1 ~ total
// LinkNodePtr 返回循环链表的首地址
LinkNodePtr createCycle(int total) {
int index = 1;
LinkNodePtr head = NULL, curr = NULL, prev = NULL;
head = (LinkNodePtr) malloc(sizeof(LinkNode));
head->value = index;
prev = head;
while (--total > 0) {
curr = (LinkNodePtr) malloc(sizeof(LinkNode));
curr->value = ++index;
prev->next = curr;
prev = curr;
}
prev->next = head;
return head;
}
//
void run(int total, int start, int tag) {
LinkNodePtr node = createCycle(total);
LinkNodePtr prev = NULL;
int index = start;
while (node->next) {
if (index == tag) {
cout << node->value << endl;
// 删除节点、改变指针指向
prev->next = node->next;
node->next = NULL;
node = prev->next;
index = start;
} else {
prev = node;
node = node->next;
index++;
}
}
}
int main() {
run(40, 1, 1);
return 0;
}
当tag = start
时,prev
为NULL
,prev->next
不合法,程序出错。
即对应:从第一个开始处刑,最后一个40没有被处刑,这种情况。
- 尝试一:
将prev
初始化指向node
,LinkNodePtr prev = node;
此时,当tag=1
时,
prev->next = node->next; // 没有意义,prev并没有记录下一个节点地址位置
node->next = NULL; // 链表断开,
node = prev->next; // prev->next 也指向 NULL,链表被断开,链表丢失
- 尝试二:
当tag=1
时,将
LinkNodePtr prev = node;
if (tag == start)
node = node->next;
即,默认处刑第一个人。
但是,由于没有把node=NULL
,导致最后一次循环,会把1输出。
即,没有真正处刑第一个人。
原因:没有办法找到 node
的 前驱节点。
- 尝试三:
start=tag
和 start!=tag
作为两种不同的删除节点方式。
void run(int total, int start, int tag) {
LinkNodePtr node = createCycle(total);
LinkNodePtr prev = NULL;
int index = start;
while (node && node->next) {
if (index == tag) {
cout << node->value << endl;
// 删除节点、改变指针指向
if (tag == start) {
prev = node->next;
node->next = NULL;
node = prev;
} else {
prev->next = node->next;
node->next = NULL;
node = prev->next;
}
} else {
prev = node;
node = node->next;
index++;
}
}
}
数学推导法
举例
假如有8个人,1 2 3 4 5 6 7 8
- 第一轮:2 4 6 被淘汰,剩下 1 3 5 7,重新编号为(1 2 3 4)
- 第二轮:3 7 淘汰,剩下1 5,重新编号为(1 2)
- 第三轮:5 淘汰,剩下1
假如有9个人,1 2 3 4 5 6 7 8 9
- 第一轮:2 4 6 8 1 被淘汰,剩下 3 5 7 9,重新编号为(1 2 3 4)
- 第二轮:5 9 淘汰,剩下 3 7,重新编号为(1 2)
- 第三轮:7 淘汰,剩下 3
递推
设有 n n n 人, k = 2 k=2 k=2,被淘汰,最后留下的人为 f ( n ) f(n) f(n)
当n为偶数时,第二轮,位置为 x x x 的人第一轮的位置为 2 x − 1 2x-1 2x−1,因此递推公式为:
f ( 2 n ) = 2 f ( n ) − 1 f(2n) = 2f(n) - 1 f(2n)=2f(n)−1
当n为奇数时,第二轮,位置为 x x x 的人第一轮的位置为 2 x + 1 2x+1 2x+1,因此递推公式为:
f ( 2 n ) = 2 f ( n ) + 1 f(2n) = 2f(n) + 1 f(2n)=2f(n)+1
下面给出一组 n n n 和 f ( n ) f(n) f(n) 的数据:
n n n | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
f ( n ) f(n) f(n) | 1 | 1 | 3 | 1 | 3 | 5 | 7 | 1 | 3 | 5 | 7 | 9 | 11 | 13 | 15 | 1 |
观察到当
n
n
n 为
2
2
2 的幂时,
f
(
n
)
=
1
f(n) = 1
f(n)=1,每次从 1 开始 1 3 5 7 9 … 重新开始。
可以把
n
n
n 表示为:
n
=
2
m
+
l
,
且
0
n = 2^m + l,且 0
n=2m+l,且0
≤
\leq
≤ l
≤
\leq
≤
2
m
2^m
2m,则
f
(
n
)
=
2
∗
l
+
1
f(n) = 2 * l + 1
f(n)=2∗l+1,(
2
m
2^m
2m 是不超过
n
n
n 的最大幂 )。
定理
可得到如下定理:
如果
n
=
2
m
+
l
且
0
n = 2 ^m + l 且 0
n=2m+l且0
≤
\leq
≤ l
≤
\leq
≤
2
m
2^m
2m,则
f
(
n
)
=
2
∗
l
+
1
f(n) = 2 * l + 1
f(n)=2∗l+1。
证明
对 n n n 用数学归纳法。
n = 1 n = 1 n=1 显然成立。
如果 n n n 是 偶数,令 l = l 1 , m = m 1 l = l_1,m = m_1 l=l1,m=m1,则 l 1 = l / 2 l_1 = l / 2 l1=l/2, n / 2 = 2 m 1 + l 1 且 0 n/2 = 2^{m_1} + l_1 且 0 n/2=2m1+l1且0 ≤ \leq ≤ l ≤ \leq ≤ 2 m 1 2^{m_1} 2m1,则 f ( n ) = 2 f ( n / 2 ) − 1 = 2 ( ( 2 l 1 ) + 1 ) − 1 = 2 l − 1 f(n) = 2f(n/2) - 1 = 2((2l_1) + 1) - 1 = 2l - 1 f(n)=2f(n/2)−1=2((2l1)+1)−1=2l−1。
如果 n n n 是 奇数,令 l = l 2 , m = m 2 l = l_2,m = m_2 l=l2,m=m2,则 l 2 = ( l − 1 ) / 2 l_2 =(l -1)/ 2 l2=(l−1)/2, ( n − 1 ) / 2 = 2 m 2 + l 2 且 0 (n - 1) / 2 = 2^{m_2} + l_2 且 0 (n−1)/2=2m2+l2且0 ≤ \leq ≤ l ≤ \leq ≤ 2 m 2 2^{m_2} 2m2,则 f ( n ) = 2 f ( ( n − 1 ) / 2 ) + 1 = 2 ( ( 2 l 2 ) + 1 ) + 1 = 2 l + 1 f(n) = 2f((n-1)/2) + 1 = 2((2l_2) + 1) + 1 = 2l + 1 f(n)=2f((n−1)/2)+1=2((2l2)+1)+1=2l+1。
证毕。
答案的另一种形式:
由
f
(
n
)
=
2
∗
l
+
1
f(n) = 2 * l + 1
f(n)=2∗l+1,可以联想到
∗
2
*2
∗2 相当于 左移,
+
1
+ 1
+1 相当于末尾
2
0
2^0
20可得到如下表示方法:
n
=
b
0
b
1
b
2
b
3
.
.
.
b
m
n = b_0b_1b_2b_3...b_m
n=b0b1b2b3...bm,则
f
(
n
)
=
b
1
b
2
b
3
b
4
.
.
.
b
m
b
0
f(n) = b_1b_2b_3b_4...b_mb_0
f(n)=b1b2b3b4...bmb0。
对于 k k k ≠ \neq = 2 2 2 ,即一般情况,考虑号码从 n − 1 n - 1 n−1 到 n n n 的变化(编号从0开始)。
f
(
n
,
k
)
=
k
∗
l
+
1
,
n
=
k
m
+
l
f(n,k)=k*l+1,n=k^m+l
f(n,k)=k∗l+1,n=km+l
f
(
n
−
1
,
k
)
=
k
∗
(
n
−
1
−
k
m
)
+
l
=
k
∗
(
n
−
l
m
)
+
1
−
k
=
f
(
n
,
k
)
−
k
f(n-1,k)=k*(n-1-k^m)+l=k*(n-l^m)+1-k=f(n,k)-k
f(n−1,k)=k∗(n−1−km)+l=k∗(n−lm)+1−k=f(n,k)−k
即:
f
(
n
,
k
)
=
f
(
n
−
1
,
k
)
+
k
f(n,k) = f(n-1,k) + k
f(n,k)=f(n−1,k)+k
当 递归 次数过多时,结果可能会一直叠加,数组下标最终超过数组个数,通过取余,来得到 f ( n , k ) f(n,k) f(n,k)的真实位置。
得到如下地推公式:
f
(
n
,
k
)
=
(
f
(
n
−
1
,
k
)
+
k
)
%
n
f(n,k)=(f(n-1,k)+k) \% n
f(n,k)=(f(n−1,k)+k)%n,
f
(
1
,
k
)
=
0
f(1,k)=0
f(1,k)=0。(如果从1开始 则
f
(
1
,
k
)
=
1
f(1,k)=1
f(1,k)=1)
// 公式法:递归
int josephus_recursion(int n, int k) {
return n > 1 ? (josephus_recursion(n-1, k) + k) % n : 0; // 从 0 开始
}
// 公式法:非递归
int josephus(int n, int k) {
int s = 0; // 从 0 开始
for (int i = 2; i <= n; i++) {
s = (s + k) % i;
}
return s;
}
int main() {
cout << josephus_recursion(8, 2) << endl;
cout << josephus(8, 2) << endl;
return 0;
}
// answer
0 // 1
0
注:对于维基百科 上,以下地方不是很明白。
当 k < n k<n k<n,可以将上述方法推广,将杀掉第k、2k、……、 ⌊ n / k ⌋ \lfloor n/k \rfloor ⌊n/k⌋个人视为一个步骤,然后把号码改变,可得如下递推公式, 运行时间为 O ( k O(k O(k log \log log n ) n) n)。
#include <cstdio>
using namespace std;
//编号1开始,结果要加1
int josephus(int n, int k) {
if (k == 1) return n - 1;
int ans = 0;
for (int i = 2; i <= n; ) {
if (ans + k >= i) {
ans = (ans + k) % i;
i++;
continue;
}
int step = (i - 1 - ans - 1) / (k - 1); //向下取整
if (i + step > n) {
ans += (n - (i - 1)) * k;
break;
}
i += step;
ans += step * k;
}
return ans;
}
int main() {
while (scanf("%d%d", &n, &k) == 2)
printf("%d\n", josephus(n, k) % n + 1);
return 0;
}