约瑟夫问题
a)特殊情况:逢2杀1情况二进制代码左循环1位讨论
N个人逢k杀1的通项公式
J(1)=1
J(n)=(J(n-1)+k)mod(n)
逢2杀1,k=2,则有
J(1)=1
J(n)=(J(n-1)+2)mod(n)
至于递推公式从何而来,可参看附录文章。除了数组,链表等常规方法外,在逢2杀1情况下,有一种优雅的简洁方法,即n的二进制代码左循环1位即为结果,这在knuth的《具体数学》与levitin的《算法设计与分析基础》中都提到过。抽象的数学问题与2进制代码挂钩,类似的还有nim游戏。此处为什么这种方法可行?
二进制代码左循环一位的方法在k=2时是可行的,但不通用,即k>2就不行了
J(1)=1
J(2)=1
J(3)=3
J(4)=1
J(5)=3
J(6)=5
J(7)=7
J(8)=1
J(9)=3
......
用归纳法
J(1)=1=1(1的2进制代码左循环1位)
假设J(n-1)=(n-1的2进制代码左循环1位)成立,n>2
显然J(n-1)的取值范围为1到n-1,J(n)的取值范围为1到n
分两种情况:
1,J(n-1)=n-1
J(n-1)=(n-1的2进制代码左循环1位)=n-1
一个数的2进制代码左循环一位,值保持不变,可见n-1为2的幂-1形式,n-1的2进制代码各位为1形式。
数N的二进制代码位数为对n取2的log,向下取整,再加1。所以这种情况下n比n-1的二进制代码多1位。由通项公式
J(n)=(J(n-1)+2)mod(n)=(n+1)mod(n)=1
而此时n为2的幂次形式,即最高位为1,其余位为0,左循环1位操作后值为1,相等,所以成立。
2,J(n-1)!=n-1
此时n-1和n的二进制代码位数相同,J(n-1)+2<=n,有
J(n)=J(n-1)+2
J(n)=J(n的2进制代码左循环1位)
J(n-1)+2=J(n-1的2进制代码左循环1位)+10(2进制)
而其实这个操作的意义,就是将n-1的二进制代码取从右第2位向左遇到第一个0这段,取反,比如说n-1为11010111,左循环1位为10101111,那么对第4,5,6,7位取反,变成了10110001。
而其实n的2进制代码,左循环一位,也就是这个操作。N的2进制代码就是将n-1的2进制代码从右到左遇到第一个0这段,取反,还是上面这个数,n为11011000,左循环1位为10110001。也成立。也可以这样考虑,将n-1的二进制代码分成两部分,从右到左遇到第1个0这段为第1部分,剩下的第2部分,那么n-1与n左循环1位操作中,第2部分无影响,只是左移1位,最高位1到了最低位。对于n-1,左循环1位后+2,即加2进制代码10,等于第1部分左移1位后取反,对于n,n和n-1的第2部分相同,n的二进制代码第1部分为n-1的第1部分取反,左移1位。可见,n的2进制代码左循环1位,与n-1的2进制代码左循环1位+2,即2进制代码10,在位运算上是一个操作。
故而数学归纳法成立。
当然k>2时候,位运算操作便没有通用性了,此时最保险的方法还是按部就班的使用通项递推公式
a)特殊情况:逢2杀1情况二进制代码左循环1位讨论
N个人逢k杀1的通项公式
J(1)=1
J(n)=(J(n-1)+k)mod(n)
逢2杀1,k=2,则有
J(1)=1
J(n)=(J(n-1)+2)mod(n)
至于递推公式从何而来,可参看附录文章。除了数组,链表等常规方法外,在逢2杀1情况下,有一种优雅的简洁方法,即n的二进制代码左循环1位即为结果,这在knuth的《具体数学》与levitin的《算法设计与分析基础》中都提到过。抽象的数学问题与2进制代码挂钩,类似的还有nim游戏。此处为什么这种方法可行?
二进制代码左循环一位的方法在k=2时是可行的,但不通用,即k>2就不行了
J(1)=1
J(2)=1
J(3)=3
J(4)=1
J(5)=3
J(6)=5
J(7)=7
J(8)=1
J(9)=3
......
用归纳法
J(1)=1=1(1的2进制代码左循环1位)
假设J(n-1)=(n-1的2进制代码左循环1位)成立,n>2
显然J(n-1)的取值范围为1到n-1,J(n)的取值范围为1到n
分两种情况:
1,J(n-1)=n-1
J(n-1)=(n-1的2进制代码左循环1位)=n-1
一个数的2进制代码左循环一位,值保持不变,可见n-1为2的幂-1形式,n-1的2进制代码各位为1形式。
数N的二进制代码位数为对n取2的log,向下取整,再加1。所以这种情况下n比n-1的二进制代码多1位。由通项公式
J(n)=(J(n-1)+2)mod(n)=(n+1)mod(n)=1
而此时n为2的幂次形式,即最高位为1,其余位为0,左循环1位操作后值为1,相等,所以成立。
2,J(n-1)!=n-1
此时n-1和n的二进制代码位数相同,J(n-1)+2<=n,有
J(n)=J(n-1)+2
J(n)=J(n的2进制代码左循环1位)
J(n-1)+2=J(n-1的2进制代码左循环1位)+10(2进制)
而其实这个操作的意义,就是将n-1的二进制代码取从右第2位向左遇到第一个0这段,取反,比如说n-1为11010111,左循环1位为10101111,那么对第4,5,6,7位取反,变成了10110001。
而其实n的2进制代码,左循环一位,也就是这个操作。N的2进制代码就是将n-1的2进制代码从右到左遇到第一个0这段,取反,还是上面这个数,n为11011000,左循环1位为10110001。也成立。也可以这样考虑,将n-1的二进制代码分成两部分,从右到左遇到第1个0这段为第1部分,剩下的第2部分,那么n-1与n左循环1位操作中,第2部分无影响,只是左移1位,最高位1到了最低位。对于n-1,左循环1位后+2,即加2进制代码10,等于第1部分左移1位后取反,对于n,n和n-1的第2部分相同,n的二进制代码第1部分为n-1的第1部分取反,左移1位。可见,n的2进制代码左循环1位,与n-1的2进制代码左循环1位+2,即2进制代码10,在位运算上是一个操作。
故而数学归纳法成立。
当然k>2时候,位运算操作便没有通用性了,此时最保险的方法还是按部就班的使用通项递推公式
b)下面就说说通用方法:
这个方法网上有称之为数学方法,但个人觉得网上的解释都有些不太清楚,本人就试着按自己的方法来解释:
我们知道第一个人(编号一定是m-1%n) 出列之后,剩下的n-1个人组成了一个新的约瑟夫环(以编号为k=m%n的人开始):
k k+1 k+2 ... n-2, n-1, 0, 1, 2, ... k-2并且从k开始报0。
现在我们把他们的编号做一下转换:
k --> 0
k+1 --> 1
k+2 --> 2
...
...
k-2 --> n-2
k-1 --> n-1
变换后就完完全全成为了(n-1)个人报数的子问题,假如我们知道这个子问题的解:例如x是最终的胜利者,那么根据上面这个表把这个x变回去不刚好就是n个人情况的解吗?!!变回去的公式很简单,相信大家都可以推出来:x'=(x+k)%n
如何知道(n-1)个人报数的问题的解?对,只要知道(n-2)个人的解就行了。(n-2)个人的解呢?当然是先求(n-3)的情况 ---- 这显然就是一个倒推问题!好了,思路出来了,下面写递推公式:
令f[i]表示i个人玩游戏报m退出最后胜利者的编号,最后的结果自然是f[n]
递推公式
f[1]=0;
f[i]=(f[i-1]+m)%i; (i>1)
有了这个公式,我们要做的就是从1-n顺序算出f[i]的数值,最后结果是f[n]。因为实际生活中编号总是从1开始,我们输出f[n]+1
由于是逐级递推,不需要保存每个f[i],程序也是异常简单:
下面的代码我又加上了更详细的解释,应该更容易理解些:
int main()
{
int n, m, i, s=0;
//n代表总人数,m则是每次从0循环加1加到m就要淘汰的人
//s=0表示,最后胜利的人号码为0,当然这个号码只是一个中间结果。
//那么我们就从人数为1,胜利者为0号为起点开始,
//去递推人数为2(这就是为什么下面的循环中i是从2开始的)时,
//根据公式人数为1时0号在人数为2时的号码为(0+m)%2也就是s=(s+m)%i
//(注这里的m和上面公式中的k是一回事儿)
//当得到人数为2时的胜利者的号码还不算完,因为它与开始的0号一样,只是中间结果。
//因此我们需要按上面的递推过程来循环推人数为3,4,...n时胜利者的号码。
//而当人数为n时所得到的胜利者的号码则是最后的结果。
printf ("N M = "); scanf("%d%d", &n, &m);
for (i=2; i<=n; i++) s=(s+m)%i;
printf ("The winner is %d\n", s+1);
}
这个方法网上有称之为数学方法,但个人觉得网上的解释都有些不太清楚,本人就试着按自己的方法来解释:
我们知道第一个人(编号一定是m-1%n) 出列之后,剩下的n-1个人组成了一个新的约瑟夫环(以编号为k=m%n的人开始):
k k+1 k+2 ... n-2, n-1, 0, 1, 2, ... k-2并且从k开始报0。
现在我们把他们的编号做一下转换:
k --> 0
k+1 --> 1
k+2 --> 2
...
...
k-2 --> n-2
k-1 --> n-1
变换后就完完全全成为了(n-1)个人报数的子问题,假如我们知道这个子问题的解:例如x是最终的胜利者,那么根据上面这个表把这个x变回去不刚好就是n个人情况的解吗?!!变回去的公式很简单,相信大家都可以推出来:x'=(x+k)%n
如何知道(n-1)个人报数的问题的解?对,只要知道(n-2)个人的解就行了。(n-2)个人的解呢?当然是先求(n-3)的情况 ---- 这显然就是一个倒推问题!好了,思路出来了,下面写递推公式:
令f[i]表示i个人玩游戏报m退出最后胜利者的编号,最后的结果自然是f[n]
递推公式
f[1]=0;
f[i]=(f[i-1]+m)%i; (i>1)
有了这个公式,我们要做的就是从1-n顺序算出f[i]的数值,最后结果是f[n]。因为实际生活中编号总是从1开始,我们输出f[n]+1
由于是逐级递推,不需要保存每个f[i],程序也是异常简单:
下面的代码我又加上了更详细的解释,应该更容易理解些:
int main()
{
int n, m, i, s=0;
//n代表总人数,m则是每次从0循环加1加到m就要淘汰的人
//s=0表示,最后胜利的人号码为0,当然这个号码只是一个中间结果。
//那么我们就从人数为1,胜利者为0号为起点开始,
//去递推人数为2(这就是为什么下面的循环中i是从2开始的)时,
//根据公式人数为1时0号在人数为2时的号码为(0+m)%2也就是s=(s+m)%i
//(注这里的m和上面公式中的k是一回事儿)
//当得到人数为2时的胜利者的号码还不算完,因为它与开始的0号一样,只是中间结果。
//因此我们需要按上面的递推过程来循环推人数为3,4,...n时胜利者的号码。
//而当人数为n时所得到的胜利者的号码则是最后的结果。
printf ("N M = "); scanf("%d%d", &n, &m);
for (i=2; i<=n; i++) s=(s+m)%i;
printf ("The winner is %d\n", s+1);
}