問題
$N$ 個の倍精度浮動小数 $x_1,\dots,x_N$ が与えられる(入力の各トークンを最も近い IEEE 754 倍精度に丸めた値。Python の float(token) と同じ)。実数としての厳密な和 $\sum x_i$ を、最近接偶数丸め(round-half-even)で倍精度に 1 回だけ丸めた値 を求め、Python の repr の形式(その値に戻る最短の 10 進表記)で出力せよ。
入力形式
N
x_1
...
x_N
制約
| # | 条件 |
|---|---|
| 1 | $1\le N\le 2\times10^5$ |
| 2 | 各 $x_i$ は有限で $|x_i|\le10^{300}$(非正規数も含む)。トークンは 30 文字以下の 10 進浮動小数表記 |
入出力例
入力例1
3
1e100
1.0
-1e100出力例1
1.0入力例2
3
1.0
1.1102230246251565e-16
1.232595164407831e-32出力例2
1.0000000000000002(例1: 左から足すと $10^{100}+1$ で 1 が消え 0.0 になる。例2: $x_2=2^{-53}$ は 1.0 の ulp のちょうど半分、$x_3=2^{-106}$。厳密な和は「ちょうど半分」をわずかに超えるので 1 つ上の $1+2^{-52}$ に丸めるのが正しい。左から足すと $1+2^{-53}$ が同点の偶数丸めで 1.0 になり、その後 $2^{-106}$ は消える。**Python 3.12 以降の組み込み sum() は Neumaier 補正で例1 は 1.0 を返すが、例2 は 1.0(誤り)を返す**——実測)
本問は Shewchuk「Adaptive Precision Floating-Point Arithmetic and Fast Robust Geometric Predicates」(Discrete & Computational Geometry 18, 1997)の展開(expansion)を使った総和で、CPython の math.fsum の実装(Modules/mathmodule.c)と同じ手順である。Python 3.12 の変更点は What's New(https://docs.python.org/3/whatsnew/3.12.html 、2026-09-25 参照)に「sum() now uses Neumaier summation」とある。
概念図:partials と half-even の同点
ヒント(クリックで段階的に開示)
ヒント1(方向性)
2 つの double の和 $x+y$ を丸めた $hi$ と、丸めで失われた分 $lo=(x+y)-hi$ は、**$lo$ もまた double で厳密に表せる**($|x|\ge|y|$ なら $lo=y-(hi-x)$ が誤差なく計算できる: Dekker の Fast2Sum)。失った分を捨てずに持ち続ければ、和は厳密に保てる。
ヒント2(アプローチ)
「ビットが重ならない double の列」partials を持ち、その実数和が常にこれまでの厳密な和に等しいように保つ。新しい $x$ を partials の各要素と順に Fast2Sum し、0 でない $lo$ だけを残し、最後に $hi$ を積む。最後に partials を大きい方から足し、初めて不正確になったところで止める。
ヒント3(誘導)
for x in xs:
i = 0
for y in partials:
if abs(x) < abs(y): x, y = y, x
hi = x + y; lo = y - (hi - x)
if lo: partials[i] = lo; i += 1
x = hi
partials[i:] = [x]
# 最後: 上から hi に足していき lo != 0 で止める
# lo がちょうど半 ulp で、次の partial が lo と同符号なら hi を 1 ulp 動かす模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
def exact_sum(xs):
partials = [] # 重なりのない double の列(絶対値の昇順)。総和が厳密に保たれる
for x in xs:
i = 0
for y in partials:
if abs(x) < abs(y):
x, y = y, x
hi = x + y # 丸めた和
lo = y - (hi - x) # 丸めで失った分(Fast2Sum: |x|>=|y| なら厳密)
if lo:
partials[i] = lo
i += 1
x = hi
partials[i:] = [x]
# 上から足し、初めて不正確になったところで止める
n = len(partials)
hi = 0.0
if n:
n -= 1
hi = partials[n]
lo = 0.0
while n:
x = hi
n -= 1
y = partials[n]
hi = x + y
lo = y - (hi - x)
if lo:
break
# 丁度中間(half-even の同点)に見えて、残りの partial が同じ向きなら切り上げる
if n and ((lo < 0 and partials[n - 1] < 0) or (lo > 0 and partials[n - 1] > 0)):
y = lo * 2.0
x = hi + y
if y == x - hi:
hi = x
return hi + 0.0 # 厳密な和が 0 のとき -0.0 を 0.0 にそろえる(実数の 0)
def main():
data = sys.stdin.buffer.read().split()
n = int(data[0])
xs = [float(t) for t in data[1:1 + n]]
print(repr(exact_sum(xs)))
main()
計算量: 時間 $O(N\cdot p)$($p$ は partials の長さ。重ならない double は指数の幅 2098 ビットを分け合うので最悪でも約 2000 本だが、実測は 24〜91 本)、空間 $O(p)$。
Step-by-Step 解説
$|x|\ge|y|$ のとき $hi=\mathrm{fl}(x+y)$、$lo=y-\mathrm{fl}(hi-x)$ とすると $x+y=hi+lo$ が 厳密に 成り立つ(Dekker 1971)。$hi-x$ は $y$ に近く厳密に計算でき、その差 $lo$ は $hi$ の半 ulp 以下で double に入る。前提 $|x|\ge|y|$ を満たすため、比べて入れ替える。
partials は「絶対値が昇順で、ビットが重ならない」double の列で、その実数和がこれまでの入力の厳密な和。新しい $x$ を小さい partial から順に Fast2Sum すると、各段の $lo$ はその段の $hi$ より十分小さい。0 の $lo$ は捨てるので列は短く保たれる。これは Shewchuk の Grow-Expansion の「0 除去(zero elimination)」版。
partials の実数和を double に丸めたい。大きい方から $hi$ に足し、$lo\ne0$ になった時点で $hi$ は「上位の partials の和を丸めた値」。残りの partials は $lo$ よりさらに小さいので、ふつうは結果を変えない。
$lo$ が $hi$ のちょうど半 ulp のとき、$hi+lo$ の丸めは偶数側へ行く(同点規則)。しかし残りの partials が $lo$ と 同じ符号 なら、真の和は半分を超えているので逆側に丸めるべき。$y=2lo$ とし $x=hi+y$ が厳密($x-hi=y$)なら $lo$ は正確に半 ulp だったと分かり、$hi\leftarrow x$。入力例2 がこのケース。この補正を外すと、2 万ケース中 264 ケースで正しい丸めとずれた(実測)。
2 種類のデータで、Fraction による厳密値を正しく丸めた値と比べた(各 200 列):
| 方法 | 条件数が中程度(各 1000 項, 部分的に相殺) | 条件数 $\approx10^{90}$($\pm2^{200}$ 級が相殺し $2^{-100}$ 級が残る) |
|---|---|---|
| 左から足す | 中央値 7 ulp・最大 418 ulp・正確 8/200 | 正確 0/200・相対誤差 中央値 $4.1\times10^{74}$ |
| Kahan | 中央値 1 ulp・最大 65 ulp・正確 90/200 | 0/200・$1.1\times10^{74}$ |
Neumaier = 組み込み sum()(3.12+) | 正確 200/200 | 0/200・$1.0\times10^{59}$ |
math.fsum/本解 | 正確 200/200 | 正確 200/200 |
補正付き総和の誤差は「$u+Nu^2\cdot$条件数」程度で、条件数が $1/u^2\approx10^{32}$ を超えると意味を失う。厳密和は条件数に依らない。
迷った他案と採らなかった理由
| 案 | 採否 | 理由 |
|---|---|---|
| 左から足す | 不採用 | 例1 で 0.0 |
Kahan / Neumaier(sum() 3.12+) | 不採用 | 速く(組み込み sum で $N=2\times10^5$ が 0.001 s)大半のデータで十分だが、例2 のような同点と、条件数 $10^{32}$ 超では誤る |
fractions.Fraction で厳密に足して float() | 比較用 | 正しい(float(Fraction) は正しく丸める)。Python では本解と同じ速さ(乱数 $N=2\times10^5$ で 0.290 s vs 0.284 s、指数 $\pm1000$ 幅で 1.01 s vs 1.05 s)。本解の価値は C で書いたときに出る(math.fsum は 0.006〜0.019 s) |
| 長い固定小数点のアキュムレータ(Kulisch accumulator, 約 4300 ビット) | 扱わない | ハードウェア・SIMD 向けの厳密和。Python では整数 1 本で書けるが、本問の学習目的(double だけで誤差を運ぶ)から外れる |
| Shewchuk の partials + half-even 補正(本解) | 採用 | double の演算だけで厳密。math.fsum と全ケース一致 |
前提が変わったら: 集計(金額など)で「正しく丸めた 1 つの値」が要るなら math.fsum を使う(自作しない)。GPU/並列の総和で再現性(実行順に依らない結果)が要るなら、厳密和か固定小数点のアキュムレータが必要——補正付き総和は順序で結果が変わる。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
入れ替えずに lo = y - (hi - x) | $\lvert x\rvert<\lvert y\rvert$ だと Fast2Sum が厳密でない | 先に if abs(x) < abs(y): x, y = y, x(または入れ替え不要の 6 演算の TwoSum) |
最後に partials を全部 sum() する | 丸めが複数回入る | 上から足し、$lo\ne0$ で止める |
| half-even の補正を忘れる | 同点のときだけ誤る(ランダムテストでは見つけにくい) | Step 4 の補正。同点を狙ったテストを作る |
出力に print(x) 以外の書式(%.17g など)を使う | 表記が repr と一致しない | repr(x)(最短の往復表記) |
入力が -0.0 だけのとき -0.0 を出す | partials に $-0.0$ が 1 本残り、そのまま返る。本問は「実数の和を丸める」ので 0 は 0.0 | 最後に hi + 0.0(最近接丸めでは $-0.0+0.0=0.0$、他の値は不変)。math.fsum([-0.0]) も 0.0(実測) |
次のステップ
- 発展1: 同じ partials を使って、厳密な和の 符号 だけを高速に判定する(Shewchuk の幾何述語 orient2d の考え方)
- 発展2: $\sum x_iy_i$(内積)の厳密計算。積の誤差は FMA(
math.fma, Python 3.13+)で $x y - \mathrm{fl}(xy)$ として取り出せる(TwoProduct) - 扱わなかった範囲: 途中のオーバーフロー($|x_i|$ が $10^{308}$ 近くで $hi$ が inf になる。
math.fsumは特別扱いしている)、inf/nan の入力、並列化
検証ログ(本セッションで実行した数値)
- 入力例: 例1
1.0(左から足すと0.0、組み込みsumは1.0)、例21.0000000000000002(左から足すと1.0、組み込みsumも1.0、math.fsumは1.0000000000000002) Fractionとの一致(実測): 2 万ケース(ランダム・巨大な相殺・ちょうど半 ulp+微小な尾・非正規数付近と ±0.0 の 4 種)で、値と 0 の符号まで比べて不一致 0、math.fsumも不一致 0。half-even 補正を外した版は 264 ケースで不一致。最初の版は-0.0単独入力で-0.0を返しており(math.fsumは0.0)、hi + 0.0を足して直した- 精度(実測): Step 5 の表
- 性能(実測, $N=2\times10^5$): 乱数($10^{-30}$〜$10^{30}$)で 左から 0.003 s、
sum0.001 s、math.fsum0.006 s、本解 0.284 s、Fraction0.290 s。指数 $\pm1000$ 幅で本解 1.05 s、Fraction1.01 s、math.fsum0.019 s - partials の最大長(実測): 24 本($10^{\pm30}$)、91 本($2^{\pm1000}$)
- 出所ラベル: 誤差・秒数・本数は実測(Python 3.14.7)。Fast2Sum の厳密性は Dekker 1971/Shewchuk 1997 の定理。
sum()の Neumaier 化は Python 3.12 What's New の記述を原文で確認。Kulisch accumulator は文献上の比較で本セッションでは実装していない - 検証スクリプト: スクラッチパッドの
d160/q5_test.py・q5_test2.py・q5_test_strict.py