丢棋子问题 ——(动态规划)

题目描述

? 一座大楼一共有0~N层,地面算第0层,最高一层为第N层。已知棋子从第0层掉落肯定不会摔碎,从第i层掉落可能回摔碎,也可能不会摔碎(1<=i<=N)。给定整数N作为楼层数,再给定整数K作为棋子数,返回如果想找到棋子不会摔碎的最高层数,即使在最差的情况下仍的最少次数。一次只能仍一个棋子。

例子

N=10, K=1.
返回10。因为只有1颗棋子,所以不得不从第一层开始一直试到第十层,最差情况要扔10次。

N=3, K=2.
返回2。现在第2层仍1颗棋子,如果碎了,试第1层;如果没碎,试第3层。

N=105, K=2.
返回14。

第一颗棋子尝试层数 若第一颗碎了第二颗棋子尝试的层数
第一次尝试 14 1~13
第一次没碎 27 15~26
第二次没碎 39 28~38
第三次没碎 50 40~49
第四次没碎 60 51~59
第五次没碎 69 61~68
第六次没碎 77 70~76
第七次没碎 84 78~83
第八次没碎 90 85~89
第九次没碎 95 91~94
第十次没碎 99 96~98
第十一次没碎 102 100,101
第十二次没碎 104 103
第十三次没碎 105

方法一:暴力算法

? 设P(N,K)的返回值时N层楼时有K个棋子在最差的情况下仍的最少次数。

  1. 如果N==0,棋子在第0层肯定不会碎,所以P(0, K) = 0;
  2. 如果K==1,楼层有N层,只有1个棋子,故只能从第一次开始尝试,P(N,1)=N;
  3. 对于N>0且K>1, 我们需考虑第1个棋子是从那层开始仍的。如果第1个棋子从第i层开始仍,那么有以下两种情况:
    ? (1) 碎了。没必要尝试第i层以上的楼层了,接下来的问题就变成了剩下i-1层楼和K-1个棋子,所以总步数为 1+P(i-1, K-1);
    ? (2)没碎。 那么可以知道没必要尝试第i层及其以下的楼层了,接下来的问题就变成了还剩下N-i层和K个棋子,所以总步数为 1+P(N-i, K).
  4. 根据题意应该选取(1)和(2)中较差的那种情况,即 max{ P(i-1, K-1), P(N-i, K)}+1。 由于i的取值范围是 1到N, 那么步数最少的情况为, P(N, K)=min{ max{P(i-1, K-1), P(N-i, K)}(1<=i<=N) }+1。
** Solution one **
** 时间复杂度 O(N!) **
    public int solutionOne(int N, int K){
        if ( N<1 || K<1 )
            return 0;
        return helper1(N, K);
    }
    private int helper1(int N, int K){
        if ( N==0 ) return 0;
        if ( K==1 ) return N;

        int min = Integer.MAX_VALUE;
        for(int i=1; i<=N; ++i){
            min = Math.min(min, Math.max( helper1(i-1, K-1), helper1(N-i, K)));
        }
        return min+1;
    }

方法二:动态规划

? 通过研究以上递归函数发现, P(N, K)过程依赖于P(0...N-1, K-1) 和 P(0...N-1, K)。所以,若把所有的递归的返回值看作是一个二维数组,可以用动态规划的方法优化递归过程。从而减少计算量。
? dp[0][K] = 0, dp[N][1] = N, dp[N][K] = min( max(dp[i-1][K-1], dp[N-i][K])) + 1。

时间复杂度 O(N^2 * K)

** Solution two **
    public int solutionTwo(int N, int K){
        if ( N<1 || K<1 )
            return 0;
        if ( K == 1 ) return N;
        int[][] dp = new int[N+1][K+1];
        for(int i=1; i<dp.length; ++i) {
            dp[i][1] = i;
        }
        for(int i=1; i<dp.length; ++i) {
            for(int j=2; j<=K; ++j) {
                int min = Integer.MAX_VALUE;
                for(int k=1; k<i+1; ++k) {
                    min = Math.min(min, Math.max(dp[k-1][j-1], dp[i-k][j]));
                }
                dp[i][j] = min + 1;
            }
        }
        return dp[N][K];
    }

方法三:优化的动态规划

