2025-10-23:合并得到最小旅行时间。用go语言,给出一条总长为 l 公里、两端已标记的直路;有 n 个路标和一个整数 k,以及两个长度为 n 的数组 position 和 time。
position 按严格递增排列,且 position[0]=0、position[n-1]=l。
对于第 i 段(即从 position[i] 到 position[i+1]),time[i] 表示每公里所需的时间(分钟/公里)。
必须恰好进行 k 次合并操作。一次合并的规则是:选取一对相邻路标(下标为 i 和 i+1,且 i>0,i+1<n),将右侧那段的单位耗时更新为 time[i]+time[i+1],然后删除下标 i 对应的路标(即合并这两个相邻区间为一个区间,新的区间的单位耗时为两段耗时之和)。
目标是完成恰好 k 次合并后,使从 0 到 l 的总行驶时间最小化(总时间等于每个区间长度乘以该区间的单位耗时,再对所有区间求和)。
返回这个最小总时间(单位:分钟)。
1 <= l <= 100000。
2 <= n <= min(l + 1, 50)。
0 <= k <= min(n - 2, 10)。
position.length == n。
position[0] = 0 和 position[n - 1] = l。
position 是严格升序排列的。
time.length == n。
1 <= time[i] <= 100。
1 <= sum(time) <= 100。
输入: l = 10, n = 4, k = 1, position = [0,3,8,10], time = [5,8,3,6]。
输出: 62。
解释: 合并下标为 1 和 2 的路标。删除下标为 1 的路标,并将下标为 2 的路标的时间更新为 8 + 3 = 11。
合并后:
position 数组:[0, 8, 10]
time 数组:[5, 11, 6]
| 路段 | 距离(公里) | 每公里时间(分钟) | 路段旅行时间(分钟) |
|---|---|---|---|
| 0 → 8 | 8 | 5 | 8 × 5 = 40 |
| 8 → 10 | 2 | 11 | 2 × 11 = 22 |
总旅行时间:40 + 22 = 62 ,这是执行 1 次合并后的最小时间。
题目来自力扣3538。
动态规划解决步骤
-
预处理前缀和数组
- 计算一个前缀和数组
s,其长度为n(路标数量)。 s[0] = 0,对于i从0到n-2,计算s[i+1] = s[i] + time[i]。这里s[i]表示前i个路段(即从position[0]到position[i]的路段)的单位时间之和。注意time数组的第n-1个元素(最后一个元素)未被使用,因为路段数量是n-1。- 前缀和的作用是快速计算任意连续路段合并后的单位时间之和。例如,合并从路标
j-sz到路标j的路段时,单位时间之和t = s[j+1] - s[j-sz]。
- 计算一个前缀和数组
-
初始化三维DP数组
- 创建一个三维数组
f,维度为n × (K+1) × (K+1),其中K是需要进行的合并次数。 f[j][sz][leftK]表示:从路标j出发,当前已合并了sz个相邻路段(即当前区间的单位时间是这些路段单位时间之和),剩余leftK次合并操作时,从j到终点l的最小总旅行时间。- 初始时,将所有
f[j][sz][leftK]设置为一个极大值(表示不可行)。然后设置边界条件:当j = n-1(即终点路标)时,对于任意sz,如果leftK = 0,则f[n-1][sz][0] = 0(因为终点后无路段,时间为0)。
- 创建一个三维数组
-
动态规划状态转移(从后往前计算)
- 从路标
j = n-2开始向下遍历到0(倒序),因为后续状态依赖于更靠后的路标。 - 对于每个
j,遍历当前合并大小sz(从0到min(K, j)),表示已合并的路段数(sz不能超过j,因为不能合并超出起点0的路标)。 - 对于每个
sz,遍历剩余合并次数leftK(从0到min(K, n-2-j)),确保剩余合并次数不超过后续可合并的路段数。 - 计算当前合并区间的单位时间
t:t = s[j+1] - s[j-sz]。这表示从路标j-sz到j的所有路段合并后的单位时间之和。例如,sz=0时,t就是time[j];sz=1时,t是time[j-1] + time[j]。
- 枚举下一个子数组的起点
k(即下一个区间的开始路标),k从j+1到j+1+leftK(且k不超过n-1)。对于每个k:- 计算当前区间的旅行时间:区间长度为
position[k] - position[j],乘以单位时间t,即(position[k] - position[j]) * t。 - 子数组大小
m = k - j - 1,表示从j+1到k-1的路段数(这些路段将被合并到下一个区间)。 - 剩余合并次数更新为
leftK - m。 - 状态转移:
r = f[k][m][leftK - m] + (position[k] - position[j]) * t。
- 计算当前区间的旅行时间:区间长度为
- 取所有
k对应的r的最小值,作为f[j][sz][leftK]的结果。
- 从路标
-
返回结果
- 最终,
f[0][0][K]即为从起点0到终点l的最小总旅行时间。其中sz=0表示起点未合并任何路段,leftK=K表示恰好进行K次合并。
- 最终,
时间复杂度和空间复杂度
-
时间复杂度:
- 动态规划状态总数为
O(n × K²)(因为j有n个值,sz和leftK各最多K+1个值)。 - 对于每个状态,需要枚举
k,枚举次数为O(K)(因为k从j+1到j+1+leftK,而leftK ≤ K)。 - 因此,总时间复杂度为 O(n × K³)。由于
n ≤ 50且K ≤ 10,实际计算量在可接受范围内。
- 动态规划状态总数为
-
空间复杂度:
- 主要开销是三维DP数组
f,大小为n × (K+1) × (K+1),因此空间复杂度为 O(n × K²)。 - 此外,前缀和数组
s占用O(n)空间,可忽略不计。
- 主要开销是三维DP数组
该方法通过动态规划高效地枚举了所有可能的合并顺序,利用前缀和优化计算,确保了在约束下找到最优解。
Go完整代码如下:
package main
import (
"fmt"
"math"
)
func minTravelTime(_, n, K int, position, time []int) int {
s := make([]int, n)
for i, t := range time[:n-1] { // time[n-1] 用不到
s[i+1] = s[i] + t // 计算 time 的前缀和
}
f := make([][][]int, n)
for j := range f {
f[j] = make([][]int, K+1)
for sz := range f[j] {
f[j][sz] = make([]int, K+1)
for leftK := range f[j][sz] {
f[j][sz][leftK] = math.MaxInt / 2
}
}
}
for sz := range K + 1 {
f[n-1][sz][0] = 0
}
for j := n - 2; j >= 0; j-- { // 转移来源 k 比 j 大,所以要倒序
for sz := range min(K, j) + 1 {
t := s[j+1] - s[j-sz] // 合并到 time[j] 的时间
for leftK := range min(K, n-2-j) + 1 {
res := math.MaxInt
// 枚举下一个子数组 [j+1, k]
for k := j + 1; k <= j+1+leftK; k++ {
r := f[k][k-j-1][leftK-(k-j-1)] + (position[k]-position[j])*t
res = min(res, r)
}
f[j][sz][leftK] = res
}
}
}
return f[0][0][K] // 第一个子数组是 [0, 0]
}
func main() {
l := 10
n := 4
k := 1
position := []int{0, 3, 8, 10}
time := []int{5, 8, 3, 6}
result := minTravelTime(l, n, k, position, time)
fmt.Println(result)
}
Python完整代码如下:
# -*-coding:utf-8-*-
import math
def minTravelTime(_, n, K, position, time):
# 计算前缀和
s = [0] * n
for i in range(n-1): # time[n-1] 用不到
s[i+1] = s[i] + time[i]
# 初始化三维DP数组
f = [[[math.inf] * (K+1) for _ in range(K+1)] for _ in range(n)]
# 初始化边界条件
for sz in range(K+1):
f[n-1][sz][0] = 0
# 动态规划,从后往前计算
for j in range(n-2, -1, -1):
for sz in range(min(K, j) + 1):
t = s[j+1] - s[j-sz] # 合并到 time[j] 的时间
for leftK in range(min(K, n-2-j) + 1):
res = math.inf
# 枚举下一个子数组 [j+1, k]
for k in range(j+1, j+1+leftK+1):
if k < n:
r = f[k][k-j-1][leftK-(k-j-1)] + (position[k]-position[j]) * t
res = min(res, r)
f[j][sz][leftK] = res
return f[0][0][K] # 第一个子数组是 [0, 0]
def main():
l = 10
n = 4
k = 1
position = [0, 3, 8, 10]
time = [5, 8, 3, 6]
result = minTravelTime(l, n, k, position, time)
print(result)
if __name__ == "__main__":
main()