动态规划(十八)可行路径数

问题描述

本问题对应 LeetCode 1575. 统计所有可行路径](https://leetcode-cn.com/problems/count-all-possible-routes/)。

具体描述如下:给定一个互不相同的整数数组 locations,其中 locations[i] 表示第 i 个 城市的位置,同时给定三个参数 startfinishfuel 分别表示出发城市、目的城市和初始的汽油量。在每一步中,可以任意选择一个一个城市 j,从上一座城市 i 移动到城市 j 需要消耗的汽油量为 |locations[j] - locations[i]||x| 表示 x 的绝对值。现在需要编写一个函数,求得从 startfinish 之间满足条件的可能行驶方案的总数。

前提条件:

  1. 可以经过一个城市多次,包括 startfinish,但是不能每次必须在城市之间进行移动
  2. 必须确保在城市之间移动的汽油量 fuel 不能是负的
  3. 由于答案可能会很大,因此需要对最终的结果对 1e9 + 7 进行取余操作

解决思路

对于路径查找的问题,首先会想到使用 dfs 的方式遍历所有的可能路径,找到符合条件的路径进行统计即可。

  • 一般DFS

    按照一般的 DFS 的方法进行处理即可,但是需要注意以下几点:

    • 由于可以在起始位置和结束位置来回,因此目的城市的位置不是 DFS 的结束条件
    • 由于需要保证有充足的汽油量在城市之间进行走动,因此汽油量就是 DFS 的终止条件
    • 每当有一条路径能够到达目的城市时,说明至少存在这么一条路径
  • 带记忆化的 DFS

    一般的 DFS 解法在这个问题中会超时,这是因为在 DFS 的过程中大量重复计算了之前的已经计算过的情况,由于重复计算的原因导致上文提到的一般的 DFS 解决思路的时间复杂度是指数级别的。

    对于这种由于重复计算导致高额的时间复杂度的情况,一般的解决方案都是使用动态规划的方式记录之前的计算结果,这样可以可以有效降低算法的时间复杂度。

    定义二维数组 dp[pos][rest] 表示当前的城市位置为 pos、可用汽油量为 rest 的条件下,可以到达目的城市的路径总数。记

    \[cost_{pos,i} = |locations[pos] - locations[i]| \]

    表示从 posi 需要消耗的汽油量,那么 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)\)

posted @ 2021-10-31 16:57  FatalFlower  阅读(95)  评论(0编辑  收藏  举报