Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

README.md

各処理の詳しい解析(図付き・Markdown)

以下は TypeScript 実装(countPairs(N, arr))が行う各処理を、逐次的な図・表数学的正当性の説明計算量/メモリ見積もり、実装上の注意点まで丁寧に分解したものです。例として配列 A = [30, 10, 30, 20, 10, 30] を使って、各ステップでの内部状態(Map と合計値)がどう変わるかを示します。


1) アルゴリズム要点(一行で)

先頭から順に走査し、各要素 v = A[i] に対して「これまでに v が何回現れたか k を取り出す」→答えに k を足す→v の出現回数を k+1 に更新する。 これを全要素で行えば条件 1 ≤ j < i かつ A_j = A_i の組数が得られる。

関数シグネチャ(参照):

function countPairs(N: number, arr: number[]): number;

2) 流れの図(高レベル)

入力: arr = [30, 10, 30, 20, 10, 30]
初期: freq = {}   total = 0

i=1: v=30  prev = freq[30] ?? 0 = 0   total += 0  -> freq[30] = 1
i=2: v=10  prev = 0                 total += 0  -> freq[10] = 1
i=3: v=30  prev = 1                 total += 1  -> freq[30] = 2
i=4: v=20  prev = 0                 total += 0  -> freq[20] = 1
i=5: v=10  prev = 1                 total += 1  -> freq[10] = 2
i=6: v=30  prev = 2                 total += 2  -> freq[30] = 3

最終 total = 4

3) ステップ毎の詳細表(同じ例)

各行で prevその時点での過去の出現回数freq.get(v) の値)を指します。

i (1-indexed) A[i] prev (= freq[A[i]] before) total (after += prev) freq[A[i]] (after)
1 30 0 0 1
2 10 0 0 1
3 30 1 1 2
4 20 0 1 1
5 10 1 2 2
6 30 2 4 3

視覚(Mapの状態を箱で表す):

初期: {}
i=1 処理後: { 30:1 }
i=2 処理後: { 30:1, 10:1 }
i=3 処理後: { 30:2, 10:1 }
i=4 処理後: { 30:2, 10:1, 20:1 }
i=5 処理後: { 30:2, 10:2, 20:1 }
i=6 処理後: { 30:3, 10:2, 20:1 }

4) なぜこの方法で正しいのか(数学的説明)

ある値 v の出現回数を最終的に c_v とする。v によって生まれる (i,j) の組の個数は、v の異なる2つの出現位置ペアの数、すなわち

$$ \binom{c_v}{2} = \frac{c_v(c_v-1)}{2} $$

です。

逐次加算法では、v の k 回目の出現(k = 1..c_v)で、その時点までに k-1 個の過去出現があるので合計に k-1 を足します。よって v について合計される値は

$$ \sum_{k=1}^{c_v} (k-1) = \sum_{t=0}^{c_v-1} t = \frac{c_v(c_v-1)}{2} $$

となり、上の式と一致します。全ての異なる値 v について和をとれば答えになります。 → 逐次加算法は組合せ式と同値であり正当です。


5) 計算量・メモリ(詳細)

時間計算量

  • 主要ループは配列を 1 回走査:O(N)
  • 各イテレーションで Map.getMap.set を 1 回ずつ:平均 O(1)(ハッシュアクセス)。 → 全体 O(N)(N=100,000 まで、タイムリミット 2 秒に余裕で収まる想定)。

操作回数の目安

1 要素につき get 1 回、set 1 回、足し算 1 回。 N=100,000 のとき概算で ~200,000 Map 操作 + 100,000 加算。JS エンジン上では非常に高速。

メモリ計算量

  • 追加の主要構造は Map(異なる値数を U とする)。空間 O(U)。最悪 U = N(全要素が異なる)。
  • 実メモリ見積もり(概算・実装依存):
    • 各 Map エントリは JS の内部表現でオーバーヘッドがある(ポインタ・ハッシュ構造等)。概算で「数十バイト〜数百バイト/エントリ」
    • 保守的に 64 バイト/エントリ と仮定すると、100,000 エントリで約 64 * 100000 = 6,400,000 バイト ≒ 6.4 MB。
    • 実際には Node.js のオブジェクト・Map はさらにオーバーヘッドがあり得ますが、数十MB を越えることは通常なく、与えられた 1024 MiB 制限の範囲で十分余裕があります。

