{ }codelper
二分探索整数の割り算P0327

標識の間の高さ

海沿いの道に高さの標識が n 本立っている。
i 番目の標識は道の始まりから x_i だけ離れた所にあり、そこの高さは h_i である。
高さは海面を 0 として測った値なので、負のこともある。
標識は始まりに近いものから与えられ、同じ所に標識が 2 本あることはない。
隣り合う 2 本の標識の間では、道はまっすぐに続くとみなす。
つまり、標識 (x0, h0) と次の標識 (x1, h1) の間の距離 d での高さは h0 + (d - x0) × (h1 - h0) ÷ (x1 - x0) である。
最初の標識より手前では最初の標識の高さ、最後の標識より先では最後の標識の高さとみなす。
距離 d が q 個与えられるので、それぞれについて、その距離での高さを超えない最大の整数を求めよ。
高さが 68.5 なら 68 であり、-0.25 なら 0 ではなく -1 である。
例では、距離 17 は (10, 100) と (20, 55) の間なので高さは 100 + 7 × (-45) ÷ 10 = 68.5 で、答えは 68 である。
距離 33 は (20, 55) と (40, -30) の間なので高さは 55 + 13 × (-85) ÷ 20 = -0.25 で、答えは -1 である。

入力
1 行目に n と q が空白で区切られて与えられる。
続く n 行に標識が 1 行に 1 本ずつ "x h" の形で与えられる。
最後の行に距離 d が q 個、空白 1 つで区切られて与えられる。
数はすべて整数である。
1 ≤ n ≤ 10,000、1 ≤ q ≤ 10,000、0 ≤ x_1 < x_2 < … < x_n ≤ 1,000,000,000、-1,000,000 ≤ h_i ≤ 1,000,000、0 ≤ d ≤ 1,000,000,000
出力
距離 d ごとに、与えられた順に答えを 1 行に 1 つずつ出力する。
数はコンマなしで数字だけを書く
例 1 · stdin
5 7
10 100
20 55
40 -30
45 -30
60 -27
0 17 20 33 46 60 99
stdout
100
68
55
-1
-30
-27
-27
main.py
入力(stdin)

実行すると、これらの行が順に input()・標準入力に渡されます(実行中に入力を求めません)。

$ 準備完了

コードを入力して解くにはパソコンで開いてください。モバイルでは問題と解答を見られます。

ほかの言語では?言語別の解答を見る · 8 言語の解答

まず d がどの 2 本の標識の間にあるかを二分探索で探します。
「x が d 以下である最後の標識」の番号です。
Python は bisect_right(xs, d) - 1、C++ は std::upper_bound の 1 つ手前です。
Java の Arrays.binarySearch は見つからないと -(挿入位置) - 1 を返すので -p - 2 に直し、C# の Array.BinarySearch は挿入位置のビットを反転した数を返すので ~p - 1 に直します。
JS・TS・C・Craykon は lo と hi で自分で書きます。
罠 1: 下り坂では割られる数が負です。
C・C++・Java・C# の整数の / は切り下げではなく 0 の方へ切り捨てるので、-315 / 10 は -31 です(答えは -32)。
例の距離 17 が 69、距離 33 が 0 になります。
Python の // は切り下げなのでそのままで正しく、Java は Math.floorDiv、JS・TS は Math.floor(a / b)、C・C++・C# は余りが負のとき商から 1 を引きます。
Craykon の 数学.商 も 0 の方へ切り捨てるので 数学.切り下げ(a / b) を使います。
高さを加重平均 (h0 × (x1 - d) + h1 × (d - x0)) ÷ (x1 - x0) に書き換えても、高さが負なら同じことが起きます(例の距離 46: -447 ÷ 15 は -29.8 なので答えは -30 ですが、切り捨てると -29)。
罠 2: 距離の差と高さの差の積は 2,000 兆まで大きくなります。
Java・C# の int はエラーなしでおかしな数になり、C・C++ では未定義動作なので、long・long long で掛けます。
JS・TS・Craykon の数は 2^53(約 9,000 兆)まで正確なのでそのままで大丈夫です。
罠 3: あふれるのを恐れて先に割ってから掛けると、傾きの小数部分が消えます。
掛けてから割ります。
罠 4: 最初の標識より手前では番号が -1 です。
Python は xs[-1] がエラーなしで最後の標識を返すので、別に分ける必要があります。
罠 5: 二分探索の範囲から最後の標識を外すと、最後の標識より先でも前の区間の傾きのまま上がり続けたり下がり続けたりします。

