动态规划(十八)可行路径数
问题描述
本问题对应 LeetCode 1575. 统计所有可行路径](https://leetcode-cn.com/problems/count-all-possible-routes/)。
具体描述如下:给定一个互不相同的整数数组 locations
,其中 locations[i]
表示第 i
个 城市的位置,同时给定三个参数 start
、finish
、fuel
分别表示出发城市、目的城市和初始的汽油量。在每一步中,可以任意选择一个一个城市 j
,从上一座城市 i
移动到城市 j
需要消耗的汽油量为 |locations[j] - locations[i]|
。|x|
表示 x
的绝对值。现在需要编写一个函数,求得从 start
到 finish
之间满足条件的可能行驶方案的总数。
前提条件:
- 可以经过一个城市多次,包括
start
和finish
,但是不能每次必须在城市之间进行移动 - 必须确保在城市之间移动的汽油量
fuel
不能是负的 - 由于答案可能会很大,因此需要对最终的结果对
1e9 + 7
进行取余操作
解决思路
对于路径查找的问题,首先会想到使用 dfs
的方式遍历所有的可能路径,找到符合条件的路径进行统计即可。
-
一般
DFS
按照一般的
DFS
的方法进行处理即可,但是需要注意以下几点:- 由于可以在起始位置和结束位置来回,因此目的城市的位置不是
DFS
的结束条件 - 由于需要保证有充足的汽油量在城市之间进行走动,因此汽油量就是
DFS
的终止条件 - 每当有一条路径能够到达目的城市时,说明至少存在这么一条路径
- 由于可以在起始位置和结束位置来回,因此目的城市的位置不是
-
带记忆化的
DFS
一般的
DFS
解法在这个问题中会超时,这是因为在DFS
的过程中大量重复计算了之前的已经计算过的情况,由于重复计算的原因导致上文提到的一般的DFS
解决思路的时间复杂度是指数级别的。对于这种由于重复计算导致高额的时间复杂度的情况,一般的解决方案都是使用动态规划的方式记录之前的计算结果,这样可以可以有效降低算法的时间复杂度。
定义二维数组
dp[pos][rest]
表示当前的城市位置为pos
、可用汽油量为rest
的条件下,可以到达目的城市的路径总数。记\[cost_{pos,i} = |locations[pos] - locations[i]| \]表示从
pos
到i
需要消耗的汽油量,那么dp[pos][i]
的状态转换函数如下所示:\[dp[pos][rest] = \sum_{i=0}^{n-1} dp[i][rest -cost_{pos,i}] (其中 rest >= cost_{pos,i}) \]边界情况:当当前所处的城市的位置为
finish
时,此时至少存在一种可能的路径,因此对dp[finish][rest]
需要额外加一进一步的优化:(原题并没有描述这一特征,但是在官方的题解中介绍到了)在两个城市之间穿梭,在最短的距离中的耗油量是最小的,因此在两个城市之间可能的路径数在这种情况下的数量是最多的(多余的
fuel
可以进行更多的路径移动),因此如果在搜索过程中发现有耗油量大于这个值的,那么就可以直接忽略掉,即dp[pos][rest] = 0
。
实现
-
一般
DFS
class Solution { private final static int mod = (int)(1e9 + 7); private int target; // 目标城市位置 public int countRoutes(int[] locations, int start, int finish, int fuel) { target = finish; return dfs(locations, start, fuel); } /** * @param location : 城市的位置信息列表 * @param cur : 当前所处的城市位置 * @param curFuel:当前行驶过程可用的汽油量 */ private int dfs(int[] locations, int cur, int curFuel) { int sum = 0, take = 0; if (cur == target) sum = 1; // 这里是边界情况,处于目的城市时至少存在一条可能的路径 for (int i = 0; i < locations.length; ++i) { if (i == cur) continue; take = Math.abs(locations[cur] - locations[i]); // 从当前城市 cur 到城市 i 需要花费的汽油量 if (take > curFuel) continue; // 要保证能够从一个城市到另一个城市 sum += dfs(locations, i, curFuel - take); // 递归搜索即可 sum %= mod; } return sum; } }
复杂度分析,由于对每个位置都需要进行
DFS
搜索,因此时间复杂度为 $ O(n^fuel) $ (其中,n 表示城市列表的长度,fuel 表示初始的可用汽油量) -
带记忆化的
DFS
class Solution { private static final int mod = (int)(1e9 + 7); int[][] dp; int n; public int countRoutes(int[] locations, int start, int finish, int fuel) { n = locations.length; dp = new int[n][fuel + 1]; for (int i = 0; i < n; ++i) Arrays.fill(dp[i], -1); // 将它填充为 -1,这是为了区分可能路径数为 0 和已经访问过这两种情况 return dfs(locations, start, finish, fuel); } private int dfs(int[] locations, int pos, int finish, int rest) { if (dp[pos][rest] != -1) // 不为 -1 表示已经被访问过了,直接拿取结果即可 return dp[pos][rest]; dp[pos][rest] = 0; // 标记为已经访问过这个情况 if (Math.abs(locations[pos] - locations[finish]) > rest) // 耗油量最小的情况是可能路径数最大的 return 0; for (int i = 0; i < n; ++i) { if (pos == i) continue; // 必须移动到别的城市 int cost = Math.abs(locations[pos] - locations[i]); if (cost > rest) continue; dp[pos][rest] += dfs(locations, i, finish, rest - cost); dp[pos][rest] %= mod; } // 注意这里的边界情况 if (pos == finish) { dp[pos][rest] += 1; dp[pos][rest] %= mod; } return dp[pos][rest]; } }
复杂度分析:相比较一般的
DFS
方式的搜索,通过动态规划的方式来记录之前的访问情况,大幅度降低了计算的时间复杂度,最终时间复杂度为 \(O(fuel*n^2)\)