菜鸟心得-鸡蛋掉落-官方题解理解【1】

332 阅读1分钟

基本技法

/**
 * 
 * 【鸡蛋掉落 - 基本方法】
 * 
 * 描述:
 *       基本方式
 * 思路:
 *   从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);

image.png

优化版 【减少计算版本 - 备忘录版】

/**
 * 备忘录版本
 * @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);