leetcode刷题(127)——1575. 统计所有可行路径,DFS解法
给你一个 互不相同 的整数数组,其中 locationsi 表示第 i 个城市的位置。同时给你 start,finish 和 fuel 分别表示出发城市、目的地城市和你初始拥有的汽油总量
每一步中,如果你在城市 i ,你可以选择任意一个城市 j ,满足 j != i 且 0 <= j < locations.length ,并移动到城市 j 。从城市 i 移动到 j 消耗的汽油量为 |locationsi - locationsj|,|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
示例 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
示例 3:
输入:locations = [5,2,1], start = 0, finish = 2, fuel = 3
输出:0
解释:没有办法只用 3 单位的汽油从 0 到达 2 。因为最短路径需要 4 单位的汽油。
提示:
2 <= locations.length <= 100
1 <= locationsi <= 109
所有 locations 中的整数 互不相同 。
0 <= start, finish < locations.length
1 <= fuel <= 200
DFS 方法签名设计:
int dfs(int[] ls, int u, int end, int fuel) {}
其中,ls 参数和 end 参数分别代表源输入的 locations 和 finish,在整个 DFS 过程都不会变化,属于不变参数。
而 u 参数和 fuel 参数则是代表了 DFS 过程中的当前位置和当前油量,属于变化参数。
因此我们可以定一个 f 二维数组,来分别表示两个可变参数。
第一维代表当前位置(对应 locations 数组的下标),第二维代表当前剩余油量。
二维数组中存储的就是我们的 DFS 方法的返回值(路径数量)。
同时结合题意,不难得知维度的取值范围:
第一维的取值范围为 [0,locations.length)
第二维的取值范围为0,fuel
做完这一步的”翻译“工作,我们就得到了「动态规划」的「状态定义」了。
fi 代表从位置 出发,当前剩余油量为 的前提下,到达目的地的路径数量。
不知道你是否发现,这个「状态定义」和我们「记忆化搜索」中的缓存器的定义是一致的。
接下来我们要从 DFS 中”翻译“出「状态转移方程」。
所谓的「状态转移方程」其实就是指如何从一个状态转移到另外一个状态。
而我们的 DFS 主逻辑就是完成这个转移的。
DFS 中的主逻辑很简单:枚举所有的位置,看从当前位置 出发,可以到达的位置有哪些。
于是我们很容易就可以得出状态转移方程:
fi = fi + fk
k 代表计算位置 i 油量 fuel 的状态时枚举的「下一位置」,need 代表从 i 到达 k 需要的油量。
从状态转移方程可以发现,在计算 fi 的时候依赖于 fk。
其中 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];
}
}
至此,我们只利用 DFS 的方法签名与主逻辑,就写出了「动态规划」解法。
我再帮你来总结一下这个过程:
其中第一点对应了「动态规划」的「状态定义」,第二点对应了「动态规划」的「状态方程转移」。
到目前为止,我们已经掌握了两种求解「动态规划」问题的方法:
能够去猜「状态定义」或者使用「记忆化搜索」求解,都有一个大前提:问题本身具有无效性。
由于「动态规划」的状态定义猜测,是一门很讲求经验的技能。
因此对于那些你接触过的模型,我建议你使用第一种方式;
如果遇到一道你从来没接触过的题目时,我建议你先想想「记忆化搜索」该如何实现,然后反推出「动态规划」。
这里说的想想「记忆化搜索」该如何实现,不需要真正动手实现一个「记忆化搜索」解法,而只需要想清楚,如果使用「记忆化搜索」的话,我的 DFS 函数签名如何设计即可。
当搞清楚「记忆化搜索」的函数签名设计之后,「状态定义」部分基本就已经出来了,之后的「状态转移方程」就还是一样的分析方法。
当然,如果你觉得「记忆化搜索」更好实现的话,大可直接使用「记忆化搜索」求解,不一定需要将其转化为「动态规划」。
因为由「记忆化搜索」直接转过来的「动态规划」,两者复杂度是一样的。而且通常「记忆化搜索」的实现难度通常要低很多。