题解 | #丢棋子问题#

丢棋子问题

http://www.nowcoder.com/practice/d1418aaa147a4cb394c3c3efc4302266

思路

  • 动态规划(k棋n层):dp(k,n)表示有k个棋子n层楼需要尝试的次数,题干要求的是dp(k,n)的结果,我们现在选择在第i层扔棋子
    如果棋子碎了,则剩下k-1个棋子,此时找的楼层更新为i-1(低楼层),即应该继续尝试的次数为dp(k-1,i-1)
    如果棋子没碎,则剩下k个棋子,此时要找的楼层更新为n-i(高楼层),即应该继续尝试的次数为dp(k,n-i)
    • 这两者我们应该取一个大的值,因为是对于未知的i来说最保险的尝试次数,则应该取尝试次数现在应该为1+max(dp(k-1,i-1), dp(k,n-i))
    • 我们这么多从第i层选择扔棋子的方案跟层高n有关,所以想要得到其中最少的次数,则需求
      图片说明
  • 动态规划+二分优化
    • 观察最内层进行比较的数字规律,从左到右是升序的
    • 如果是升序我们还通过遍历的方法,一定是没有利用到升序的特点的。所以升序该给我们的第一个想法就应该是二分,省成 O(logn) 的时间,这是需要锻炼的想法
    • max(dp[i][j-m],dp[i-1][m-1])我们选取的数字上一行选取索引为 m-1 的数字的时候,下一行选取的就是 j-m(因为上下两行本来是升序的,但是我们在选点的时候是上一行升序选取,下一行倒序为降序选取的,这里要把索引值对应起来)
    • 这里的二分的思路其实应该看(dp[i][j-m],dp[i-1][m-1])两者之间的差值,二者作差的差值是单调的!!!
    • 然后我就直接标记 l=0,r=j-1 作为边界,然后取mid值,其实我这里比大小的思想就是看差值的~利用这个单调性进行二分
    • 根据三张图来看,有一种情况是不重合的情况,这里单独做了一个abs(l-r)==1的条件判断,也返回一个值在这里!
  • 改变思路,从尝试k个棋子n层楼找方案,改变为k个棋子尝试t次可以满足最高的层高i,只有满足到层高n才算结束,t则是最终结果。

方法一:动态规划(超时or超空间)(传统二维数组解法)

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     * 返回最差情况下扔棋子的最小次数
     * @param n int整型 楼层数
     * @param k int整型 棋子数
     * @return int整型
     */
    int solve(int N, int K) {
        // write code here
        vector<vector<int>> dp(K+1,vector<int>(N+1,N));
        for(int i=0;i<=K;i++) dp[i][0]=0;        // 层高=0的时候不需要次数
        for(int i=0;i<=N;i++) dp[0][i]=0;        // 棋子=0的时候不需要次数
        for(int i=1;i<=K;i++) dp[i][1]=1;        // 层高=1的时候只需要试1次
        for(int i=1;i<=N;i++) dp[1][i]=i;        // 棋子=1的时候则需要试i次

        for(int i=2;i<=K;i++){
            for(int j=2;j<=N;j++){
                for(int m=1;m<=j;m++){
                    dp[i][j]=min(dp[i][j],max(1+dp[i][j-m],1+dp[i-1][m-1]));    // 根据转移方程列出的表示式
                }
        return dp[K][N];
    }
};

复杂度分析——最终超时超空间

  • 时间复杂度:O(kN^2),三重循环的时间代价
  • 空间复杂度:O(KN),二维数组存储每个解

方法二:动态规划+二分优化(超空间不超时)

我们在原有的基础上采用二分的思想来做最小值的选取比较查找,由于在求min的过程中,dp(k-1,i-1)随着i的增大而增大,dp(k,n-i)随着i的增大而减小,因此在求这两者最大的最小时,他们两个值只要谁高谁就不能取,但是还是要取这两个高的部分里面最小的,我们定义为权衡值。

  • 两者随i的变化是有序的变化,因此考虑用二分的方法来求
  • i-1 = mid时候,n-i = n-1-mid,所以两个值变成了dp(k-1,mid) dp(k, n-i-mid)在比大小
    • 如果dp(k-1,mid)小,由于它单增,则权衡值在mid右侧,则l = mid再循环查找
    • 如果dp(k-1,mid)大,由于它单增,则权衡值在mid左边,则r = mid再循环查找
      直到abs(l-r) == 1时候,当前的值就达到了权衡值的要求
class Solution {
public:
    int solve(int N, int K) {
        vector<vector<int>> dp(K+1,vector<int>(N+1,N));
        for(int i=0;i<=K;i++) dp[i][0]=0;        // 层高=0的时候不需要次数
        for(int i=0;i<=N;i++) dp[0][i]=0;        // 棋子=0的时候不需要次数
        for(int i=1;i<=K;i++) dp[i][1]=1;        // 层高=1的时候只需要试1次
        for(int i=1;i<=N;i++) dp[1][i]=i;        // 棋子=1的时候则需要试i次

        for(int i=2;i<=K;i++){
            for(int j=2;j<=N;j++){
                // for(int m=1;m<=j;m++){
                //     dp[i][j]=min(dp[i][j],max(1+dp[i][j-m],1+dp[i-1][m-1]));
                // }

                // 看上面第三重循环的遍历范围是  dp[i-1][0] ~ dp[i-1][j-1] 因此对应上一行选取的dp数组用的是x=0 ~ j-1
                //               所以对应关系为  dp[i][j-1] ~ dp[i][0]
                // x = 0 to j-1 找所有对儿 (每对组合中最大值)的最小值:上一行索引=x,下一行索引=j-1-x
                int l=0;
                int r=j-1;
                while(l<r){                //二分查找
                    int mid=(l+r)/2;
                    int idx=j-1-mid;
                    if(abs(l-r)==1) {dp[i][j]=1+max(dp[i][l],dp[i-1][r]);break;}    // 查找结束
                    if(dp[i-1][mid]==dp[i][idx]) {dp[i][j]=dp[i][idx]+1;break;}     // 直接找到最终结果
                    if(dp[i-1][mid]>dp[i][idx]) {r=mid;continue;}  //如果dp(k-1,mid)大,由于它单增,则权衡值在mid左侧,则r = mid再循环查找 
                    if(dp[i-1][mid]<dp[i][idx]) {l=mid;continue;}  //如果dp(k-1,mid)小,由于它单增,则权衡值在mid右侧,则l = mid再循环查找
                }

            }
        }
        return dp[K][N];
    }
};

复杂度分析

  • 时间复杂度:O(knlogn),我们将前一种方案寻找min的方法的O(n)量级缩小到了O(logN)
  • 空间复杂度:O(kn),我们需要kn的空间状态存储每个解

方法三:改变思路为【棋子方法凑层高】(通过)

图片说明

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     * 返回最差情况下扔棋子的最小次数
     * @param n int整型 楼层数
     * @param k int整型 棋子数
     * @return int整型
     */
    int calF(int k, int t) {
        if(t == 1 || k == 1) return t + 1;            // 如果只有一次尝试机会或者棋子只有一个,则只能确定尝试机会+1数量的楼层哪一层棋子会碎
        return calF(k - 1, t - 1) + calF(k, t - 1);   // 非上述情况则递归(棋子-1,则机会-1)和(棋子没碎,机会-1)
    }

    int solve(int n, int k) {
        // write code here
        int t = 1;
        while(calF(k, t) < n + 1) t++;                // 凑够n层的时候输出尝试的次数
        return t;
    }
};

