还是Josephus问题,昨天用简单粗暴的链表方法得以解决,其时间复杂度为O(nm)。虽然能得以复现每个数字出列的顺序,但是在n和m较大的时候,能明显感觉到电脑正在艰难计算……
昨晚加上今早一直在琢磨:要想让电脑轻松一些,干活快一点,人脑就不能一点不动。如果仅仅需要Josephus问题的最终结果,而不关心中间的出列过程的话,是否可以不用链表这么笨重的东西?
仍然假设m<n。这回规则微调一下:编号从0开始,到n-1结束;报数也从0开始,那么报到m-1号的同学将要出列。
第1次报数,m-1号同学不幸出列,接下来要从m号开始从0报数,此时剩下n-1人。
其实,可以发现,如果这n-1个人当中的某个人足够幸运,留到了最后,那么他也是原先n个人当中的幸运儿,有点类似于"金子到哪都会发光"的道理。因此,规模为n的问题转化为了规模为n-1的问题。但需要像下面的映射一样重新为他们编号,左边是n个人时候的编号,右边是n-1个人时候的新编号。
m --> 0
m+1 --> 1
……
n-1 --> n-m-1
0 --> n-m
……
m-2 --> n-2
发现规律了,如果把规模为n时的编号叫"老编号",规模为n-1的编号叫"新编号"的话,那么
老编号 = (新编号 + m )% n
所以,如果找到幸运儿的新编号,那么按上面的公式就可以推出幸运儿的原始编号。
以此类推,接下来就要把n-1规模转化为n-2规模、n-3规模……直到只剩1个人,这时他的编号肯定是0,逆推回去就可以了。
这样的话,实际上只需要写一个循环就能解决问题了。最后不能忘记实际编号是从1开始,要记得+1。
输入输出部分与上一篇相同,就不贴了。核心部分的代码如下:
//返回Josephus问题中最后留下的数字
int SolveJosephus(unsigned int n, unsigned int m)
{
unsigned int k, fk;
fk = 0;
k = 2;
while(k <= n)
{
fk = (fk + m) % k;
k++;
}
return fk+1; //实际的编号是从1开始的,而不是0
}
由于只有一个循环,因此该方法的时间复杂度仅为O(n),也就是说,与报数间隔m无关了。空间复杂度上来看,起码也节省了n*sizeof(Node)这么多内存。不过缺点呢,就是只能得到最后留下来的幸运数字,不能看到出列的过程。