首页 > 代码库 > 项目中的有趣题目 -- 吃饺子问题

项目中的有趣题目 -- 吃饺子问题

题目描述:

近日,项目中偶遇一个有趣的题目,感慨多多,备忘之。抽象出来,大致是:

桌上一共有100个饺子,其中有10个饺子包了硬币,问:连续吃到硬币的期望次数是多少次?

首先,定义一下这里的连续,如果我们将吃饺子的顺序抽象为一个100位的二进制数。并且吃到饺子表示为1,没吃到则为0,那么:
  • 如果一次和第二次吃到,那么可表示为: 110.....,那么这里的连续吃到的次数为1.
  • 如果数字为: ...1111.... ,那么这里连续的4个1表示3次连续,也就是说只要连续,就算1次。

    期望次数,也就是说我们需要算出0个连续,1个连续,....9个连续。这10个计数。

注:这里的连续其实就是取决于当前位置的前一个位置的状态即可。因此可以用DP来解决

此题目是一个数数的问题,显然可以使用DP来进行求解。

global[i][j][k] : 表示前i个饺子中,已经吃到了j个硬币,并且连续的次数为k。

local[i][j][k] : 表示前i个饺子中,吃到了j个硬币,并且第k次连续发生在第i个位置上。(也就是说第i-1次吃到的也是饺子)。并且连续的次数为k。

  • local[i][j][k] = local[i-1][j-1][k-1] + global[i-3][j-2][k-1]

  • global[i][j][k] = local[i][j][k] + global[i-2][j][k] + global[i-2][j-1][k] + local[i-3][j-2][k-1] + global[i-3][j-1][k]

显然上式以最后一次是否连续作为子问题是不明智的,递推关系式太过复杂。

下面从另一个角度来分析:

以最后两位的状态来决定:

  • 11 : 那就是连续一次,
  • 10 , 01, 00 , 都没有连续,子问题直接递推即可。

    因此,定义如下子问题:

    f[i][j][k][0/1] : 表示前i个饺子中,吃到了j个硬币,并且有k个连续。且最后一个是0/1的次数。

    那么我们要求的就是: f[100][10][k][0] + f[100][10][k][1]; k = {1,2,3,...,9}

    注意:这里的最后一维其实就是一个0/1的记录,当然你也可以使用两个三维数组来表示。

    Python代码实现如下:

def compute(m,n,f,g):
    if n > m: return -1
    for i in range(m+1):
        f[i][0][0] = 1
    for j in range(n+1):
        f[0][j][0] = 0
        g[0][j][0] = 0
    f[0][0][0] = 1
    g[0][0][0] = 1
 
    for i in range(1,m+1):
        for j in range(1,min(i,n)+1):
            for k in range(0,j):
                f[i][j][k] = f[i-1][j][k] + g[i-1][j][k]
                if k != 0: g[i][j][k] = f[i-1][j-1][k] + g[i-1][j-1][k-1]
                else: g[i][j][k] = f[i-1][j-1][k]
 
    cnt = [0 for i in range(n)]
    for k in range(0,n):
        cnt[k] = f[100][10][k] + g[100][10][k]
    print cnt[1:]
 
    allSum = 1.0
    for i in range(91,101):  allSum = allSum * i
    for i in range(1,11): allSum /= i
    print "allSum = ", allSum
 
    print [float(x)/allSum for x in cnt]
    allSum2 = 0
    for i in range(1,n): allSum2 += (i * cnt[i])
    print allSum2/float(allSum)
 
if __name__ == "__main__":
    m = 100
    n = 10
    k = n-1
    f = [[[0 for k in range(k+1)] for j in range(n+1)] for i in range(m+1)]
    g = [[[0 for k in range(k+1)] for j in range(n+1)] for i in range(m+1)]
    compute(m,n,f,g)

结果为: 0.9

由于这个函数在目前的项目中可能会出现多次调用的情况。那么计算次数要尽量低。另一方面,这个内存占用非常大,在项目中n和m的取值大概是10^5。

如何快速有效的实现。

目前的方案是:由于这是一个三位的数组,那么每一维其实是一个平面,那么我隔固定的区间保存这样的一个平面,比如说,保存f[i][j][100], f[i][j][200], ...,这样下去,在计算时,选取最接近的平面,向下推。(因为每一个平面的计算只依赖于上一个平面)。

上述讲法可能有点儿晦涩,类比一个简单的例子,就清晰了:

比如,我们现在要编写一个求解斐波拉契数的函数,这个函数调用次数非常的频繁,那么我们回想到使用备忘录,但是呢,我们现在的内存十分有限,不可能将前面计算过的值全部保存下来。那么我们保存哪些值呢,很简单,因为每一个数之依赖于它的前两个数,因此,我们隔一段区间保存两个数。比如,我们保存 f[1000], f[1001], f[2000], f[2001],f[3000],f[3001],....我们计算f[3076]的时候,直接从f[3000],f[3001]开始算起即可,不需要再从0开始,这样既降低了空间的消耗的同时尽可能地减少时间的消耗。

其实这就是一个一维的情况,每次保存的是两个数。上面的三维矩阵,每次保存的是一个二维的数组。

Final words:

  • 这里调BUG时,对Python这种动态语言又进了一步,下面的代码正确吗?自己试试吧:
      tmp = [0 for i in range(k+1)]
      tmp2 = [tmp[:] for i in range(n+1)]
      f = [tmp2[:] for i in range(m+1)]
      g = [tmp2[:] for i in range(m+1)]
    这样定义f和g两个数组对吗?
  • DP的子问题定义是technique。子问题定义的好坏直接影响递推式的复杂程度。

项目中的有趣题目 -- 吃饺子问题