Skip to content

Commit cc18929

Browse files
authored
Merge pull request #208 from myoshi2891/dev-from-macmini
Math: leetcode Palindrome Number
2 parents c18aec6 + 3707743 commit cc18929

7 files changed

Lines changed: 3549 additions & 376 deletions

File tree

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
# Palindrome Number - 数値のまま回文判定
2+
3+
<h2 id="toc">目次</h2>
4+
5+
- [概要](#overview)
6+
- [アルゴリズム要点(TL;DR)](#tldr)
7+
- [図解](#figures)
8+
- [正しさのスケッチ](#correctness)
9+
- [計算量](#complexity)
10+
- [Python実装](#impl)
11+
- [CPython最適化ポイント](#cpython)
12+
- [エッジケースと検証観点](#edgecases)
13+
- [FAQ](#faq)
14+
15+
---
16+
17+
<h2 id="overview">概要</h2>
18+
19+
**問題**: 整数 `x` が 10 進表記で回文(左右対称)かどうかを判定する。
20+
21+
**要件**:
22+
23+
- 負数は必ず `False`(先頭に `-` が付くため対称にならない)
24+
- `0` 以外で末尾が `0` の数は `False`(例: `10``"01"` とは読めない)
25+
- Follow up: **文字列変換なし**で解く
26+
- 制約: `-2^31 <= x <= 2^31 - 1`
27+
28+
**正当性**: 右半分だけを数値のまま反転し、左半分と比較することで回文性を判定。
29+
30+
**安定性**: 追加メモリ `O(1)` で、整数演算のみを使用。
31+
32+
---
33+
34+
<h2 id="tldr">アルゴリズム要点(TL;DR)</h2>
35+
36+
- **戦略**: 数値を文字列に変換せず、右半分だけを反転して左半分と比較
37+
- **データ構造**: 整数変数 2 つ(`x``reverted`)のみ
38+
- **計算量**: 時間 `O(d)`(d = 桁数)、空間 `O(1)`
39+
- **メモリ**: 定数個の整数のみ使用。リスト・文字列などの追加データ構造は不要
40+
41+
**核心ロジック**:
42+
43+
1. 負数と末尾 0(0 自身を除く)を早期 return で弾く
44+
2. `x` の末尾から 1 桁ずつ取り出して `reverted` に積み上げる
45+
3. `x <= reverted` になったら終了
46+
4. 偶数桁: `x == reverted`、奇数桁: `x == reverted // 10` なら回文
47+
48+
---
49+
50+
<h2 id="figures">図解</h2>
51+
52+
## フローチャート
53+
54+
```mermaid
55+
flowchart TD
56+
Start[Start solve] --> Neg{x &lt; 0}
57+
Neg -- Yes --> RetF[Return False]
58+
Neg -- No --> Trail{x % 10 == 0 and x != 0}
59+
Trail -- Yes --> RetF
60+
Trail -- No --> Single{x &lt; 10}
61+
Single -- Yes --> RetT[Return True]
62+
Single -- No --> Init[rev = 0]
63+
Init --> Loop{x &gt; rev}
64+
Loop -- No --> Check{x == rev or x == rev // 10}
65+
Loop -- Yes --> Extract[digit = x % 10]
66+
Extract --> Update[rev = rev * 10 + digit]
67+
Update --> Divide[x = x // 10]
68+
Divide --> Loop
69+
Check -- Yes --> RetT
70+
Check -- No --> RetF
71+
```
72+
73+
**説明**: 負数・末尾 0 を早期判定した後、1 桁数は即座に True を返す。それ以外は右半分を反転しながら `x` を縮小し、`x <= rev` になった時点で偶数桁・奇数桁の判定を行う。
74+
75+
### データフロー図
76+
77+
```mermaid
78+
graph LR
79+
subgraph Input_Check
80+
A[Input x] --> B[Negative check]
81+
B --> C[Trailing zero check]
82+
C --> D[Single digit check]
83+
end
84+
subgraph Revert_Loop
85+
D --> E[Initialize rev = 0]
86+
E --> F[Extract digit from x]
87+
F --> G[Build rev = rev * 10 + digit]
88+
G --> H[Shrink x = x // 10]
89+
H --> I{x &gt; rev}
90+
I -- Yes --> F
91+
I -- No --> J[Compare x with rev]
92+
end
93+
J --> K[Output boolean]
94+
```
95+
96+
**説明**: 入力をまず 3 段階でフィルタリング(負数・末尾 0・1 桁数)し、その後ループで右半分を反転。最後に左半分と比較して結果を返す。
97+
98+
---
99+
100+
<h2 id="correctness">正しさのスケッチ</h2>
101+
102+
**不変条件**:
103+
104+
- ループ中、`rev` には常に「まだ見ていない x の右側の桁を反転したもの」が格納される
105+
- `x` は毎回末尾 1 桁を削除され、左側の桁だけが残る
106+
107+
**網羅性**:
108+
109+
- 負数: 必ず False(先頭に `-` が付く)
110+
- 末尾 0(0 自身を除く): 必ず False(先頭に 0 は来ない)
111+
- 1 桁数(0 〜 9): すべて回文
112+
- 2 桁以上: ループで右半分と左半分を分離して比較
113+
114+
**基底条件**:
115+
116+
- `x < 10` の場合、ループに入る前に True を返す
117+
- ループは `x <= rev` で終了(右半分が左半分以上の桁数になった時点)
118+
119+
**終了性**:
120+
121+
- 毎回 `x //= 10` で桁数が減るため、有限回で `x <= rev` が成立
122+
123+
---
124+
125+
<h2 id="complexity">計算量</h2>
126+
127+
| 項目 | 計算量 | 説明 |
128+
| -------- | ------ | -------------------------------------------------------- |
129+
| **時間** | `O(d)` | d は x の桁数(≒ log₁₀( \| x \| ))。ループは高々 d/2 回 |
130+
| **空間** | `O(1)` | 追加メモリは `rev`, `digit` など定数個の整数のみ |
131+
132+
**in-place vs Pure 比較**:
133+
134+
- この問題は入力を直接書き換えないため、本質的には Pure
135+
- ローカル変数 `x` をコピーとして扱い、元の引数は不変
136+
137+
---
138+
139+
<h2 id="impl">Python実装</h2>
140+
141+
```python
142+
from __future__ import annotations
143+
144+
145+
class Solution:
146+
"""
147+
Palindrome Number 判定クラス(競技プログラミング向け最小構成)
148+
149+
LeetCode が呼び出すのは isPalindrome(x) のみ。
150+
"""
151+
152+
def isPalindrome(self, x: int) -> bool:
153+
"""
154+
整数 x が 10 進表記で回文かどうかを判定する。
155+
156+
Args:
157+
x: 判定対象の整数(32bit 符号付き整数)
158+
159+
Returns:
160+
x が 10 進表記で回文であれば True、そうでなければ False。
161+
162+
Time Complexity:
163+
O(d) (d は x の桁数 ≒ log10(|x|))
164+
165+
Space Complexity:
166+
O(1) 追加メモリは定数個の整数のみ。
167+
"""
168+
# 負数、0 以外で末尾が 0 の数は回文にならない
169+
if x < 0 or (x % 10 == 0 and x != 0):
170+
return False
171+
172+
# 0〜9 は 1 桁なので必ず回文
173+
if x < 10:
174+
return True
175+
176+
rev: int = 0
177+
178+
# 右半分を反転しつつ、左半分と比較できる状態まで進める
179+
# ループを抜ける条件:
180+
# - 偶数桁: x と rev が同じ桁数になった時点で x <= rev
181+
# - 奇数桁: 中央 1 桁を含んだ rev の方が 1 桁多くなった時点で x < rev
182+
while x > rev:
183+
digit: int = x % 10 # 末尾 1 桁を取得
184+
rev = rev * 10 + digit # rev に桁を追加
185+
x //= 10 # 整数除算で末尾 1 桁を削除
186+
187+
# 偶数桁: x == rev
188+
# 奇数桁: 中央 1 桁を無視するため、rev // 10 と比較
189+
return x == rev or x == rev // 10
190+
```
191+
192+
**主要ステップ**:
193+
194+
1. **早期リターン**: 負数と末尾 0(0 自身を除く)を弾く
195+
2. **1 桁数の特別処理**: 0〜9 は必ず True
196+
3. **反転ループ**: `x > rev` の間、`x` の末尾を `rev` に追加し、`x` を縮小
197+
4. **最終比較**: 偶数桁と奇数桁の両方に対応した判定
198+
199+
---
200+
201+
<h2 id="cpython">CPython最適化ポイント</h2>
202+
203+
1. **整数演算のみ使用**:
204+
- `//``%` は C レベルで実装されており、Python の文字列操作より高速
205+
- `str()` やスライスを使わないことで、オブジェクト生成コストを回避
206+
207+
2. **ローカル変数のみ**:
208+
- ループ内は `x`, `rev`, `digit` のみで完結
209+
- 属性アクセス(`self.x` など)や関数呼び出しを排除
210+
211+
3. **早期リターン**:
212+
- 負数・末尾 0・1 桁数を事前に弾くことで、不要なループを回避
213+
214+
4. **追加データ構造なし**:
215+
- `list`, `deque`, `dict` などを使わないため、GC 負荷がゼロ
216+
- メモリアロケーションも最小限
217+
218+
5. **計測値のブレ**:
219+
- LeetCode の Runtime は環境ノイズが大きい(6ms〜10ms 程度は普通にブレる)
220+
- 同じコードでも再提出で順位が変わることがあるため、51% という数字はあくまで参考値
221+
222+
**文字列版との比較**:
223+
224+
- `str(x)` + スライス反転の方が実測で速いケースもある(C 実装のため定数倍が小さい)
225+
- ただし Follow up(文字列変換なし)を満たすなら、数値版が正統派
226+
227+
---
228+
229+
<h2 id="edgecases">エッジケースと検証観点</h2>
230+
231+
| ケース | 入力例 | 期待出力 | 備考 |
232+
| ---------------- | ----------------------- | -------- | ----------------------------------- |
233+
| **0 単体** | `x = 0` | True | 1 桁数なので回文 |
234+
| **1 桁数** | `x = 5`, `9` | True | すべて回文 |
235+
| **負数** | `x = -121`, `-1` | False | 先頭に `-` が付くため対称にならない |
236+
| **末尾 0** | `x = 10`, `100` | False | 先頭に 0 は来ないため回文ではない |
237+
| **2 桁回文** | `x = 11`, `22` | True | 偶数桁の典型例 |
238+
| **2 桁非回文** | `x = 12`, `10` | False | 左右が異なる |
239+
| **奇数桁回文** | `x = 121`, `12321` | True | 中央 1 桁を無視した比較 |
240+
| **偶数桁回文** | `x = 1221`, `123321` | True | 左右が完全に一致 |
241+
| **奇数桁非回文** | `x = 123`, `12345` | False | 中央を無視しても一致しない |
242+
| **制約上限** | `x = 2147483647` | False | 32bit 最大値(回文ではない) |
243+
| **制約下限** | `x = -2147483648` | False | 32bit 最小値(負数) |
244+
| **大きな回文** | `x = 1000000001` (10桁) | True | 桁数が多くても正しく判定 |
245+
| **末尾複数の 0** | `x = 1000` | False | 0 以外で末尾が 0 なら False |
246+
247+
**検証時の注意**:
248+
249+
- 型安全性: LeetCode は入力を保証するが、業務用では `isinstance(x, int)` チェックが必要
250+
- オーバーフロー: Python の `int` は任意精度なので心配不要(他言語では注意が必要)
251+
252+
---
253+
254+
<h2 id="faq">FAQ</h2>
255+
256+
**Q1. なぜ文字列変換を使わないのか?**
257+
258+
- Follow up で「文字列変換なしで解けるか?」と問われているため
259+
- 数値版の方が空間計算量 `O(1)` を達成でき、理論的にも優れている
260+
261+
**Q2. 偶数桁と奇数桁で処理が違うのはなぜ?**
262+
263+
- 奇数桁の場合、中央の 1 桁はどちらの半分にも属さない
264+
- 例: `121``x = 1`, `rev = 12` となった時点で、`1 == 12 // 10` で判定
265+
266+
**Q3. `x <= rev` で終了する理由は?**
267+
268+
- 右半分を反転しながら `x` を縮小すると、ちょうど半分の桁数に達した時点で `x <= rev` が成立
269+
- これ以上進めると、左半分が足りなくなるため停止
270+
271+
**Q4. LeetCode で 51% という順位は悪いのか?**
272+
273+
- Python の実行環境は計測ノイズが大きく、同じコードでも 6ms〜10ms でブレる
274+
- アルゴリズム的には最適なので、51% は十分良好な結果
275+
276+
**Q5. 業務開発で使う場合の注意点は?**
277+
278+
- 型チェック(`isinstance(x, int)`)を追加
279+
- 値域チェック(32bit 整数の範囲内か)を追加
280+
- エラーハンドリング(`TypeError`, `ValueError`)を実装
281+
282+
**Q6. より高速化する方法は?**
283+
284+
- LeetCode 用にクラス・docstring を最小化(定数倍の改善)
285+
- 文字列版(`str(x)[::-1]`)も実測で速いケースがある(ただし Follow up 不満足)
286+
287+
**Q7. なぜ `rev``reverted` と書かないのか?**
288+
289+
- 競技プログラミングでは変数名を短くするのが一般的(タイプ数削減)
290+
- 業務開発では `reverted_half` など明示的な名前が推奨される
291+
292+
**Q8. `x //= 10``x = x // 10` の違いは?**
293+
294+
- 両者は同じ(代入演算子の短縮形)
295+
- CPython では最適化されるため、パフォーマンス差はほぼゼロ

0 commit comments

Comments
 (0)