zoukankan      html  css  js  c++  java
  • 约瑟夫环问题

    约瑟夫环问题(基本)

    n个人围成圈,依次编号为1,2,..,n,现在从1号开始依次报数,当报到m时,报m的人退出,下一个人重新从1报起,循环下去,问最后剩下那个人的编号是多少?

    递归法

    参见百度百科:Josephus(约瑟夫)问题的数学方法

    递推式:

                fc1

    将这些人的编号用对总人数取模所得余数代替(即原编号减一),其中 Fn 表示n个人的环,最后剩下的那个人的编号。

    由这个递推式不难写出相应代码:

    /* 约瑟夫环问题 */
    # include <stdio.h>
    int main()
    {
        int m, n, i, s;
     
        while (~scanf("%d%d", &m, &n))
        {
            s = 0;                               // F1 = 0;
            for (i = 2; i <= n; ++i)
                s = (s + m) % i;          // 注意这个地方是 %i,因为递推的过程是由1->2->3->...->n,人数n逐渐增大的过程,所以f[i]表示i个人的问题
            printf("%d
    ", s+1);       // 原问题的编号是从1开始的
        }
     
        return 0;
    }
    //上述代码的复杂度为O(n)。

    模拟法

    采用循环链表进行模拟题目描述的过程,退出通过删除节点表示;
    使用链表模拟时,维护一个充当报数角色的变量 i ,每当 i 等于 m 时,重置 i 为 1;
    循环终止的条件是只剩余一个节点(p->next = p),或者已经删除了 n-1 个节点;
    由于每次删除一个节点需要报够 m 次数(遍历 m 个节点),一共要删除 n-1 个数,所以链表模拟的复杂度O(m*n)。

    /* 约瑟夫环问题——链表模拟*/
    # include <stdio.h>
    # include <stdlib.h>
     
    typedef struct node
    {
        int id;
        struct node* next;
    } node;
     
    int main()
    {
        int m, n, i;
        node *p, *q, *head;
     
        while (~scanf("%d%d", &m, &n))
        {
            // 创建含 n 个节点的循环链表,初始化编号
            head = (node *)malloc(sizeof(node));
            head->id = 1;
            q = head;
            for (i = 2; i <= n; ++i)
            {
                p = (node *)malloc(sizeof(node));
                p->id = i;
                q->next = p;
                q = p;
            }
            q->next = head;
     
            // 模拟退出过程
             i = 1;
            p = head;
            while (p->next != p)
            {
                q = p;
                p = p->next;
                ++i;
                if (i == m)         //delete(p); i = 1;   这个算法没有考虑到m可能等于1的情况下面的算法每次寻找待删除节点的前驱,则解决了m为1情况。
                {
                    q->next = p->next;
                    free(p);
                    p = q->next;
                    i = 1;
                }
            }
     
            // 打印剩余节点的编号
            printf("%d
    ", p->id);
           free(p);
        }
     
        return 0;
    }

    扩展
    当最先报数的那个人编号不是 1 时,对于递归法,需要对结果进行调整,对于链表只需要将循环起点的 head 改成相应编号的指针;
    当报数前去掉一个编号为 k 的人,并从 k+1 开始报数时,可以认为从某个人报起,到编号为 k 时,刚好为 m ,因此去掉了这个人,这样对应上述递归方法的结果为:(s+1) + (m-k),并将这个结果的范围调整到 [1, n] 内。

    从0开始编号的约瑟夫问题(每次寻找待删除节点的前驱)

    #include<cstdio>
    /*
    	N个候选人围成一个圈,依次编号为0..N-1。然后随机抽选一个数K,并0号候选人开始按从1到K的顺序依次报数,
    	N-1号候选人报数之后,又再次从0开始。当有人报到K时,这个人被淘汰,从圈里出去。下一个人从1开始重新报数。
    	也就是说每报K个数字,都会淘汰一人。这样经过N-1轮报数之后,圈内就只剩下1个人了,这个人就作为新的班长。
    */
    struct Node
    {
    	int sno;
    	Node *next;
    };
    
    int N,K;
    
    int main()
    {
    	int T;
    	scanf("%d",&T);
    	while(T>0)
    	{
    		--T;
    		scanf("%d%d",&N,&K);
    		Node *head=NULL;
    		Node *List=new Node;
    		List->sno=0,List->next=NULL;
    		head=List;
    		for(int i=1;i<N-1;++i)
    		{
    			Node *p=new Node;
    			p->sno=i,p->next=NULL;
    			List->next=p;
    			List=List->next;
    		}
    		Node *p=new Node;         // 建立循环链表
    		p->sno=N-1,p->next=head;
    		List->next=p;
    		List=List->next;          // List指向循环链表的末尾
    		int cnt=1;
    	/*	while(cnt<=N)
    		{
    			printf("%d
    ",List->sno); cnt++ ; List=List->next;
    		}
    	*/
    		while(cnt<N)
    		{
    			for(int t=1;t<K;t++)   // 始终保持 List 指向待删除节点的前一个位置
    			{
    				List=List->next;
    			}
    			Node *cur=List->next;   // cur 是报数到 K 要删除的节点
    			Node *tmp=cur->next;
    			List->next=tmp;         //将List 与被删除节点 cur 后段部分连接起来
    			delete cur;
    			cnt++;
    		}
    		printf("%d
    ",List->sno);
    	}
    	return 0;
    }

    两类递推思想解决约瑟夫环的问题

    小Hi和小Ho的班级正在进行班长的选举,他们决定通过一种特殊的方式来选择班长。

    首先N个候选人围成一个圈,依次编号为0..N-1。然后随机抽选一个数K,并0号候选人开始按从1到K的顺序依次报数,N-1号候选人报数之后,又再次从0开始。当有人报到K时,这个人被淘汰,从圈里出去。下一个人从1开始重新报数。

    也就是说每报K个数字,都会淘汰一人。这样经过N-1轮报数之后,圈内就只剩下1个人了,这个人就作为新的班长。

    举个例子,假如有5个候选人,K=3,最后当选的人编号是3:

    小Hi:这个问题其实还蛮有名的,它被称为约瑟夫的问题。

    最直观的解法是用循环链表模拟报数、淘汰的过程,复杂度是O(NM)。

    今天我们来学习两种更高效的算法,一种是递推,另一种也是递推。第一种递推的公式为:

    令f[n]表示当有n个候选人时,最后当选者的编号。
    f[1] = 0
    f[n] = (f[n - 1] + K) mod n	

    接下来我们用数学归纳法来证明这个递推公式的正确性:

    (1) f[1] = 0

    显然当只有1个候选人时,该候选人就是当选者,并且他的编号为0。

    (2) f[n] = (f[n - 1] + K) mod n

    假设我们已经求解出了f[n - 1],并且保证f[n - 1]的值是正确的。

    现在先将n个人按照编号进行排序:

    0 1 2 3 ... n-1

    那么第一次被淘汰的人编号一定是K-1(假设K < n,若K > n则为(K-1) mod n)。将被选中的人标记为"#":

    0 1 2 3 ... K-2 # K K+1 K+2 ... n-1

    第二轮报数时,起点为K这个候选人。并且只剩下n-1个选手。假如此时把k+1看作0',k+2看作1'...

    则对应有:

     0   1 2 3 ... K-2  # K  K+1 K+2 ... n-1
     n-K'          n-2'   0'  1'  2' ... n-K-1'

    此时在0',1',...,n-2'上再进行一次K报数的选择。而f[n-1]的值已经求得,因此我们可以直接求得当选者的编号s'。

    但是,该编号s'是在n-1个候选人报数时的编号,并不等于n个人时的编号,所以我们还需要将s'转换为对应的s。

    通过观察,s和s'编号相对偏移了K,又因为是在环中,因此得到s = (s'+K) mod n

    f[n] = (f[n-1] + k) mod n。


    至此递推公式的两个式子我们均证明了其正确性,则对于任意给定的n,我们可以使用该递推式求得f[n],写成伪代码为:

    Josephus(N, K):
    	f[1] = 0
    	For i = 2 .. N
    		f[i] = (f[i - 1] + K) mod i
    	End For
    	Return f[N]

    同时由于计算f[i]时,只会用到f[i-1],因此我们还可以将f[]的空间节约,改进后的代码为:

    Josephus(N, K):
    	ret = 0
    	For i = 2 .. N
    		ret = (ret + K) mod i
    	End For
    	Return ret

    该算法的时间复杂度为O(N),空间复杂度为O(1)。对于N不是很大的数据来说,可以解决。

    小Ho:要是N特别大呢?

    小Hi:那么我们就可以用第二种递推,解决的思路仍然和上面相同,而区别在于我们每次减少的N的规模不再是1。

    同样用一个例子来说明,初始N=10,K=4:

    初始序列:

    0 1 2 3 4 5 6 7 8 9

    当7号进行过报数之后:

    0 1 2 - 4 5 6 - 8 9

    在这里一轮报数当中,有两名候选人退出了。而对于任意一个N,K来说,退出的候选人数量为N/K("/"运算表示整除,即带余除法取商)

    由于此时起点为8,则等价于:

    2 3 4 - 5 6 7 - 0 1

    因此我们仍然可以从f[8]的结果来推导出f[10]的结果。

    但需要注意的是,此时f[10]的结果并不一定直接等于(f[8] + 8) mod 10。

    若f[8]=2,对于原来的序列来说对应了0,(2+8) mod 10 = 0,是对应的;若f[8]=6,则有(6+8) mod 10 = 4,然而实际

    上应该对应的编号为5。

    这是因为在序列(2 3 4 - 5 6 7 - 0 1)中,数字并不是连续的。

    因此我们需要根据f[8]的值进行分类讨论。假设f[8]=s,则根据s和N mod K的大小关系有两种情况:

    1) s < N mod K : s' = s - N mod K + N
    2) s ≥ N mod K : s' = s - N mod K + (s - N mod K) / (K - 1)

    此外还有一个问题,由于我们不断的在减小N的规模,最后一定会将N减少到小于K,此时N/K=0。

    因此当N小于K时,就只能采用第一种递推的算法来计算了。

    最后优化方法的伪代码为:

    Josephus(N, K):
    	If (N == 1) Then
    		Return 0
    	End If
    	If (N < K) Then
    		ret = 0
    		For i = 2 .. N
    			ret = (ret + K) mod i
    		End For
    		Return ret
    	End If 
    	ret = Josephus(N - N / K, K);
    	If (ret < N mod K) Then 
    		ret = ret - N mod K + N
    	Else
    		ret = ret - N mod K + (ret - N mod K) / (K - 1)
    	End If
    	Return ret

    改进后的算法可以很快将N的规模减小到K,对于K不是很大的问题能够快速求解。

    #include<iostream>
    
    using namespace std;
    /*
    	用一个例子来说明,初始N=10,K=4:
    	初始序列:
    	0 1 2 3 4 5 6 7 8 9
    	当7号进行过报数之后:
    	0 1 2 - 4 5 6 - 8 9
        在一轮报数当中,而对于任意一个N,K来说,退出的候选人数量为N/K("/"运算表示整除,即带余除法取商)
        因此我们仍然可以从f[8]的结果来推导出f[10]的结果。
    	但需要注意的是,此时f[10]的结果并不一定直接等于(f[8] + 8) mod 10。
    	若f[8]=2,对于原来的序列来说对应了0,(2+8) mod 10 = 0,是对应的;若f[8]=6,则有(6+8) mod 10 = 4,
    	然而实际上应该对应的编号为5。
    	这是因为在序列(2 3 4 - 5 6 7 - 0 1)中,数字并不是连续的。
    	因此我们需要根据f[8]的值进行分类讨论。假设f[8]=s,则根据s和N mod K的大小关系有两种情况:
    	1) s < N mod K : s' = s - N mod K + N
    	2) s ≥ N mod K : s' = s - N mod K + (s - N mod K) / (K - 1)
    	此外还有一个问题,由于我们不断的在减小N的规模,最后一定会将N减少到小于K,此时N/K=0。
    	因此当N小于K时,就只能采用第一种递推的算法来计算了。
    */
    
    //改进后的算法可以很快将N的规模减小到K,对于K不是很大的问题能够快速求解。
    int Josephu(int n,int k)
    {
    	if(n<=1)
    	{
    		return 0;
    	}
    	if(n<k)
    	{
    		int ret=0;
    		for(int i=2;i<=n;++i)
    		{
    			// 注意这个地方是 %i,因为递推的过程是由1->2->3->...->n,人数n逐渐增大的过程,f[i]表示i个人的问题
    			ret=(ret+k)%i;
    		}
    		return ret;
    	}
    	int ret=Josephu(n-n/k,k);
    	if(ret<(n%k)) ret=ret-n%k+n;
    	else          ret=ret-n%k+(ret-n%k)/(k-1);
    	return ret;
    }
    
    int main()
    {
    	int T;
    	cin>>T;
    	while(T>0)
    	{
    		T--;
    		int N,K;
    		cin>>N>>K;
    		cout<<Josephu(N,K)<<endl;
    	}
    	return 0;
    }

  • 相关阅读:
    MySql cmd下的学习笔记 —— 引擎和事务(engine,transaction)
    MySql cmd下的学习笔记 —— 有关视图的操作(algorithm)
    MySql cmd下的学习笔记 —— 有关视图的操作(建立表)
    MySql cmd下的学习笔记 —— 有关常用函数的介绍(数学函数,聚合函数等等)
    MySql cmd下的学习笔记 —— 有关多表查询的操作(多表查询练习题及union操作)
    MySql 在cmd下的学习笔记 —— 有关多表查询的操作(内连接,外连接,交叉连接)
    MySql cmd下的学习笔记 —— 有关子查询的操作(where型,from型,exists型子查询)
    MySql cmd下的学习笔记 —— 有关select的操作(order by,limit)
    剑指Offer--第21题 调整数组顺序使奇数位于偶数前面;
    剑指Offer--和为s的连续正数序列
  • 原文地址:https://www.cnblogs.com/tham/p/6827153.html
Copyright © 2011-2022 走看看