Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions 78. Subsets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# **78. Subsets**

# 進め方

Step1 : 問題を解く。

Step2 : 他の人のPRを参照し、コメントする。

Step3 : 3回続けてエラーが出ないように書く。ドキュメントを参照する。

# 実践

## Step1

### 思考ログ

```python
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
def subsets_helper(subset: List[int], rest_nums) -> List[List[int]]:
if not rest_nums:
return [subset]
result = []
result.append(subset)
for i, num in enumerate(rest_nums):
target = subset[:]
target.append(num)
result.append(target)
next_rest = rest_nums[:i] + rest_nums[i+1:]
Comment on lines +28 to +29
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この2行を

next_rest = rest_nums[i+1:]

にしたら動きますかね。

next_subsets = subsets_helper(target, next_rest)
result.extend(next_subsets)
return result

return subsets_helper([], nums)
```

樹形図を想像して、書いたがWA。

並び替えで区別してしまっていた。

2**n個なので何か使えないかな。

```python
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
def subsets_helper(subset: List[int], index: int) -> List[List[int]]:
if index == len(nums):
return [subset]
result = []
# not contain
result.extend(subsets_helper(subset, index + 1)) # copy_subsetと書いてしまうとWA

# contain
copy_subset = subset[:]
copy_subset.append(nums[index])
result.extend(subsets_helper(copy_subset, index + 1))
return result

return subsets_helper([], 0)
```

配列を共有してしまって沼にはまった。

計算量は時間空間ともにO(2^n) 1024回のループ

10msくらい

1. **ベースケースでの参照渡し (`return [subset]`)**
- `if index == len(nums): return [subset]`
- ここで返される `subset` は、呼び出し元から渡されたリストオブジェクトそのものです(またはその途中でコピーされたリストオブジェクト)。
- もしこの `subset`(`subsets_helper` の中の `copy_subset` が深い階層でこれに相当する)が、この後呼び出し元のスタックフレームで変更されると、`result` に追加されたリストも変更されてしまいます。
- **修正案**: `return [subset[:]]` または `return [list(subset)]` のように、リストの**コピー**を返す必要があります。これにより、`result` に追加される各サブセットが独立したものになります。
2. **`copy_subset` の変更と再利用**
- `copy_subset = subset[:]` で `subset` のコピーが作られます。
- この `copy_subset` は、まず「`nums[index]` を含まない」場合の再帰呼び出し `subsets_helper(copy_subset, index + 1)` に渡されます。
- この呼び出しが結果を返し(ベースケースでコピーが返されなかった場合、`copy_subset` への参照を含むリストが返る可能性があります)、`result` に追加されます。
- **その後**、`copy_subset.append(nums[index])` によって、**先ほど「含まない」場合で使われたのと同じ `copy_subset` オブジェクトが変更されます。**
- もし、1つ目の再帰呼び出し `subsets_helper(copy_subset, ...)` が `copy_subset` への参照を結果として返していた場合、この `append` 操作によって、既に `result` に追加されたはずのサブセットが意図せず変更されてしまいます。これが「WA (Wrong Answer)」の主な原因です。


これ難しい。

理由は分かるんですが、直観的ではないですね。

名前空間とオブジェクト空間の理屈は分かるのですが、再帰という形になるとデバッグできない。

慣れの問題でしょうか。

## Step2

### 同じ問題を解いた人のプルリクを見る

https://github.com/olsen-blue/Arai60/pull/52/files#diff-ddd8c09ee41837c8d5bde978403f850a0b08217fb8ec8eac6d0f2ae10e369d04

- bitでの解き方とスタックでの解き方をしている
- 再起のやり方は副作用で値を書き換えているのが気になる。
- なるべく純粋関数っぽくやりたいと感じます。

https://github.com/hroc135/leetcode/pull/48/files

- 色々なやり方で試していて参考になる。
- 末尾最適化、Pythonはないらしい。
- https://qiita.com/pebblip/items/cf8d3230969b2f6b3132
- https://discord.com/channels/1084280443945353267/1235971495696662578/1253008119458562119

ちょっと書く練習が必要そうです。

解法2:bit

```python
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
result = []
for bit in range(1 << len(nums)):
subset = []
for i in range(bit):
if bit & (1 << i):
subset.append(nums[i])
result.append(subset)
return result
```

nums = [1,2,3] のとき

bitが001のときは subset = [1] になります。

一旦理解すると再現は簡単。

解法3:スタック

```python
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
stack = [(0, [])]
result = []
while stack:
index, subset = stack.pop()
if index >= len(nums):
result.append(subset)
continue
stack.append((index+1, subset))
added_subset = subset + [nums[index]]
stack.append((index+1, added_subset))
return result
```

## Step3

### 3回連続で再現

解法1:再帰

```python
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
def subsets_helper(subset, index):
if index == len(nums):
return [subset]
added_subset = subset + [nums[index]]
return subsets_helper(subset, index+1) + subsets_helper(added_subset, index+1)

return subsets_helper([], 0)
```

```python
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
def helper(subset, index):
if index == len(nums):
return [subset]
added = subset + [nums[index]]
return helper(subset, index + 1) + helper(added, index + 1)

return helper([], 0)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

個人的には認知負荷が高いような気がして、原因は return helper(subset, index + 1) + helper(added, index + 1) にあるように思いました。例えば、nums = [1, 2, 3] のときに return [[]] + [[2]] となって [[], [2]] が返るわけですが、[] と [2] のペアに何か意味があるわけではないですね。

自分なら subset を随時入れていく箱をヘルパー関数の外に用意したくなりました。

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        result = []
        def subsets_helper(subset, index):
            if index == len(nums):
                result.append(subset)
                return
            added_subset = subset + [nums[index]]
            subsets_helper(subset, index+1)
            subsets_helper(added_subset, index+1)
        
        subsets_helper([], 0)
        return result

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

副作用を嫌ってこう書きましたが、今見るとわかりにくいですね。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

私は副作用のせいで時間を溶かしたことがなんどかあるので、ほかの人より避けたい気持ちが強いかもしれません。

```

このくらいシンプルでもよいかもしれない。

解法2:bit

```python
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
result = []
for bit in range(1 << len(nums)):
subset = []
for i in range(bit):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここは for i in range(len(nums)) の方が意味として正確だと思います。
例えば nums = [1, 2, 3] とします。bit は 0 ~ 7 をレンジしますが、bit = 7 (2進数で111)のとき、if bit & (1 << 3) は知りたいですが、if bit & (1 << 7) には興味がないと思います。

if bit & (1 << i):
subset.append(nums[i])
result.append(subset)
return result
```

解法3:スタック

```python
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
result = []
stack = [(0, [])]
while stack:
i, subset = stack.pop()
if i >= len(nums):
result.append(subset)
continue
added_subset = subset + [nums[i]]
stack.append((i+1, subset))
stack.append((i+1, added_subset))
return result
```

だいぶ分かってきた感覚になりました。