Skip to content

Commit 949f92f

Browse files
committed
leetcode 30. Substring with Concatenation of All Words スライディングウィンドウ + ハッシュマップ
1 parent 2a73882 commit 949f92f

7 files changed

Lines changed: 1070 additions & 0 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
以下にて、あなたのPythonコードに含まれる主要な処理を **図解・段階的に詳細に解析** します。
2+
3+
---
4+
5+
## 🔍 問題の背景
6+
7+
与えられた文字列 `S` の各位置に対して **一文字更新**が行われた後、ハッシュ値 `H(S)` を高速に求め直す問題です。
8+
9+
---
10+
11+
## 📘 ハッシュ値の定義
12+
13+
### ハッシュ計算式:
14+
15+
$$
16+
H(S) = \sum_{i=1}^{N} T_i \cdot X^{N-i} \mod P
17+
$$
18+
19+
* $T_i$:`S[i]` を英大文字 A〜Z → 1〜26 に変換した整数
20+
* $X$:基数
21+
* $P$:素数(mod)
22+
* 文字列 S = "HELLO" の例では:
23+
24+
```
25+
T = [8, 5, 12, 12, 15] // A=1, B=2,... Z=26
26+
pow_table = [X^4, X^3, X^2, X^1, X^0]
27+
```
28+
29+
### 図1:ハッシュ計算(初期)
30+
31+
```
32+
S = H E L L O
33+
Ti = 8 5 12 12 15
34+
pow = X^4 X^3 X^2 X^1 X^0
35+
H(S) = 8·X^4 + 5·X^3 + 12·X^2 + 12·X + 15
36+
```
37+
38+
---
39+
40+
## 🧮 ステップ1:累乗の事前計算
41+
42+
### 処理内容:
43+
44+
```python
45+
pow_table[i] = X^(N-i-1) % P
46+
```
47+
48+
### 理由:
49+
50+
* クエリで **再計算** を高速にするため
51+
* 更新対象 `S[i]` の影響は `T_i × X^{N - i - 1}` の1項のみ
52+
53+
### 図2:累乗テーブル pow\_table の構築(N=5, X=3)
54+
55+
```
56+
i 0 1 2 3 4
57+
pow[i] = X^4 X^3 X^2 X^1 X^0
58+
= 81 27 9 3 1
59+
```
60+
61+
---
62+
63+
## 🧮 ステップ2:初期ハッシュの構築
64+
65+
```python
66+
hash_val = Σ (char_to_val(S[i]) * pow_table[i]) % P
67+
```
68+
69+
### 図3:初期ハッシュ計算(例:S="HELLO")
70+
71+
```
72+
hash_val = 8·X^4 + 5·X^3 + 12·X^2 + 12·X + 15
73+
```
74+
75+
---
76+
77+
## 🧩 ステップ3:1クエリごとの更新処理
78+
79+
```python
80+
hash_val = (hash_val + (new_val - old_val) * pow_table[i]) % P
81+
```
82+
83+
### 概要:
84+
85+
* 差分 `(new_val - old_val)` を計算し
86+
* 元のハッシュに `(差分 × pow_table[i])` を加算
87+
88+
### 図4:差分更新(例:i=2のLをC=3に変更)
89+
90+
```
91+
変更前:T[2] = 12(L)
92+
変更後:T[2] = 3(C)
93+
94+
差分: 3 - 12 = -9
95+
hash_val += -9 × pow_table[2] // X^2の重みを持つ項の差分
96+
```
97+
98+
---
99+
100+
## 📘 クエリ処理の流れ
101+
102+
### 図5:クエリ `"2 D"` を処理するステップ(例)
103+
104+
```
105+
1. S = "HELLO"
106+
2. i = 2, 文字 D (→ val = 4)
107+
108+
3. old_val = 12 (L)
109+
new_val = 4 (D)
110+
111+
4. 差分 = (4 - 12) = -8
112+
5. hash_val += -8 × pow_table[2]
113+
6. hash_val %= P
114+
```
115+
116+
---
117+
118+
## 🔁 最後に全体の流れを図でまとめる
119+
120+
### 図6:全体フロー(簡略図)
121+
122+
```
123+
┌────────────┐
124+
│ 入力読み込み │
125+
└────┬───────┘
126+
127+
┌────────────┐
128+
│ pow_table 構築 │←──── X^(N-i-1)を事前計算
129+
└────┬───────┘
130+
131+
┌────────────┐
132+
│ 初期ハッシュ計算 │←──── Σ T_i × pow[i]
133+
└────┬───────┘
134+
135+
┌─────────────────────┐
136+
│ 各クエリ i の変更処理 │←──── 差分更新
137+
└────────┬────────┘
138+
139+
┌─────────────────────┐
140+
│ 更新後ハッシュを出力 │
141+
└─────────────────────┘
142+
```
143+
144+
---
145+
146+
## 🧠 結論:なぜ高速なのか?
147+
148+
* **全クエリで再ハッシュしない**(O(N×Q)ではなくO(Q))
149+
* **pow\_table によって、各変更の影響をO(1)で特定**
150+
* Pythonの `pow()` も事前計算すればループ内不要
151+
152+
---
153+
154+
## ご希望あれば...
155+
156+
* `pow()` の都度計算版(メモリ削減)
157+
* `ファイル入力` への変更
158+
* **図をSVGや画像で出力**
159+
160+
などにも対応できます。必要であればお知らせください。
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
2+
// 以下は、与えられた問題を **Node.js (v18.16.1)**、`fs` モジュールを用いて効率的に処理するJavaScript実装です。
3+
4+
// 大量データ(N, Q ≤ 200000)を高速に扱うために:
5+
6+
// * **累乗の事前計算(X^(N-i) mod P)**
7+
// * **初期ハッシュの一括計算**
8+
// * **差分更新による高速再計算**
9+
10+
// を採用しています。
11+
12+
// ---
13+
14+
// ### ✅ **コード:**
15+
16+
// ```javascript
17+
// Node.js 18.16.1 で動作確認済み
18+
const fs = require('fs');
19+
20+
// 入力の読み込み(高速処理)
21+
const input = fs.readFileSync('/dev/stdin', 'utf8').trim().split('\n');
22+
23+
function solve(input) {
24+
const [P, X, N, Q] = input[0].split(' ').map(Number);
25+
const S = input[1].trim().split('');
26+
const queries = input.slice(2).map(line => {
27+
const [i, c] = line.trim().split(' ');
28+
return [parseInt(i) - 1, c]; // 0-indexed に変換
29+
});
30+
31+
const MOD = BigInt(P);
32+
const base = BigInt(X);
33+
const len = S.length;
34+
35+
// 英大文字 'A'〜'Z' を 1〜26 に変換する関数
36+
const charToVal = c => BigInt(c.charCodeAt(0) - 'A'.charCodeAt(0) + 1);
37+
38+
// X^i % P の累乗テーブル prePow[i] = X^i (mod P)
39+
const prePow = new Array(len);
40+
prePow[len - 1] = BigInt(1);
41+
for (let i = len - 2; i >= 0; i--) {
42+
prePow[i] = (prePow[i + 1] * base) % MOD;
43+
}
44+
45+
// 初期ハッシュ H(S)
46+
let hash = BigInt(0);
47+
for (let i = 0; i < len; i++) {
48+
const val = charToVal(S[i]);
49+
hash = (hash + val * prePow[i]) % MOD;
50+
}
51+
52+
const output = [];
53+
54+
// クエリ処理
55+
for (const [i, newChar] of queries) {
56+
const oldVal = charToVal(S[i]);
57+
const newVal = charToVal(newChar);
58+
const diff = (newVal - oldVal + MOD) % MOD;
59+
60+
// H(V) = H(S) + (T'_i - T_i) × X^{N-i} mod P
61+
hash = (hash + diff * prePow[i]) % MOD;
62+
63+
S[i] = newChar; // 更新
64+
output.push(hash.toString());
65+
}
66+
67+
// 出力
68+
console.log(output.join('\n'));
69+
}
70+
71+
solve(input);
72+
// ```
73+
74+
// ---
75+
76+
// ### 💡 補足と解説
77+
78+
// * `charToVal`:`A=1`〜`Z=26`に変換(仕様準拠)
79+
// * `prePow[i]`:`X^(N-i-1)` を前計算して `O(1)` アクセス
80+
// * 差分更新:`hash += (T'_i - T_i) × X^(N-i)` を `mod P` で計算
81+
// * すべて **BigInt** で正確に計算(`P` が最大 2e9 のため)
82+
83+
// ---
84+
85+
// ### 📊 計算量・リソース使用
86+
87+
// | 処理内容 | 時間計算量 | 空間計算量 |
88+
// | -------- | -------------- | ------------ |
89+
// | 累乗の事前計算 | O(N) | O(N) |
90+
// | 初期ハッシュ計算 | O(N) | - |
91+
// | 各クエリ処理 | O(1) × Q | - |
92+
// | 合計 | O(N + Q) ≦ 4e5 | O(N)(最大約8MB) |
93+
94+
// ---
95+
96+
// ### ✅ 入力例テスト(標準入力)
97+
98+
// ```
99+
// 1000000007 77777 14 7
100+
// HELLOWORLDNANA
101+
// 1 G
102+
// 4 O
103+
// 7 O
104+
// 2 D
105+
// 14 B
106+
// 13 Y
107+
// 7 E
108+
// ```
109+
110+
// ### ✅ 出力結果(期待通り)
111+
112+
// ```
113+
// 792612268
114+
// 411858279
115+
// 411858279
116+
// 311466064
117+
// 311466065
118+
// 312321612
119+
// 686941116
120+
// ```
121+
122+
// ---
123+
124+
// 必要であれば `readline` にも対応できますが、大規模データ処理にはこの `fs.readFileSync('/dev/stdin')` の方が圧倒的に高速です。
125+
126+
// 他の実装方針や拡張が必要であればお知らせください。

0 commit comments

Comments
 (0)