AtCoderの全ペア積和を寄与分解で解く:ABC177 Cを小さい例からPythonで理解

読了 約12分 たびすけ
1から4の全6組を示すABC177 Cの全ペア積和の図

次に読む記事

関連するテーマの記事を、先に確認できます。

ABC177 Cの積和は、全ての組を二重ループで列挙せず、左側の添字を一つ固定して計算できます。現在の値を先に右側の合計から引き、その合計を現在の値に掛けて答えへ加えます。こうすると、条件 1 <= i < j <= N に合う組だけを、各位置一回の更新で数えられます。

この見方を使えるのは、固定した一要素と、残りの要素との組合せを単純な集約値へまとめられる場合です。ABC177 Cでは積の分配法則によって右側の要素の合計にまとめられますが、任意のペア条件へそのまま一般化するものではありません。

左側の添字を固定すると、全ペアの和が一つの寄与になる

公式の添字は1から始まります。iを固定すると、組になるji+1からNまでです。したがって、iを左側に持つ積の合計は、次のように書けます。

Ai × Ai+1 + Ai × Ai+2 + ... + Ai × AN

ここでAiをくくると、右側の合計を一度だけ求めればよくなります。右側の合計をSiとすると、Si = Ai+1 + Ai+2 + ... + AN、その位置の寄与はAi × Siです。全体の答えは、i = 1からN - 1までの寄与を足したものです。

右側の合計を毎回配列の後ろから足し直すと、各iでの計算が長くなります。そこで、最初に配列全体の合計を持ち、現在の値Aiを引いてから使います。引く前の合計には現在値も含まれているため、先に掛けるとAi × Aiまで入ってしまいます。i < jを守るため、順序は必ず「引く、掛ける、足す」です。

[1, 2, 3, 4]で後続和と寄与を追う

次の配列は、式と状態変化を確かめるために構成した例です。公式サンプルではありません。全ての組を直接列挙すると、1 × 2 + 1 × 3 + 1 × 4 + 2 × 3 + 2 × 4 + 3 × 4 = 35になります。

左から順に現在値を処理します。最初の後続和は全体の合計10から始め、各行で現在値を先に引きます。

i 引く前の合計 現在値を引く 引いた後の後続和 寄与 答えの累積
1 1 + 2 + 3 + 4 = 10 10 – 1 2 + 3 + 4 = 9 1 × 9 = 9 9
2 2 + 3 + 4 = 9 9 – 2 3 + 4 = 7 2 × 7 = 14 23
3 3 + 4 = 7 7 – 3 4 3 × 4 = 12 35
4 4 4 – 4 0 4 × 0 = 0 35

例えば1行目の1 × 9は、1 × 21 × 31 × 4をまとめたものです。2行目は2 × 32 × 4、3行目は3 × 4に対応します。最後の値には右側の要素がないので、後続和も寄与も0です。この4行で、6組を重複なく一度ずつ数えています。

ABC177 Cの条件と公式サンプル

ABC177 C – Sum of product of pairsは、N個の整数について、1 <= i < j <= Nを満たす全ての組のAi × Ajの和を109 + 7で割った余りを出力する問題です。入力は1行目がN、2行目がA1 ... ANです。

項目 公式の条件
N 2 <= N <= 2 × 105
Ai 0 <= Ai <= 109
109 + 7
Time Limit 2 sec
Memory Limit 1024 MiB

公式サンプル1は次の入力と出力です。入力の形は問題文のままです。

3
1 2 3
11

この例では1 × 2 + 1 × 3 + 2 × 3 = 11となります。公式サンプル2も無加工で示します。

4
141421356 17320508 22360679 244949
437235829

式の導出と掲載コードの対応は、ABC177 Cの公式解説でも確認できます。

入力を読み、後続和を更新する完全なPython参考コード

コードでは、まず標準入力から整数を全て読みます。N = data[0]で個数を取り出し、A = data[1:1 + N]で問題文の配列をそのまま取り出します。問題の入力はこの1組だけなので、追加のクエリや位置更新を処理するコードは必要ありません。

import sys

MOD = 1_000_000_007
data = list(map(int, sys.stdin.buffer.read().split()))
N = data[0]
A = data[1:1 + N]

suffix_sum = sum(A) % MOD
answer = 0
for value in A:
    suffix_sum = (suffix_sum - value) % MOD
    answer = (answer + value * suffix_sum) % MOD

print(answer)

