📋 アルゴリズム概要
LeetCode 90: Subsets II では、重複要素を含む可能性のある整数配列から、重複のないすべての部分集合(パワーセット)を生成します。
核心戦略
- 入力配列を昇順ソートし、同値を隣接させる
- 反復的拡張法で部分集合を段階的に生成
- prev_size 変数で重複要素の拡張範囲を制御
- 新規要素は全体拡張、重複要素は前回追加分のみ拡張
📥 入力例
nums = [1, 2, 2]
📤 出力例
[[], [1], [1,2], [1,2,2], [2], [2,2]]
🔄 ステップバイステップ解説
ソートと初期化
配列を昇順ソート、空集合で初期化、prev_size = 0
最初の要素 (1) を追加
新規要素: 全体を拡張。[] に 1 を追加 → [1]
2番目の要素 (2) を追加
新規要素: 全体を拡張。[2], [1,2] を追加
3番目の要素 (2) - 重複
重複要素: 前回追加分のみ拡張。[2,2], [1,2,2] を追加
完了
6個のユニークな部分集合を生成完了
💻 Python実装(LeetCode形式)
以下は class Solution 形式の実装です。型注釈完備で、Pylance での静的解析に対応しています。
from typing import List
class Solution:
"""
LeetCode 90: Subsets II
重複要素を含む配列からユニークな全部分集合を生成
反復的拡張法 + prev_size による重複制御
Time: O(n·2^n), Space: O(1) extra (excluding output)
"""
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
"""
重複要素を含む配列からユニークな全部分集合を返す
Args:
nums: 整数配列(重複を含む可能性あり)
Returns:
重複のないすべての部分集合のリスト
Examples:
>>> Solution().subsetsWithDup([1,2,2])
[[], [1], [1,2], [1,2,2], [2], [2,2]]
>>> Solution().subsetsWithDup([0])
[[], [0]]
"""
# ステップ1: ソートして同値を隣接させる
# これにより重複判定が O(1) で可能になる
arr: List[int] = sorted(nums)
# ステップ2: 空集合で初期化
res: List[List[int]] = [[]]
# prev_size: 直前イテレーション開始時の res の長さ
# 重複要素の拡張範囲を制御するために使用
prev_size: int = 0
# ステップ3: 各要素について部分集合を拡張
for i, v in enumerate(arr):
# 現在の部分集合数を保存
size: int = len(res)
# ステップ4: 拡張開始位置を決定
# 重複要素なら前回追加分のみ拡張、そうでなければ全体を拡張
if i > 0 and v == arr[i - 1]:
# 重複: 前回イテレーションで追加された部分のみ拡張
start: int = prev_size
else:
# 新規要素: すべての既存部分集合を拡張
start = 0
# ステップ5: 部分集合を拡張
# start から size-1 までの各部分集合に v を追加
for j in range(start, size):
base = res[j]
# Python の list 連結は C 実装で高速
res.append(base + [v])
# ステップ6: 次回イテレーション用に現在のサイズを保存
prev_size = size
return res
📊 視覚的図解・フローチャート
Subsets II アルゴリズムフロー
重複要素を含む配列から重複なしの全部分集合を生成
flowchart TD
Start([開始: subsetsWithDup]) --> Sort["配列をソート
arr = sorted(nums)"]
Sort --> Init["結果を初期化
res = [[]]
prev_size = 0"]
Init --> LoopCheck{"配列の全要素を
処理したか?"}
LoopCheck -->|No| GetElement["現在の要素 v を取得
i番目の要素"]
LoopCheck -->|Yes| Return([結果を返す: res])
GetElement --> SaveSize["現在のサイズを保存
size = len(res)"]
SaveSize --> DupCheck{"重複要素か?
i > 0 かつ
v == arr[i-1]"}
DupCheck -->|Yes| SetStartPrev["拡張開始位置を設定
start = prev_size
前回追加分のみ拡張"]
DupCheck -->|No| SetStartZero["拡張開始位置を設定
start = 0
全体を拡張"]
SetStartPrev --> InnerLoop["内側ループ開始
j = start to size-1"]
SetStartZero --> InnerLoop
InnerLoop --> InnerCheck{"j < size?"}
InnerCheck -->|Yes| CreateSubset["新しい部分集合を作成
base = res[j]
new = base + [v]"]
InnerCheck -->|No| UpdatePrev["prev_sizeを更新
prev_size = size"]
CreateSubset --> AppendRes["結果に追加
res.append(new)"]
AppendRes --> IncrJ["j++"]
IncrJ --> InnerCheck
UpdatePrev --> IncrI["i++"]
IncrI --> LoopCheck
style Start fill:#e1f5e1
style Return fill:#e1f5e1
style DupCheck fill:#fff4e1
style LoopCheck fill:#fff4e1
style InnerCheck fill:#fff4e1
style CreateSubset fill:#e1e5f5
style AppendRes fill:#e1e5f5
style Sort fill:#f5e1e1
アルゴリズムの特徴
- 時間計算量: O(n·2^n) - n個の要素から2^n個の部分集合を生成
- 空間計算量: O(1) 追加空間(出力を除く)
- 重複排除の仕組み: ソート後、重複要素は前回追加分のみを拡張することで重複を防ぐ
- prev_sizeの役割: 前回のイテレーション開始時点でのresのサイズを記録し、重複要素時の拡張範囲を制限
処理例: [1, 2, 2]
- 初期: res = [[]]
- 要素1追加: res = [[], [1]]
- 要素2(1つ目)追加: res = [[], [1], [2], [1,2]]
- 要素2(2つ目・重複)追加: res = [[], [1], [2], [1,2], [2,2], [1,2,2]]
データフロー
入力のソートと初期化後、各要素を順次処理。重複判定により拡張範囲を制御し、最終的にユニークな全部分集合を出力します。
⚡ 計算量説明
⏱️ 時間計算量
O(n · 2n)
- ✓ ソート: O(n log n) - Timsort
- ✓ 部分集合生成: 最大 2n 個の部分集合
- ✓ 各生成: リストコピーに O(平均サイズ) ≈ O(n)
- ✓ 支配項: O(n · 2n) が支配
💾 空間計算量
O(1)
追加メモリ(出力除く)
- ✓ ローカル変数: prev_size, size, start のみ
- ✓ ソート: in-place(実質 O(1))
- ✓ 出力配列: 問題要件のため別計上
- ✓ 再帰なし: スタック深度の心配不要
アプローチ比較
| 手法 | 時間 | 空間(追加) | 特徴 |
|---|---|---|---|
| 再帰バックトラック | O(n·2n) | O(n) | 関数呼び出しスタック深度 n |
| 反復拡張(採用) | O(n·2n) | O(1) | 関数呼び出しなし、最小オーバーヘッド |
| ビットマスク列挙 | O(n·2n) | O(1) | 重複排除に追加ロジック必要 |
🎯 計算量のポイント
- • 出力サイズが 2n のため、理論的な下限も O(n·2n)。これ以上の高速化は不可能。
- • 本実装は prev_size による O(1) の重複判定で、追加メモリを最小化。
-
•
CPython の
list + [item]は C 実装で効率的。 - • 制約 n ≤ 10 のため、最大 1024 個の部分集合。実用上十分高速。
🔍 エッジケースと検証観点
境界ケース
最小入力
nums = [0]
期待出力:
[[], [0]]
すべて同じ要素
nums = [1, 1, 1]
期待:
[[], [1], [1,1], [1,1,1]]
すべて異なる要素
nums = [1, 2, 3]
期待: 23 = 8 個の部分集合
最大長
len(nums) = 10
最大 210 = 1024 個の部分集合
特殊ケース
負の数を含む
nums = [-1, 0, 1, 1]
ソートの正しさ、負数処理の確認
連続する重複
nums = [1, 1, 2, 2]
複数の重複グループの処理確認
重複が末尾
nums = [1, 2, 2]
Example 1 - 重複が最後に来る場合の動作確認
正当性の検証観点
✓ 一意性(Uniqueness)
同一の部分集合が複数生成されないこと
✓ 完全性(Completeness)
理論的な部分集合数と一致すること
✓ 順序不変性
入力の順序によらず同じ結果集合
✓ 型安全性
Pylance で警告・エラーなし