约瑟夫环问题——顺推法与递归法

题目

0,1,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

来源:力扣(LeetCode)

前不久在LeetCode上刷到一个比较有意思的题,一开始准备用数组暴力破解,在思索良久之后放弃了自己天真的想法。网上找了许多方法之后才弄懂了其中的原理,总结为以下两种方法:顺推法与递归法。

这两种方法看似只是相反的过程,但其中的原理确实不太相同。不过不管黑猫白猫,逮到耗子的就是好猫。

让我们来分析一下题意

如下表,在这几个数之中每次找到第三个数字然后去掉,直到最后一个数字留下来为止。(注意:每次数三个数字不是又从数字0开始,而是从上一次去掉的数字后面开始)
题意
可以很直观的看出第三个数字一个一个被去掉,直到最后剩下两个角逐,花落谁家?答案当然是3。

顺推法:顾名思义,其实就是类似于暴力破解,顺着思路一步一步做下去。

我们可以借助ArrayList动态数组实现数字的增加与删除,在此之前我们所知道的参数也就是n个数字,以及每次要去掉的第m个数字,之后进行的就是初始化数组,然后再一个一个减去就好了。

这里的问题在于怎么求出你要去掉的数字,第一次我们减的是2号(这里是数字2),第二次就要减4号(因为不够减呀,轮回去就是数字1),轮回去这里就是一个重要的结点,试分析一手。

数组ArrayList

第一次我们要去掉的数字下标可以用m-1表示(例子里是2号),
第二次我们要去掉的数字下标打算用(m-1)+m-1表示(例子里应该是4号),结果和我们肉眼看出来的不符,因为已经超过数组长度了,怎么解决呢?

此时我们引入一个求余算法,可以回到数组开头。(业内术语都叫求模
我查了查对于正数,这两个概念似乎没什么不同,我们暂且这样俗气的叫。
除法

余数嘛,
如果被除数小于除数,那么商为0(很简单的样子),直接把被除数拿到余数里面就好了;
如果被除数大于除数,那么商为一个整数,余数也就可以看作是从零开始数的标号。

这里就很符合我们开始的思维逻辑了吧。

因此,每次只需要对要求数组长度求余就好了,
这样我们的第一次要去掉的数字的标号可以求出来(3-1)%5=2,
那第二次呢,(2+3-1)%4=0,
第三次,(0+3-1)%3=2,
第四次,(2+3-1)%2=0,
第五次不用说,数组只剩一个元素,下标确实为0。

java代码如下:

public int Circle1( int n,int m ) {
  	ArrayList<Integer> list = new ArrayList<>();
  	for( int i = 0;i < n; i++ ) 	//数组初始化
  		list.add(i);
  	int x = 0;    			//记录每次去掉数字的下标
  	while( n != 1 ) {
    		x = ( x + m - 1 )%n;
    		list.remove(x);
    		n--;   			//数组长度减一
  	}
  	int result = list.get(0);	//最后赢家只有一个,所以下标为0
  	return result;
 }

既然我们顺着推已经思路明确,虽然有点复杂但终究还是求出来了,那么有简单一点的思路吗?请继续往下看。

递归法:可以用数学方法推导出来,重要的是我们知道一个隐含的关键条件:最后一个数字其下标为0

所谓递归,
就是我们去掉n个数字时的标号m-1,
接下来我们要继续求n-1个数字的情况,
然后是n-2个数字的情况…
以此类推,
直到剩下1个数字。

如图:

递归

假设我们已知一个函数 f(1)=0,意思就是最后一次仅剩的数字其标号为0;
那么上一个轮回(倒数第二次)它的标号呢? f(2)
没错,直到最开始n个数字,那么最终赢家的标号为 f(n),答案不就出来了吗 。

那么问题来了,我们怎么求出 f(2) 甚至是 f(n)呢?

观察下图(蓝色为标号,绿色为轮回去补上的数字,红色为我们要求的数字)
下图
最后一次数字3的标号为 f(1)=0;
两个数时,细心的你会发现,上一次标号f(1)往后数m个数字会是f(2),
这里主要是去掉的数字后其后面的数字要进入下一步的话,就需要标号全部往前移动m
只不过标号需要稍稍变换,即我们所说的求模(因为超出数组范围了),那么则有f(2)=(f(1)+m)%2,答案是1没有问题;
三个数时,f(3)=(f(2)+m)%3,答案是1没有问题,后面步骤相同。
那么可以得到一个函数:
函数
java代码如下:

 public int Circle2( int n,int m ) {
	  if( n == 1 )		//最后一个数字标号为0
	   	return 0;
	  else
	   	return ( Circle2( n-1 , m ) + m ) % n;
}

综上,便是两种方法的具体过程,
其中比较难懂的还是求模的道理,那个是说要在一个循环的表之中找数,如果超出范围,则可重新从0开始进行计算;
还有就是对递归方法的掌握,观察法真的不太容易看出来,目前只能出此下策解释,如果有更好的理解方法大家可以与我讨论。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章