约瑟夫环(循环链表+数学归纳)

描述

人们站在一个等待被处决的圈子里。 计数从圆圈中的指定点开始,并沿指定方向围绕圆圈进行。 在跳过指定数量的人之后,处刑下一个人。 对剩下的人重复该过程,从下一个人开始,朝同一方向跳过相同数量的人,直到只剩下一个人,并被释放。

问题即,给定人数起点方向和要跳过的数字,选择初始圆圈中的位置以避免被处决。
注:跳过的数字,并不是间隔,间隔=跳过的数字-开始报数的人(从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时,prevNULLprev->next不合法,程序出错。
即对应:从第一个开始处刑,最后一个40没有被处刑,这种情况。

  • 尝试一:
    prev 初始化指向 nodeLinkNodePtr 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=tagstart!=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 2x1,因此递推公式为:

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 n12345678910111213141516
f ( n ) f(n) f(n)1131357135791113151

观察到当 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+l0 ≤ \leq l ≤ \leq 2 m 2^m 2m,则 f ( n ) = 2 ∗ l + 1 f(n) = 2 * l + 1 f(n)=2l+1,( 2 m 2^m 2m 是不超过 n n n 的最大幂 )。

定理

可得到如下定理
如果 n = 2 m + l 且 0 n = 2 ^m + l 且 0 n=2m+l0 ≤ \leq l ≤ \leq 2 m 2^m 2m,则 f ( n ) = 2 ∗ l + 1 f(n) = 2 * l + 1 f(n)=2l+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=l1m=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+l10 ≤ \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=2l1

如果 n n n 是 奇数,令 l = l 2 , m = m 2 l = l_2,m = m_2 l=l2m=m2,则 l 2 = ( l − 1 ) / 2 l_2 =(l -1)/ 2 l2=(l1)/2 ( n − 1 ) / 2 = 2 m 2 + l 2 且 0 (n - 1) / 2 = 2^{m_2} + l_2 且 0 (n1)/2=2m2+l20 ≤ \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((n1)/2)+1=2((2l2)+1)+1=2l+1

证毕。

答案的另一种形式:
f ( n ) = 2 ∗ l + 1 f(n) = 2 * l + 1 f(n)=2l+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 n1 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)=kl+1n=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(n1,k)=k(n1km)+l=k(nlm)+1k=f(n,k)k
即: f ( n , k ) = f ( n − 1 , k ) + k f(n,k) = f(n-1,k) + k f(n,k)=f(n1,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(n1,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;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值