{ }codelper
二分查找累计64 位整数P0304

按快车会超车的时刻表换乘

有 m + 1 个车站排成一列,第 i 段(1 ≤ i ≤ m)的列车只从第 i 个车站开到第 i + 1 个车站。
要到终点站,必须从第 1 段到第 m 段,每一段依次恰好乘坐一趟列车。
每一段都有时刻表,每趟列车用出发时刻和行驶时间表示。
列车在出发时刻加上行驶时间的时刻到达下一站。
同一段内出发时刻各不相同,但有的列车很快,所以后出发的列车可能先到。
如果在时刻 x 位于某个车站,就可以在该站乘坐出发时刻不早于 x 的列车(与到达时刻相同时刻出发的列车也能乘坐)。
回答 q 个问题。
问题 "A t" 问:在时刻 t 位于第一个车站时,最早什么时刻能到终点站。
问题 "B d" 问:要在时刻 d 或之前到达终点站,在第 1 段乘坐的列车的出发时刻最晚可以是多少。
每个问题的答案输出一行。
问题 A 中无法到达终点站,或问题 B 中无法在时刻 d 或之前到达终点站时,输出 -1。

输入
第一行给出用空格隔开的 m 和 q。
接着从第 1 段开始,每段给出三行:列车数 c、c 个出发时刻(从小到大,用一个空格隔开)、c 个行驶时间(与出发时刻顺序相同)。
接下来 q 行每行一个问题 "A t" 或 "B d"。
1 ≤ m ≤ 10,1 ≤ c,所有段的 c 之和 ≤ 10,000,1 ≤ q ≤ 5,000,0 ≤ 出发时刻 ≤ 2,000,000,000,1 ≤ 行驶时间 ≤ 1,000,000,000,0 ≤ t, d ≤ 3,000,000,000
输出
q 行。
问题 A 输出到达终点站的最早时刻,问题 B 输出第 1 段列车的最晚出发时刻,无法到达则输出 -1。
数只写数字,不带逗号
样例 1 · stdin
2 6
3
10 20 30
50 15 20
3
35 50 60
10 20 5
A 5
A 25
A 31
B 45
B 65
B 44
stdout
45
65
-1
20
30
-1
main.py
输入(stdin)

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

$ 就绪

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

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

二分查找是找“从哪个位置开始条件变为真”,所以列表必须朝一个方向增大。
出发时刻是有序的,但到达时刻因为超车而乱七八糟。
所以从后往前累计最小值,得到“乘坐第 j 趟或更靠后的列车时的最早到达”,这个列表只增不减,可以二分查找,问题 A 的答案也直接从这里读出。
问题 A 要找“出发 ≥ 当前时刻的第一个位置”(lower bound),问题 B 要找“累计到达 ≤ 期限的最后一个位置”(upper bound 的前一个)。
各语言的工具不同:Python 的 bisect_left、bisect_right 和 C++ 的 std::lower_bound、std::upper_bound 正合适。
Java 的 Arrays.binarySearch 和 C# 的 Array.BinarySearch 在找不到值时把插入位置变成负数返回(Java 是 -(位置) - 1,C# 是 ~位置),而且有多个相同的值时不保证返回哪一个,所以对累计到达这种相同值连成一片的列表,自己写更安全。
C 的 bsearch 只能找完全相等的值,找不到就返回 NULL,在这里用不上。
JS、TS 和 Craykon 没有现成的,要自己写(Craykon 用 数学.商(lo + hi, 2) 求中间位置)。
陷阱一:直接坐能赶上的第一趟车是错的。
后出发的快车可能先到。
陷阱二:对没有累计的到达时刻做二分查找,不会报错,却得到错误的位置。
陷阱三:边界。
与到达时刻相同时刻出发的车可以坐(>=),正好在期限时刻到达也可以(<=)。
把 lower bound 和 upper bound 用反了,恰好只在这些情况下出错。
陷阱四:出发时刻 20 亿放得进 32 位 int,但加上行驶时间后的 30 亿会溢出。
Java、C# 不报错地变成负数,C、C++ 则是未定义行为,所以要用 long、long long 读入。
问题中的 t 和 d 也到 30 亿,用 int 连读都读不了。
Python、JS、TS、Craykon 直接用就行。