? 分析动态规划的过程发现,dp[N][K]只需要它左边的数据dp[0...N-1][K-1] 和它上面一排的数据dp[0...N-1][K]。那么在动态规划计算时,就可以用两个数组不停复用的方式实现,而不需要申请整个二维数组的空间。

** Solution Three **
    public int solutionThree(int N, int K){
        if ( N<1 || K<1 )
            return 0;
        if ( K == 1 ) return N;
        int[] preArr = new int[N+1];
        int[] curArr = new int[N+1];
        for(int i=1; i<curArr.length; ++i) {
            curArr[i] = i;
        }
        for(int i=1; i<K; ++i) {
            int[] tmp = preArr;
            preArr = curArr;
            curArr = tmp;
            for(int j=1; j<curArr.length; ++j){
                int min = Integer.MAX_VALUE;
                for(int k=1; k<j+1; ++k) {
                    min = Math.min(min,  Math.max(preArr[k-1], curArr[j-k]));
                }
                curArr[j] = min + 1;
            }
        }
        return curArr[curArr.length-1];
    }

方法四:四边形不等式优化动态规划

? 方法二和三的时间复杂度还是很高。但我们注意到,求解动态规划表中的值时,有枚举的过程,此时往往可以用“四边形不等式”及其相关猜想来进行优化。
? 优化方式:四边形不等式及其猜想:
? 1. 如果已经求出了 k+1 个棋子在解决 n 层楼时的最少步骤 (dp[n][k+1]), 那么如果在这个尝试的过程中发现,第 1 个棋子仍在 m 层楼的这种尝试导致最终的最优解。 则在求 k 个棋子在解决 n 层楼时 (dp[n][k]), 第 1 个棋子不需要去尝试 m 层以上的楼。
? 举个例子,3个棋子在解决100层楼时,第1个棋子仍在37层楼时最终导致了最优解,那么2个棋子在解决100层楼时,第1个棋子不需要去尝试37层楼以上的楼层。
? 2. 如果已经求出了 k 个棋子在解决 n 层楼时的最少步骤 (dp[n][k]), 那么如果在这个尝试的过程中发现,第 1 个棋子仍在 m 层楼的这种尝试最终导致了最优解。则在求 k 个棋子在解决 n+1 层楼时 (dp[n+1][k]), 不需要尝试 m 层以下的楼。
? 举个例子,2个棋子在解决10层楼时,第1个棋子仍在4层楼时最终导致了最优解。那么2个棋子在解决11层楼或者更多的楼层时,第1个棋子也不需要去尝试1,2和3层楼,只需才4层及其以上楼层开始尝试。
? 也就是说,动态规划表中的两个参数分别为棋子数和楼层数,楼数变多以后,第1个棋子的尝试楼层的下限是可以确定的。棋子变少之后,第1个棋子的尝试楼层的上限也是确定的。这样就省去了还多无效的枚举过程。
? **一般通过“四边形不等式”的优化可以把时间复杂度降低一个维度,可以从 O(N^2 * k) 或 O(N^3) 降低到 O(N^2)。

** Solution Four **
    public int solutionFour(int N, int K){
        if ( N<1 || K<1 )
            return 0;
        if ( K == 1 ) return N;
        int[][] dp = new int[N+1][K+1];
        for(int i=1; i<dp.length; ++i) {
            dp[i][1] = i;
        }
        int[] cands = new int[K+1];
        for(int i=1; i<K+1; ++i) {
            dp[1][i] = 1;
            cands[i] = 1;
        }
        for(int i=2; i<N+1; ++i) {
            for(int j=K; j>1; --j) {
                int min = Integer.MAX_VALUE;
                int minEnum = cands[j];
                int maxEnum = j==K? i/2+1 : cands[j+1];
                for(int k=minEnum; k<maxEnum+1; ++k ) {
                    int cur = Math.max(dp[k-1][j-1], dp[i-k][j]);
                    if (cur<=min) {
                        min = cur;
                        cands[j] = k;
                    }
                }
                dp[i][j] = min+1;
            }
        }
        return dp[N][K];
    }

方法五: 最优解

? 最优解比一上各种方法都快。首先我们换个角度来看这个问题,以上各种方法解决问题是N层楼有K个棋子最少仍多少次。现在反过来看K个棋子如果可以仍M次,最多可以解决多少楼层这个问题。根据上文实现的函数可以生成下表。在这个表中记作map, map[i][j]的意义为i个棋子仍j次最多搞定的楼层数。

