POJ 1012 Joseph题解与心得

来源:互联网 发布:上海行知小学对口 编辑:程序博客网 时间:2024/04/28 23:59

POJ 1012 Joseph题解与心得

    2010-06-06

   这几天都在收集一些DP的资料,但读了一些入门的资料后我发现DP比我想象中的还要难入门,主要是DP问题各概念都比较抽象,想要一口把它的概念吃透有着不小难度,心急的朋友不要在这里踩进深坑了。经过几天思考,还没有摸到边缘的我打算先将DP放下,不然自己的信心越来越受打击了,为了不让自己对ACM的激情出现丝毫的减弱并且不陷进自卑的恶性循环当中,我决定选一些比较简单而且有趣的题目练练手,增加信心。

    POJ 1012 Joseph问题,当我看到这个问题的描述时,我就被这个问题的趣味性深深吸引,于是便动起了首手……

   题目的描述是这样的:

Description

The Joseph's problem is notoriously known. For those who are not familiar with the original problem: from among n people, numbered 1, 2, . . ., n, standing in circle every mth is going to be executed and only the life of the last remaining person will be saved. Joseph was smart enough to choose the position of the last remaining person, thus saving his life to give us the message about the incident. For example when n = 6 and m = 5 then the people will be executed in the order 5, 4, 6, 2, 3 and 1 will be saved.

Suppose that there are k good guys and k bad guys. In the circle the first k are good guys and the last k bad guys. You have to determine such minimal m that all the bad guys will be executed before the first good guy.

n个人,分别编号为1,2,3,n,让他们围成一个圈站着,从指定的第一个人开始数起,每次第m个人被处死,然后在剩下的人中重复数数,只有最后一个剩下的人才能活下来。例如:n=6m=5时,这些人当中被处死的顺序是:5,4,6,2,3,最后1活下来了。

现在假设有k个好人和k个坏人,也让他们围成一圈,前面的k人都是好人,后面的k人都是坏人,你不得不决定一个最小的数m,让坏人在第一个好人被处死前全部被处死。编一个程序实现它。

输入输出描述如下:

Input

The input file consists of separate lines containing k. The last line in the input file contains 0. You can suppose that 0 < k < 14.

Output

The output file will consist of separate lines containing m corresponding to k in the input file.

Sample Input

3

4

0

Sample Output

5

30

 

   我当时的第一想法便是把圈化直线,通过让一个位置i不断累加指定计数值m,然后计算出与当前总人数n的倍数的差值hc*n<=i),当h<=k(输入数)时说明该m不满足第一个好人被处死前所有坏人都被处死的条件,所以m加一(m++)跳到下一个计数值重新开始,当h>k时,即处死的是坏人时,继续下一轮对i累加m,而循环k次均满足h>k条件时,该m就为所求。输出该m

   程序如下:

#include <stdio.h>

 

int main()

{

     long i;

     int k, n, m, flag, count;

     while(scanf("%d", &k)==1)

     {

         if(k==0)

              break;

         for(m=k+1; ; m++)

         {

              n = 2*k;

              flag=0;

              i=0;

              count=1;

              while(n>k)

              {

                   i+=m;

                   if(i%n>k || i%n==0)

                   {

                       i-=count;

                       n--;

                       count++;

                   }

                   else

                   {

                       flag=1;

                       break;

                   }

              }

              if(flag==0)

              {

                   printf("%d/n", m);

                   break;

              }

         }

     }

     return 0;

}

 

 

但是当我调试这个程序时,我发现测试结果不正确。思考整个过程时我发现用count来存放每次处死一个坏人时直线编排中剔除该坏人编号时所要减少的位数,因为我是把圈化为直线,那么就会有很多重复的按编号排列的组。但当m的个数超过一组甚至多组人数时,count就不能正常反映出减少的数位了。

例如:

n=6m=5时,

此时m<n,可正确反映,直线编排为123456123456123456……123456。第一次i+=m时,i=5,所以将5处死,直线编排就变为123461234612346……12346。此时count1icount位,变为4开始,count++变为2n减一位变为5;第二次i+=5变为9i%n=4大于k3),则icount位变为7count++变为3n减一位变为4,直线编排变为1236123612361236……,如此重复。

但当m的值大于总人数时,i累加一次就大于n此时count累加的量就不为1了,所以按照上面的程序不能求得正确结果。如果非要在圈化直线的思想上改动上述程序,那么这个count值就应该做一下调整,具体调整的值设为i的值与剩余总人数n的商,每次处死坏人后直线编排的总人数总会减少,而i的编号总会减少count+1,不过有一个问题需要注意的是当i%n==0in的倍数时,当前位置i的减少量就比一般情况时少一,这是因为i/n的值代表不包括位置i所在组的前面出现的重复编号组数,当in倍数时,i指向的当前组也被列为当前组之前的重复编号组,此时就相当于count多加了一,而i就多减了一,所以在出现i正除ncount就要先减一。

