Skip to content

🍕 动态规划

优化过程(面试中的题目一定可以优化的,生活中的递归不一定可以优化)

  • 暴力递归
  • 记忆化搜索 - 在递归过程中加缓存
  • 严格表结构的动态规划 - 整理依赖关系
    • 有几个可变参数,表示是几个维度的表
    • 计算可变参数的范围,该范围+1就是表的大小
    • 标出表中的默认值,也就是 base case。
    • 推理出普遍的位置是如何依赖其他位置的
    • 确定依次计算的顺序。是从左到右,还是从右到做。是从上到下还是从下到上。

机器人步数问题

一共 1...N 个位置,要你必须只用指定步数走到指定位置,并且每一步必须走,不能停。问有几种方式

暴力递归

java
// 一共有 N 个位置
// 目的地是位置 E
// 还剩 rest 步要走
// 当前在 cur 位置
// 返回值: 方法数 —— 只用 rest 步从 cur 位置走到 E 位置有多少中方法
int f(int N, int E, int rest, int cur) {
    if (rest == 0) {
        // 只剩 0 步可走。如果当前是在 E 位置,说明是一种方法,否则不是一种方法
        return cur == E ? 1 : 0;
    }
    // 如果在 1 位置,则只能往右走
    if (cur == 1) {
        return f(N, E, rest-1, 2)
    }
    if (cur == N) { // 同理,在右边界,则只能往左走
        return f(N, E, rest-1, N-1)
    }
    // 可以往右也可以往左。返回的是方法数,所以要加起来。
    return f(N, E, rest-1, cur-1) + f(N, E, rest-1, cur+1)
}

记忆化搜索

java
// 从 S 位置开始走,只能走 K 步
int walkWays2(int N, int E, int S, int K) {
    int[][] dp = new int[K+1][N+1]
    for (int i = 0; i <= K; i++) {
        for (int j = 0; j <= N; j++) {
            dp[i][j] = -1;
        }
    }
    return f2(N, E, K, S, dp);
}
int f2(int N, int E, int rest, int cur, dp) {
    if (dp[rest][cur] != -1) {
        return dp[rest][cur];
    }

    if (rest == 0) {
        dp[rest][cur] = cur == E ? 1 : 0
        return dp[rest][cur];
    }

    if (cur == 1) {
        dp[rest][cur] =return f(N, E, rest-1, 2)
    } else if (cur == N) {
        dp[rest][cur] = f(N, E, rest-1, N-1)
    } else {
        dp[rest][cur] = f(N, E, rest-1, cur-1) + f(N, E, rest-1, cur+1)
    }
    return dp[rest][cur];
}

严格表结构的动态规划

java
// N 表示有 1...N 个位置。
// 目标是走到 target 位置
// start 表示初始位置
// K 表示只允许 K 步
int walkWays3(int N, int target, int start, int K) {

    // dp[rest][cur] 表示花费 rest 步 走到 cur 位置有多少种走法。
    int[][] dp = new int[K+1][N+1]; // 默认 0

    // 初始位置
    dp[0][start] = 1;

    // 填表
    for (int rest = 1; rest <= K; rest++) {
        for (int cur = 1; cur <= N; cur++) {
            if (cur == 1) { // 走到最左侧,只能往右走
                dp[rest][cur] = dp[rest-1][2];
            } else if (cur == N) { // 走到最右侧,只能往左走
                dp[rest][cur] = dp[rest-1][N-1];
            } else {
                // 中间位置,既可以往左也可以往右。
                dp[rest][cur] = dp[rest-1][cur-1] + dp[rest-1][cur+1];
            }
        }
    }
    // 返回使用 K 步走到 targets 位置。
    return dp[K][target];
}

最少硬币

暴力递归

