Skip to content
Open
Show file tree
Hide file tree
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
100 changes: 100 additions & 0 deletions 78_subsets_medium/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# 問題へのリンク
[Subsets - LeetCode](https://leetcode.com/problems/subsets/description/)


# 言語
Python

# 問題の概要
与えられた整数のリストから、すべての部分集合を生成する問題。


# 自分の解法
## step1:ビット全探索
全ての部分集合は、`nums`の要素数を`n`としたとき、`2^n`通り存在する。各部分集合は、`0`から`2^n - 1`までの整数をビットマスクとして解釈することで生成できる。

- 時間計算量:`O(n * 2^n)`
- 空間計算量:`O(n * 2^n)`

## step2
- `_ith_binary_digit`関数を定義して、整数をビットマスクとして解釈する際に、`i`番目のビットが立っているかどうかを判定する処理を分離した。

# 別解1. backtracking
- backtrackingを用いて部分集合を生成する方法。再帰的に要素を選択するかどうかを決定し、部分集合を構築していく。`subset`リストはオブジェクトの操作で更新され、最後に`all_subsets`に追加するときにコピーされる。そのため、思わぬところに影響が出ないように更新の順序に注意が必要。
- なお、Pythonの値渡し、参照渡し、参照の値渡しについては、[こちら](https://note.com/crefil/n/n7a0d2dec929b)を参照。
- 値渡し
- 呼び出し先で再代入した場合
- 呼び出し元に影響なし
- 呼び出し先でオブジェクトの操作をした場合
- 呼び出し元に影響なし
- 参照渡し
- 呼び出し先で再代入した場合
- 呼び出し元変数の参照先も変わる
- 呼び出し先でオブジェクトの操作をした場合
- 呼び出し元変数が参照しているオブジェクトも変わる
- 参照の値渡し
- 呼び出し先で再代入した場合
- 呼び出し元変数の参照先は変わらず、影響を受けなくなる
- 呼び出し先でオブジェクトの操作をした場合
- 呼び出し元変数が参照しているオブジェクトも変わる
- backtrackingはコードが簡潔だが、可読性が低くなることがあると感じた

- 時間計算量:`O(n * 2^n)`
- 空間計算量:`O(n * 2^n)`
[こちら](https://www.youtube.com/watch?v=3JWtSMlq0Sw)の動画を参考にした。

# 別解2. 再帰的な方法
- (backtrackingとは異なる方法で)再帰的に部分集合を生成する方法。
- `subsets(nums: list[int])`関数を用いて再帰を回す
- `subsets(nums[1:])`に`nums[0]`を含める場合と含めない場合の2通りを考えて、全ての部分集合を生成する。
Copy link

Choose a reason for hiding this comment

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

スライスを使うとコピーが発生し、O(n)かかるので、indexで範囲指定をしたほうが良いかもしれません。

Copy link
Owner Author

Choose a reason for hiding this comment

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

確かにおっしゃる通りですね、ご指摘ありがとうございます。

- 時間計算量:`O(n * 2^n)`
- 空間計算量:`O(n * 2^n)`

Copy link

Choose a reason for hiding this comment

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

特に問題ないです。このあたりからいくつか別解を見てもいいかもしれません。
https://github.com/olsen-blue/Arai60/pull/52/files

# 別解3. スタックを用いた反復的な方法

- スタックを用いて反復的に部分集合を生成する方法。
- スタックに途中状態の部分集合と、走査中のインデックスのタプルを格納し、各部分集合に対して要素を含める場合と含めない場合の2通りを考えて、全ての部分集合を生成する。スタックの定義が非直感的で思いつくのが難しいと感じた。

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

- スタックに格納する部分集合`subset`は、新しい要素を追加する際に`subset + [nums[index]]`のように新しいリストを生成している。これにより、リストのコピーを作成して、元のリストを変更しないようにする。以下のコードでもこの仕様は確認できる。
```python
a = [1,2,3]
b = a + [4] # bはaのコピーに4を追加した新しいリスト
c = a # cはaの参照を受け取る
print(id(a) == id(b)) # False
print(id(a) == id(c)) # True
```

# 別解4. `extend`を用いた方法
- `nums`の各要素`num`に対して、既存の部分集合にその要素を追加した新しい部分集合を生成し、全ての部分集合を構築する方法。イメージは倍々ゲームのように、部分集合の数が倍々に増えていく。最も実装がシンプル。

`double.py`
```python
class Solution:
def subsets(self, nums: list[int]) -> list[list[int]]:
all_subsets = [[]]
for num in nums:
subsets_containing_num = [subset + [num] for subset in all_subsets]
all_subsets.extend(subsets_containing_num)
return all_subsets
```


# 次に解く問題の予告
- [Evaluate Division - LeetCode](https://leetcode.com/problems/evaluate-division/description/)
- [Subsets II - LeetCode](https://leetcode.com/problems/subsets-ii/description/)
16 changes: 16 additions & 0 deletions 78_subsets_medium/double.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#
# @lc app=leetcode id=78 lang=python3
#
# [78] Subsets
#

# @lc code=start
class Solution:
def subsets(self, nums: list[int]) -> list[list[int]]:
all_subsets = [[]]
for num in nums:
subsets_containing_num = [subset + [num] for subset in all_subsets]
all_subsets.extend(subsets_containing_num)
return all_subsets

# @lc code=end
21 changes: 21 additions & 0 deletions 78_subsets_medium/stack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#
# @lc app=leetcode id=78 lang=python3
#
# [78] Subsets
#

# @lc code=start
class Solution:
def subsets(self, nums: list[int]) -> list[list[int]]:
all_subsets = []
stack = [([], 0)]
while stack:
subset, index = stack.pop()
if index == len(nums):
all_subsets.append(subset[:])
continue
stack.append((subset, index + 1))
stack.append((subset + [nums[index]], index + 1))
return all_subsets

# @lc code=end
44 changes: 44 additions & 0 deletions 78_subsets_medium/step1_backtrack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#
# @lc app=leetcode id=78 lang=python3
#
# [78] Subsets
#


# @lc code=start
class Solution:
def subsets(self, nums: list[int]) -> list[list[int]]:
all_subsets = []
self.backtrack(nums, nums, [], all_subsets)
return all_subsets

def backtrack(
self,
nums: list[int],
remaining: list[int],
chosen: list[int],
all_subsets: list[list[int]],
) -> None:
"""
the function backtrack count subset and add found subset to all_subsets. To count subset, backtrack choose an element num from remaining list for each step, and consider two patterns:
1. the subset containing num
2. the subset not containing num

backtrack([1,2,3], [1,2,3], [], all)
-> backtrack([1,2,3], [2,3], [1], all), backtrack([1,2,3], [2,3], [], all)

backtrack([1,2,3], [2,3], [1], all)
-> backtrack([1,2,3], [3], [2, 1], all), backtrack([1,2,3], [3], [1], all), ...etc
"""

if not remaining:
all_subsets.append(chosen)
return
num = remaining[0]

self.backtrack(nums, remaining[1:], chosen.copy(), all_subsets)
chosen.append(num)
self.backtrack(nums, remaining[1:], chosen.copy(), all_subsets)


# @lc code=end
24 changes: 24 additions & 0 deletions 78_subsets_medium/step1_bit_operation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#
# @lc app=leetcode id=78 lang=python3
#
# [78] Subsets
#


# @lc code=start
class Solution:
def subsets(self, nums: list[int]) -> list[list[int]]:
all_subsets: list[list[int]] = []
num_subsets = pow(2, len(nums))
for subset_expression in range(num_subsets):
subset: list[int] = []
for i, num in enumerate(nums):
# i th digit of subset_expression represents if nums[i] is inclued in subset or not
include_i = (subset_expression >> i) & 1
if include_i:
subset.append(num)
all_subsets.append(subset)
return all_subsets


# @lc code=end
25 changes: 25 additions & 0 deletions 78_subsets_medium/step1_recursive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#
# @lc app=leetcode id=78 lang=python3
#
# [78] Subsets
#


# @lc code=start
class Solution:
def subsets(self, nums: list[int]) -> list[list[int]]:
if not nums:
return []
elif len(nums) == 1:
return [[], nums]
num = nums[0]
sub_subsets = self.subsets(nums[1:])
all_subsets = []
for subset in sub_subsets:
all_subsets.append(subset.copy())
subset.append(num)
all_subsets.append(subset.copy())
return all_subsets


# @lc code=end
27 changes: 27 additions & 0 deletions 78_subsets_medium/step2_backtrack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#
# @lc app=leetcode id=78 lang=python3
#
# [78] Subsets
#


# @lc code=start
class Solution:
def subsets(self, nums: list[int]) -> list[list[int]]:
all_subsets: list[list[int]] = []
subset = []

def create_subset(i: int) -> None:
if i == len(nums):
all_subsets.append(subset[:])
return
subset.append(nums[i])
create_subset(i + 1)
subset.pop()
create_subset(i + 1)

create_subset(0)
return all_subsets


# @lc code=end
29 changes: 29 additions & 0 deletions 78_subsets_medium/step2_bit_operation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#
# @lc app=leetcode id=78 lang=python3
#
# [78] Subsets
#


# @lc code=start
# TC: O(n*2^n)
# SC: O(n*2^n)
class Solution:
def subsets(self, nums: list[int]) -> list[list[int]]:
all_subsets: list[list[int]] = []
num_subsets = pow(2, len(nums))
for subset_expression in range(num_subsets):
subset: list[int] = []
for i, num in enumerate(nums):
# i th digit of subset_expression represents if nums[i] is inclued in subset or not
is_num_selected = self._ith_binary_digit(subset_expression, i)
if is_num_selected:
subset.append(num)
all_subsets.append(subset)
return all_subsets

def _ith_binary_digit(self, num: int, i: int) -> int:
return (num >> i) & 1


# @lc code=end
25 changes: 25 additions & 0 deletions 78_subsets_medium/step2_recursive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#
# @lc app=leetcode id=78 lang=python3
#
# [78] Subsets
#


# @lc code=start
class Solution:
def subsets(self, nums: list[int]) -> list[list[int]]:
if not nums:
return []
elif len(nums) == 1:
return [[], nums]
first_num = nums[0]
subsets_without_first = self.subsets(nums[1:])
all_subsets = []
for subset in subsets_without_first:
all_subsets.append(subset.copy())
subset.append(first_num)
all_subsets.append(subset.copy())
return all_subsets


# @lc code=end
31 changes: 31 additions & 0 deletions 78_subsets_medium/step3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#
# @lc app=leetcode id=78 lang=python3
#
# [78] Subsets
#


# @lc code=start
class Solution:
def subsets(self, nums: list[int]) -> list[list[int]]:
all_subsets = []

subset = []

# insert every possible subset to all_subsets
def backtrack(i: int) -> None:
if i == len(nums):
all_subsets.append(subset.copy())
return
# case 1. nums[i] is selected
subset.append(nums[i])
backtrack(i + 1)
# case 2. nums[i] is not selected
subset.pop()
backtrack(i + 1)

backtrack(0)
return all_subsets


# @lc code=end
33 changes: 33 additions & 0 deletions 78_subsets_medium/step3_bit_operation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#
# @lc app=leetcode id=78 lang=python3
#
# [78] Subsets
#


# @lc code=start
class Solution:
def subsets(self, nums: list[int]) -> list[list[int]]:
num_all_subsets = pow(2, len(nums))
all_subsets: list[list[int]] = []

for subset_binary_expression in range(num_all_subsets):
subset: list[int] = []
for i, num in enumerate(nums):
is_num_selected = self._digit_i(subset_binary_expression, i)
if is_num_selected:
subset.append(num)
all_subsets.append(subset)

return all_subsets

def _digit_i(self, num: int, i: int) -> int:
"""
_digit_i returns i th binary digit value of num.
e.g. _digit_i(4,0) = _digit_i(4,1) = 0, _digit_i(4,2) = 1
"""

return (num >> i) & 1


# @lc code=end
Loading