Pythonmain.py · 50 行
import sysfrom bisect import bisect_left, bisect_right  def main():    data = sys.stdin.buffer.read().split()    m, q = int(data[0]), int(data[1])    pos = 2    deps = []  # deps[i][j]: i번 구간 j번째 열차의 출발 시각(커지는 차례)    best = []  # best[i][j]: j번째나 그 뒤 열차를 탔을 때 가장 이른 도착 시각    for _ in range(m):        c = int(data[pos])        pos += 1        dep = [int(x) for x in data[pos : pos + c]]        pos += c        arr = [dep[j] + int(data[pos + j]) for j in range(c)]        pos += c        # 뒤에서부터 최솟값을 누적하면 커지기만 하는 목록이 되어 이분 탐색을 할 수 있다        for j in range(c - 2, -1, -1):            if arr[j + 1] < arr[j]:                arr[j] = arr[j + 1]        deps.append(dep)        best.append(arr)     out = []    for _ in range(q):        kind = data[pos]        x = int(data[pos + 1])        pos += 2        if kind == b"A":            # 앞에서부터: 지금 시각 이후(같아도 됨)에 떠나는 첫 열차의 자리를 찾는다            for i in range(m):                j = bisect_left(deps[i], x)                if j == len(deps[i]):                    x = -1                    break                x = best[i][j]        else:            # 뒤에서부터: 기한 안에(같아도 됨) 닿는 가장 늦은 열차의 자리를 찾는다            for i in range(m - 1, -1, -1):                j = bisect_right(best[i], x) - 1                if j < 0:                    x = -1                    break                x = deps[i][j]        out.append(x)    print("\n".join(map(str, out)))  main()
JavaScriptmain.js · 70 行
const lines = require("fs").readFileSync(0, "utf8").split("\n");const [m, q] = lines[0].trim().split(/\s+/).map(Number); const deps = []; // deps[i][j]: i번 구간 j번째 열차의 출발 시각(커지는 차례)const best = []; // best[i][j]: j번째나 그 뒤 열차를 탔을 때 가장 이른 도착 시각let row = 1;for (let i = 0; i < m; i++) {  const c = Number(lines[row].trim());  const dep = lines[row + 1].trim().split(/\s+/).map(Number);  const dur = lines[row + 2].trim().split(/\s+/).map(Number);  row += 3;  const arr = dep.map((t, j) => t + dur[j]);  // 뒤에서부터 최솟값을 누적하면 커지기만 하는 배열이 되어 이분 탐색을 할 수 있다  for (let j = c - 2; j >= 0; j--) arr[j] = Math.min(arr[j], arr[j + 1]);  deps.push(dep);  best.push(arr);} // a[j] >= x인 첫 자리(없으면 a.length)function lowerBound(a, x) {  let lo = 0;  let hi = a.length;  while (lo < hi) {    const mid = (lo + hi) >> 1;    if (a[mid] < x) lo = mid + 1;    else hi = mid;  }  return lo;} // a[j] > x인 첫 자리(없으면 a.length)function upperBound(a, x) {  let lo = 0;  let hi = a.length;  while (lo < hi) {    const mid = (lo + hi) >> 1;    if (a[mid] <= x) lo = mid + 1;    else hi = mid;  }  return lo;} const out = [];for (let k = 0; k < q; k++) {  const [kind, value] = lines[row + k].trim().split(/\s+/);  let x = Number(value);  if (kind === "A") {    // 앞에서부터: 지금 시각 이후(같아도 됨)에 떠나는 첫 열차의 자리를 찾는다    for (let i = 0; i < m; i++) {      const j = lowerBound(deps[i], x);      if (j === deps[i].length) {        x = -1;        break;      }      x = best[i][j];    }  } else {    // 뒤에서부터: 기한 안에(같아도 됨) 닿는 가장 늦은 열차의 자리를 찾는다    for (let i = m - 1; i >= 0; i--) {      const j = upperBound(best[i], x) - 1;      if (j < 0) {        x = -1;        break;      }      x = deps[i][j];    }  }  out.push(x);}console.log(out.join("\n"));
TypeScriptmain.ts · 72 行
import { readFileSync } from "node:fs"; const lines: string[] = readFileSync(0, "utf8").split("\n");const [m, q] = lines[0].trim().split(/\s+/).map(Number); const deps: number[][] = []; // deps[i][j]: i번 구간 j번째 열차의 출발 시각(커지는 차례)const best: number[][] = []; // best[i][j]: j번째나 그 뒤 열차를 탔을 때 가장 이른 도착 시각let row = 1;for (let i = 0; i < m; i++) {  const c = Number(lines[row].trim());  const dep: number[] = lines[row + 1].trim().split(/\s+/).map(Number);  const dur: number[] = lines[row + 2].trim().split(/\s+/).map(Number);  row += 3;  const arr: number[] = dep.map((t, j) => t + dur[j]);  // 뒤에서부터 최솟값을 누적하면 커지기만 하는 배열이 되어 이분 탐색을 할 수 있다  for (let j = c - 2; j >= 0; j--) arr[j] = Math.min(arr[j], arr[j + 1]);  deps.push(dep);  best.push(arr);} // a[j] >= x인 첫 자리(없으면 a.length)function lowerBound(a: number[], x: number): number {  let lo = 0;  let hi = a.length;  while (lo < hi) {    const mid = (lo + hi) >> 1;    if (a[mid] < x) lo = mid + 1;    else hi = mid;  }  return lo;} // a[j] > x인 첫 자리(없으면 a.length)function upperBound(a: number[], x: number): number {  let lo = 0;  let hi = a.length;  while (lo < hi) {    const mid = (lo + hi) >> 1;    if (a[mid] <= x) lo = mid + 1;    else hi = mid;  }  return lo;} const out: number[] = [];for (let k = 0; k < q; k++) {  const [kind, value] = lines[row + k].trim().split(/\s+/);  let x = Number(value);  if (kind === "A") {    // 앞에서부터: 지금 시각 이후(같아도 됨)에 떠나는 첫 열차의 자리를 찾는다    for (let i = 0; i < m; i++) {      const j = lowerBound(deps[i], x);      if (j === deps[i].length) {        x = -1;        break;      }      x = best[i][j];    }  } else {    // 뒤에서부터: 기한 안에(같아도 됨) 닿는 가장 늦은 열차의 자리를 찾는다    for (let i = m - 1; i >= 0; i--) {      const j = upperBound(best[i], x) - 1;      if (j < 0) {        x = -1;        break;      }      x = deps[i][j];    }  }  out.push(x);}console.log(out.join("\n"));
JavaMain.java · 80 行
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.StringTokenizer; public class Main {    // a[j] >= x인 첫 자리(없으면 a.length)    static int lowerBound(long[] a, long x) {        int lo = 0, hi = a.length;        while (lo < hi) {            int mid = (lo + hi) >>> 1;            if (a[mid] < x) lo = mid + 1;            else hi = mid;        }        return lo;    }     // a[j] > x인 첫 자리(없으면 a.length)    static int upperBound(long[] a, long x) {        int lo = 0, hi = a.length;        while (lo < hi) {            int mid = (lo + hi) >>> 1;            if (a[mid] <= x) lo = mid + 1;            else hi = mid;        }        return lo;    }     public static void main(String[] args) throws IOException {        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));        StringTokenizer st = new StringTokenizer(br.readLine());        int m = Integer.parseInt(st.nextToken());        int q = Integer.parseInt(st.nextToken());         // 도착 시각은 30억까지 가므로 long이어야 한다        long[][] deps = new long[m][]; // 출발 시각(커지는 차례)        long[][] best = new long[m][]; // j번째나 그 뒤 열차를 탔을 때 가장 이른 도착 시각        for (int i = 0; i < m; i++) {            int c = Integer.parseInt(br.readLine().trim());            deps[i] = new long[c];            best[i] = new long[c];            st = new StringTokenizer(br.readLine());            for (int j = 0; j < c; j++) deps[i][j] = Long.parseLong(st.nextToken());            st = new StringTokenizer(br.readLine());            for (int j = 0; j < c; j++) best[i][j] = deps[i][j] + Long.parseLong(st.nextToken());            // 뒤에서부터 최솟값을 누적하면 커지기만 하는 배열이 되어 이분 탐색을 할 수 있다            for (int j = c - 2; j >= 0; j--) best[i][j] = Math.min(best[i][j], best[i][j + 1]);        }         StringBuilder sb = new StringBuilder();        for (int k = 0; k < q; k++) {            st = new StringTokenizer(br.readLine());            String kind = st.nextToken();            long x = Long.parseLong(st.nextToken());            if (kind.equals("A")) {                // 앞에서부터: 지금 시각 이후(같아도 됨)에 떠나는 첫 열차의 자리를 찾는다                for (int i = 0; i < m; i++) {                    int j = lowerBound(deps[i], x);                    if (j == deps[i].length) {                        x = -1;                        break;                    }                    x = best[i][j];                }            } else {                // 뒤에서부터: 기한 안에(같아도 됨) 닿는 가장 늦은 열차의 자리를 찾는다                for (int i = m - 1; i >= 0; i--) {                    int j = upperBound(best[i], x) - 1;                    if (j < 0) {                        x = -1;                        break;                    }                    x = deps[i][j];                }            }            sb.append(x).append('\n');        }        System.out.print(sb);    }}
C#Main.cs · 94 行
using System;using System.Text; class Program{    // a[j] >= x인 첫 자리(없으면 a.Length)    static int LowerBound(long[] a, long x)    {        int lo = 0, hi = a.Length;        while (lo < hi)        {            int mid = lo + (hi - lo) / 2;            if (a[mid] < x) lo = mid + 1;            else hi = mid;        }        return lo;    }     // a[j] > x인 첫 자리(없으면 a.Length)    static int UpperBound(long[] a, long x)    {        int lo = 0, hi = a.Length;        while (lo < hi)        {            int mid = lo + (hi - lo) / 2;            if (a[mid] <= x) lo = mid + 1;            else hi = mid;        }        return lo;    }     static void Main()    {        string[] first = Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries);        int m = int.Parse(first[0]);        int q = int.Parse(first[1]);         // 도착 시각은 30억까지 가므로 long이어야 한다        long[][] deps = new long[m][]; // 출발 시각(커지는 차례)        long[][] best = new long[m][]; // j번째나 그 뒤 열차를 탔을 때 가장 이른 도착 시각        for (int i = 0; i < m; i++)        {            int c = int.Parse(Console.ReadLine());            string[] d = Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries);            string[] u = Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries);            deps[i] = new long[c];            best[i] = new long[c];            for (int j = 0; j < c; j++)            {                deps[i][j] = long.Parse(d[j]);                best[i][j] = deps[i][j] + long.Parse(u[j]);            }            // 뒤에서부터 최솟값을 누적하면 커지기만 하는 배열이 되어 이분 탐색을 할 수 있다            for (int j = c - 2; j >= 0; j--) best[i][j] = Math.Min(best[i][j], best[i][j + 1]);        }         var sb = new StringBuilder();        for (int k = 0; k < q; k++)        {            string[] t = Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries);            long x = long.Parse(t[1]);            if (t[0] == "A")            {                // 앞에서부터: 지금 시각 이후(같아도 됨)에 떠나는 첫 열차의 자리를 찾는다                for (int i = 0; i < m; i++)                {                    int j = LowerBound(deps[i], x);                    if (j == deps[i].Length)                    {                        x = -1;                        break;                    }                    x = best[i][j];                }            }            else            {                // 뒤에서부터: 기한 안에(같아도 됨) 닿는 가장 늦은 열차의 자리를 찾는다                for (int i = m - 1; i >= 0; i--)                {                    int j = UpperBound(best[i], x) - 1;                    if (j < 0)                    {                        x = -1;                        break;                    }                    x = deps[i][j];                }            }            sb.Append(x).Append('\n');        }        Console.Write(sb);    }}
C++main.cpp · 58 行
#include <algorithm>#include <iostream>#include <vector>using namespace std; int main() {    ios::sync_with_stdio(false);    cin.tie(nullptr);     int m, q;    cin >> m >> q;    // 도착 시각은 30억까지 가므로 long long이어야 한다    vector<vector<long long>> deps(m); // 출발 시각(커지는 차례)    vector<vector<long long>> best(m); // j번째나 그 뒤 열차를 탔을 때 가장 이른 도착 시각    for (int i = 0; i < m; i++) {        int c;        cin >> c;        deps[i].resize(c);        best[i].resize(c);        for (auto& t : deps[i]) cin >> t;        for (int j = 0; j < c; j++) {            long long dur;            cin >> dur;            best[i][j] = deps[i][j] + dur;        }        // 뒤에서부터 최솟값을 누적하면 커지기만 하는 배열이 되어 이분 탐색을 할 수 있다        for (int j = c - 2; j >= 0; j--) best[i][j] = min(best[i][j], best[i][j + 1]);    }     for (int k = 0; k < q; k++) {        char kind;        long long x;        cin >> kind >> x;        if (kind == 'A') {            // 앞에서부터: 지금 시각 이후(같아도 됨)에 떠나는 첫 열차 → lower_bound            for (int i = 0; i < m; i++) {                auto it = lower_bound(deps[i].begin(), deps[i].end(), x);                if (it == deps[i].end()) {                    x = -1;                    break;                }                x = best[i][it - deps[i].begin()];            }        } else {            // 뒤에서부터: 기한 안에(같아도 됨) 닿는 가장 늦은 열차 → upper_bound의 바로 앞            for (int i = m - 1; i >= 0; i--) {                auto it = upper_bound(best[i].begin(), best[i].end(), x);                if (it == best[i].begin()) {                    x = -1;                    break;                }                x = deps[i][it - best[i].begin() - 1];            }        }        cout << x << '\n';    }    return 0;}
Cmain.c · 85 行
#include <stdio.h>#include <stdlib.h> // a[j] >= x인 첫 자리(없으면 n)static int lower_bound(const long long *a, int n, long long x) {    int lo = 0, hi = n;    while (lo < hi) {        int mid = lo + (hi - lo) / 2;        if (a[mid] < x) lo = mid + 1;        else hi = mid;    }    return lo;} // a[j] > x인 첫 자리(없으면 n)static int upper_bound(const long long *a, int n, long long x) {    int lo = 0, hi = n;    while (lo < hi) {        int mid = lo + (hi - lo) / 2;        if (a[mid] <= x) lo = mid + 1;        else hi = mid;    }    return lo;} int main(void) {    int m, q;    if (scanf("%d %d", &m, &q) != 2) return 1;     // 도착 시각은 30억까지 가므로 long long이어야 한다    int cnt[10];    long long *deps[10]; // 출발 시각(커지는 차례)    long long *best[10]; // j번째나 그 뒤 열차를 탔을 때 가장 이른 도착 시각    for (int i = 0; i < m; i++) {        int c;        scanf("%d", &c);        cnt[i] = c;        deps[i] = malloc(sizeof(long long) * c);        best[i] = malloc(sizeof(long long) * c);        for (int j = 0; j < c; j++) scanf("%lld", &deps[i][j]);        for (int j = 0; j < c; j++) {            long long dur;            scanf("%lld", &dur);            best[i][j] = deps[i][j] + dur;        }        // 뒤에서부터 최솟값을 누적하면 커지기만 하는 배열이 되어 이분 탐색을 할 수 있다        for (int j = c - 2; j >= 0; j--) {            if (best[i][j + 1] < best[i][j]) best[i][j] = best[i][j + 1];        }    }     for (int k = 0; k < q; k++) {        char kind;        long long x;        scanf(" %c %lld", &kind, &x);        if (kind == 'A') {            // 앞에서부터: 지금 시각 이후(같아도 됨)에 떠나는 첫 열차의 자리를 찾는다            for (int i = 0; i < m; i++) {                int j = lower_bound(deps[i], cnt[i], x);                if (j == cnt[i]) {                    x = -1;                    break;                }                x = best[i][j];            }        } else {            // 뒤에서부터: 기한 안에(같아도 됨) 닿는 가장 늦은 열차의 자리를 찾는다            for (int i = m - 1; i >= 0; i--) {                int j = upper_bound(best[i], cnt[i], x) - 1;                if (j < 0) {                    x = -1;                    break;                }                x = deps[i][j];            }        }        printf("%lld\n", x);    }     for (int i = 0; i < m; i++) {        free(deps[i]);        free(best[i]);    }    return 0;}
Craykonmain.cray · 98 行
첫줄 = 控制台.读数字();m = 첫줄[0];q = 첫줄[1]; deps = []; // deps[i][j]: i번 구간 j번째 열차의 출발 시각(커지는 차례)best = []; // best[i][j]: j번째나 그 뒤 열차를 탔을 때 가장 이른 도착 시각循环 (i = 0; i < m; i++){    c = 控制台.读数字()[0];    dep = 控制台.读数字();    arr = 控制台.读数字();    循环 (j = 0; j < c; j++)    {        arr[j] = dep[j] + arr[j];    }    // 뒤에서부터 최솟값을 누적하면 커지기만 하는 목록이 되어 이분 탐색을 할 수 있다    循环 (j = c - 2; j >= 0; j--)    {        arr[j] = 数学.较小值(arr[j], arr[j + 1]);    }    deps.添加(dep);    best.添加(arr);} 循环 (k = 0; k < q; k++){    t = 控制台.读行().去空白().分割();    x = t[1].转数字();    如果 (t[0] == "A")    {        // 앞에서부터: 지금 시각 이후(같아도 됨)에 떠나는 첫 열차의 자리를 찾는다        循环 (i = 0; i < m; i++)        {            j = 첫자리이상(deps[i], x);            如果 (j == deps[i].数量)            {                x = -1;                跳出;            }            x = best[i][j];        }    }    否则    {        // 뒤에서부터: 기한 안에(같아도 됨) 닿는 가장 늦은 열차의 자리를 찾는다        循环 (i = m - 1; i >= 0; i--)        {            j = 첫자리초과(best[i], x) - 1;            如果 (j < 0)            {                x = -1;                跳出;            }            x = deps[i][j];        }    }    控制台.输出(x);} // a[j] >= x인 첫 자리(없으면 a.개수)첫자리이상(a, x){    lo = 0;    hi = a.数量;    当 (lo < hi)    {        mid = 数学.商(lo + hi, 2);        如果 (a[mid] < x)        {            lo = mid + 1;        }        否则        {            hi = mid;        }    }    返回 lo;} // a[j] > x인 첫 자리(없으면 a.개수)첫자리초과(a, x){    lo = 0;    hi = a.数量;    当 (lo < hi)    {        mid = 数学.商(lo + hi, 2);        如果 (a[mid] <= x)        {            lo = mid + 1;        }        否则        {            hi = mid;        }    }    返回 lo;}