约瑟夫环问题

题目:0,1,2,....n-1这个数字排成一个圆圈,从数字0开始每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。
例如,0,1,2,3,4这5个数字组成一个圆圈,从数字0开始每次删除第三个数字,则删除的前四个数字依次是2,0,4,1因此最后剩下的数字是3.(淡黄色表示将要删除的结点,粉红色表示下一次开始的起点)
 
 
这里我们可以通过std::list来模拟一个环形链表,由于std::list本身并不是一个环形结构,因此每当迭代器(Iterator)扫描到链表末尾的时候,我们要记得把迭代器移到链表的头部,这样就相当于按照顺序在一个圆圈里遍历了.这种思路的代码如下:
int LastRemaining(unsigned int n,unsigned int m)
{
 if(n<1||m<1)
 return -1;
unsigned int i=0;
list<int> numbers;
for(i=0;i<n;++i)
numbers.push_pack(i);
list<int>::iterator current=numbers.begin();
while(numbers.size()>1)
{
 for(int i=0;i<m;++i)
{
 current++;
 if(current==numbers.end())//找到"淡黄色"结点
 current=numbers.begin();
}
list<int>::iterator next = ++current;//next即为"粉红色"结点
if(next==numbers.end())
next=numbers.begin();
--current;
numbers.erase(current);//清除对应"淡黄色"结点
current = next;//开始下一轮遍历
}
return *(current);//最后一个结点
}
 
 
创新的解法:
首先我们定义一个关于n和m的方程f(n,m),表示每次在n个数字0,1.......,n-1中每次删除第m个数字最后剩下的数字。
在这n个数字中,第一个被删除的数字是(m-1)%n.为了简单起见,我们把(m-1)%n记为k,那么删除K之后剩下的n-1个数字为0,1......,k-1,k+1,.......n-1,并且下一次删除从数字k+1开始计数。相当于在剩下的序列中,K+1排在最前面,从而形成k+1.....,n-1,0,1.....,k-1.该序列最后剩下的数字也应该是关于n和m的函数。由于这个序列的规律和前面最初的序列不一样(最初的序列是从0开始的连续序列),因此这个序列的规律和前面最初的序列不一样(最初的序列是从0开始的连续序列),因此该函数不同于前面的函数,记为f'(n-1,m).最初序列最后剩下的数字f(n,m)一定是删除一个数字之后的序列最后剩下的数字,即f(n,m)=f'(n-1,m).
接下来我们把剩下的这n-1个数字的序列k+1,......,n-1,0,1,........,k-1做一个映射,映射的结果是形成一个从0到n-2的序列:
                 k+1 -> 0
                 k+2 -> 1
                 .....
                 n-1 -> n-k-2
                 0    -> n-k-1
                 1    -> n-k
                 .....
                 k-1 -> n-2
我们把映射定义为p,则p(x)=(x-k-1)%n.它表示如果映射前的数字是x,那么映射后的数字是(x-k-1)%n.该映射的逆映射是p''(x)=(x+k+1)%n.
由于映射之后的序列和最初的序列具有相同的形式,即都是从0开始的连续序列,因此依然可以用函数f来表示,记为f(n-1,m).根据我们的映射规则,映射之前的序列中最后剩下的数字f'(n-1,m).根据我们的映射规则,映射之前的序列中最后剩下的数字f'(n-1,m)=p''[f(n-1,m)]=[f(n-1,m)+k+1]%n,把k=(m-1)%n代入得到f(n,m)=f'(n-1,m)=[f(n-1,m)+m]%n.
经过上面复杂的分析,我们终于找到了一个递归公式。要得到n个数字的序列中最后剩下的数字,只需要得到n-1个数字的序列中最后剩下的数字,并以此类推。当n=1时,也就是序列中开始只有一个数字0,那么很显然最后剩下的数字就是0.我们把这种关系表示为:
              ->0    n=1
f(n,m)=
             ->[f(n-1,m)+m]%n  n>1
 
下面是一段基于循环实现的代码:
int LastRemaining(unsigned int n,unsigned int m)
{
 if(n<1||m<1)
  return -1;
  int last=0;
  for(int i=2;i<=n;i++)
  last=(last+m)%i;
  return last;
}
可以看出,这种思路的分析过程尽管非常复杂,但写出的代码却非常简洁,这就是数学的魅力.最重要的是,这种算法的时间复杂度是O(n),空间复杂度是O(1),因此无论在时间效率还是空间效率都优于第一种方法.
 

posted on 2016-04-30 11:09  wxdjss  阅读(169)  评论(0编辑  收藏  举报

导航