{ }codelper
图(邻接表)优先队列(堆)P0151

一路换电池的无人机

配送无人机使用 n 个中继站,编号从 1 到 n。
中继站之间有 m 条只能单向飞行的航线。
一条航线 "a b w" 表示无人机可以用 w 秒从第 a 个中继站飞到第 b 个中继站。
航线不能反向使用,同样两个中继站之间可能有多条耗时不同的航线。
无人机离开任何中继站之前都必须在那里更换电池,在第 i 个中继站更换电池需要 cᵢ 秒。
无人机在第 0 秒带着满电的电池从第 1 个中继站出发,所以只有第一次离开第 1 个中继站时不换电池(之后再回到第 1 个中继站并离开时要换)。
除了换电池和飞行,其他事情都不花时间。
到达某个中继站的时刻是无人机在那里降落的时刻,不包括在那里换电池的时间。
按顺序输出到达第 1 到第 n 个中继站的最早时刻,用空格分隔输出在一行。
第 1 个中继站的值是 0。
无论怎样走航线都到不了的中继站输出 -1。

输入
第一行 n 和 m,第二行 c₁ … cₙ,接下来 m 行每行 a、b、w,都用空格分隔。
1 ≤ n ≤ 10,000,0 ≤ m ≤ 10,000,0 ≤ cᵢ ≤ 1,000,000,1 ≤ a, b ≤ n,a ≠ b,1 ≤ w ≤ 1,000,000
输出
n 个数(到达各中继站的最早时刻或 -1),用空格分隔输出在一行
样例 1 · stdin
5 6
5 1 0 2 3
1 2 4
1 3 1
3 2 2
2 4 5
3 4 8
4 1 3
stdout
0 3 1 9 -1
main.py
输入(stdin)

运行时这些行会依次作为 input()/标准输入传入(运行中不会等待输入)。

$ 就绪

请在电脑上打开以编写和运行代码。在手机上可以查看题目和题解。

其他语言怎么写?查看各语言题解 · 8 种语言题解

把电池时间算在要离开的中继站上:在 t 秒降落到 v,离开 v 的时刻就是 t + c[v](v 是第 1 站时就是 t),飞一条航线就在 离开时刻 + w 到达。
所有时间都不小于 0,所以最早时刻用 Dijkstra 算法求:把(时刻, 中继站)放进最小堆,每次取出最小的时刻,某个中继站第一次被取出的那一刻,它的时刻就确定了(之后不会有更早到达的路)。
同一个中继站以更早的时刻再次放入时,旧记录还留在堆里,所以取出时如果是“已确定的站”(done)就跳过。
第 1 站在 0 秒就立刻确定,所以离开第 1 站只算一次,那正是“第一次离开”,不换电池。
陷阱一:像广度优先搜索那样把第一次到达的时刻定为答案是错的——例子中第 2 站直飞要 4 秒,经过第 3 站(换电池 0 秒)只要 3 秒。
陷阱二:在第 1 站也换电池(加上 c[1]),或者把到达站的换电池时间也加上,都是错的——只有离开时才换电池,第一次离开第 1 站时不换。
陷阱三:时刻最多 9,999 × (1,000,000 + 1,000,000) ≈ 200 亿,超过 32 位 int(约 21 亿)。
C、C++ 需要 long long,Java、C# 需要 long(Java、C# 的 int 溢出后会绕成负数);Python 整数没有大小上限,JavaScript、TypeScript、Craykon 的数是双精度浮点数,精确到 2^53(约 9×10^15),直接用就行。
陷阱四:C++ 的 std::priority_queue 默认是最大堆,会先取出最晚的时刻——要传 std::greater<std::pair<long long, int>>。
堆各语言不同:Python heapq 从第一项开始比较元组 (时刻, 中继站),直接可用;Java PriorityQueue<long[]> 要传比较函数 (x, y) -> Long.compare(x[0], y[0]);C# PriorityQueue<(long t, int v), long> 把值和优先级(时刻)分开放。
JavaScript、TypeScript、C、Craykon 没有堆,用列表(数组)自己写:i 的父节点是 (i - 1) / 2(整数除法——Craykon 数学.商(i - 1, 2)),子节点是 2i + 1、2i + 2。
Craykon 的列表最多 10,000 个元素,所以把编号减 1 从 0 号开始存(新列表(n, -1)),而且取出第 1 站之后才开始往堆里放,堆里最多 m 个。
答案用 控制台.输出(best) 写列表就会用空格连接。

