思维的体操——李白喝酒(2014年春蓝桥杯个人赛)

上周六去参加蓝桥杯,第一次参加比赛还真是有点小激动和小紧张,不过还好啦,虽然估计自己成绩可能不理想,但既然花了四个小时,就不能让自己毫无收获,心态上我更放得开了,我最擅长的是C,结果鬼使神差的报了java组,直到比赛头天晚上,才真正看了一会java,温习了一下。因为有点感触,大家别嫌我废话多,最近一直在用C++写程序,突然转变到java,还真有点不习惯,我就列举了一下我忘了的内容,也算是对java的一点温习吧,这在比赛中遇到真让人发笑。首先,java中连main函数都被封装到了public类中,你要在主函数中调用方法,这个方法必须是静态的,其次,在定义全局变量上,也必须是静态变量,其次,数组是要new出来的,不能直接像C++似的,这一点害得我好苦啊!最后,我发现java的取余操作和除法操作真的是天壤之别,此处先按下不表。

感慨了这么多,还是回到我们今天的主题吧——李白喝酒,这道题是第二道题,拦了我好久,最后也没做出来,但我就是个较真的人,今天我就来对这道题进行一番解析,总共花了我两天时间,我才真正意义上完全弄懂了这道题。先看题目:

“李白街上走,提壶去买酒,遇店加一倍,见花喝一斗”,途中,遇见5次店,见了10此花,壶中原有2斗酒,最后刚好喝完酒,要求最后遇见的是花,求可能的情况有多少种?

刚拿到这题时,我想法比较直接,很显然这又是一个枚举法的应用,看过我上篇博客的童鞋应该知道,我最近很迷暴力枚举法,自然很熟悉,顺着这个思路,就是一一枚举了,我把遇到店看成0,遇到花看成1,这样就能得到一个不是0就是1的长度为15的序列,这样的序列总共有2的15次方即32768个,请注意这个数量级是完全可以接受的,然后再利用题目给出的条件逐一判断进行取舍,这是很自然的思路。可问题是,我竟然在接下来的2个小时里,不知道该如何一一枚举,比赛回来后,我想到首先可以利用15层循环,进行判断,估计只有疯子才会这么干,接下来,我想到的是递归,因为很显然这枚举的全部情况可以在空间中形成一支完全二叉树,我第一次引入图来说明,哈哈,如图所示:

应该很容易想到采用树的深度优先遍历来做,我花了两个小时写出这段代码,又花了一个多小时调试才OK。附上代码:(代码注释中可能有点胡言乱语)

#include <iostream>

using namespace std;

int counting = 0;
int A[15];
int sum = 2;

int  collectArray()
{
    int collect = 0;
    for(int i =0;i<15;++i)
    {
        if(A[i] == 0)
        collect++;
    }
    return collect;
}

void enumAll(int pos,int sonSum)
//直接采用枚举方法,A[]中每个元素不是0就是1
//若采用if判断则会产生15层嵌套
//因此采用树的深度优先遍历
//建议我使用栈来操作
//问题已经被发现,我TMD就是个天才,sum是个全局变量,在递归的时候,已经变不回来了!
{
    if(pos == 15)
    {
        if(sonSum == 0 && A[14] == 1 &&  collectArray() == 5)
        {
            counting++;
            for(int i =0;i<15;++i)
                cout << A[i] << "  ";
            cout << endl;
            return;
        }
    }else{
        //if(collectArray() <= 5)
            A[pos] = 0;
            sonSum  *= 2;
            enumAll(pos+1,sonSum);
            A[pos] = 1;
            sonSum = sonSum / 2;
            sonSum -= 1;
            enumAll(pos+1,sonSum);
            return;
        //}
        /*if(   <= 10)
        {
            A[pos] = 1;
            enuAll(pos+1);
            A[pos] = 0;
            enumAll(pos+1);
        }*/
    }
}

int main()
{
    for(int i =0;i<15;++i)
    {
        A[i] = -1;
    }
    enumAll(0,sum);
    cout << counting << endl;

    return 0;
}


这道题如果就到这里,就不是我的追求了,我在被此题卡住期间,在论坛上向大神们请教了这个问题,就如我所想的那样,天外有天,人外有人,大神们的答案都不加注释,这里,我则非常详尽的添加上了注释,仅供欣赏,代码如下:

#include <iostream>

using namespace std;

int main()
{
    int cnt=0;
    for (int i=0; i<1<<15; i++)//位运算,等价于i<2^15次方,别被吓到了,继续看
    {
        int k=2;   //壶中酒的量
        int n=0;   //记录遇到店的次数,对应于下面的小于5
        int x=i;  //这里的i是一个长度为15的二进制序列,这里是全部枚举
        int flag=1;  //标志变量,判断酒喝完没
        for (int j=1; j<=15; j++)
        {
            if (x&1)//这应该是本程序最难理解的地方了,为什么与1进行与运算,就能判断出遇到店的次数呢?
                k*=2;//其实很简单,因为x是一个长度为15的二进制序列的数,如果序列中的最低一位
                //是1的话,与1进行与运算的结果就是1,但是这个序列中有多少个1呢?这就是 x>>=1在起作用了,右移一位,这样
                //使得每次被判断的数位都被移走了,这样就可以很简单的进行判断和统计了,对不对!
            else
                k--;
            if (k==0&&x)    //酒喝完就停
            {
                flag=0;
                break;
            }
            n+=x&1;
            x>>=1;
        }
        if ( n !=5 || (i & (1<<14)) || !flag || k != 0)    //高实在是高,作者实在是心细,
        //这里不是直接给出判断cnt++的条件,而是否定,从反面用或的关系给出cnt++不能的条件
            continue;
        cnt++;
    }
    cout<<cnt<<endl;
    return 0;
}

这道题带着如果结束的话,我还是不能满足,结果,一位大神给出了我眼中的终极完美答案,我只能一遍又一遍的感慨,无招胜有招啊!

每见一次花喝 1 斗,由于最开始有 2 斗,总共见了 10 次花,说明总共喝了 10 斗。所以因为遇见店而增加的酒为 8 斗。

所以问题转化为把 8 拆成 5 个 2 的幂,也就是考虑每次遇见店增加多少斗。有三种:

1 1 2 2 2
1 1 1 1 4

1  2 3 1 1

但是没有 2 直接出现 4 是不可能的,所以只有 1 1 2 2 2 是可行的。

所以问题转化为 1 1 2 2 2 这 5 个数有多少种排列方法,共 C(5,2) = 10 种加上C(4,1)为14种,第三种情况很容易忽视:加3斗的情况会在如下情境中触发:当前酒为2斗时候,遇店加至4斗,遇花喝掉一斗,此时有3斗,再遇店加3斗。所以这个组合中3必须紧挨着2,在2的后面,相当于"23"捆绑在一起。此种情况下有C(4,1) = 4种。

仅靠数学分析就得到了最终的答案,这才是编程的最高境界吧!

希望大家在看完我的文章后,能够发表一下观点和评论,教学相长,互相切磋啊!微笑









猜你喜欢

转载自blog.csdn.net/u011995233/article/details/22091627