Pythonmain.py · 22 行
import sysfrom bisect import bisect_right def main():    data = sys.stdin.read().split()    n, q = int(data[0]), int(data[1])    xs = [int(v) for v in data[2:2 + 2 * n:2]]    hs = [int(v) for v in data[3:3 + 2 * n:2]]    out = []    for v in data[2 + 2 * n:2 + 2 * n + q]:        d = int(v)        i = bisect_right(xs, d) - 1          # d 이하인 마지막 표지 (없으면 -1)        if i < 0:            out.append(hs[0])                # 첫 표지보다 앞        elif i == n - 1:            out.append(hs[n - 1])            # 마지막 표지이거나 그 뒤        else:            # //는 음수에서도 내림이다: -85 // 10 == -9            out.append(hs[i] + (d - xs[i]) * (hs[i + 1] - hs[i]) // (xs[i + 1] - xs[i]))    print("\n".join(map(str, out))) main()
JavaScriptmain.js · 34 行
const data = require("fs").readFileSync(0, "utf8").split(/\s+/).filter(Boolean).map(Number);const n = data[0];const q = data[1];const xs = [];const hs = [];for (let i = 0; i < n; i++) {  xs.push(data[2 + 2 * i]);  hs.push(data[3 + 2 * i]);} // d 이하인 마지막 표지의 번호 (없으면 -1)function lastAtOrBefore(d) {  let lo = 0;  let hi = n; // xs[lo..hi) 가운데 d보다 큰 첫 칸을 찾는다  while (lo < hi) {    const mid = (lo + hi) >> 1;    if (xs[mid] <= d) lo = mid + 1;    else hi = mid;  }  return lo - 1;} const out = [];for (let k = 0; k < q; k++) {  const d = data[2 + 2 * n + k];  const i = lastAtOrBefore(d);  if (i < 0) out.push(hs[0]);  else if (i === n - 1) out.push(hs[n - 1]);  else {    // 곱이 2^53보다 작아 정확하고, Math.floor는 음수에서도 내림이다    out.push(hs[i] + Math.floor(((d - xs[i]) * (hs[i + 1] - hs[i])) / (xs[i + 1] - xs[i])));  }}console.log(out.join("\n"));
TypeScriptmain.ts · 35 行
import { readFileSync } from "node:fs";const data: number[] = readFileSync(0, "utf8").split(/\s+/).filter(Boolean).map(Number);const n: number = data[0];const q: number = data[1];const xs: number[] = [];const hs: number[] = [];for (let i = 0; i < n; i++) {  xs.push(data[2 + 2 * i]);  hs.push(data[3 + 2 * i]);} // d 이하인 마지막 표지의 번호 (없으면 -1)function lastAtOrBefore(d: number): number {  let lo = 0;  let hi = n; // xs[lo..hi) 가운데 d보다 큰 첫 칸을 찾는다  while (lo < hi) {    const mid = (lo + hi) >> 1;    if (xs[mid] <= d) lo = mid + 1;    else hi = mid;  }  return lo - 1;} const out: number[] = [];for (let k = 0; k < q; k++) {  const d = data[2 + 2 * n + k];  const i = lastAtOrBefore(d);  if (i < 0) out.push(hs[0]);  else if (i === n - 1) out.push(hs[n - 1]);  else {    // 곱이 2^53보다 작아 정확하고, Math.floor는 음수에서도 내림이다    out.push(hs[i] + Math.floor(((d - xs[i]) * (hs[i + 1] - hs[i])) / (xs[i + 1] - xs[i])));  }}console.log(out.join("\n"));
JavaMain.java · 40 行
import java.io.BufferedReader;import java.io.InputStreamReader;import java.io.StreamTokenizer;import java.util.Arrays; public class Main {    public static void main(String[] args) throws Exception {        StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));        in.nextToken();        int n = (int) in.nval;        in.nextToken();        int q = (int) in.nval;        int[] xs = new int[n];        long[] hs = new long[n];        for (int i = 0; i < n; i++) {            in.nextToken();            xs[i] = (int) in.nval;            in.nextToken();            hs[i] = (long) in.nval;        }        StringBuilder out = new StringBuilder();        for (int k = 0; k < q; k++) {            in.nextToken();            int d = (int) in.nval;            // 찾으면 그 번호, 못 찾으면 -(끼울 자리) - 1 을 돌려준다            int p = Arrays.binarySearch(xs, d);            int i = p >= 0 ? p : -p - 2;     // d 이하인 마지막 표지 (없으면 -1)            long ans;            if (i < 0) ans = hs[0];            else if (i == n - 1) ans = hs[n - 1];            else {                // long으로 곱하고, 음수에서도 내림인 floorDiv를 쓴다 (/ 는 0 쪽으로 버린다)                long num = (long) (d - xs[i]) * (hs[i + 1] - hs[i]);                ans = hs[i] + Math.floorDiv(num, (long) (xs[i + 1] - xs[i]));            }            out.append(ans).append('\n');        }        System.out.print(out);    }}
C#Main.cs · 40 行
using System;using System.Text; class Program{    // C#의 / 는 0 쪽으로 버리므로 내림 나눗셈을 직접 만든다 (b > 0)    static long FloorDiv(long a, long b)    {        long quot = a / b;        if (a % b < 0) quot--;        return quot;    }     static void Main()    {        string[] data = Console.In.ReadToEnd().Split((char[])null, StringSplitOptions.RemoveEmptyEntries);        int n = int.Parse(data[0]), q = int.Parse(data[1]);        int[] xs = new int[n];        long[] hs = new long[n];        for (int i = 0; i < n; i++)        {            xs[i] = int.Parse(data[2 + 2 * i]);            hs[i] = long.Parse(data[3 + 2 * i]);        }        var sb = new StringBuilder();        for (int k = 0; k < q; k++)        {            int d = int.Parse(data[2 + 2 * n + k]);            // 찾으면 그 번호, 못 찾으면 끼울 자리의 비트를 뒤집은 수(~자리)를 돌려준다            int p = Array.BinarySearch(xs, d);            int i = p >= 0 ? p : ~p - 1;     // d 이하인 마지막 표지 (없으면 -1)            long ans;            if (i < 0) ans = hs[0];            else if (i == n - 1) ans = hs[n - 1];            else ans = hs[i] + FloorDiv((long)(d - xs[i]) * (hs[i + 1] - hs[i]), xs[i + 1] - xs[i]);            sb.Append(ans).Append('\n');        }        Console.Write(sb);    }}
C++main.cpp · 31 行
#include <algorithm>#include <iostream>#include <vector> // C++의 / 는 0 쪽으로 버리므로 내림 나눗셈을 직접 만든다 (b > 0)long long floorDiv(long long a, long long b) {    long long quot = a / b;    if (a % b < 0) quot--;    return quot;} int main() {    std::ios::sync_with_stdio(false);    std::cin.tie(nullptr);    int n, q;    std::cin >> n >> q;    std::vector<int> xs(n);    std::vector<long long> hs(n);    for (int i = 0; i < n; i++) std::cin >> xs[i] >> hs[i];    for (int k = 0; k < q; k++) {        int d;        std::cin >> d;        // upper_bound: d보다 큰 첫 칸 → 그 앞 칸이 d 이하인 마지막 표지 (없으면 -1)        int i = static_cast<int>(std::upper_bound(xs.begin(), xs.end(), d) - xs.begin()) - 1;        long long ans;        if (i < 0) ans = hs[0];        else if (i == n - 1) ans = hs[n - 1];        else ans = hs[i] + floorDiv(static_cast<long long>(d - xs[i]) * (hs[i + 1] - hs[i]), xs[i + 1] - xs[i]);        std::cout << ans << '\n';    }}
Cmain.c · 34 行
#include <stdio.h> int xs[10000];long long hs[10000]; /* C의 / 는 0 쪽으로 버리므로 내림 나눗셈을 직접 만든다 (b > 0) */long long floor_div(long long a, long long b) {    long long quot = a / b;    if (a % b < 0) quot--;    return quot;} int main(void) {    int n, q;    scanf("%d %d", &n, &q);    for (int i = 0; i < n; i++) scanf("%d %lld", &xs[i], &hs[i]);    for (int k = 0; k < q; k++) {        int d;        scanf("%d", &d);        int lo = 0, hi = n;                 /* xs[lo..hi) 가운데 d보다 큰 첫 칸을 찾는다 */        while (lo < hi) {            int mid = (lo + hi) / 2;            if (xs[mid] <= d) lo = mid + 1;            else hi = mid;        }        int i = lo - 1;                     /* d 이하인 마지막 표지 (없으면 -1) */        long long ans;        if (i < 0) ans = hs[0];        else if (i == n - 1) ans = hs[n - 1];        else ans = hs[i] + floor_div((long long)(d - xs[i]) * (hs[i + 1] - hs[i]), xs[i + 1] - xs[i]);        printf("%lld\n", ans);    }    return 0;}
Craykonmain.cray · 50 行
첫줄 = コンソール.数読み();n = 첫줄[0];q = 첫줄[1];xs = [];hs = [];繰り返す (i = 0; i < n; i++){    줄 = コンソール.数読み();    xs.追加(줄[0]);    hs.追加(줄[1]);} // d 이하인 마지막 표지의 번호 (없으면 -1)마지막표지(d){    lo = 0;    hi = n; // xs[lo..hi) 가운데 d보다 큰 첫 칸을 찾는다    間 (lo < hi)    {        mid = 数学.商(lo + hi, 2);        もし (xs[mid] <= d)        {            lo = mid + 1;        }        でなければ        {            hi = mid;        }    }    返す lo - 1;} 질문 = コンソール.数読み();それぞれ (d の中 질문){    i = 마지막표지(d);    もし (i < 0)    {        コンソール.書く(hs[0]);    }    でなければ もし (i == n - 1)    {        コンソール.書く(hs[n - 1]);    }    でなければ    {        // 수학.몫은 0 쪽으로 버리므로, 실수로 나눈 뒤 수학.내림으로 내린다        コンソール.書く(hs[i] + 数学.切り下げ((d - xs[i]) * (hs[i + 1] - hs[i]) / (xs[i + 1] - xs[i])));    }}