Algorithm Gossip: 約瑟夫問題(Josephus Problem)

說明

據說著名猶太歷史/數學家約瑟夫(Josephus)有過以下的故事:在羅馬人佔領喬塔帕特後,40個猶太士兵與 約瑟夫 躲到一個洞中,眼見脫逃無望,一群人決定集體自殺,然而 私下 約瑟夫 與某個傢伙並不贊成, 於是 約瑟夫 建議 自殺方式,41個人排成圓圈,由第1個人 開始報數,每報數到3的人就必須自殺,然後由下一個重新報數,直到所有人都自殺身亡為止。 約瑟夫 與不想自殺的那個人分別排在第16個與第31個位置,於是逃過了這場死亡遊戲。  

解法

只要畫兩個圓圈就可以讓 約瑟夫與不想死的傢伙 免於死亡遊戲,這兩個圓圈內圈是排列順序,而外圈是自殺順序,如下圖所示: 
約瑟夫問題
使用程式來求解的話,只要將陣列當作環狀來處理就可以了,在陣列中由計數1開始,每找到三個無資料區就填入一個計數,直而計數達41為止,然後將陣列由索引1開始列出,就可以得知每個位置的自殺順序,這就是約瑟夫排列,41個人而報數3的約琴夫排列如下所示:
14 36 1 38 15 2 24 30 3 16 34 4 25 17 5 40 31 6 18 26 7 37 19 8 35 27 9 20 32 10 41 21 11 28 39 12 22 33 13 29 23

由上可知,最後一個自殺的是在第31個位置,而倒數第二自殺的要排在第16個位置。 

如果從另一角度來看,每次有人自殺後,剩下的人重新編號,那問題就可以分解為數個子問題。

如果使用動態規畫,有個遞迴推導公式可在 Josephus problem 找到:
g(1, k) = 0
g(n, k) = (g(n - 1, k) + k) % n

其 中k表示報數k的人就自殺,g(n, k)就是n人組成的圓最後存活者編號,編號是從0開始,由於1人時該人就是存活者,所以g(1, k)為0。簡單來說,如果可以求得n - 1的圓時最後存活者編號x,就可以用來求得n的圓時最後存活者編號(x + k) % n(如果要得到1開始的編號,就對結果加一即可)。

如果使用鏈結串列,問題會更簡單,A串列中是1到n的編號,逐一移出A串列中的編號至B串列,最後A串列為空,B串列就是自殺編號順序,1到n編號所在的索引加一就是約瑟夫排列。

#include <stdio.h> 
#include <stdlib.h> 
#define LEN 41
#define STEP 3

void josephus(int*, int, int);
int next(int*, int, int, int);
int winner(int, int);

int main(void) { 
    int man[LEN] = {0}; 
    josephus(man, LEN, STEP);

    printf("約琴夫排列:"); 
    int i;
    for(i = 0; i < LEN; i++) {
        printf("%d ", man[i]); 
    }
    printf("\nWinner: %d", winner(LEN, STEP));

    return 0; 
} 

void josephus(int* man, int len, int k) {
    int i, n;
    for(i = 1, n = -1; i <= len; i++) {
        n = next(man, len, k, n);
        man[n] = i;
    }
}

int next(int* man, int len, int k, int n) {
    int count = 0;
    while(count < k) {
        n = (n + 1) % len;
        if(man[n] == 0) { count++; }
    }    
    return n;
}

int winner(int len, int k) {
    int g, n;
    for(g = 0, n = 1; n <= len; n++) {
        g = (g + k) % n;
    }
    return g + 1;
}


#include<stdio.h> 
#include <stdlib.h>

typedef struct node//节点存放一个数据和指向下一个节点的指针
{
	int data;
	struct node* pnext;
} Node;

Node *link_create(int n)//创建n个节点的循环链表
{
	//先创建第1个节点
	Node *p,*q,*head;
	int i ;
	p = (Node *)malloc(sizeof(Node));
	head = p;
	p->data = 1;
	
	for(i = 2;i<=n;i++)//再创建剩余节点
	{
		q = (Node *)malloc(sizeof(Node));
		q->data = i;
		p->pnext = q;
		p = q;
	}
	p->pnext = head;//最后一个节点指向头部,形成循环链表
	return head;
}

void link_process(Node *head,int k,int m)//从编号为k(1<=k<=n)的人开始报数,数到m的那个人出列;
{
	int i;
	Node *p = head,*tmp1;
	while(p->data != k)//p指向的节点data为k
		p = p->pnext;
	
	while(p->pnext != p)
	{
		for(i=1;i<m;i++)
		{
			tmp1 = p;
			p = p->pnext;
		}
		printf("%d ",p->data);
		tmp1->pnext= p->pnext;
		free(p);
		p = tmp1->pnext;
	}
	printf("%d ",p->data);
	free(p);
}

int main()
{
	
	Node *head = link_create(15);
	link_process(head,1,2);
	system("pause");
	return 0;
}


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值