棋子数\次数 0 1 2 3 4 5 6 7 8 9 10
1 0 1 2 3 4 5 6 7 8 9 10
2 0 1 3 6 10 15 21 28 36 45 55
3 0 1 3 7 14 25 41 63 92 129 175
4 0 1 3 7 15 30 56 98 162 255 385
5 0 1 3 7 15 31 62 119 218 381 637

? 通过研究map表发现,第一排的值从左到有一次为1,2,3...,第一纵列都为0, 初次之外的其他位置(i, j),都有 map[i][j] == map[i][j-1] + map[i-1][j-1] + 1.
? 将设i个棋子仍j次最多搞定m层楼,“搞定最多”说明每次仍的位置都是最优的且在棋子肯定够用的情况下,若第1个棋子仍在a层楼是最优的。
? 1. 如果第1个棋子以碎,那么就向下,看i-1个棋子扔j-1次最多搞定多少层楼;
? 2. 如果第1个棋子没碎,那么就向上,看i个棋子扔j-1次最多搞定多少层楼;
? 3. a层楼本身也是被搞定的1层;
? 1、2、3的总楼数就是i个棋子扔j次最多搞定的楼数,map表的生成过程极为简单,同时数值增长的极快。原始问题可以通过map表得到很好的解决。
? 例如,想求5个棋子搞定200层楼最少扔多少次的问题,注意到第5行第8列对应的值为218,是第5行的所有值中第一次超过200的值,则可以知道5个棋子搞定200层楼最少扔8次。同时在map表中其实9列10列的值也完全可以不需要计算,因为算到第8列就已经搞定,那么时间复杂度得到进一步的优化。另外还有一个特别重要的优化,我们知道N层楼完全用二分的方式扔logN+1次就直接可以确定哪层楼是会碎的最低层楼,所以当棋子数大于logN+1时,我们就可以返回logN+1.
? 如果棋子数位K,楼数位N, 最终的结果位M次,那么最优解的时间复杂度为O(KxM), 在棋子数大于logN+1时,时间复杂度为O(logN). 在只要1个棋子的时候, KxM等于N, 在其他情况下 KxM要比N得多。

** Solution Five **
    public int solutionFive(int N, int K){
        if ( N<1 || K<1 )
            return 0;
        int bsTimes = log2N(N) + 1;
        if (K >= bsTimes) {
            return bsTimes;
        }
        int[] dp = new int[K];
        int res = 0;
        while (true) {
            ++res;
            int previous = 0;
            for(int i = 0; i < dp.length; ++i) {
                int tmp = dp[i];
                dp[i] = dp[i] + previous + 1;
                previous = tmp;
                if (dp[i] >= N) {
                    return res;
                }
            }
        }
    }

原文地址:https://www.cnblogs.com/willwuss/p/12256475.html

时间: 2024-08-25 08:21:01

丢棋子问题 ——(动态规划)的相关文章

9.32 丢棋子问题

[题目]: 一座大楼有0~N层,地面算作第0层,最高的一层为第N层,已知棋子从第0层掉落肯定不会摔碎,从第i层掉落可能会摔碎,也可能不会摔碎(1<=i<=N),给定整数N作为楼层数,再给定整数K作为棋子数,返回如果想找到棋子不会摔碎的最高层数,即使在最差的情况下扔的最少次数.一次只能扔一个棋子 举例: N=10,K=1 返回10,因为只有1棵棋子,所以不得不从第1层开始一直试到第10层,在最差的情况下,即第10层是不会摔坏的最高层,最少也要扔10次 N=3,K=2 返回2,先在2层扔1棵棋子,

程序员代码面试指南 IT名企算法与数据结构题目最优解 ,左程云著pdf高清版免费下载

下载地址:网盘下载 备用地址:网盘下载 内容简介  · · · · · ·这是一本程序员面试宝典!书中对IT名企代码面试各类题目的最优解进行了总结,并提供了相关代码实现.针对当前程序员面试缺乏权威题目汇总这一痛点,本书选取将近200道真实出现过的经典代码面试题,帮助广大程序员的面试准备做到万无一失.“刷”完本书后,你就是“题王”!__eol__本书采用题目+解答的方式组织内容,并把面试题类型相近或者解法相近的题目尽量放在一起,读者在学习本书时很容易看出面试题解法之间的联系,使知识的学习避免碎片化

