基本技法
/**
*
* 【鸡蛋掉落 - 基本方法】
*
* 描述:
* 基本方式
* 思路:
* 从x0层开始扔鸡蛋。
* 如果鸡蛋碎裂。 那么步数为 1 + dp(k - 1, x0);
* 如果鸡蛋没有碎裂。 那么步数为 1 + dp(k, n - x0);
* 则要满足 碎 和 没碎,都走完的步数为 max(1 + dp(k - 1, x0), 1 + dp(k, n - x0))
* 那么最小的次数. 所有的x0【1-n】中, 次数最小的。
* 基本方程:
* dp = min(
* max(1 + dp(k - 1, x0), 1 + dp(k, n - x0))
* max(1 + dp(k - 1, x1), 1 + dp(k, n - x1)),
* ...
* )
*
*/
function dp(k, n) {
// 结束条件
if (n === 0 || n === 1 || k === 1) return n;
// 初始化
let minTimes = n;
// dp
for (let x = 1; x <= n; x++) {
// 碎 和 不碎的步数。 // 最大值为,第x层开始扔,完成整栋楼层,所需要的步数。
const xtimes = Math.max(1 + dp(k - 1, x - 1), 1 + dp(k, n - x));
// 从x(1,,,n)楼层中 开扔, 最小的次数。
minTimes = Math.min(minTimes, xtimes) ;
}
return minTimes;
}
// -----> 测试用例
const res = dp(2, 6);
console.log('========res=========', res);
备忘录版本
由于上面的递归,都会递归到dp(1, n) 或者 dp(n, 1).
所以其中,有重复的部分。我们用cache 缓存计算过的结果。 这就是备忘录。
/**
* 添加备忘录
* @param {*} k
* @param {*} n
*/
function dpEggs(k, n) {
const memo = {};
return dpMemo(k, n, memo);
}
function dpMemo(k, n, memo) {
// 结束条件
if (k === 1 || n === 0 || n === 1) return n;
// 从备忘录中获取
if (memo[`${n}-${k}`]) return memo[`${n}-${k}`];
// dp
let minTimes = n;
for (let x = 1; x <= n; x++) {
const xtimes = 1 + Math.max(dpMemo(k - 1, x - 1, memo), dpMemo(k, n - x, memo));
minTimes = Math.min(minTimes, xtimes);
}
// 添加第n层楼的备忘录
memo[`${n}-${k}`] = minTimes;
return minTimes;
}
// -----> 测试用例
const result = dpEggs(2, 100);
console.log('========result=========', result);
优化版 【减少计算版本】
我们看一下上面的代码
for (let x = 1; x <= n; x++) {
// 碎 和 不碎的步数。 // 最大值为,第x层开始扔,完成整栋楼层,所需要的步数。
const xtimes = Math.max(1 + dp(k - 1, x - 1), 1 + dp(k, n - x));
// 从x(1,,,n)楼层中 开扔, 最小的次数。
minTimes = Math.min(minTimes, xtimes) ;
}
上面的代码,我们x都是从1层开始扔,扔到第n层.
那我们什么时候最小次数呢,由 max(1 + dp(k - 1, x0), 1 + dp(k, n - x0)) 可以猜测,尽当1 + dp(k - 1, x0) 和 1 + dp(k, n - x0)) 差不多相等时候,步数才是最小的。
/**
*
* 【二分优化法 - 寻找合适的x, 减少计算】
*
* 描述:
*
* 思路:
* 扔鸡蛋的步数
* times = max(1 + dp(k - 1, x - 1), 1 + dp(k, n - x));
* 仅当 dp(k - 1, x - 1) 【碎】 和 dp(k, n - x)) 【不碎】差不多的时候,
* 此时的times, dp(k, n)才是最小的.
*/
function dpEgg(k, n) {
// 结束条件
if (k === 1 || n === 0 || n === 1) return n;
// 当 dp(k - 1, x - 1) 和 dp(k, n - k)差不多时,此时的x0, x1.
const {x0, x1} = halfSearch(k, n);
const x0Times = 1 + Math.max(dpEgg(k - 1, x0 - 1), dpEgg(k, n - x0));
const x1Times = 1 + Math.max(dpEgg(k - 1, x1 - 1), dpEgg(k, n - x1));
// 最小次数
return Math.min(x0Times, x1Times);
}
/**
*
* 思路:
* 二分搜索. 查找:
* dp(k - 1, x - 1) 和 dp(k, n - x)差不多时,
* 此时的 {x0, x1}
* 可以结合下面的图理解
* @param {*} k
* @param {*} n
*/
function halfSearch(k, n) {
let clow = 1, chigh = n;
while (clow + 1 < chigh) {
// 二分法,中间值
const middle = (clow + chigh) >> 1;
const t1 = dpEgg(k - 1, middle - 1);
const t2 = dpEgg(k, n - middle);
if (t1 > t2) {
chigh = middle;
} else if (t1 < t2) {
clow = middle;
} else {
clow = chigh = middle;
}
}
return { x0: clow, x1: chigh };
}
const result = dpEgg(2, 6);
console.log('========result=========', result);
优化版 【减少计算版本 - 备忘录版】
/**
* 备忘录版本
* @param {*} k
* @param {*} n
* @returns
*/
function dpEggOptimize(k, n) {
// 初始化备忘录
const memo = {};
// 添加备忘录
return dpEggMemo(k, n, memo);
}
function dpEggMemo(k, n, memo) {
// 结束条件
if (k === 1 || n === 0 || n === 1) return n;
// 备忘录中存在
const cacheKey = `${n}-${k}`;
if (memo[cacheKey]) return memo[cacheKey];
// 当 dp(k - 1, x - 1) 和 dp(k, n - k)差不多时,此时的x0, x1.
const {x0, x1} = halfSearchMemo(k, n, memo);
const x0Times = 1 + Math.max(dpEggMemo(k - 1, x0 - 1, memo), dpEggMemo(k, n - x0, memo));
const x1Times = 1 + Math.max(dpEggMemo(k - 1, x1 - 1, memo), dpEggMemo(k, n - x1, memo));
memo[cacheKey] = Math.min(x0Times, x1Times);
return memo[cacheKey];
}
function halfSearchMemo(k, n, memo) {
let clow = 1, chigh = n;
while (clow + 1 < chigh) {
// 二分法,中间值
const middle = (clow + chigh) >> 1;
const t1 = dpEggMemo(k - 1, middle - 1, memo);
const t2 = dpEggMemo(k, n - middle, memo);
if (t1 > t2) {
chigh = middle;
} else if (t1 < t2) {
clow = middle;
} else {
clow = chigh = middle;
}
}
return { x0: clow, x1: chigh };
}
const result = dpEggOptimize(2, 100);
console.log('========result=========', result);