改进的程序如下:

#include <stdio.h>

 

int main()

{

     long i;

     int k, n, m, flag, count;

     while(scanf("%d", &k)==1)

     {

         if(k==0)

              break;

         for(m=k+1; ; m++)

         {

              n = 2*k;

              flag=0;

              i=0;

              while(n>k)

              {

                   i+=m;

                   if(i%n>k || i%n==0)

                   {

                       count=i/n;

                       if(i%n==0)

                            count--;

                       i-=(count+1);

                       n--;

                      

                   }

                   else

                   {

                       flag=1;

                       break;

                   }

              }

              if(flag==0)

              {

                   printf("%d/n", m);

                   break;

              }

         }

     }

     return 0;

}

 

   经过改进后的程序经调试可以得到正确结果。

不过把圈化直线的想法实现起来是比较麻烦的,就刚才对每次处死一坏人就要进行一次位置的处理上就可以看出。这里我变换了一下思维,不把圈化直线了,而是直接是用循环链的思想来实现。

但是我并没有用循环链的结构来存储人编号,而是模拟该情况,比如此时剩余n人,当前的计数值为mi每次数m位,在循环链结构中,超过最大值n后,就会循环回到链头并继续向前计数,程序中的句子为i+=mi=i%n;位置i就只会在1~n的范围内循环移位,当数到的是坏人时,坏人被处死,而位置i就会向后移一位重新计数,其他过程与圈化直线同。

程序如下:

#include <stdio.h>

 

int main()

{

     int k, n, m, flag, i;

     while(scanf("%d", &k)==1)

     {

         if(k==0)

              break;

         for(m=k+1; ; m++)

         {

              n = 2*k;

              flag=0;

              i=0;

              count=1;

              while(n>k)

              {

                   i+=m;

                   i=i%n;

                   if(i>k || i==0)

                   {

                       if(i==0)

                            i=n-1;

                       else

                            i--;

                       n--;

                   }

                   else

                   {

                       flag=1;

                       break;

                   }

              }

              if(flag==0)

              {

                   printf("%d/n", m);

                   break;

              }

         }

     }

     return 0;

}

 

     虽然解题过程是正确的,得到答案也是正确的,但是提交的时候超时了,这个程序还能优化吗?我苦思了一段时间,未果,于是我到网上去搜了搜别人的题解,他们的解法思路与我的思路大致相同,但有一点不同的地方就是他们大多通过打表实现,并且我发现了一个神奇得令我神往的短小精干版本。

代码如下:

#include <stdio.h>

 

int a[15]={0, 2, 7, 5, 30, 169, 441, 1872, 7632, 1740, 93313, 459901, 1358657, 2504881, 13482720};

int main()

{

     int k;

     while(scanf("%d", &k)==1)

     {

         if(k==0)

              break;

         printf("%d/n", a[k]);

     }

     return 0;

}

而我自己的版本再经过修改后所得的最终版代码如下:

#include <stdio.h>

 

int a[15];

int main()

{

     long i;

     int k, n, m, flag, count, j;

     for(j=1; j<=14; j++)

     {

         for(m=j+1; ; m++)

         {

              n = 2*j;

              flag=0;

              i=0;

              count=1;

              while(n>j)

              {

                   i+=m;

                   i=i%n;

                   if(i>j || i==0)

                   {

                       if(i==0)

                            i=n-1;

                       else

                            i--;

                       n--;

                   }

                   else

                   {

                       flag=1;

                       break;

                   }

              }

              if(flag==0)

                   break;

         }

         a[j] = m;

     }

     while(scanf("%d", &k)==1)

     {

         if(k==0)

              break;

         printf("%d/n", a[k]);

     }

     return 0;

}

 

此时我把我的最终版提交后终于可以AC~

 

学会了打表,该程序就不会超时了。为什么他们就能如此轻易想到用打表来防止超时而自己从没有过半点这样的思路呢?我看只能用两个字来解答这个问题了--能力啊,我再回头看了看题目描述的输入要求,发现输入值范围为1~14,即最大值不超过15,我觉得这个应该就是可以打表的依据了吧。于是我想,如果在以后的解题当中,如果遇到需要求解数的上限不是很大,自己的程序可以求出正确的结果但遗憾地超时并且已经想不出优化方法的时候,我们不妨考虑下用打表的方式来解决它了。   

 

原创粉丝点击