目录

题目描述

1575. 统计所有可行路径

题意分析

数轴上有 n 座城市,locations[i] 是第 i 座城市的坐标。从 start 出发、油箱里有 fuel 升油,每次可以开到任意一座不同的城市,耗油量等于两地坐标差的绝对值。问总共有多少条不同的路径能到达 finish,答案对 $10^9 + 7$ 取模。

有几处措辞必须抠死。第一,路径可以经过 finish 之后继续开走再回来,只要最终停在 finish 就算数——所以到达 finish 不是终止条件,而是一个可以「记一笔」但仍要继续展开的时刻。第二,城市可以重复访问,路径长度没有上限,唯一的限制是油量。第三,start 可以等于 finish,此时「原地不动」本身就是一条合法路径。

既然能反复往返,路径条数是可以爆炸的,这解释了为什么要取模。

约束给得很克制:n 不超过 100,fuel 不超过 200。两者相乘只有两万,这个数字明确指向「以(当前城市,剩余油量)为状态」的动规——状态数两万,每个状态枚举 100 个下一站,总计两百万次转移,绰绰有余。反过来,若按路径去搜索则是指数级,必然超时。

油量单调递减是这个问题能被拆解的根本原因:任意一次移动都严格消耗至少 1 升油(城市坐标互不相同),所以状态不会成环,递归一定会终止。

边界上,油量耗尽时若恰好停在 finish 计 1 条,否则计 0 条;start == finish 时初始状态就贡献 1。

解法:记忆化搜索

核心思路

定义 dfs(city, remain):当前位于 city、剩余 remain 油量时,最终停在 finish 的路径数。后续选择只由这两个量决定,因此用二维记忆表缓存。

city == finish,当前立即停止就是一条路径,所以初值为 1;但不能直接返回,因为还可以离开终点再回来。随后枚举所有其他城市,移动费用不超过剩余油量时,累加下一状态的答案。

燃料单调性:坐标互不相同,每次移动费用至少为 1,递归中的 remain 严格减小。因此状态图无环,不需要城市访问标记,也不会无限递归。

不变量memo[city][remain] 一旦写入,就等于从该状态出发的完整合法路径数,并已按模数归一化。

正确性:任意合法路径要么在当前终点立即结束,要么选择唯一的第一站,再接上一条对应子状态的合法路径。转移枚举所有付得起费用的第一站,分类互斥且完整;记忆化只复用相同状态结果,不改变计数。

解题步骤

  1. 创建 n x (fuel+1) 的缓存并填为 -1,区分“未计算”和合法答案 0。
  2. 递归入口先查缓存;当前位置是终点时令答案初值为 1,但继续枚举移动。
  3. 枚举不同的下一城市,计算坐标绝对差;费用不超过剩余油量才递归。
  4. 每次累加后取模,写入缓存并返回。

locations=[2,3,6,8,4]start=1finish=3fuel=5 有 4 条路径,包括直达以及经城市 2、4 的组合。

边界 start==finish 时空路径先贡献 1,但仍需统计有油时离开再返回的路线。locations=[1,2]、油量 1 时,恰好耗尽到达终点也必须计入。

代码实现

class Solution {
    private static final int MOD = 1_000_000_007;
    private int[] loc;
    private int finish;
    private int[][] memo;

    public int countRoutes(int[] locations, int start, int finish, int fuel) {
        this.loc = locations;
        this.finish = finish;
        int n = locations.length;
        memo = new int[n][fuel + 1];
        for (int i = 0; i < n; i++) {
            for (int f = 0; f <= fuel; f++) {
                memo[i][f] = -1;
            }
        }
        return dfs(start, fuel);
    }

    private int dfs(int pos, int fuel) {
        if (memo[pos][fuel] != -1) {
            return memo[pos][fuel];
        }
        long answer = 0;
        if (pos == finish) {
            answer = 1;
        }
        for (int next = 0; next < loc.length; next++) {
            if (next == pos) {
                continue;
            }
            int cost = Math.abs(loc[pos] - loc[next]);
            if (cost <= fuel) {
                answer += dfs(next, fuel - cost);
                answer %= MOD;
            }
        }
        memo[pos][fuel] = (int) answer;
        return memo[pos][fuel];
    }
}
func countRoutes(locations []int, start int, finish int, fuel int) int {
	const mod = 1_000_000_007
	n := len(locations)
	memo := make([][]int, n)
	for i := 0; i < n; i++ {
		memo[i] = make([]int, fuel+1)
		for f := 0; f <= fuel; f++ {
			memo[i][f] = -1
		}
	}

	var dfs func(pos int, fuel int) int
	dfs = func(pos int, fuel int) int {
		if memo[pos][fuel] != -1 {
			return memo[pos][fuel]
		}
		answer := 0
		if pos == finish {
			answer = 1
		}
		for next := 0; next < n; next++ {
			if next == pos {
				continue
			}
			cost := locations[pos] - locations[next]
			if cost < 0 {
				cost = -cost
			}
			if cost <= fuel {
				answer += dfs(next, fuel-cost)
				answer %= mod
			}
		}
		memo[pos][fuel] = answer
		return answer
	}

	return dfs(start, fuel)
}

复杂度分析

  • 时间复杂度:$O(n^2 \cdot fuel)$。共有 $O(n \cdot fuel)$ 个状态,每个状态枚举 n 个下一站。
  • 空间复杂度:$O(n \cdot fuel)$ 用于缓存;递归栈深度至多为 fuel

关键点总结

  • 状态是“当前城市 + 剩余油量”,历史访问顺序不影响后续选择。
  • 到达终点只是多一种“现在结束”的选择,不能终止搜索。
  • 坐标互异保证每次移动严格耗油,状态不会成环。
  • 缓存哨兵必须用 -1,因为 0 是合法路径数。
  • 城市允许重复访问,不能使用普通图搜索的 visited

易错点总结

  • 到达 finishreturn 1:会漏掉离开终点再返回的合法路径。
  • 允许 next==city:产生零油耗自调用,递归无法终止。
  • 距离不取绝对值:向坐标较小的城市移动会得到负费用。
  • 使用 cost<remain:恰好耗尽燃料到达终点的路径被漏掉。
  • 缓存第二维长度只开 fuel:初始状态访问下标 fuel 时越界。
  • 添加城市级 visited:合法路线可以重复经过同一城市,会被错误剪掉。

相似题目

题目 难度 考察点
576. 出界的路径数 中等 状态是(位置,剩余步数),计数目标改为走出边界,转移只有四个方向
1155. 掷骰子等于目标和的方法数 中等 状态是(骰子数,剩余目标和),资源同样单调递减,是本题的一维版
62. 不同路径 中等 转移方向单一且无资源维度,可用组合数直接闭式求解
63. 不同路径 II 中等 在计数基础上加入障碍物,考的是不可达状态的初始化
980. 不同路径 III 困难 要求恰好走遍所有空格,状态需用位掩码记录访问集合,无法只靠资源量
787. K 站中转内最便宜的航班 中等 同为「位置 + 剩余额度」的二维状态,但目标从计数换成最小化代价
322. 零钱兑换 中等 单维资源递减的最优化问题,适合对比自顶向下与自底向上的写法