李白喝酒问题的算法

“李白街上走,提壶去买酒,遇店加一倍,见花喝一斗”,途中,遇见5次店,见了10此花,壶中原有2斗酒,最后刚好喝完酒,要求最后遇见的是花,求可能的情况有多少种?希望大家分享一下思路,谢谢!
我的思路很混乱,觉得直接暴力枚举能解决,但是枚举所有的情况比较不现实,希望大家解答啊!

阅读 18.3k
13 个回答

henix的思路非常好,只是这一句“所以问题转化为把 8 拆成 5 个 2 的幂”略有问题,漏掉了类似12311的组合(即漏掉了可能+3的情形)。
加3斗的情况会在如下情境中触发:当前酒为2斗时候,遇店加至4斗,遇花喝掉一斗,此时有3斗,再遇店加3斗。所以这个组合中3必须紧挨着2,在2的后面,相当于"23"捆绑在一起。此种情况下有C(4,1) = 4种。总答案为C(5,2)+C(4,1)为14种。

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

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

1 1 2 2 2
1 1 1 1 4

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

所以问题转化为 1 1 2 2 2 这 5 个数有多少种排列方法,共 C(5,2) = 10 种。

这样的问题用枚举加剪枝的方式应该是可以接受的吧,下面我写的python代码:

#! /usr/bin/env python
# *-* coding: utf-8 -*-

dou = 2
dian = 5
hua = 10
num = 0
def walk(dou, dian, hua, path):
    global num
    if dou < 0 or dian < 0 or hua < 0:
        return
    if dou == 1 and dian == 0  and hua == 1:
        print path
        num += 1
    walk(dou+dou, dian-1, hua, path + ['dian']) #遇到店
    walk(dou-1, dian, hua-1, path + ['hua']) # 遇到花

walk(dou, dian, hua, [])
print num

貌似楼主用C++,那个路径可以stack实现。补充一下运行结果(共14种):

['dian', 'hua', 'dian', 'hua', 'hua', 'hua', 'hua', 'hua', 'dian', 'hua', 'dian', 'hua', 'dian', 'hua']
['dian', 'hua', 'hua', 'dian', 'hua', 'hua', 'dian', 'hua', 'hua', 'hua', 'dian', 'hua', 'dian', 'hua']
['dian', 'hua', 'hua', 'dian', 'hua', 'hua', 'hua', 'dian', 'dian', 'hua', 'hua', 'hua', 'dian', 'hua']
['dian', 'hua', 'hua', 'dian', 'hua', 'hua', 'hua', 'dian', 'hua', 'dian', 'dian', 'hua', 'hua', 'hua']
['dian', 'hua', 'hua', 'hua', 'dian', 'dian', 'hua', 'hua', 'dian', 'hua', 'hua', 'hua', 'dian', 'hua']
['dian', 'hua', 'hua', 'hua', 'dian', 'dian', 'hua', 'hua', 'hua', 'dian', 'dian', 'hua', 'hua', 'hua']
['dian', 'hua', 'hua', 'hua', 'dian', 'hua', 'dian', 'dian', 'hua', 'hua', 'dian', 'hua', 'hua', 'hua']
['hua', 'dian', 'dian', 'hua', 'dian', 'hua', 'hua', 'hua', 'hua', 'hua', 'dian', 'hua', 'dian', 'hua']
['hua', 'dian', 'dian', 'hua', 'hua', 'dian', 'hua', 'hua', 'dian', 'hua', 'hua', 'hua', 'dian', 'hua']
['hua', 'dian', 'dian', 'hua', 'hua', 'dian', 'hua', 'hua', 'hua', 'dian', 'dian', 'hua', 'hua', 'hua']
['hua', 'dian', 'dian', 'hua', 'hua', 'hua', 'dian', 'dian', 'hua', 'hua', 'dian', 'hua', 'hua', 'hua']
['hua', 'dian', 'hua', 'dian', 'dian', 'hua', 'dian', 'hua', 'hua', 'hua', 'hua', 'hua', 'dian', 'hua']
['hua', 'dian', 'hua', 'dian', 'dian', 'hua', 'hua', 'dian', 'hua', 'hua', 'dian', 'hua', 'hua', 'hua']
['hua', 'dian', 'hua', 'dian', 'hua', 'dian', 'dian', 'hua', 'dian', 'hua', 'hua', 'hua', 'hua', 'hua']
14

哎……大家都各贴各的代码,也不说说重要的优化思路

翻了一下还几乎没在答案里找到靠谱的优化思路解说,如果这是我在面试的话这里的答案几乎没人到我心理预估60分……

为了方便描述,定义一下李白(x, y, z) => 还有x次花,y次店,z斗酒的答案

先说不重要的吧,剪枝条件是可以比各位的答案更激进一些的,比如z > x的时候玩命喝也喝不完了可以剪,同理z * (2 ^ y) < x的时候即使马上狂打酒也不够喝了可以剪。


正文分隔线


这个问题最明显的特征就是有规模,并且大规模版本的答案是和小规模版本的答案密切相关的,也就是已知李白(x, y, z),我们马上就能知道李白(x+1, y, z+1) 或者李白(x, y+1, 2z)的答案。
不知道到这里有没有人能想起菲波纳契数列和对应的经典计算机算法动态规划。动态规划是非常经典的空间换时间的算法,可以把这类问题 最笨拙的暴力递归算法瞬间改造成性能相当良好的算法。

