有趣的题目,思考了一下,应用场景可能包括压力测试这种逐步加量、且有尝试成本的实际问题上。

1884. 鸡蛋掉落-两枚鸡蛋

先考虑1884题,是2枚鸡蛋的情况。
乍一看想不到适合套用的数据结构,考虑是DP的解法。
dp[i][j],i为当前允许摔碎的鸡蛋数,j为当前需要判定的楼层数,值为操作数。
当i为0时,我们只能逐层判断,所以dp[0][i] = i
当i为1时,我们可以选择在[0, j]范围内的一个楼层k扔鸡蛋,对于每一个k,会存在两种情况:

  • 情况1:摔碎了,那么操作数+1,并转化为i-=1,转化为i=0的情况,从底层向上逐层搜索,即dp[0][k-1]+1
  • 情况2:没摔碎,那么操作数+1,i不变,再向上,搜索的空间(楼层数)减掉j,即dp[1][j-k]+1
    综合来看,状态转移方程是:
dp[1][j] = min{
        max(dp[0][0]+1, dp[1][j-1]),
        max(dp[0][1]+1, dp[1][j-2]),
        ...
        max(dp[0][k-1]+1, dp[1][j-k]),
        ...
        max(dp[0][j]+1, dp[1][0]),
}

代码写出来长这样:

func twoEggDrop(n int) int {
    const kk = 2
    dp := make([][]int, kk, kk)
    for i:=0;i<kk;i++{
        dp[i] = make([]int, n+1, n+1)
    }
    for i:=0;i<=n;i++{
        dp[0][i] = i
    }

    dp[1][0] = 1
    dp[1][1] = 1
    min := 1000
    sub_max := 0
    for j:=1;j<=n;j++{
        min = 1000
        for k:=1;k<=j;k++{
            sub_max = max(dp[0][k-1]+1, dp[1][j-k]+1)
            if sub_max < min{
                min = sub_max
            }
        }
        dp[1][j] = min
    }

    return dp[1][n-1]
}

func max(i, j int) int{
    if i>j{
        return i
    }else{
        return j
    }
}

887. 鸡蛋掉落

在实际情况中,可能允许我们尝试的次数限制是一个变量。

上面1884题两个鸡蛋的解法,已经将鸡蛋数抽离出来成为变量,可以容易拓展到e个鸡蛋的情况。但是需要注意到,1884的时间复杂度为O(eN^2)。对于e个鸡蛋的情况,效率太低,答题时会出现超时错误。需要寻找优化的方法。

首先观察到上面的式子:

dp[1][j] = min{
        max(dp[0][0]+1, dp[1][j-1]),
        max(dp[0][1]+1, dp[1][j-2]),
        ...
        max(dp[0][k-1]+1, dp[1][j-k]),
        ...
        max(dp[0][j]+1, dp[1][0]),
}

将i=1,j=6带入,可以看看到:

k=0     dp[0][0]=0          dp[1][6-1]=dp[1][5]=3
k=1     dp[0][1]=1      dp[1][6-2]=dp[1][4]=3
k=2     dp[0][2]=2      dp[2][6-3]=dp[1][3]=3
k=3     dp[0][3]=3      dp[3][6-4]=dp[1][2]=2
k=4     dp[0][4]=4      dp[4][6-5]=dp[1][1]=2
k=5     dp[0][5]=5      dp[4][6-6]=dp[1][0]=1
}

可以发现dp[i-1][k-1]与dp[i][j-k]是关于k的两条单调曲线(其实是一条直线和一条分段直线),且存在交点。如图:
file
交点处的位置也是两条的max合并后的min值,交点对应的值就是dp[i][j]。
这里求交点可以使用二分搜索。

时间复杂度到此被优化到O(eNlogN),进一步用决策单调性可以优化到O(N),但是我不会写了Orz。对了,这道题有个彩蛋,把第一题的dp打印出来,可以发现很明显的数学规律,也可以很容易写出一种O(N)时间的神奇解法。


func superEggDrop(e int, n int) int {
    if e == 1 {
        return n
    }

    dp := make([][]int, e+1, e+1)
    for i := 0; i <= e; i++ {
        dp[i] = make([]int, n+1, n+1)
    }

    for i := 1; i <= e; i++ {
        for j := 0; j <= n; j++ {
            dp[i][j] = -1 // -1 视为没有计算过的结果
        }
    }

    return dps(e, n, &dp)
}

// 对dp做二分查找
func dps(i, j int, dp *[][]int) int {
    if (*dp)[i][j] != -1{
        return (*dp)[i][j]
    }
    low := 1 // 0?
    high := j
    var val int
    if j == 0{
        val = 0
    }else if i == 1{
        val = j
    }else{
        k := 0 // mid, sum/2
        for low+1 < high {
            k = (high + low) / 2
            y1 := dps(i-1,k-1,dp)
            y2 := dps(i, j-k, dp)

            if y1<y2{
                low = k
            }else if y1>y2{
                high = k
            }else{
                high, low = k, k
            }
        }
        val = 1+min(max(dps(i-1,high-1,dp), dps(i,j-high,dp)),max(dps(i-1,low-1,dp), dps(i-1,j-low,dp)))
    }
    (*dp)[i][j] = val
    return (*dp)[i][j]
}

func max(i, j int) int {
    if i > j {
        return i
    } else {
        return j
    }
}

func min(i, j int) int{
    if i<j{
        return i
    }else{
        return j
    }
}
最后修改日期: 2022年3月5日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。