注意: 上のバイト数は概算です。JS の内部実装や V8 バージョンで変わります。正確なバイト数を得るにはプロファイラ(--inspect 等)でヒープスナップショットを取る必要があります。


6) 数値の安全性(整数のオーバーフロー)

最悪ケース(全要素が同じ)で組数は

$$ \binom{N}{2} = \frac{N(N-1)}{2}. $$

N = 100000 のとき逐次的に計算すると:

計算(桁ごとに):

  • 100000 × 99999 = 100000 × (100000 − 1) = 100000×100000 − 100000 = 10,000,000,000 − 100,000 = 9,999,900,000
  • ½ をとると 4,999,950,000

よって最大でも 4,999,950,000(約 5×10⁹)で、JavaScript の Number(最大安全整数 2^53−1 ≈ 9×10^15)に十分収まります。BigInt は不要


7) 入出力と実装上の注意点

  • 入力読み取り: 提出環境では fs.readFileSync(0, 'utf8') でまとめて読み込む方法が速くて簡単。N=100,000 程度のサイズならメモリ的にも十分。

    • ただし非常に大きな入力(例えば数百万行)を扱う場合はストリーム/readline を使って逐次処理する方が安全です(今回は不要)。
  • Map vs Plain Object:

    • Map<number, number> は数値キーに対して明確で安全。JS オブジェクトはキーが文字列化されるため、数値→文字列の変換コストや潜在的キー衝突が気になります。
    • 性能差は実装により変わるが、可読性・安全性の点で Map を推奨。
  • 関数化:単体テストや再利用のために countPairs(N, arr) のように関数化しておくと良い(引数・戻り値の型注釈を付ける)。


8) 代替アプローチとトレードオフ

(A) ハッシュ(現行) — 長所/短所

  • 長所:O(N),実装が簡単,入力順に逐次処理できる(ストリーミング可能)。
  • 短所:Map のオーバーヘッドでメモリが増える(ただしこの問題サイズでは問題にならない)。

(B) ソートして走査(O(N log N)

  • 手順:配列をソート → 連続する同値区間を走査して長さ c ごとに c*(c-1)/2 を足す。
  • 長所:追加 Map を使わない(もしくは少なくともキー領域を小さくできる)。メモリは in-place sort で O(1)O(log N) 追加。
  • 短所:ソートの計算量 O(N log N)。N が大きいと速度で不利。標準 Array.prototype.sort() は数値ソート時に比較関数が必要で遅くなる場合がある。

(C) 外部ソート/ストリーム処理

  • データが非常に大きい場合、外部ソートまたはストリーム処理(頻度をディスク等に分割して計算)を検討。

9) 実装上の微最適化(ちょっとしたコツ)

  • const prev = freq.get(v) ?? 0; count += prev; freq.set(v, prev + 1); が最もシンプルで一度の get で済む。
  • 二度 hasget といった冗長チェックは避ける(余計なハッシュアクセスを増やすため)。
  • 入力を Number に変換する際は .map(Number) を使うと分かりやすいが、大きな入力だと一時配列の生成コストに注意。

10) まとめ(実務的チェックリスト)

  • アルゴリズム:ハッシュカウントを累積加算O(N)
  • 正当性:各値 v に対する累積和が C(c_v,2) と一致するため正しい。
  • 時間:N=100,000 なら Node.js で 2 秒制限内に実行可能。
  • メモリ:Map のオーバーヘッドはあるが、100k エントリでも数 MB〜数十MB 程度で収まり、1024 MiB の制限に余裕あり。
  • 数値安全性:最大でも約 5×10^9(N=100k の場合)で JavaScript Number の範囲内。