经典面试题楼层丢鸡蛋问题的动态规划解法与数学解法

原题: 有2个鸡蛋,从100层楼上往下扔,以此来测试鸡蛋的硬度.比如鸡蛋在第9层没有摔碎,在第10层摔碎了,那么鸡蛋不会摔碎的临界点就是9层. 问:如何用最少的尝试次数,测试出鸡蛋不会摔碎的临界点? 注意:只有两个鸡蛋.第一个鸡蛋碎了,第二个鸡蛋只能挨个楼层测试了. 动态规划解法: //height为楼层数 const int maxHeight = 100; int dp[maxHeight + 5] = { 0 }; for (int height = 1; height <= maxHei

51Nod1306 高楼和棋子 动态规划

原文链接https://www.cnblogs.com/zhouzhendong/p/51Nod1306.html 题目传送门 - 51Nod1306 题意 有个N层的高楼和若干个棋子,所有的棋子都是一样的.棋子从楼的某层E扔到地上不会碎(0 <= E <= N),但从比这个楼层高的地方扔到地上都会碎.给出楼的高度N,以及棋子的数量M,你来找出这个E(0 <= E <= N),问最坏情况下需要实验多少次才能计算出准确的E(如果棋子摔碎了,就不能继续用这个棋子进行测试了). 1 &l

动态规划:背包问题

例题:装箱问题 ( http://www.wikioi.com/problem/1014/  ) 题目描述 有一个箱子容量为V(正整数,0<=V<=20000),同时有n个物品(0<n<=30),每个物品有一个体积(正整数). 要求n个物品中,任取若干个装入箱内,使箱子的剩余空间为最小. 输入描述 一个整数v,表示箱子容量,一个整数n,表示有n个物品 接下来n个整数,分别表示这n 个物品的各自体积 输出描述 一个整数,表示箱子剩余空间. 样例输入 24 6 8 3 12 7 9 7

2017清北学堂集训笔记——动态规划Part2

啊~到下午啦,我们进入Part2!--一个简洁的开头 我们来探讨第一类问题--路径行走问题 经典例题:方格取数(Luogu 1004) 设有 N*N 的方格图 (N<=9),我们将其中的某些方格中填入正整数,而其他的方格中则放入数字 0.* 某人从图的左上角的 A 点出发,可以向下行走,也可以向右走,直到到达右下角的 B 点.在走过的路上,他可以取走方格中的数(取走后的方格中将变为数字 0).* 此人从 A 点到 B 点共走两次,试找出 2 条这样的路径,使得取得的数之和为最大.- 与数字金字塔

学习笔记-动态规划-poj1050-修改中

// // main.c // poj1050 // // Created by 韩雪滢 on 10/13/16. // Copyright © 2016 韩雪滢. All rights reserved. // #include <stdio.h> #include <stdlib.h> #define MAXLEN 10000 /* poj1050 失败的小demo *其中包括 *读取一行字符串直到换行符 *将字符串按照空格拆分,将拆分的char[] 专为 int,正负整数 *

[题解+总结]动态规划大合集II

1.前言 大合集总共14道题,出自江哥之手(这就没什么好戏了),做得让人花枝乱颤.虽说大部分是NOIP难度,也有简单的几道题目,但是还是做的很辛苦,有几道题几乎没思路,下面一道道边看边分析一下. 2.lis 最长上升子序列 唯一一道裸题,但是O(n^2)过不了,临时看了看O(n log n)的二分做法和线段树做法.先来讲讲简单的二分做法,其本质就是在O(n^2)上进行优化,需要证明一个结论.设当前处理数列第k位,存在: (1)a[i]<a[j]<a[k]: (2)i<j<k: (3

黑白棋子的移动(分治)

黑白棋子的移动(chessman) [问题描述] 有2n个棋子(n≥4)排成一行,开始位置为白子全部在左边,黑子全部在右边,如下图为n=5的情形: ○○○○○●●●●● 移动棋子的规则是:每次必须同时移动相邻的两个棋子,颜色不限,可以左移也可以右移到空位上去,但不能调换两个棋子的左右位置.每次移动必须跳过若干个棋子(不能平移),要求最后能移成黑白相间的一行棋子.如n=5时,成为: ○●○●○●○●○● 任务:编程打印出移动过程. [输入样例]chessman.in 7 [输出样例]chessma