查看: 89| 回复: 0
跳转到指定楼层
上一主题 下一主题
收起左侧

[高频题] 【面经】Stripe Coding:自行车路线规划 —— K 条最便宜的简单路径(过滤+复合成本)

全局:

注册一亩三分地论坛,查看更多干货!

您需要 登录 才可以下载或查看附件。没有帐号?注册账号

x
分享一道我 Stripe 电面遇到的题:城市地图上的自行车路线规划。说实话这一轮难的不是题,是和面试官的沟通,就这样吧。把题和我的解法贴出来供讨论。

**题面。** 地图是一个无向带权图,每条路是 `(u, v, distance, bike_lane, elevation_gain, traffic_risk)`,一条路的骑行成本是 `distance + 2 * elevation_gain + 3 * traffic_risk`。给定 `start`、`end`、`k`、`require_bike_lanes` 开关和一份 `closed_roads`(无向对,`(u, v)` 和 `(v, u)` 是同一条路),返回 k 条最便宜的**简单路径**(不重复经过同一个路口),形式是 `(total_cost, path)`,按总成本升序、成本相同按路径字典序排序。不足 k 条就全返回。`start == end` 时直接返回 `[(0, [start])]`,哪怕一条路都没有。无路可走返回 `[]`。

两个例子。(1)8 条路的图,k=3,不做过滤 → `[(9, [0,3,4]), (12, [0,1,4]), (12, [0,2,4])]`,注意两条成本 12 的路线按字典序排。(2)`require_bike_lanes=True` 且 0-3 这条路关闭(在关闭列表里写的是 `(3, 0)`)时,只剩 `[(4, [0,2,3])]`:0-1 没有自行车道被过滤,0-3 直达路被关了(哪怕列表里写的是反方向)。

**第一步是过滤建图。** 每次调用都在函数内重新建图——`closed_roads` 每次调用都可能不同(这就是题里说的动态更新),不能跨调用缓存。关闭对按两个方向都放进集合;然后删掉关闭的路,按开关删掉没有自行车道的路,边权就是复合成本:

```python
def build_graph(roads, require_bike_lanes, closed_roads):
    closed = set()
    for u, v in closed_roads:
        closed.add((u, v))
        closed.add((v, u))                      # 关闭对是无向的
    adj = {}
    for (u, v, distance, bike_lane, elev, risk) in roads:
        if (u, v) in closed:
            continue
        if require_bike_lanes and bike_lane != 1:
            continue
        w = distance + 2 * elev + 3 * risk
        adj.setdefault(u, []).append((v, w))
        adj.setdefault(v, []).append((u, w))
    return adj
```

**方法 A —— 对部分路径做 best-first 枚举。** 优先队列里放部分路径,键是 `(cost, path)`。每次弹出最便宜的部分路径:如果它正好到 `end`,就是下一条答案;否则向每个不在当前路径上的邻居延长一步。因为每条路成本为正,完整路径从队列里弹出的顺序恰好就是题目要求的顺序——而且键里带了路径本身,成本相同时自动按字典序弹出,排序规则不需要任何额外处理。

```python
import heapq

def solution(roads, start, end, k, require_bike_lanes, closed_roads):
    if start == end:
        return [(0, [start])]
    adj = build_graph(roads, require_bike_lanes, closed_roads)
    results, heap = [], [(0, [start])]
    while heap and len(results) < k:
        cost, path = heapq.heappop(heap)
        if path[-1] == end:
            results.append((cost, path))
            continue
        on_path = set(path)
        for nxt, w in adj.get(path[-1], []):
            if nxt not in on_path:
                heapq.heappush(heap, (cost + w, path + [nxt]))
    return results
```

代价:第 k 条路线弹出时搜索就停,但在此之前展开的部分路径数量最坏是指数级的(稠密图)。这道题的规模(n ≤ 100,k ≤ 10)实际跑没问题;最坏情况靠方法 B 兜底。

**方法 B —— Yen 算法。** 先用 Dijkstra 求最短路线;然后反复取最后一条已接受的路线,在它每个 spur 位置上:禁掉已接受路线从同一 root 前缀出发的那条边,禁掉 root 里的其他节点,从 spur 节点向 `end` 再跑一次 Dijkstra,每次得到一条候选,取最便宜的候选作为下一条路线。两个细节是关键:

- Dijkstra 本身要返回「最便宜里字典序最小」的那条——堆的键用 `(cost, path)` 就自然成立;
- **边界平局:** 恰好收满 k 条就停,可能丢掉一条与第 k 条成本相同、但字典序更小的路线。所以要继续接受,直到下一候选的成本大于第 k 条的成本,最后统一按 `(cost, path)` 排序再截断到 k。

每条路线的复杂度是多项式的:O(n) 个 spur 位置 × 每个位置一次 Dijkstra。

**我记下的坑:**
- 把关闭对当成有向的——`(3, 0)` 必须能关掉 0-3;
- 只按成本排序、把平局丢给运气:平局规则必须在搜索内部就成立(方法 A 靠堆的键),最后才排序是救不回来已被搜索截断掉的路线的;
- 跨调用缓存过滤后的图——`closed_roads` 作为入参就是因为它会变;
- `start == end` 要在过滤之前短路处理:没有路时也要返回 `[(0, [start])]`。

*验证说明:* 发帖前实现实际跑过:两个给定例子、上面全部边界情况、在 3,000 个随机小图上和暴力枚举全部简单路径对比、再在 300 个中型图上交叉验证方法 A 与方法 B——结果全部一致。

觉得有帮助的话欢迎收藏、加米,谢谢。

上一篇:今天复习了二维dp 72. Edit Distance
您需要登录后才可以回帖 登录 | 注册账号
隐私提醒:
  • ☑ 禁止发布广告,拉群,贴个人联系方式:找人请去🔗同学同事飞友,拉群请去🔗拉群结伴,广告请去🔗跳蚤市场,和 🔗租房广告|找室友
  • ☑ 论坛内容在发帖 30 分钟内可以编辑,过后则不能删帖。为防止被骚扰甚至人肉,不要公开留微信等联系方式,如有需求请以论坛私信方式发送。
  • ☑ 干货版块可免费使用 🔗超级匿名:面经(美国面经、中国面经、数科面经、PM面经),抖包袱(美国、中国)和录取汇报、定位选校版
  • ☑ 查阅全站 🔗各种匿名方法

本版积分规则

>
快速回复 返回顶部 返回列表