复杂度分析

  • 时间复杂度:O(nt),t就是最终的结果,其实就是尝试t的过程,一直给t赋值+1然后凑到n层为止
  • 空间复杂度:O(t),递归栈空间的大小
不会做题写的题解 文章被收录于专栏

哎呀我好笨呀,一不小心又不会了一道题

全部评论
如果觉得方法而有些难理解(至少我认为),可以尝试在坐标轴第一象限画出一个形成“X”图像的一条递增曲线与一条递减曲线,向左为X轴、向上为Y轴,递增曲线为dp[k-1][i-1],递减曲线为dp[k][n-i]。 当取两者之中的最大值时,等价于取垂直于X轴线段交于两直线的两点的最大值点。再取所有x=i的最小值,等价于求取图像上Y>=交点Y值的图像“V”的最小值。所以才有求取权衡值这个概念,即求取交点Y值。如果交点X为整数,那么最终会在第一个if时判断出(max中总有一个为最小值);如果交点X非整数,那即取最靠近交点的两个值(V曲线上的Y值)的最小值即可。
1 回复 分享
发布于 2021-11-26 00:30
话说大兄弟动图不是那么好描述,跳得太快了,解析很好就是动图太不舒服了
点赞 回复 分享
发布于 2022-01-27 15:19
方法三可以改成动态规划的形式,避免很多重复的计算
点赞 回复 分享
发布于 2022-02-09 16:49
不是很理解
点赞 回复 分享
发布于 2022-06-24 21:36
第三种 真不是很理解
点赞 回复 分享
发布于 2022-06-24 21:36
消耗一次机会(丢一次鸡蛋),要么碎cal(k-1,t-1),要么不碎cal(k,t-1),这两无法同时满足啊,为啥要相加啊~!!!!。
点赞 回复 分享
发布于 2022-06-24 21:39
同问,第三种,碎 不碎 是or的关系,为什么要相加呢?
点赞 回复 分享
发布于 2022-08-15 23:37

相关推荐

小红书 后端选手 n*16*1.18+签字费期权
点赞 评论 收藏
分享
我即大橘:耐泡王
点赞 评论 收藏
分享
过往烟沉:我说什么来着,java就业面就是广!
点赞 评论 收藏
分享
15 2 评论
分享
牛客网
牛客企业服务