• leetcode刷题(128)——1575. 统计所有可行路径,动态规划解法


    leetcode刷题(127)——1575. 统计所有可行路径,DFS解法

    给你一个 互不相同 的整数数组,其中 locations[i] 表示第 i 个城市的位置。同时给你 start,finish 和 fuel 分别表示出发城市、目的地城市和你初始拥有的汽油总量

    每一步中,如果你在城市 i ,你可以选择任意一个城市 j ,满足 j != i 且 0 <= j < locations.length ,并移动到城市 j 。从城市 i 移动到 j 消耗的汽油量为 |locations[i] - locations[j]|,|x| 表示 x 的绝对值。

    请注意, fuel 任何时刻都 不能 为负,且你 可以 经过任意城市超过一次(包括 start 和 finish )。

    请你返回从 start 到 finish 所有可能路径的数目。

    由于答案可能很大, 请将它对 10^9 + 7 取余后返回。

    示例 1:

    输入:locations = [2,3,6,8,4], start = 1, finish = 3, fuel = 5
    输出:4
    解释:以下为所有可能路径,每一条都用了 5 单位的汽油:
    1 -> 3
    1 -> 2 -> 3
    1 -> 4 -> 3
    1 -> 4 -> 2 -> 3
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    示例 2:

    输入:locations = [4,3,1], start = 1, finish = 0, fuel = 6
    输出:5
    解释:以下为所有可能的路径:
    1 -> 0,使用汽油量为 fuel = 1
    1 -> 2 -> 0,使用汽油量为 fuel = 5
    1 -> 2 -> 1 -> 0,使用汽油量为 fuel = 5
    1 -> 0 -> 1 -> 0,使用汽油量为 fuel = 3
    1 -> 0 -> 1 -> 0 -> 1 -> 0,使用汽油量为 fuel = 5
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    示例 3:

    输入:locations = [5,2,1], start = 0, finish = 2, fuel = 3
    输出:0
    解释:没有办法只用 3 单位的汽油从 0 到达 2 。因为最短路径需要 4 单位的汽油。
    
    • 1
    • 2
    • 3

    提示:

    2 <= locations.length <= 100
    1 <= locations[i] <= 109
    所有 locations 中的整数 互不相同 。
    0 <= start, finish < locations.length
    1 <= fuel <= 200

    动态规划

    DFS 方法签名设计:

    int dfs(int[] ls, int u, int end, int fuel) {}
    
    • 1

    其中,ls 参数和 end 参数分别代表源输入的 locations 和 finish,在整个 DFS 过程都不会变化,属于不变参数。

    而 u 参数和 fuel 参数则是代表了 DFS 过程中的当前位置和当前油量,属于变化参数。

    因此我们可以定一个 f[][] 二维数组,来分别表示两个可变参数

    第一维代表当前位置(对应 locations 数组的下标),第二维代表当前剩余油量。

    二维数组中存储的就是我们的 DFS 方法的返回值(路径数量)。

    同时结合题意,不难得知维度的取值范围:

    第一维的取值范围为 [0,locations.length)
    第二维的取值范围为[0,fuel]

    做完这一步的”翻译“工作,我们就得到了「动态规划」的「状态定义」了。

    f[i][j] 代表从位置 出发,当前剩余油量为 的前提下,到达目的地的路径数量。

    不知道你是否发现,这个「状态定义」和我们「记忆化搜索」中的缓存器的定义是一致的。

    接下来我们要从 DFS 中”翻译“出「状态转移方程」。

    所谓的「状态转移方程」其实就是指如何从一个状态转移到另外一个状态。

    而我们的 DFS 主逻辑就是完成这个转移的。

    DFS 中的主逻辑很简单:枚举所有的位置,看从当前位置 出发,可以到达的位置有哪些。

    于是我们很容易就可以得出状态转移方程:

    f[i][fuel] = f[i][fuel] + f[k][fuel-need]

    k 代表计算位置 i 油量 fuel 的状态时枚举的「下一位置」,need 代表从 i 到达 k 需要的油量。

    从状态转移方程可以发现,在计算 f[i][fuel] 的时候依赖于 f[k][fuel-need]。

    其中 i 和 k 并无严格的大小关系,而 fuel 和 fuel - need 具有严格的大小关系(fuel>=fuel-need)。

    因此我们需要先从小到大枚举油量这一维。

    class Solution {
        int mod = 1000000007;
        public int countRoutes(int[] ls, int start, int end, int fuel) {
            int n = ls.length;
    
            // f[i][j] 代表从位置 i 出发,当前油量为 j 时,到达目的地的路径数
            int[][] f = new int[n][fuel + 1];
            
            // 对于本身位置就在目的地的状态,路径数为 1
            for (int i = 0; i <= fuel; i++) f[end][i] = 1;
    
            // 从状态转移方程可以发现 f[i][fuel]=f[i][fuel]+f[k][fuel-need]
            // 在计算 f[i][fuel] 的时候依赖于 f[k][fuel-need]
            // 其中 i 和 k 并无严格的大小关系
            // 而 fuel 和 fuel-need 具有严格大小关系:fuel >= fuel-need
            // 因此需要先从小到大枚举油量
            for (int cur = 0; cur <= fuel; cur++) {
                for (int i = 0; i < n; i++) {
                    for (int k = 0; k < n; k++) {
                        if (i != k) {
                            int need = Math.abs(ls[i] - ls[k]);
                            if (cur >= need) {
                                f[i][cur] += f[k][cur-need];
                                f[i][cur] %= mod;
                            }
                        }
                    }
                }
            }
            return f[start][fuel];
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    在这里插入图片描述
    至此,我们只利用 DFS 的方法签名与主逻辑,就写出了「动态规划」解法。

    我再帮你来总结一下这个过程:

    1. 从 DFS 方法签名出发。分析哪些入参是可变的,将其作为 DP 数组的维度;将返回值作为 DP 数组的存储值。

    2. 从 DFS 的主逻辑可以抽象中单个状态的计算方法。

    其中第一点对应了「动态规划」的「状态定义」,第二点对应了「动态规划」的「状态方程转移」。

    到目前为止,我们已经掌握了两种求解「动态规划」问题的方法:

    1. 根据经验猜一个「状态定义」,然后根据「状态定义」去推导一个「状态转移方程」。

    2. 先写一个「记忆化搜索」解法,再将「记忆化搜索」改写成「动态规划」。

    能够去猜「状态定义」或者使用「记忆化搜索」求解,都有一个大前提:问题本身具有无效性。

    由于「动态规划」的状态定义猜测,是一门很讲求经验的技能。

    因此对于那些你接触过的模型,我建议你使用第一种方式;

    如果遇到一道你从来没接触过的题目时,我建议你先想想「记忆化搜索」该如何实现,然后反推出「动态规划」。

    这里说的想想「记忆化搜索」该如何实现,不需要真正动手实现一个「记忆化搜索」解法,而只需要想清楚,如果使用「记忆化搜索」的话,我的 DFS 函数签名如何设计即可。

    当搞清楚「记忆化搜索」的函数签名设计之后,「状态定义」部分基本就已经出来了,之后的「状态转移方程」就还是一样的分析方法。

    当然,如果你觉得「记忆化搜索」更好实现的话,大可直接使用「记忆化搜索」求解,不一定需要将其转化为「动态规划」。

    因为由「记忆化搜索」直接转过来的「动态规划」,两者复杂度是一样的。而且通常「记忆化搜索」的实现难度通常要低很多。

  • 相关阅读:
    SSL/TLS工作原理:密钥、证书、SSL握手
    1024 CSDN 程序员节-知存科技-基于存内计算芯片开发板验证语音识别
    适配器策略模式
    Socket发送缓冲区接收缓冲区快问快答
    冒泡排序和快速排序
    神经网络迭代次数的一个近似关系
    【JVM】类加载相关面试题——类加载过程、双亲委派模型
    低代码和人工智能助力疫情期间抗原自测信息自动化收集和处理
    常用 Git 命令
    详解位段+枚举+联合(接结构体)
  • 原文地址:https://blog.csdn.net/u012124438/article/details/127751757