• 约瑟夫环问题


    约瑟夫环是一个数学的应用问题:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为k的人開始报数,数到m的那个人出列;他的下一个人又从1開始报数,数到m的那个人又出列;依此规律反复下去,直到圆桌周围的人所有出列。

    C代码例如以下(joseph.cpp):

    #include<stdio.h>
    #include<stdlib.h>
    #include<malloc.h>
    
    typedef struct _node
    {
    	struct _node* next;
    	int number;
    }node,*linklist;
    
    linklist create(int n);
    void joseph(linklist head, int k, int m);
    
    int main()
    {
    	linklist head;
    	int m, n, k;
    	printf("please input n:");
    	scanf("%d",&n);
    	printf("please input m:");
    	scanf("%d",&m);
    	printf("please input k:");
    	scanf("%d",&k);
    	head = create(n);
    	printf("the sequences of leaving the list are:");
    	joseph(head,k,m);
    	return 0;
    }
    linklist create(int n)
    {
    	linklist head = (linklist)malloc(sizeof(node));
    	node *tail;
    	int i;
    	head->next = head;
    	head->number = 1;
    	tail = head;
    	for(i=2;i<=n;i++)
    	{
    		node *p = (node*)malloc(sizeof(node));
    		p->number = i;
    		p->next = tail->next;
    		tail->next = p;
    		tail = p;
    	}
    	return head;
    }
    
    void joseph(linklist head, int k, int m)
    {
    	int j;
    	node *p;
    	node *q;
    	if(m == 1 && k == 1)
    	{
    		p = head;
    		while(p->next != head)
    		{
    			printf("%d ",p->number);
    			q = p->next;
    			free(p);
    			p = q;
    		}
    		printf("%d
    ",p->number);
    	}
    	else if(m == 1 && k != 1)
    	{
    		p = head;
    		for(j=1; j<k-1; j++)
    			p = p->next;
    		while(head->next != head)
    		{
    			q = p->next;
    			p->next = q->next;
    			printf("%d ",q->number);
    			if(q == head)
    				head = q->next;
    			free(q);
    		}
    		printf("%d
    ",head->number);
    	}
    	else
    	{
    		p = head;
    		for(j=1; j<k; j++)
    			p = p->next;
    		while(head->next != head)
    		{
    			for(j=1; j<m-1; j++)
    				p = p->next;
    			q = p->next;
    			p->next = q->next;
    			printf("%d ",q->number);
    			if(q == head)
    				head = q->next;
    			free(q);
    			p = p->next;
    		}
    		printf("%d
    ",head->number);
    	}
    }

    须要特别注意m和k的值是否等于1。


    几组測试用例结果例如以下:

    1、m != 1,k != 1



    2、m != 1,k == 1


    3、m == 1,k != 1


    4、m == 1,k == 1



    上面程序中,之所以要分别讨论m==1和k==1的情况,是由于在单向循环链表中要想删除某一个结点,必须先找到该结点的前驱结点,然后更改相关指针域,使循环链表不断链,而m=1,k=1时,要想使循环链表不断链,必须先找到链表的尾结点,所以要分不同情况讨论。

    鉴于此,想到使用双向循环链表,要想删除某一个结点,不须要找前驱结点,即使是删除第一个结点,也不须要找尾结点。

    C代码例如以下所看到的(joseph2.cpp),能够看到代码逻辑简洁了不少:

    #include<stdio.h>
    #include<stdlib.h>
    #include<malloc.h>
    
    typedef struct _node
    {
    	struct _node* prev;
    	struct _node* next;
    	int number;
    }node,*linklist;
    
    linklist create(int n);
    void joseph(linklist head, int k, int m);
    
    int main()
    {
    	linklist head;
    	int m, n, k;
    	printf("please input n:");
    	scanf("%d",&n);
    	printf("please input m:");
    	scanf("%d",&m);
    	printf("please input k:");
    	scanf("%d",&k);
    	head = create(n);
    	printf("the sequences of leaving the list are:");
    	joseph(head,k,m);
    	return 0;
    }
    linklist create(int n)
    {
    	linklist head = (linklist)malloc(sizeof(node));
    	node *tail;
    	int i;
    	head->next = head;
    	head->prev = head;
    	head->number = 1;
    	tail = head;
    	for(i=2;i<=n;i++)
    	{
    		node *p = (node*)malloc(sizeof(node));
    		p->number = i;
    		p->next = tail->next;
    		p->prev = tail;
    		tail->next = p;
    		tail = p;
    		head->prev = tail;
    	}
    	return head;
    }
    
    void joseph(linklist head, int k, int m)
    {
    	int i;
    	node *p;
    	node *q;
    	p = head;
    	for(i=1; i<k; i++)//获取開始计数的结点
    			p = p->next;
    	while(head->next != head)
    	{
    		for(i=1; i<m; i++)
    			p = p->next;//获取每轮计数的第m个结点,即待删除结点
    		q = p->next;
    		q->prev = p->prev;
    		p->prev->next = q;
    		printf("%d ",p->number);
    		if(p == head)//假设删除的是第一个结点,则须要又一次设置head指针
    			head = q;
    		free(p);
    		p = q;//删除一个结点之后,从该结点的下一个结点又一次開始计数
    	}
    	printf("%d
    ",head->number);
    }
    

    能够得到与第一种代码同样的结果:






    假设能使用C++标准库中的list来模拟循环链表,那么逻辑更清晰,代码更简洁。

    C++代码例如以下(joseph3.cpp):

    #include<iostream>
    #include<list>
    using namespace std;
    
    void joseph(int n, int m, int k);
    
    int main()
    {
    	int n,m,k;
    	cout<<"please input n:";
    	cin>>n;
    	cout<<"please input m:";
    	cin>>m;
    	cout<<"please inpur k:";
    	cin>>k;
    	cout<<"the sequences of leaving the list are:";
    	joseph(n,m,k);
    	return 0;
    }
    
    void joseph(int n, int m, int k)
    {
    	list<int> numbers;
    	int i,j;
    	for(i=1; i<=n; i++)
    		numbers.push_back(i);
    	list<int>::iterator current = numbers.begin();
    	list<int>::iterator next;
    	for(i=1; i<k; i++)
    	{
    		++current;
    		if(current == numbers.end())
    			current = numbers.begin();
    	}
    	while(numbers.size()>1)
    	{
    		for(i=1; i<m; i++)
    		{
    			++current;
    			if(current == numbers.end())
    				current = numbers.begin();
    			/*
    			因为list本身并非一个循环链表,所以每当到达
    			最后一个元素的下一个位置时,须要改动迭代器指向第一个元素
    			*/
    		}
    		next = ++current;
    		if(next == numbers.end())
    			next = numbers.begin();
    		--current;
    		cout<<*current<<" ";
    		numbers.erase(current);
    		current = next;
    	}
    	cout<<*current<<endl;
    }

    能够得到与上面两种代码同样的结果。


    上面编写的解约瑟夫环的程序模拟了整个报数的过程,程序执行时间还能够接受,非常快就能够出计算结果。但是,当參与的总人数n及出列值m非常大时,其运算速度就慢下来。比如,当n的值有上百万,m的值为几万时,到最后尽管仅仅剩2个人,也须要循环几万次(m的数量)才干确定2个人中下一个出列的序号。显然,在这个程序的执行过程中,非常多步骤都是进行反复没用的循环。那么,能不能设计出更有效率的程序呢?
    在约瑟夫环中,假设仅仅是须要求出最后的一个出列者最初的序号,就没有必要去模拟整个报数的过程。因此,为了追求效率,能够考虑从数学角度进行推算,找出规律然后再编敲代码就可以。
    为了讨论方便,先依据原意将问题用数学语言进行描写叙述。
    问题:将编号为1~n这n个人进行圆形排列,按顺时针从1開始报数,报到m的人退出圆形队列,剩下的人继续从1開始报数,不断反复。求最后出列者最初在圆形队列中的编号。
    以下首先列出0~n这n个人的原始编号例如以下:
    1、2、3、……、m-2、m-1、m、m+1、m+2、……、n-2、n-1、n
    第一个出列人的编号一定是m%n。比如,在41个人中,若报到3的人出列,则第一个出列人的编号一定是3%41=3,1人出列后的列表例如以下:
    1、2、3、……、m-2、m-1、m+1、m+2、……、n-2、n-1、n
    依据规则,当有人出列之后,下一个位置的人又从1開始报数,则以上列表可调整为下面形式(即以m+1位置開始,n之后再接上0、1、2……,形成环状):
    m+1、m+2、……、n-2、n-1、n、1、2、3、……、m-2、m-1
    按上面排列的顺序又一次进行编号,可得到以下的相应关系:
    1、       2、        3、   ……、n-2、n-1
    m+1、m+2、m+3、……、m-2、m-1
    即,将出列1人后的数据又一次组织成了1~n-1的列表,继续求n–1个參与人员,按报数到m即出列,求解最后一个出列者最初在圆形队列中的编号。
    通过一次处理,将问题的规模缩小了。即,对于n个人报数的问题,能够分解为先求解(n–1)个人报数的子问题;而对于(n–1)个人报数的子问题,又可分解为先求[(n–1)–1]人个报数的子问题,……。
    问题中的规模最小时是什么情况?就是仅仅有1个人时(n=1),报数到m的人出列,这时最后出列的是谁?当然仅仅有编号为1这个人。因此,可设有下面函数:
    F(1)= 1
    那么,当n=2,报数到m的人出列,最后出列的人是谁?应该是仅仅有一个人报数时得到的最后出列的序号加上m+1(由于已经有1个人出了队列,求F(n)时由于已经有n-1个人出了队列,所以须要加上n-1),可用公式表示为下面形式:
    F(2)= F(1)+ m + 1
    通过上面的算式计算时,F(2)的结果可能会超过n值(人数的总数)。比如,设n=2,m=3(即2个人,报数到3时就出列),则按上式计算得到的值是:
    F(2)= F(1)+ 3 + 1 = 1 + 3 + 1 = 5
    一共仅仅有2人參与,编号为5的人显然没有。怎么办?由于是环状报数,因此当两个人报完数之后,又从编号为1的人開始接着报数。依据这个原理,就可以对求得的值与总人数n进行模运算,然后再加上1,由于不是从0開始计数的,即:
    F(2)= [F(1)+ m + 1] % n + 1 = [1 + 3 + 1]%2 + 1 = 2
    即,n=2,m=3(即有2个人,报数到3的人出列)时,循环报数最后一个出列的人的编号为2(编号从1開始)。
    依据上面的推导过程,能够非常easy推导出,当n=3时的公式:
    F(3)= [F(2)+ m + 2]%3 + 1
    同理,也能够推导出參与人数为N时,最后出列人员编号的公式:
    F(n)= [F(n-1)+ m + n - 1]%n + 1
    事实上,这就是一个递推公式,公式包括下面两个式子:
    F(1)= 1;                                                     n=1
    F(n)= [F(n-1)+ m + n - 1]%n + 1;     n>1 
    有了这个递推公式,再来设计程序就非常easy了。

    使用递归方式的代码例如以下(joseph4.cpp):

    #include<stdio.h>
    #include<stdlib.h>
    
    int joseph(int n, int m);
    
    int main()
    {
    	int n,m;
    	printf("please input n:");
    	scanf("%d",&n);
    	printf("please input m:");
    	scanf("%d",&m);
    	printf("the last number is: %d
    ", joseph(n,m));
    	return 0;
    }
    
    int joseph(int n, int m)
    {
    	if(n == 1)
    		return 1;
    	else
    		return (joseph(n-1,m)+m+n-1)%n + 1;
    }
    
    几组測试用例结果例如以下:


    使用递归函数会占用计算机较多的内存,当递归层次太深时可能导致程序不能运行,因此,也能够将程序直接编写为下面的迭代形式。
    joseph5.cpp:

    #include<stdio.h>
    #include<stdlib.h>
    
    int joseph(int n, int m);
    
    int main()
    {
    	int n,m;
    	printf("please input n:");
    	scanf("%d",&n);
    	printf("please input m:");
    	scanf("%d",&m);
    	printf("the last number is: %d
    ", joseph(n,m));
    	return 0;
    }
    
    int joseph(int n, int m)
    {
    	int last = 1;//相当于F(1)
    	int i;
    	for(i=2; i<=n; i++)//一步一步求F(2)到F(n)
    		last = (last + m + i - 1)%i + 1;
    	return last;
    }
    也能够得到与上面同样的结果。


  • 相关阅读:
    Go语言版本的helloworld
    编译Elasticsearch源码
    Intellij IDEA将java源码打成jar包
    搭建Elasticsearch集群常见问题
    棣小天儿的第一个python程序
    Json反序列化Map的key不能是Object
    mac本配置python环境
    Timestamp解析0000-00-00 00:00:00报格式错误
    Spring-Mybatis配置多数据源
    mysql新建数据库时的collation选择(转)
  • 原文地址:https://www.cnblogs.com/bhlsheji/p/4233157.html
Copyright © 2020-2023  润新知