Pythonmain.py · 30 行
import heapqimport sys data = sys.stdin.read().split()n, m = int(data[0]), int(data[1])swap = [0] + [int(x) for x in data[2 : 2 + n]]  # swap[v] = v번에서 배터리를 가는 데 걸리는 초 routes = [[] for _ in range(n + 1)]  # routes[a] = a에서 떠나는 (도착, 초) 항로들for i in range(m):    k = 2 + n + 3 * i    a, b, w = int(data[k]), int(data[k + 1]), int(data[k + 2])    routes[a].append((b, w)) INF = -1best = [INF] * (n + 1)  # 지금까지 찾은 가장 짧은 시간(-1은 아직 못 감)done = [False] * (n + 1)  # 가장 짧은 시간이 확정된 중계소best[1] = 0heap = [(0, 1)]  # (시간, 중계소): 늘 가장 작은 시간을 먼저 꺼낸다while heap:    t, v = heapq.heappop(heap)    if done[v]:        continue  # 더 짧은 시간으로 이미 꺼낸 중계소(남아 있던 옛 기록)    done[v] = True    leave = t if v == 1 else t + swap[v]  # 처음 1번을 떠날 때만 배터리를 갈지 않는다    for nxt, w in routes[v]:        if best[nxt] == INF or leave + w < best[nxt]:            best[nxt] = leave + w            heapq.heappush(heap, (leave + w, nxt)) print(" ".join(map(str, best[1:])))
JavaScriptmain.js · 68 行
const data = require("fs").readFileSync(0, "utf8").trim().split(/\s+/).map(Number);const [n, m] = data; // 최소 힙: 칸마다 [시간, 중계소]. heap[0]이 가장 작은 시간이고, i의 자식은 2i+1, 2i+2class MinHeap {  constructor() {    this.items = [];  }  get size() {    return this.items.length;  }  push(x) {    const a = this.items;    a.push(x);    let i = a.length - 1;    while (i > 0) {      const parent = (i - 1) >> 1;      if (a[parent][0] <= a[i][0]) break;      [a[parent], a[i]] = [a[i], a[parent]];      i = parent;    }  }  pop() {    const a = this.items;    const top = a[0];    const last = a.pop();    if (a.length > 0) {      a[0] = last;      let i = 0;      for (;;) {        const l = 2 * i + 1, r = l + 1;        let small = i;        if (l < a.length && a[l][0] < a[small][0]) small = l;        if (r < a.length && a[r][0] < a[small][0]) small = r;        if (small === i) break;        [a[small], a[i]] = [a[i], a[small]];        i = small;      }    }    return top;  }} const swap = [0, ...data.slice(2, 2 + n)]; // swap[v] = v번에서 배터리를 가는 데 걸리는 초const routes = Array.from({ length: n + 1 }, () => []); // routes[a] = a에서 떠나는 [도착, 초] 항로들for (let i = 0; i < m; i++) {  const k = 2 + n + 3 * i;  routes[data[k]].push([data[k + 1], data[k + 2]]);} const best = new Array(n + 1).fill(-1); // 지금까지 찾은 가장 짧은 시간(-1은 아직 못 감)const done = new Array(n + 1).fill(false); // 가장 짧은 시간이 확정된 중계소best[1] = 0;const heap = new MinHeap();heap.push([0, 1]);while (heap.size > 0) {  const [t, v] = heap.pop();  if (done[v]) continue; // 더 짧은 시간으로 이미 꺼낸 중계소(남아 있던 옛 기록)  done[v] = true;  const leave = v === 1 ? t : t + swap[v]; // 처음 1번을 떠날 때만 배터리를 갈지 않는다  for (const [next, w] of routes[v]) {    if (best[next] === -1 || leave + w < best[next]) {      best[next] = leave + w;      heap.push([leave + w, next]);    }  }}console.log(best.slice(1).join(" "));
TypeScriptmain.ts · 74 行
import { readFileSync } from "node:fs"; const data: number[] = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);const [n, m] = data; type Entry = [time: number, station: number]; // 최소 힙: heap[0]이 가장 작은 시간이고, i의 자식은 2i+1, 2i+2class MinHeap {  private items: Entry[] = [];   get size(): number {    return this.items.length;  }   push(x: Entry): void {    const a = this.items;    a.push(x);    let i = a.length - 1;    while (i > 0) {      const parent = (i - 1) >> 1;      if (a[parent][0] <= a[i][0]) break;      [a[parent], a[i]] = [a[i], a[parent]];      i = parent;    }  }   pop(): Entry {    const a = this.items;    const top = a[0];    const last = a.pop()!; // size > 0일 때만 부른다    if (a.length > 0) {      a[0] = last;      let i = 0;      for (;;) {        const l = 2 * i + 1, r = l + 1;        let small = i;        if (l < a.length && a[l][0] < a[small][0]) small = l;        if (r < a.length && a[r][0] < a[small][0]) small = r;        if (small === i) break;        [a[small], a[i]] = [a[i], a[small]];        i = small;      }    }    return top;  }} const swap: number[] = [0, ...data.slice(2, 2 + n)]; // swap[v] = v번에서 배터리를 가는 데 걸리는 초// routes[a] = a에서 떠나는 [도착, 초] 항로들const routes: [number, number][][] = Array.from({ length: n + 1 }, (): [number, number][] => []);for (let i = 0; i < m; i++) {  const k = 2 + n + 3 * i;  routes[data[k]].push([data[k + 1], data[k + 2]]);} const best: number[] = new Array<number>(n + 1).fill(-1); // 지금까지 찾은 가장 짧은 시간(-1은 아직 못 감)const done: boolean[] = new Array<boolean>(n + 1).fill(false); // 가장 짧은 시간이 확정된 중계소best[1] = 0;const heap = new MinHeap();heap.push([0, 1]);while (heap.size > 0) {  const [t, v] = heap.pop();  if (done[v]) continue; // 더 짧은 시간으로 이미 꺼낸 중계소(남아 있던 옛 기록)  done[v] = true;  const leave = v === 1 ? t : t + swap[v]; // 처음 1번을 떠날 때만 배터리를 갈지 않는다  for (const [next, w] of routes[v]) {    if (best[next] === -1 || leave + w < best[next]) {      best[next] = leave + w;      heap.push([leave + w, next]);    }  }}console.log(best.slice(1).join(" "));
JavaMain.java · 60 行
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.ArrayList;import java.util.Arrays;import java.util.List;import java.util.PriorityQueue;import java.util.StringTokenizer; public class Main {    public static void main(String[] args) throws IOException {        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));        StringTokenizer st = new StringTokenizer(in.readLine());        int n = Integer.parseInt(st.nextToken());        int m = Integer.parseInt(st.nextToken());        int[] swap = new int[n + 1]; // swap[v] = v번에서 배터리를 가는 데 걸리는 초        st = new StringTokenizer(in.readLine());        for (int v = 1; v <= n; v++) swap[v] = Integer.parseInt(st.nextToken());         List<List<int[]>> routes = new ArrayList<>(); // routes.get(a) = a에서 떠나는 {도착, 초} 항로들        for (int v = 0; v <= n; v++) routes.add(new ArrayList<>());        for (int i = 0; i < m; i++) {            st = new StringTokenizer(in.readLine());            int a = Integer.parseInt(st.nextToken());            int b = Integer.parseInt(st.nextToken());            int w = Integer.parseInt(st.nextToken());            routes.get(a).add(new int[] {b, w});        }         long[] best = new long[n + 1]; // 시간의 합은 int(약 21억)를 넘을 수 있다        Arrays.fill(best, -1);        boolean[] done = new boolean[n + 1];        best[1] = 0;        // {시간, 중계소}를 시간이 작은 것부터 꺼내는 최소 힙        PriorityQueue<long[]> heap = new PriorityQueue<>((x, y) -> Long.compare(x[0], y[0]));        heap.add(new long[] {0, 1});        while (!heap.isEmpty()) {            long[] top = heap.poll();            long t = top[0];            int v = (int) top[1];            if (done[v]) continue; // 더 짧은 시간으로 이미 꺼낸 중계소(남아 있던 옛 기록)            done[v] = true;            long leave = v == 1 ? t : t + swap[v]; // 처음 1번을 떠날 때만 배터리를 갈지 않는다            for (int[] r : routes.get(v)) {                int next = r[0];                if (best[next] == -1 || leave + r[1] < best[next]) {                    best[next] = leave + r[1];                    heap.add(new long[] {best[next], next});                }            }        }         StringBuilder out = new StringBuilder();        for (int v = 1; v <= n; v++) {            if (v > 1) out.append(' ');            out.append(best[v]);        }        System.out.println(out);    }}
C#Main.cs · 55 行
using System;using System.Collections.Generic;using System.Text; class Program{    static void Main()    {        string[] first = Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries);        int n = int.Parse(first[0]);        int m = int.Parse(first[1]);        var swap = new int[n + 1]; // swap[v] = v번에서 배터리를 가는 데 걸리는 초        string[] swaps = Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries);        for (int v = 1; v <= n; v++) swap[v] = int.Parse(swaps[v - 1]);         var routes = new List<(int to, int w)>[n + 1]; // routes[a] = a에서 떠나는 항로들        for (int v = 0; v <= n; v++) routes[v] = new List<(int to, int w)>();        for (int i = 0; i < m; i++)        {            string[] p = Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries);            routes[int.Parse(p[0])].Add((int.Parse(p[1]), int.Parse(p[2])));        }         var best = new long[n + 1]; // 시간의 합은 int(약 21억)를 넘을 수 있다        Array.Fill(best, -1L);        var done = new bool[n + 1];        best[1] = 0;        // PriorityQueue<값, 우선순위>: 우선순위(시간)가 작은 것부터 나온다        var heap = new PriorityQueue<(long t, int v), long>();        heap.Enqueue((0, 1), 0);        while (heap.Count > 0)        {            var (t, v) = heap.Dequeue();            if (done[v]) continue; // 더 짧은 시간으로 이미 꺼낸 중계소(남아 있던 옛 기록)            done[v] = true;            long leave = v == 1 ? t : t + swap[v]; // 처음 1번을 떠날 때만 배터리를 갈지 않는다            foreach (var (next, w) in routes[v])            {                if (best[next] == -1 || leave + w < best[next])                {                    best[next] = leave + w;                    heap.Enqueue((best[next], next), best[next]);                }            }        }         var sb = new StringBuilder();        for (int v = 1; v <= n; v++)        {            if (v > 1) sb.Append(' ');            sb.Append(best[v]);        }        Console.WriteLine(sb.ToString());    }}
C++main.cpp · 45 行
#include <functional>#include <iostream>#include <queue>#include <utility>#include <vector> int main() {    std::ios::sync_with_stdio(false);    std::cin.tie(nullptr);    int n, m;    std::cin >> n >> m;    std::vector<int> swap_time(n + 1); // swap_time[v] = v번에서 배터리를 가는 데 걸리는 초    for (int v = 1; v <= n; v++) std::cin >> swap_time[v];     std::vector<std::vector<std::pair<int, int>>> routes(n + 1); // routes[a] = a에서 떠나는 (도착, 초) 항로들    for (int i = 0; i < m; i++) {        int a, b, w;        std::cin >> a >> b >> w;        routes[a].push_back({b, w});    }     std::vector<long long> best(n + 1, -1); // 시간의 합은 int(약 21억)를 넘을 수 있다    std::vector<bool> done(n + 1, false);    // std::priority_queue는 기본이 최대 힙이라 std::greater를 줘서 가장 작은 시간이 위로 오게 한다    using Entry = std::pair<long long, int>; // (시간, 중계소)    std::priority_queue<Entry, std::vector<Entry>, std::greater<Entry>> heap;    best[1] = 0;    heap.push({0, 1});    while (!heap.empty()) {        auto [t, v] = heap.top();        heap.pop();        if (done[v]) continue; // 더 짧은 시간으로 이미 꺼낸 중계소(남아 있던 옛 기록)        done[v] = true;        long long leave = v == 1 ? t : t + swap_time[v]; // 처음 1번을 떠날 때만 배터리를 갈지 않는다        for (auto [next, w] : routes[v]) {            if (best[next] == -1 || leave + w < best[next]) {                best[next] = leave + w;                heap.push({best[next], next});            }        }    }     for (int v = 1; v <= n; v++) std::cout << (v > 1 ? " " : "") << best[v];    std::cout << "\n";}
Cmain.c · 88 行
#include <stdio.h> #define MAXN 10001#define MAXM 10000 /* 인접 목록을 배열로: head[a]는 a에서 떠나는 첫 항로 칸, next[e]는 다음 칸(-1이면 끝) */static int head[MAXN], next[MAXM], to[MAXM], cost[MAXM];static int swap_time[MAXN]; /* swap_time[v] = v번에서 배터리를 가는 데 걸리는 초 */static long long best[MAXN]; /* 시간의 합은 int(약 21억)를 넘을 수 있다 */static int done[MAXN]; /* 최소 힙: 칸마다 (시간, 중계소). heap_t[0]이 가장 작다 */static long long heap_t[MAXM + 1];static int heap_v[MAXM + 1], heap_size = 0; static void heap_swap(int i, int j) {    long long t = heap_t[i];    heap_t[i] = heap_t[j];    heap_t[j] = t;    int v = heap_v[i];    heap_v[i] = heap_v[j];    heap_v[j] = v;} static void heap_push(long long t, int v) {    int i = heap_size++;    heap_t[i] = t;    heap_v[i] = v;    while (i > 0 && heap_t[(i - 1) / 2] > heap_t[i]) {        heap_swap((i - 1) / 2, i);        i = (i - 1) / 2;    }} static void heap_pop(long long* t, int* v) {    *t = heap_t[0];    *v = heap_v[0];    heap_size--;    heap_t[0] = heap_t[heap_size];    heap_v[0] = heap_v[heap_size];    int i = 0;    for (;;) {        int l = 2 * i + 1, r = l + 1, small = i;        if (l < heap_size && heap_t[l] < heap_t[small]) small = l;        if (r < heap_size && heap_t[r] < heap_t[small]) small = r;        if (small == i) break;        heap_swap(small, i);        i = small;    }} int main(void) {    int n, m;    scanf("%d %d", &n, &m);    for (int v = 1; v <= n; v++) {        head[v] = -1;        best[v] = -1; /* -1은 아직 못 감 */        scanf("%d", &swap_time[v]);    }    for (int e = 0; e < m; e++) {        int a;        scanf("%d %d %d", &a, &to[e], &cost[e]);        next[e] = head[a];        head[a] = e;    }     best[1] = 0;    heap_push(0, 1);    while (heap_size > 0) {        long long t;        int v;        heap_pop(&t, &v);        if (done[v]) continue; /* 더 짧은 시간으로 이미 꺼낸 중계소(남아 있던 옛 기록) */        done[v] = 1;        long long leave = v == 1 ? t : t + swap_time[v]; /* 처음 1번을 떠날 때만 배터리를 갈지 않는다 */        for (int e = head[v]; e != -1; e = next[e]) {            int b = to[e];            if (best[b] == -1 || leave + cost[e] < best[b]) {                best[b] = leave + cost[e];                heap_push(best[b], b);            }        }    }     for (int v = 1; v <= n; v++) printf(v > 1 ? " %lld" : "%lld", best[v]);    putchar('\n');    return 0;}
Craykonmain.cray · 96 行
// 최소 힙(목록 heap): 칸마다 [시간, 중계소]. heap[0]이 가장 작은 시간이고, i의 자식은 2i+1, 2i+2힙넣기(heap, x){    heap.添加(x);    i = heap.数量 - 1;    当 (i > 0)    {        부모 = 数学.商(i - 1, 2);        如果 (heap[부모][0] <= heap[i][0])        {            跳出;        }        t = heap[부모];        heap[부모] = heap[i];        heap[i] = t;        i = 부모;    }} 힙꺼내기(heap){    top = heap[0];    heap[0] = heap[heap.数量 - 1];    heap.按位置移除(heap.数量 - 1);    i = 0;    当 (真)    {        l = 2 * i + 1;        r = l + 1;        small = i;        如果 (l < heap.数量 && heap[l][0] < heap[small][0])        {            small = l;        }        如果 (r < heap.数量 && heap[r][0] < heap[small][0])        {            small = r;        }        如果 (small == i)        {            跳出;        }        t = heap[small];        heap[small] = heap[i];        heap[i] = t;        i = small;    }    返回 top;} 첫줄 = 控制台.读数字();n = 첫줄[0];m = 첫줄[1];swap = 控制台.读数字(); // swap[v] = v번(0번부터)에서 배터리를 가는 데 걸리는 초 // 목록은 10,000칸까지라 중계소 번호에서 1을 빼 0번부터 둔다routes = []; // routes[a] = a에서 떠나는 [도착, 초] 항로들循环 (v = 0; v < n; v++){    routes.添加([]);}循环 (i = 0; i < m; i++){    항로 = 控制台.读数字();    routes[항로[0] - 1].添加([항로[1] - 1, 항로[2]]);} best = 新列表(n, -1); // 지금까지 찾은 가장 짧은 시간(-1은 아직 못 감)done = 新列表(n, 假); // 가장 짧은 시간이 확정된 중계소best[0] = 0;heap = [[0, 0]];当 (heap.数量 > 0){    꺼낸것 = 힙꺼내기(heap);    t = 꺼낸것[0];    v = 꺼낸것[1];    如果 (!done[v])    {        done[v] = 真;        leave = t + swap[v]; // 처음 1번(0번 칸)을 떠날 때만 배터리를 갈지 않는다        如果 (v == 0)        {            leave = t;        }        遍历 (항로 在 routes[v])        {            next = 항로[0];            如果 (best[next] == -1 || leave + 항로[1] < best[next])            {                best[next] = leave + 항로[1];                힙넣기(heap, [best[next], next]);            }        }    }}控制台.输出(best);