java
// arr 中如何取出最少数量的硬币凑成 aim 金额。
int minCoins1(int[] arr, int aim) {
    return process1(arr, 0, aim);
}
// rest: 从 [index...] 要凑出的金额
// 返回值: 从 [index...] 凑出 rest 金额所需要的最少硬币
// 返回 -1 表示无解
int process1(int[] arr, int index, int rest) {
    if (rest < 0) { // 要凑成的金额是负数,无解
        return -1;
    }
    if (rest == 0) { // 前面已经凑够了,有解
        return 0; // 不需要再加上我(index) 这个硬币
    }
    if (index == arr.length) { // 没有硬币了
        return -1;
    }

    int p1 = process1(arr, index+1, rest); // 假如不选当前硬币
    int p2 = process1(arr, index+1, rest - arr[index]) // 假如选取当前硬币
    if (p1 == -1 && p2 == -1) { // 两个都无解
        return -1;
    } else {
        if (p1 == -1) { //  p1 无解
            return p2 + 1; // 直接返回 p2+1 ,+1 表示选取了当前硬币
        }
        if (p2 == -1) { // p2 无解
            return p1;
        }
        // 都有解,看谁使用的硬币数量少
        return Math.min(p1, p2 +1)
    }
}

记忆化搜索

java
int minCoins2(int[] arr, int aim) {
    int[][] dp = new int[arr.length+1][aim+1];
    for (int i = 0; i < arr.length; i++) {
        for (int j = 0; j < aim; j++) {
            dp[i][j] = -2;
        }
    }
    return process2(arr, 0, aim, dp);
}
int process2(int[] arr, int index, int rest, int[][] dp) {
    if (rest < 0) { // 下标越界
        return -1;
    }

    if (dp[index][rest] != -2) {
        return dp[index][rest];
    }

    if (rest == 0) {
        dp[index][rest] = 0;
    } else if (index == arr.length) {
        dp[index][rest] = -1;
    } else {

        int p1 = process2(arr, index+1, rest), dp;
        int p2 = process2(arr, index+1, rest - arr[index], dp)
        if (p1 == -1 && p2 == -1) {
            dp[index][rest] = -1;
        } else {
            if (p1 == -1) {
                dp[index][rest] = p2 + 1;
            } else if (p2 == -1) {
                dp[index][rest] = p1;
            } else {
                dp[index][rest] = Math.min(p1, p2 +1);
            }
        }
    }
    return dp[index][rest];
}

严格表结构的动态规划

java
int minCoins3(int[] arr, int aim) {
    int N = arr.length;
    int[][] dp = new int[arr.length+1][aim+1];

    for (int index = 0; index < arr.length; index++) { // if (rest == 0)  dp[index][rest] = 0;
        dp[index][0] = 0; // base case
    }
    for (int rest = 1; rest <= aim; rest++) { // if (index == arr.length)  dp[index][rest] = -1;
        d[N][rest] = -1;
    }


    // 从下到上,从左到右
    for (int index = N-1; index >= 0; index--) {
        for (int rest = 0; rest < aim; rest++) {

            // 因为我们规定了表格的推理方向,所以不需要考虑下面几种情况:
            // if (rest < 0) { return -1; }
            // if (dp[index][rest] != -2) { return dp[index][rest]; }
            // if (rest == 0) { dp[index][rest] = 0; }
            // if (index == arr.length) { dp[index][rest] = -1; }

            int p1 = dp[index+1][rest];
            // 这里因为下标又可以越界,所以需要考虑一下
            int p2 = -1;
            if (rest - arr[index] >= 0) {
                p2 = dp[index+1][rest - arr[index]]
            }

            if (p1 == -1 && p2 == -1) {
                dp[index][rest] = -1;
            } else {
                if (p1 == -1) {
                    dp[index][rest] = p2 + 1;
                } else if (p2 == -1) {
                    dp[index][rest] = p1;
                } else {
                    dp[index][rest] = Math.min(p1, p2 +1);
                }
            }
        }
    }



    return dp[0][aim];
}