


我们知道,如果要实现 DFS 的话,通常有以下几个步骤:
以本题为例,我们来剖析一下该如何找 Base Case。
那么如何确立 无效情况 呢?
locations = [0,2,2,2],
start = 0,
finish = 3,
fuel = 1代码:
class Solution {
// 缓存器:用于记录「特定状态」下的结果
// cache[i][fuel] 代表从位置 i 出发,当前剩余的油量为 fuel 的前提下,到达目标位置的「路径数量」
vector<vector<int>> cache;
static constexpr int mod = 1000000007;
public:
int countRoutes(vector<int>& locations, int start, int finish, int fuel)
{
int n = locations.size();
// 初始化缓存器
// 之所以要初始化为 -1
// 是为了区分「某个状态下路径数量为 0」和「某个状态尚未没计算过」两种情况
cache.resize(n,vector<int>(fuel+1,-1));
return dfs(locations, start, finish, fuel);
}
//ls:入参 locations u 当前所在位置(ls 的下标)
//end 目标哦位置(ls 的下标) fuel 剩余油量
//返回值: 在位置 u 出发,油量为 fuel 的前提下,到达 end 的「路径数量」
int dfs(vector<int>& ls, int u, int end, int fuel)
{
// 如果缓存器中已经有答案,直接返回
if (cache[u][fuel] != -1)
return cache[u][fuel];
int n = ls.size();
//base case 1:如果油量为 0,且不在目标位置
// 将结果 0 写入缓存器并返回
if (fuel == 0 &&u != end)
{
cache[u][fuel] = 0;
return 0;
}
// base case 2:油量不为 0,且无法到达任何位置
// 将结果 0 写入缓存器并返回
//当前油量不为0,但是如果当前起点到终点最短距离所需要的油量都不够,那么直接返回0
int minNeed = abs(ls[u] - ls[end]);
if (minNeed>fuel&&fuel!=0)
{
cache[u][fuel] = 0;
return 0;
}
// 计算油量为 fuel,从位置 u 到 end 的路径数量
// 由于每个点都可以经过多次,如果 u = end,那么本身就算一条路径
int sum = (u == end) ? 1 : 0;
for (int i = 0; i < n; i++)
{
if (i != u)
{
int need = abs(ls[i] - ls[u]);
if (fuel >=need)
{
sum += dfs(ls, i, end, fuel-need);
sum %= mod;
}
}
}
cache[u][fuel] = sum;
return sum;
}
};
int dfs(int[] ls, int u, int end, int fuel) {}f[i][fuel]=f[i][fuel]+f[k][fuel-need]代码:
class Solution {
public:
int countRoutes(vector<int>& locations, int start, int finish, int fuel)
{
int n = locations.size();
// f[i][j] 代表从位置 i 出发,当前油量为 j 时,到达目的地的路径数
vector<vector<int>> f(n, vector<int>(fuel + 1,0));
static constexpr int mod = 1000000007;
// 对于本身位置就在目的地的状态,路径数为 1
for (int i = 0; i <= fuel; i++) f[finish][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++)//cur表示油量大小
{
//这里暴力思路:枚举每一个位置作为起点,计算当前起点到终点的所有路径数目
for (int i = 0; i < n; i++)//当前位置起点
{
for (int j = 0; j < n; j++)//当前起点要到达的下一位置
{
if (i != j)//当前位置不能呆在原地不动
{
int need = abs(locations[i] - locations[j]);//当前位置到下一个位置消耗的油量
if (cur >= need)//当前油量能满足到达下一个位置
{
//f[i][cur] 代表从位置 i 出发,当前剩余油量为 cur 的前提下,到达目的地的路径数量。
//j是当前位置的下一个位置,当前位置到达目的地的路径数量应该是下一个位置到达目的地的数量加上自身到达目的地的数量
f[i][cur] += f[j][cur - need];
f[i][cur] %= mod;
}
}
}
}
}
return f[start][fuel];
}
};
f[i][cur] += f[j][cur - need];


f[i][cur] += f[j][cur - need];我再帮你来总结一下这个过程:
其中第一点对应了「动态规划」的「状态定义」,第二点对应了「动态规划」的「状态方程转移」。
我希望你借此好好体会一下「记忆化搜索」与「动态规划」的联系。
今天,我与你分享了如何直接将「记忆化搜索」改成「动态规划」,而无需关心具体的「状态定义」和「状态转移方程」。
到目前为止,我们已经掌握了两种求解「动态规划」问题的方法:
1. 根据经验猜一个「状态定义」,然后根据「状态定义」去推导一个「状态转移方程」。
2. 先写一个「记忆化搜索」解法,再将「记忆化搜索」改写成「动态规划」。
能够去猜「状态定义」或者使用「记忆化搜索」求解,都有一个大前提:问题本身具有无后效性。
由于「动态规划」的状态定义猜测,是一门很讲求经验的技能。
因此对于那些你接触过的模型,我建议你使用第一种方式;
如果遇到一道你从来没接触过的题目时,我建议你先想想「记忆化搜索」该如何实现,然后反推出「动态规划」。
这里说的想想「记忆化搜索」该如何实现,不需要真正动手实现一个「记忆化搜索」解法,而只需要想清楚,如果使用「记忆化搜索」的话,我的 DFS 函数签名如何设计即可。
当搞清楚「记忆化搜索」的函数签名设计之后,「状态定义」部分基本就已经出来了,之后的「状态转移方程」就还是一样的分析方法。
当然,如果你觉得「记忆化搜索」更好实现的话,大可直接使用「记忆化搜索」求解,不一定需要将其转化为「动态规划」。
因为由「记忆化搜索」直接转过来的「动态规划」,两者复杂度是一样的。而且通常「记忆化搜索」的实现难度通常要低很多。