前言
当前所有算法都使用测试用例运行过,但是不保证100%的测试用例,如果存在问题务必联系批评指正~
在此感谢左大神让我对算法有了新的感悟认识!
问题介绍
原问题
给定两个整数,level和chessNum,其中level代表楼层总高,chessNum代表当前你拥有的棋子数量,楼层存在0层,在0层扔棋子坑定,棋子坑定不会摔碎掉,但是从1层开始一直到level层棋子都有可能碎掉,如果棋子碎掉则不能再投掷,如果棋子没有碎掉则可以再次投掷,现在要尝试投掷来确定哪个楼层是刚刚碎掉的临界值楼层,也就是当前楼层往上一定都会碎掉,不高于当前楼层一定不会碎掉(如果只有一个棋子就只能从0层往上尝试了),求 获取临界值楼层在最坏的情况下所需要尝试的最少次数
解决方案
暴力递归
递归的入参有两个,即 level和chessnum,让第一个棋子不断循环尝试每一层,如果是0层则不用尝试,如果是1个棋子则需要一直尝试,剩下的交给chessnum-1颗棋子解决即可。时间空间复杂度都比较高,代码略。
动态规划:
首先动态规划的dp[i][j]代表的含义如果写了暴力递归就比较容易想到,这里我横坐标 i 代表棋子数量,纵坐标 j 代表层高
如果当前棋子扔了第x层,那么就产生两种可能性,碎了和没碎,很显然两种可能性会影响到未来的走向,碎了则少一个棋子,并且当前层上面层都不用再尝试了,所以状态变成了dp[i-1][j- (j-k)],如果没有碎,那么当前层一下的层都不用再尝试了,状态变成了dp[i][j-x]。两种未来在最坏的情况下应该是取最大值的那种未来,然后再取当前棋子尝试每一层产生的 最大值 中的 最小值 作为当前层的结果 dp[i][j]。具体看代码
四边形不等式优化动态规划
在原动态规划基础上,限制第三层循环尝试的x的范围:
·如果dp[i-1][j]获取到最少尝试次数时第一个棋子尝试的楼层数为l,那么dp[i][j]在尝试的时候不需要再尝试l层以下的楼层
·如果dp[i][j+1]获取到最少尝试次数时第一个棋子尝试的楼层数为r,那么dpdp[i][j]在尝试的时候不需要再尝试r层以上的楼层
根据以上两点可以确定尝试x的范围在[l…r],具体四边形不等式的证明非常多,这里不再证明
最优解
最优解并没有使用动态规划,而是换了一个角度看问题,首先如果棋子数量大于层数,那么直接使用二分法是最快的方法,这里可以拦截一波
其次,我们上面的想法都是chessnum个棋子解决level个楼层最少需要多少次,这里将想法转变成 chessnum个棋子扔 k 次能够最多解决level个楼层。因此设计dp[i][j]代表 i个棋子扔j次,能够最多解决的的楼层
· 首先一个棋子扔一次只能解决一层楼,扔两次解决2层,三次三层,dp[1][k]即可解决
· 其次 计算dp[i][j]时可以这么想,每一次的投掷都是最优解才能搞定最多的楼层,dp公式:dp[i][j] = dp[i-1][j-1] + dp[i][j-1] + 1.解释在思考中
· 最后每次计算dp都比较一次大小看看最多搞定的层是否已经超过了规定的需求level,第一次超过时即可返回当前尝试的次数(j)
代码编写
java语言版本
基本动态规划:
public static int dpSolutionCp2(int levelNum, int chessNum) {
if (levelNum < 1 || chessNum < 1) {
return 0;
}
if (chessNum == 1) {
// 剩余一颗棋子在最坏的情况下就要扔层数次
return levelNum;
}
// dp[i][j] 还剩下i个棋子,j层最坏情况下的最少次数
int[][] dp = new int[chessNum][levelNum+1];
for (int i = 0; i < chessNum; i ++) {
dp[i][0] = 0;
}
for (int j = 0; j < levelNum+1; j++) {
dp[0][j] = j;
}
for (int i = 1; i < chessNum; i++) {
for (int j = 1; j <levelNum+1; j ++) {
int min = Integer.MAX_VALUE;
for (int k = 1; k <= j; k++) {
min = Math.min(min, Math.max(dp[i-1][k-1], dp[i][j-k]));
}
dp[i][j] = min+1;
}
}
return dp[chessNum-1][levelNum];
}
四边形不等式优化动态规划
/**
* 二轮测试-方法4:四边形不等式
* @return
*/
public static int solutionCp4(int levelNum, int chessNum) {
if (levelNum < 1 || chessNum < 1){
return 0;
}
if (chessNum == 1){
return levelNum;
}
// dp[i][j] 还剩下i个棋子,j层最坏情况下的最少次数
int[][] dp = new int[levelNum+1][chessNum+1];
int[] cands = new int[chessNum + 1];
for (int i = 1; i < levelNum + 1; i++) {
dp[i][1] = i;
}
for (int j = 1; j < chessNum+1; j++) {
dp[1][j] = 1;
// 最优解都是1
cands[j] = 1;
}
for (int i = 2; i < levelNum + 1; i++) {
for (int j = chessNum; j >= 2; j--) {
int min = Integer.MAX_VALUE;
int down = cands[j];
// todo 为什么?
int up = j == chessNum? i/2 + 1 : cands[j+1];
for (int k = down; k <= up; 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[levelNum][chessNum];
}
最优解
/**
* 二轮测试-最优解
* 换个角度:m个棋子扔k次最多搞定的楼层,第一次超过目标值的次数就是最优解
* @param levelNum
* @param chessNum
* @return
*/
public static int bestSolutionCp5(int levelNum, int chessNum) {
if (levelNum < 1 || chessNum < 1){
return 0;
}
int bsTimes = log2N(levelNum);
// 二分法如果能够解决就用二分法
if (chessNum > bsTimes){
return bsTimes;
}
int[] dp = new int[chessNum+1];
// 上一轮,相当于[i-1,j-1]
int count = 1;
while (true) {
int pre = 0;
for (int i = 1; i < dp.length; i++) {
int tmp = dp[i];
dp[i] = dp[i] + pre + 1;
pre = tmp;
if (dp[i] >= levelNum) {
return count;
}
}
count++;
}
}
/**
* 找到2的几次方边界
* 因为楼层用二分的方式一定能够找到,这里求出二分的方式需要几个棋子,如果给的棋子大于这个数值,那就直接用这个数值
* @param num
* @return
*/
public static int log2N(int num){
int res = -1;
while (num != 0){
num >>>= 1;
res++;
}
return res;
}
c语言版本
正在学习中
c++语言版本
正在学习中
思考感悟
首先当前动态规划方法可以通过优化空间方式进行优化,但是略显难懂所以这里为了能够让算法变得更加容易看懂就使用了经典的动态规划形式,如果想在此基础上进行dp空间维度的优化也是非常简单的但不是当前文章主要表达的内容。
其次最优解的想法很奇特,为什么dp[i][j]的公式是这样的呢?首先扔一次不管碎没碎坑定能够搞定一层楼,消耗一次投掷,这也是为什么一定要+1的原因,其次消耗一次投掷,问题一定变成了 i个棋子扔 j-1次或者 i-1个棋子扔j-1次。最后如果最优解中 第一个棋子碎了 则看 i-1个棋子扔j-1次 最多搞定的楼层数dp[i-1][j-1],没碎则 dp[i][j-1],那么 关键在于 假设当前投掷的层为 a,a层以上用”碎掉的dp“来解决也就是dp[i-1][j-1]来解决能够尝试的最多层高,a层一下用没碎的解决 dp[i][j-1]来解决能够尝试的最多的层高,所以总的来说当前状态能够解决的总楼层就是 a层以上和a层以下能够搞定的总层数+当前搞定的一层的总和!这个我很难理解,也只能解释到这里了,书中的解释我也想了很久,再用自己的话说就是dp[i][j]的含义其实是能够尝试出的最多层高,这句话需要理解。
写在最后
方案和代码仅提供学习和思考使用,切勿随意滥用!如有错误和不合理的地方,务必批评指正~
如果需要git源码可邮件给2260755767@qq.com
再次感谢左大神对我算法的指点迷津!