ACM解题报告 POJ1012

14 篇文章 0 订阅
7 篇文章 0 订阅

       本问题的原始版在智力题中是有名的约瑟夫问题。

       我看到的第一个版本是奴隶英雄斯巴达(汗,是斯巴达克)的一个故事。当时看到答案,没记清,后来自己写链表实现时和印象中的答案不一致,还让我怀疑了半天大笑

       先上网址:http://poj.org/problem?id=1012

       我先解释下问题吧:

       约瑟夫问题:有n个人,从1-n按序围成一圈。然后从第一个人开始报数,到第m个人时,此人出列(故事里面就是拖出去杀了),下一个人再从1开始报数,第m个人出列,直到只剩下一人为止。

       比如,n = 6, m = 5时,顺序就是5,4,6,2,3。1是最后一人。
       本题不一样之处在于,现在假设有k个好人(好人卡啊),和k个坏人。好人正好是1-k号,坏人就是k+1 - 2*k号。现在你要找出一个m,使得坏人先下地狱!

       所以就是求m,让前n次选中的数都落到[n+1, 2 * n]的范围中,不用输出每次选中的数字。用模拟的方法来求这题绝对超时了,所以,我们要用别的方法。

       当然,如果你先在自己的电脑上不辞辛苦地,把它全算出来(从1-13的结果),再把结果直接记到程序里面,你绝对可以以最快速度AC本题偷笑

       话说回来,本题我也用了打表记录的方法,不确定有几人完全不打表就过的。

       不用模拟,那有什么方法呢?据说,认真看过具体数学(别的书不清楚)就会解决本题的方法,好像叫构造法。

       跑题一扯:我去本校图书馆看了,那本具体数学已经可以跟50年代的书相媲美了……改天我去take a photo 给大家一观。

       这个方法听起来好像很高深的样子,其实,还是脱离不了模拟法的范围,只不过没有了一个个报数的过程,而是直接报到第m人,而且,好像还真只能是这种不要求输出过程的题才能使用。原因我会一一分析的。

       构造法,这里我看,和递推也差不多。设n = 2 * k,m……

       假设此时,我们已经进行了k回合(从0开始),则当前剩余人数为n - k。本轮中枪者为now——先不考虑是1-n顺序的now,还是剩余人数顺序的now,那下一位躺枪者就是now + m。

       n-k人一字排开,我们可以分析,如果now是按1-n编号来算的,那我们无法知道他现在有哪些人已经离开(为了节约时间,不记录离开的人,也不查询),正好本题要保证now在前k次取值都大于k,而不考虑具体数值。所以,这里的now是剩余人数的编号。

       剩余人数的编号有什么用?你想啊,这次中枪的是now,则前面有now - 1个人,后面呢?有n - k - now个人对不?从now + 1开始,又重新报数了,所以,下一位躺枪者是now + m,而我们实际上也把队列进行调整,把now前面的那now - 1个人,转到队尾去了——实际上,因为是围成一圈,自然就是队尾了。

       所以,now后面的人数变成 n - k - now + now - 1 = n - k - 1人,剩余的人数又少了一个。

       现在我们就可以从now推出now + m在新形成的队列中排第几了。这种循环转圈的,肯定是要用到mod 这个操作,所以我们先得个初步的式子:

       在第k + 1回合,new-now  = (old-now + m ) % (n - k - 1);

       这里就有个 % 运算的小陷阱,%(n-k-1)后,取值范围是多少?[0, n - k - 2],那让我们来检验下这个公式,old-now + m从old-now + 1开始报数,假设先数了 n - k - 1,正好是old-now + m,那会是什么情况,new-now = 0……

       明显不合理,到n - k - 1时,应该是队尾,而不是队首,所以,公式有错误,但是,n-k-1是剩余人数,绝对不用改了,所以,就是old-now + m出错了,这时再想到取值范围,就明白了,应该把里面的值-1,这样1->0……(n-k-1)->(n-k-2),就对得上了。另外外面要再+1,才能回复到原位上。

       于是,再把k+1改成k,我们得到:

       在第k回合,new-now = (old-now + m) %(n - k);

       所以,上面这个过程就是我说的递推的过程,百度百科上也讲了这个,不知道我写得如何。得意

       回到原题上,我们可以确定一点,就是在前k次计算时,得到的now值都一定大于k。所以,只要now <= k,我们就结束这个m的检测,到下一个值开始。

       另外,如果m 在 [2*x*k +1, 2 * x * k + k]的范围,第一关就被刷了,所以我们只计算

[(2 * x + 1)k + 1, (2 * x + 2)k]里的数字。

       于是代码如下:

#include <stdio.h>
int Joseph(int n, int m)
{
    int now = 0, rest = 2 * n, i;
    for (i = 0; i < n; i++)             //用rest 代替 2 * n - i, 第i回
    {
        now = (now + m - 1) % rest;     //now 外面加一次,后面还是要减回来的,所以直接省略
        if (now < n)                    //如果在前n回中,把好人挑出来,证明有问题
            break;
        rest--;
    }
    return i == n;			//如果顺利过n次,则为1,否则0
}
int main()
{
    int n;				//这里用n来代替k,各位小心,变量命名是很头痛的事尴尬
    int a[14];
    for (n = 1; n < 14; n++)
    {
        int i, m;
        for (i = n + 1; ; i+= n + n)	// n + 1开始,2*k*n + n + 1
        {
            for (m = 0; m < n; m++)     // 1 到 n
                if (Joseph(n, m + i) == 1) //第一次时,就忘了里面是m+i,写成m了
                    break;
            if (m < n)
                break;
        }
        a[n] = i + m;			//不打表,还是要超时的……
    }
    while (scanf("%d", &n) && n)
        printf("%d\n", a[n]);
    return 0;
}
         现在来总结下,这个方法在不计算具体号码时,可以得到结果。否则,还是要使用模拟,至于是像链表那样实况模拟,点一个删一个,能不能先用这个递推的方法求出位置,再去查以前的记录,不是本题的讨论范围。

       递推法(构造法,我不知道具体数学里的构造法是怎么个构造法偷笑),在本题中作用极大,在做ACM中,也是很重要的方法,比如DP其实也是递推。

       另外还有传说中的打表法,单是上面的递推法,我遭遇了TLE的尴尬,只好打表,还有在N皇后问题时,因为剪枝效果不佳,最后,也打了表。所以,在一些题目上,打表是最装逼的技术,也最让人哭笑不得的技术。在POJ本题的讨论版中,有一仁兄,帖子号称绝对暴力,结果是直接打表,递推都省了。楼下各种无语+赞颂……得意

       本题是解决了,想来,是不会忘了奋斗

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值