|
| 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 < 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 < 10} |
| 61 | + Single -- Yes --> RetT[Return True] |
| 62 | + Single -- No --> Init[rev = 0] |
| 63 | + Init --> Loop{x > 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 > 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