suffix_sumは、処理前には現在値を含む残りの合計です。ループの最初でvalueを引くと、現在値より右だけの合計になります。その値をvalueに掛け、answerへ加えます。sum(A)、減算、乗算、累積加算の各所で法を適用しているため、保持する値は常に出力の余りとして扱えます。

不変条件で、各組を一度だけ数える

このループの正しさは、各反復の直前に成り立つ不変条件で説明できます。value = Aiを処理する直前、suffix_sumAi + Ai+1 + ... + ANと法の下で同じ値です。最初は全体の合計なので成り立ち、前の反復で一つの現在値を引いた後は、次の位置から末尾までの合計が残ります。

このanswerの添字範囲も、掲載コードのループの向きに合わせて帰納的に確認できます。1-indexedで現在の左端iを処理する直前、answerは、1 <= p < q <= Nかつp < iを満たす積の総和です。式で書けば∑ Ap × Aq(条件 1 <= p < q <= Np < i)です。suffix_sumからAiを引くと、q=i+1..N Aqが残ります。そこへAi × ∑q=i+1..N Aqを加えた直後は、同じ全ペア条件のうちp <= iを満たす積の総和、つまり∑ Ap × Aq(条件 1 <= p < q <= Np <= i)です。i = Nでは引いた後の後続和が0で追加寄与も0ですが、p <= Nに広がったこの条件は全ての1 <= p < q <= Nを含むため、終了時のanswerは全ペアの積和に一致します。

現在値を先に引くと、suffix_sumAi+1 + ... + ANになります。したがって、その反復で足すAi × suffix_sumは、iを左側の添字とする全ての組の積の和です。iを左から右へ進めると、任意の組(i, j)は小さい方の添字を処理した行でだけ現れます。大きい方の添字の行では、すでに左側にある要素を組み合わせないため、同じ組をもう一度足しません。

減算や乗算を各段階で法に対して行っても、元の値と同じ合同関係が保たれます。そのため、最後のanswerは問題が求める積和の余りです。

計算量と、制約の端で確認すること

配列全体の合計を一度計算し、配列を一度走査するため、時間計算量は O(N) です。入力を保持するdataAの大きさはNに比例し、補助変数の数は一定なので、空間計算量はO(N)です。これは掲載したPythonコードの操作と保持するデータから導いており、特定環境での実測時間を保証するものではありません。

  • N = 2では、最初の値を引いた後に二つ目の値が残るため、最初の寄与が唯一の組A1 × A2になります。二つ目の反復では自分自身を引いて後続和が0になり、余分な組は加わりません。
  • Ai = 0なら、その位置の寄与は0です。それでも現在値を後続和から引く更新は行います。更新を省くと、次の位置で右側の合計がずれます。
  • 最後の値を処理すると、現在値を引いた後の後続和は0です。ここで自分自身との積を足さないことが、i < jの条件に対応します。
  • 大きな合計に対しては、コードのように各更新で109 + 7を適用します。入力の個数と値の範囲は、問題文に合わせてN個の整数として扱います。
  • 同じ値でも、位置が異なりi < jを満たすなら、通常の一組としてAi × Ajを数えます。

この問題には位置の更新や順位の問い合わせがないため、後続和を一つ持つ方法で足ります。残りを一つの集約値にできない問題へ、BITやセグメント木などの別の構造を追加する説明は、ここでは扱いません。

次はABC206 Cで、集約する対象を変える

次の練習には、ABC206 C – Swappableを選べます。ここではi < jかつAi ≠ Ajを満たす組の数を求めます。公式サンプル1は次のとおりです。

3
1 7 1
2

(1, 2)(2, 3)は異なる値なので数え、(1, 3)は同じ値なので数えません。この区別を確認したうえで、ABC177 Cで右側の合計を集約したのと同じように、ABC206 Cでは値ごとの出現回数を集約します。ABC206 Cの制約は2 <= N <= 3 × 1051 <= Ai <= 109です。公式解説の方法では、頻度をmapで数えるとO(N)、配列をsortしてから数えるとO(N log N)です。公式解説は、全組数から同値の組を引く方法を示しており、値の出現回数をqとすると同値ペアはq × (q - 1) / 2です。まずこの式を紙で追い、重複値を二重に数えないことを確かめてからコードへ進む、という目的で選ぶ一問です。この記事ではABC206 Cの完全解答までは扱いません。

ABC206 Cの公式解説には、頻度を使う方法と、ソートまたはmapを使う場合の計算量の説明があります。ABC177 Cの後続和からABC206 Cの値の頻度へ、集約する対象が変わる点を意識すると、寄与を分けて数える発想を別の条件で試せます。

公式資料