实际上,不用动态规划的话,这类问题的递归算法会很快随着规模的增长而崩溃,具体O多少还给算法老师了,反正肯定是逆天指数级的。动态规划以后基本可以降到多项式级别

最后给出我的实现,嗯,JS的

http://jsfiddle.net/e3Ns4/

_.memoize(fn, hasher)是underscore里的一个helper,作用是生成一个新的函数,调用时会先用haser计算一个hash,然后把结果缓存在这个hash里,下次相同hash值就不调用fn了,也就是动态规划。源代码我抄过来

  function (func, hasher) {
    var memo = {};
    hasher || (hasher = _.identity);
    return function() {
      var key = hasher.apply(this, arguments);
      return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
    };
  }

我想一个穷举的思路吧~其实不用很多步骤的~
首先要递归
开始2斗,5酒店,10花;
楼上都完整了...
提供优化条件吧
如果酒店完了,并且斗和花不等,就能退出;
如果斗大于花也能退出;
斗已经是0直接退出;
花是零,但没到15步也退出;

额...代码如下(python版):

def w(f,d,h): return (1 if f==h else 0) if d==0 else (0 if f*h==0 or f>h else w(f*2, d-1, h)+w(f-1, d, h-1))

执行w(2,5,10) 结果14

自己写不来,让某大神写的枚举代码:

int main()
{
    int cnt=0;
    for (int i=0; i<1<<15; i++)
    {
        int k=2;
        int n=0;
        int x=i;
        int flag=1;
        for (int j=1; j<=15; j++)
        {
            if (x&1)
                k*=2;
            else
                k--;
            if (k==0&&x)
            {
                flag=0;
                break;
            }
            n+=x&1;
            x>>=1;
        }
        if ( n !=5 || (i & (1<<14)) || !flag || k != 0)
            continue;
        cnt++;
    }
    cout<<cnt<<endl;
    return 0;
}

让我来个haskell递归版的

drink :: Int -> Int -> Int -> Int
drink 1 0 1 = 1
drink wine inn flower
 | wine < 0 = 0
 | inn < 0 = 0
 | flower < 0 = 0
 | otherwise = drink (wine-1) inn (flower-1) + (drink (wine*2) (inn-1) flower)
*Main> drink 2 5 10
14

给你一段更容易理解的代码,用递归做的:

#include <iostream>
using namespace std;
int count = 0;
int path[ 20 ];

void dfs( int m, int n, int r, int c )//m 遇店的次数,n 遇花的次数
{
    if( m < 0 || n < 0 || r < 0 )
        return ;
    if( m ==0 && n == 0 && r == 1 )
    {
        path[ c ] = '\0';
        cout << path << endl;
        count++;
    }
    path[ c ] = 'a';
    dfs( m -1 , n, r * 2, c + 1 ); 
    path[ c ] = 'b';
    dfs( m, n - 1, r - 1 , c + 1 ); 
}

int main() 
{
    dfs( 5, 9, 2, 0 );
    cout << count << endl;
    return 0;
}

自己先粗略的写了一下,刚调试了一下,还是有点问题,已经被他折磨了两个小时了,先休息会儿等会儿再来debug!
结果debug了好久,也没我想的那么简单,也没人提示,今天又花了半个小时左右的时间,终于找到了问题的所在!

#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)
{
if(pos == 15)
{
    if(sum ==0 && A[14] == 1 )
    {
        counting++;
    }
}else{
    if(collectArray() <= 5)
    {
        A[pos] = 0;
        sum *= 2;
        enumAll(pos+1);
        A[pos] = 1;
        sum -= 1;
        enumAll(pos+1);
    }
    /*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);
    cout << counting;
    return 0;
}

更新版本,已通过测试:

#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;
}
"""
这是今年蓝桥杯java中的一道填空题,当时我还以为做对了,写了33
结果我忽略了最明显的判断条件,5次店和10次花,少了下面的 check1 函数
简直为自己的智商捉鸡,加了之后就是14了,
最近学python,现用python实现这题,跟大家分享
"""

count=0;
def check(t):
    sum = 2;
    for ch in t:
        if ch=='1':
            sum=sum*2;
        if ch=='0':
            sum=sum-1;
    return sum;
def check1(t):
    c = 0;
    for ch in t:
        if(ch=='1'):
            c+=1;
    if c==5:
        return True;
    return False;
def f(t):
    if(len(t)==15):
        if check(t)==0 and t[14:15]=='0' and check1(t):
            global count;
            count+=1;
            print(" ".join(t));
        return;
    f(t+'1');
    f(t+'0');
f('');
print(count);

简单的给一个非递归的python代码:

queue = [(2, 5, 10)]
total = 0
def iter():
    global queue
    while queue:
        i, queue = queue[0], queue[1:]
        yield i

for (left, dian, hua) in iter():
    if dian == 0:
        total += 1 if left == hua else 0
    else:
        if left > hua or left == 0 or hua ==0:
            continue
        queue.append((left*2, dian-1, hua))
        queue.append((left-1, dian, hua-1))

暴力穷举很好做,不再说了
最后一次是见花的话,2经过说明经过14次变换之后值是1,
10次见花之后是0的话,初始是2斗酒,说明一共加了8斗酒
加酒最小值是1,最大是4(11114),
因为加酒是加倍方法,数列中相邻两个数倍差不能大于2倍,即11114这类被排除,可能加酒的数字在1-3的范围内,同样因为倍差原因,1后面只能是2(但是3后面可是是1)
因为初期值是2,第一次加酒只可能是1或者2斗
于是找有几个满足条件的数列就行

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