問題
長さ $N$ の整数列 $A$ が与えられる。$A$ を 極大な広義単調増加区間(run) $R_1,R_2,\dots,R_m$ に分ける($A_{i}<A_{i-1}$ の位置で区切る)。$R_j$ の開始位置を $s_j$(0 始まり)、長さを $\ell_j$ とする。
隣り合う run $R_j,R_{j+1}$ の境界の power を次で定める。2 つの中点 $a=(s_j+\ell_j/2)/N$、$b=(s_{j+1}+\ell_{j+1}/2)/N$ に対し
$$\mathrm{power}_j=\min\{k\ge1 : \lfloor a\cdot2^k\rfloor\ne\lfloor b\cdot2^k\rfloor\}$$
次の手順(Powersort, Munro–Wild 2018)で run を併合する。長さ $x$ と $y$ の列の併合のコストを $x+y$ とし、コストの総和 を出力せよ。
cur = R_1; stack = []
for j = 1 .. m-1:
p = power_j
while stack が空でなく、stack の一番上の power > p:
top = stack.pop(); cur = merge(top, cur)
stack.push((cur, p)); cur = R_{j+1}
while stack が空でない:
top = stack.pop(); cur = merge(top, cur)
入力形式
N
A_1 A_2 ... A_N
制約
| # | 条件 |
|---|---|
| 1 | $1\le N\le 2\times10^5$、$0\le A_i\le10^9$ |
入出力例
入力例1
15
1 2 3 4 5 6 7 2 3 4 5 6 3 2 1出力例1
28(run の長さは 7, 5, 1, 1, 1。中点は $3.5/15=0.233$、$9.5/15=0.633$、$0.833$、$0.900$、$0.967$。境界 1 は 0.5 をまたぐので power 1、境界 2 は 0.75 をまたいで 2、境界 3 は 0.875 で 3、境界 4 は 0.9375 で 4。power が右へ単調に増えるので全部積まれ、右から $1+1=2$、$1+2=3$、$5+3=8$、$7+8=15$ と併合してコスト 28。これは最適な併合順のコスト 28 と一致する。Timsort の併合規則では 38、隣同士を 2 つずつ併合する素朴な方式では 43)
Powersort は J. Ian Munro, Sebastian Wild「Nearly-Optimal Mergesorts: Fast, Practical Sorting Methods That Optimally Adapt to Existing Runs」(ESA 2018、https://arxiv.org/abs/1805.04154 、2026-09-25 参照)の手法で、CPython の Objects/listsort.txt(https://github.com/python/cpython/blob/main/Objects/listsort.txt 、同上参照)に「The code now uses the "powersort" merge strategy」とあり、list.sort()/sorted() の併合順に使われている(導入は bpo-34561)。
概念図:power と併合木
ヒント(クリックで段階的に開示)
ヒント1(方向性)
併合のコストは「各要素が何回併合に参加したか」の総和、つまり併合木での各 run の深さ × 長さの和。長い run を浅く、短い run を深く 置き、しかも run の並び順は変えられない(安定ソートなので隣同士しか併合できない)。これは最適アルファベット順二分木(Day158-Q3 の Garsia–Wachs)と同じ問題で、最適解は $O(m\log m)$ だが、Powersort は左から 1 回走査するだけで近い木を作る。
ヒント2(アプローチ)
$[0,1)$ を二分割し続ける完全二分木を考える。各 run を中点の位置に置くと、境界 $j$ が「何段目の分割線をまたぐか」が power。power が小さい境界ほど木の根に近い(後で併合する)。スタックには「右隣との境界の power」が単調増加になるように積み、より小さい power が来たら大きいものから併合する。
ヒント3(誘導)
def node_power(s1, n1, n2, n):
a = 2 * s1 + n1 # 中点 × 2(分母 2n)で整数化
b = 2 * (s1 + n1) + n2
k = 0
while True:
k += 1
if (a << k) // (2 * n) != (b << k) // (2 * n):
return k模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
def node_power(s1, n1, n2, n):
"""境界の power: 2 つの run の中点 a, b([0,1) に正規化)の二進展開が初めて食い違う桁。"""
a = 2 * s1 + n1 # 中点×2(分母 2n)
b = 2 * (s1 + n1) + n2
k = 0
while True:
k += 1
# floor(a * 2^k / 2n) と floor(b * 2^k / 2n) が異なる最小の k
if (a << k) // (2 * n) != (b << k) // (2 * n):
return k
def main():
data = sys.stdin.buffer.read().split()
n = int(data[0])
A = list(map(int, data[1:1 + n]))
# 1) 極大な広義単調増加 run に分解
runs = [] # (開始位置, 長さ)
s = 0
for i in range(1, n + 1):
if i == n or A[i] < A[i - 1]:
runs.append((s, i - s))
s = i
# 2) Powersort の併合方針。コスト = 併合した 2 列の長さの和の総和
cost = 0
stack = [] # (開始位置, 長さ, 右隣との境界の power)
cs, cl = runs[0]
for bs, bl in runs[1:]:
p = node_power(cs, cl, bl, n)
while stack and stack[-1][2] > p: # 深い(power が大きい)境界から先に併合
ts, tl, _ = stack.pop()
cost += tl + cl
cs, cl = ts, tl + cl
stack.append((cs, cl, p))
cs, cl = bs, bl
while stack:
ts, tl, _ = stack.pop()
cost += tl + cl
cs, cl = ts, tl + cl
print(cost)
main()
計算量: run 分解 $O(N)$、power は 1 境界あたり $O(\log N)$ 回のループ(CPython はビット演算で $O(1)$)、スタック操作は償却 $O(1)$。合計 $O(N+m\log N)$。スタックの深さは power の最大値 $\lceil\log_2 N\rceil+1$ 以下。
Step-by-Step 解説
run を葉とする二分木で、各内部節点が 1 回の併合。要素は根までの深さの回数だけ併合に参加するので、コスト $=\sum_j \ell_j\cdot\mathrm{depth}(R_j)$。下界はエントロピー $nH=\sum_j\ell_j\log_2(N/\ell_j)$(ハフマン符号の下界と同じ理屈)。入力例1 は $nH=27.34$、最適 28。
区間 $[0,1)$ を $1/2$、$1/4, 3/4$、… と分割する線を考える。長い run は幅が広いので、その中点と隣の中点の間には 浅い(粗い)分割線 が入りやすい。境界 $j$ の power は「中点 $a,b$ の間にある最も粗い分割線の段数」で、$\lfloor a2^k\rfloor\ne\lfloor b2^k\rfloor$ はそのまま「$k$ 段目の線がまたがれている」。
境界の power 列について、最小の power の境界で左右に分け、再帰する木(Cartesian tree)が Powersort の併合木。隣り合う境界の power は必ず異なる(同じ段の線が 2 本入ると、間に 1 段粗い線が必ずあり、どちらかの power が小さくなる)ので木は一意。スタックの手順はこの木を左から 1 回の走査で作る方法で、実測で再帰版と 2000 ケース一致。
$m=2000$ 本の run、各 5 試行。最適は Garsia–Wachs(Day158-Q3)で計算:
| run 長の分布 | Powersort / 最適 | Timsort / 最適 | 隣同士 2 つずつ / 最適 |
|---|---|---|---|
| 一様 [1,100] | 1.0068 | 1.0638 | 1.0160 |
| 幾何 $p=0.02$ | 1.0138 | 1.0739 | 1.0448 |
| パレート $\alpha=1.1$(裾が重い) | 1.0373 | 1.0752 | 1.2252 |
山登りで最悪入力を探すと(24 本)、Timsort / 最適 は 1.383 まで悪化、Powersort / 最適 は 1.071 止まり。Buss–Knop(arXiv:1801.04641)は Timsort の併合コストに $(1.5-o(1))n\log n$ の下界を示しており、これと整合する。
Munro–Wild は Powersort のコストが $nH+2n$ 以下であることを示した。実測でランダム 2000 ケースの $(\text{cost}-nH)/n$ の最大は 1.637(2 未満)。
迷った他案と採らなかった理由
| 案 | 最適との比(実測・パレート) | 採否 | 理由 |
|---|---|---|---|
| 隣同士 2 つずつ(ボトムアップ) | 1.225 | 不採用 | run 長の偏りを無視する。偏りが大きい現実データで弱い |
Timsort の不変条件(merge_collapse) | 1.075(最悪 1.38) | 不採用 | CPython が 2002 年(Python 2.3)から powersort 導入まで使った方式。2015 年に不変条件の抜けが形式検証(de Gouw ら, CAV 2015)で見つかり修正された経緯もある |
| 最適(Garsia–Wachs / Hu–Tucker) | 1.000 | 不採用 | 全 run を見てから木を作るので、左から流しながら併合できない(キャッシュ効率が悪い)。定数も大きい |
| Peeksort(同論文のもう一方) | — | 扱わない | 再帰でトップダウンに割る。性能は Powersort と同等と報告。スタック 1 本で済む Powersort の方が既存の Timsort 実装に入れやすい |
| Powersort(本解) | 1.037(最悪 1.07) | 採用 | 左から 1 回走査・$O(\log N)$ スタック・上界 $nH+2n$ |
前提が変わったら: 本問は run の検出を単純化している。実際の list.sort は (1) 狭義減少の run を反転して使う (2) 短い run を二分挿入ソートで最小長 minrun まで伸ばす (3) 併合中に galloping で比較回数を減らす。入力がほぼランダムなら run は短く、コストの差はほぼ出ない($N=2\times10^5$ の乱数で run は約 $10^5$ 本)。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 中点を浮動小数で計算する | $N$ が大きいと $a\cdot2^k$ の切り捨てがずれうる | 中点を 2 倍して分母 $2N$ の整数で比べる |
条件を >= にする | 隣り合う power は異なるので結果は同じだが、「同じ power は右結合」という定義を読み違えている | 厳密に >。等号の扱いを説明できるようにする |
| 最後にスタックを左から併合する | 併合順が変わりコストが変わる | 上(右)から pop して cur と併合 |
| 狭義減少区間を 1 本の run として扱う | 本問の run は広義増加のみ(反転は実際の list.sort の仕様) | A[i] < A[i-1] で区切る |
| コストに比較回数を使う | 本問のコストは長さの和(要素の移動量) | cost += tl + cl |
次のステップ
- 発展1: 最適な併合木(Garsia–Wachs)と Powersort の木を同じ入力で描き、深さが違う run を特定する
- 発展2: Timsort の
merge_collapse(修正版)を実装し、本問の入力例で 38 になることを確かめる。さらに 2015 年の修正前の条件(直前 3 本だけを見る)で不変条件が壊れる入力を探す - 扱わなかった範囲: galloping mode、minrun の決め方(listsort.txt には 2025 年の新方式の記述がある)、比較回数の解析
検証ログ(本セッションで実行した数値)
- 入力例1: run 長 [7,5,1,1,1]、power [1,2,3,4]、出力
28。同じ run 列で Timsort(CPython の修正版merge_collapseを移植)38、隣同士 43、最適($O(m^3)$ 区間 DP)28、$nH=27.34$ - スタック版と Cartesian tree の再帰版の一致(実測): ランダム 2000 ケースで不一致 0。入力配列から run を切る部分も含めたプログラム全体で 300 ケース不一致 0
- Garsia–Wachs と区間 DP の一致(実測): 300 ケースで一致(最適コストの計算に使う前の検算)
- 最適比(実測, $m=2000$, 各 5 試行): Step 4 の表。最大値は一様 1.0073 / 1.0807、幾何 1.0151 / 1.1055、パレート 1.0576 / 1.1018(Powersort / Timsort)
- 最悪入力の探索(実測, 24 本, 山登り 6000 回 × 3): Timsort/最適 1.3827、Powersort/最適 1.0714
- 上界(実測): $(\text{cost}-nH)/n$ の最大 1.637(ランダム 2000 ケース)、$m=2000$ の分布別では 0.199/0.306/0.577
- 性能(実測): $N=2\times10^5$ の乱数列(run 約 $10^5$ 本)で 0.36 s、出力 3,316,864
- 出所ラベル: 比・秒数は実測(Python 3.14.7)。上界 $nH+2n$ は Munro–Wild の定理、$(1.5-o(1))n\log n$ は Buss–Knop の要旨、listsort.txt の記述は 2026-09-25 に原文を確認。de Gouw ら 2015 の経緯は文献の記述で本セッションでは再現していない
- 検証スクリプト: スクラッチパッドの
d160/q4_test.py・q4_common.py