整数列 A1..N が与えられます。 1 ≤ j < i ≤ N かつ Aj = Ai を満たす組 (i, j) の総数を求めてください。
配列を先頭から順に走査し、各値 v について「これまでに v が何回出現したか」を保持する Map(または連想配列)を使います。
現在の位置 i の値 v を見ると、過去に v が k 回出現していれば、(i, j) の組はちょうど k 個増えます。走査を最後まで行うと合計が答えになります。
式で書くと、最終的な答えは各値 v の出現回数 cv に対して sum_v C(c_v, 2)(= sum_v c_v*(c_v-1)/2)と等価です。これは上の逐次加算の結果と一致します。
このデモは配列 [30,10,30,20,10,30] を例に、各ステップで Map と合計がどう変化するかを示します。
| 値 (v) | 出現回数 |
|---|
現在の合計 (組数): 0
次の要素を処理する際、もしその値 v の現在のカウントが k なら、合計は k 増え、その後 v のカウントを k+1 に更新します。
// TypeScript 5.1 / Node.js 18.16.1
// 高速入出力: fs
import * as fs from 'fs';
const input = fs.readFileSync(0, 'utf8').trim().split(/\s+/).map(Number);
/**
* 条件を満たす (i, j) の組数を数える関数
* @param N - 配列の要素数
* @param arr - 整数配列 A1...AN
* @returns 組の総数(number)
*
* 時間計算量: O(N)(Map の get/set は平均 O(1))
* 空間計算量: O(U)(U は異なる値の個数。最悪で N)
*/
function countPairs(N: number, arr: number[]): number {
const freq: Map = new Map();
let count = 0;
for (let i = 0; i < N; i++) {
const v = arr[i];
const prev = freq.get(v) ?? 0; // これまでの出現数
count += prev; // prev 個だけ (i, j) の組が追加される
freq.set(v, prev + 1);
}
return count;
}
const N = input[0];
const arr = input.slice(1);
console.log(countPairs(N, arr));
ある値 v の出現回数を c_v とすると、v によって作られる (i, j) の組の数は C(c_v, 2) = c_v*(c_v-1)/2 です。
逐次加算法は各 v の第 k 回目の出現(k を 1..c_v とする)で、その時点で過去に k-1 回出現しているため合計に k-1 を足します。よって合計は sum_{k=1..c_v}(k-1) = C(c_v,2) に一致します。
下はこのドキュメント内で動く簡易デモです。配列を順に処理すると Map と合計がどのように変化するかを表示します。
本問は Map による走査で簡潔に解けます。逐次加算の観点と組合せ C(n,2) の観点は同値であり、TypeScript の実装は提出用としてそのまま使えます。
もし、この HTML を 印刷用 PDF にしたり、別の入力例でインタラクティブに確認したければ、配列の部分を編集して試してみてください。追加で「異なる例のステップ図」や「より正確なメモリ見積もり」を入れることも可能です — ご希望あれば反映します。