問題
平面上に $N$ 個の点が与えられる。$Q$ 個の矩形クエリそれぞれについて「$x_1\le x\le x_2$ かつ $y_1\le y\le y_2$」を満たす点の個数を求めよ。
入力形式
N
x_1 y_1
...
x_N y_N
Q
qx1 qx2 qy1 qy2
... (Q行)
制約
$1 \le N,Q \le 2\times10^5$
$0 \le x_i,y_i \le 10^9$
$qx1\le qx2,\ qy1\le qy2$
入出力例
入力例1
5
1 1
2 5
3 3
4 2
5 4
2
2 4 1 4
1 5 1 5
出力例1
2
5
1個目: $x\in[2,4],y\in[1,4]$ を満たすのは$(3,3)$と$(4,2)$の2点。2個目: 全区間なので5点全て。
概念図: x軸セグメント木 × 各ノードのソート済みy配列
ヒント(段階的開示)
ヒント1: 方向性
「xの範囲」と「yの範囲」という2つの独立条件を同時に満たす点数えは、BITやセグ木単体では直接扱えない。1次元の構造をもう1次元分「入れ子」にする発想が核心。
ヒント2: アプローチ
点をxでソートし、その順序でセグメント木を構築。各ノードは担当区間の点のyをソートしたリストを持つ(Merge Sort Tree構築法)。クエリはxの二分探索でインデックス範囲を求め、セグ木を$O(\log N)$ノードに分解し、各ノードのyリストを
bisectで数える。全体 $O(\log^2 N)$/クエリ。ヒント3: 誘導(コード骨格)
def _build(node, l, r):
if l == r:
tree[node] = [points[l][1]]
return
mid = (l+r)//2
_build(node*2, l, mid); _build(node*2+1, mid+1, r)
tree[node] = merge_sorted(tree[node*2], tree[node*2+1]) # 線形マージ
def query(x1, x2, y1, y2):
lo, hi = bisect_index_range(x1, x2)
# セグ木を [lo,hi] に分解しながら bisect でyの個数を数える
模範解答 (Python)
import sys, bisect
class RangeTree:
def __init__(self, points):
self.points = sorted(points)
self.n = len(self.points)
self.xs = [p[0] for p in self.points]
self.tree = [None] * (4 * self.n) if self.n > 0 else []
if self.n > 0:
self._build(1, 0, self.n - 1)
def _build(self, node, l, r):
if l == r:
self.tree[node] = [self.points[l][1]]
return
mid = (l + r) // 2
self._build(node * 2, l, mid)
self._build(node * 2 + 1, mid + 1, r)
left = self.tree[node * 2]
right = self.tree[node * 2 + 1]
merged = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
merged.append(left[i]); i += 1
else:
merged.append(right[j]); j += 1
merged.extend(left[i:])
merged.extend(right[j:])
self.tree[node] = merged
def _count_y(self, ys, y1, y2):
lo = bisect.bisect_left(ys, y1)
hi = bisect.bisect_right(ys, y2)
return hi - lo
def query(self, x1, x2, y1, y2):
if self.n == 0:
return 0
lo = bisect.bisect_left(self.xs, x1)
hi = bisect.bisect_right(self.xs, x2) - 1
if lo > hi:
return 0
return self._query(1, 0, self.n - 1, lo, hi, y1, y2)
def _query(self, node, l, r, ql, qr, y1, y2):
if qr < l or r < ql:
return 0
if ql <= l and r <= qr:
return self._count_y(self.tree[node], y1, y2)
mid = (l + r) // 2
return (self._query(node * 2, l, mid, ql, qr, y1, y2)
+ self._query(node * 2 + 1, mid + 1, r, ql, qr, y1, y2))
def solve():
data = sys.stdin.read().split()
idx = 0
n = int(data[idx]); idx += 1
points = []
for _ in range(n):
x = int(data[idx]); y = int(data[idx+1]); idx += 2
points.append((x, y))
rt = RangeTree(points)
q = int(data[idx]); idx += 1
out = []
for _ in range(q):
x1 = int(data[idx]); x2 = int(data[idx+1])
y1 = int(data[idx+2]); y2 = int(data[idx+3])
idx += 4
out.append(str(rt.query(x1, x2, y1, y2)))
print("\n".join(out))
solve()
計算量: 構築 $O(N\log N)$、クエリ1回 $O(\log^2 N)$、全体 $O((N+Q)\log^2 N)$。$N,Q\approx200$の2000ケースで愚直な全点走査と全結果一致を確認済み。
Step-by-Step 解説
1xでソートする
セグ木の葉順序をxの昇順に固定し、「x範囲」を連続インデックス範囲に変換する。
セグ木の葉順序をxの昇順に固定し、「x範囲」を連続インデックス範囲に変換する。
2各ノードにソート済みyリストを持たせる
子2つのソート済みリストを線形マージするだけで親のリストが得られ、全体構築は$O(N\log N)$。
子2つのソート済みリストを線形マージするだけで親のリストが得られ、全体構築は$O(N\log N)$。
3x範囲をノード集合に分解
通常のセグ木クエリと同様に$O(\log N)$個の完全一致ノードに分解する。
通常のセグ木クエリと同様に$O(\log N)$個の完全一致ノードに分解する。
4各ノードでy範囲を二分探索
bisect_left/rightで$O(\log N)$のカウントを$O(\log N)$個のノード分合計する。5計算量を確認
構築$O(N\log N)$、クエリ$O(\log^2 N)$。Fractional Cascadingでさらに$O(\log N)$に落とせる。
構築$O(N\log N)$、クエリ$O(\log^2 N)$。Fractional Cascadingでさらに$O(\log N)$に落とせる。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| x範囲の二分探索で境界がずれる | bisect_left/rightの意味の取り違え | 上限はbisect_right(x2)-1で閉区間インデックスを求める |
| 各ノードでyを線形走査してしまう | ソート済みであることを活かさない | bisect_left/rightで二分探索する |
| セグ木のノード分解を省略する | 標準的な区間分解パターンの省略 | ql<=l and r<=qrでの打ち切りを必ず書く |
sorted(left+right)でマージし$O(N\log^2N)$にしてしまう | ソート済み2本のマージが線形時間でできることの見落とし | 2ポインタで線形マージする |
次のステップ
- 発展: Fractional Cascading(Day082, Day107)でクエリを$O(\log N)$に高速化する
- 発展: 点の追加に対応する動的Range Treeを考える
- 発展: 3次元領域カウントへの一般化を検討する