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
110 changes: 110 additions & 0 deletions Python3/39. Combination Sum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
## Step 1. Initial Solution

- 再帰で候補を使っていき結果を一つのリストに追加していく
- target_sumに到達したらリストに追加
- それ以外はtargetを削って次に託す
- 特に問題文に記載はないがcandidatesが昇順ならここでbreakしたい

Choose a reason for hiding this comment

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

candidatesをソートしてbreakすれば良い(したほうが良い)のではないでしょうか。

全体の計算量に対してcandidatesのソートはボトルネックにならないので。

- 同じ組み合わせを入れないようにしたい
- previous_target_indexで前に戻れないようにする

```python
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
target_combinations = []

def combination_sum_helper(prefix: list[int], target: int, previous_index: int) -> None:
for i in range(previous_index, len(candidates)):
if candidates[i] < target:
combination_sum_helper(prefix + [candidates[i]], target - candidates[i], i)
elif candidates[i] == target:
target_combinations.append(prefix + [candidates[i]])
Comment on lines +17 to +20

Choose a reason for hiding this comment

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

if-continue-ifでもいいですね。趣味の範囲だと思います。


combination_sum_helper([], target, 0)
return target_combinations
```

### Complexity Analysis

- 時間計算量:
- 空間計算量:
- 見積もりが難しいので一旦後回し

## Step 2. Alternatives

- https://github.com/tokuhirat/LeetCode/pull/52/files#diff-91af53ad1e7f521bd1b22fbd7e06ea4139f8e95a23cb1c16d71b784e1343e4dbR10-R23
- 方針は似ていてヘルパー関数にはすでに出来ている組み合わせと今のインデックスを保持
- targetは変えずに毎回sum(combination)している
- 個人的には毎回sumするよりはtargetを変えたい
- `generate_combination(index + 1, combination[:])`
- ここでlist[:]の意味がよく分かっていなかったことに気づいた
- 全体スライスはコピーを作るのに使えるということらしい
- https://github.com/tokuhirat/LeetCode/pull/52/files#diff-91af53ad1e7f521bd1b22fbd7e06ea4139f8e95a23cb1c16d71b784e1343e4dbR10-R23
- `sum_to_combinations = [[] for _ in range(target + 1)]` を更新していく方法
- https://github.com/olsen-blue/Arai60/pull/53/files#r2021976335
- 個人的にはバックしていく方法が思いついていなかったがこれは確かにバックトラックと言える
- https://github.com/olsen-blue/Arai60/pull/53/files#diff-f084bff8e4dbd771bf8a202d43b499bc30bffb7c10d4c5ccd2102f021910fd19R71-R90
- スタックでやる方法
- 中身は再帰と同じ
- stackという変数名は使いたくない気がした

Choose a reason for hiding this comment

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

これは自分はあんまりないですね。

理由はDFS/BFSのstack/queueは宣言・初期化のすぐ下でpopが書かれることが多く、中身や役割がわかりやすいからです。変数名を意味的につけるか、データ型でつけるか、DFS/BFSに限れば趣味の範囲だと考えています。普段は意味的につけたいですね。

- 自分でも実装

```python
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
target_combinations = []
subtarget_combination_sum_index = [([], 0, 0)]
while subtarget_combination_sum_index:
combination, combination_sum, candidate_index = subtarget_combination_sum_index.pop()
for i in range(candidate_index, len(candidates)):
new_combination = combination + [candidates[i]]
new_sum = combination_sum + candidates[i]
if new_sum == target:
target_combinations.append(new_combination)
elif new_sum < target:
subtarget_combination_sum_index.append((new_combination, new_sum, i))

return target_combinations
```

- https://github.com/olsen-blue/Arai60/pull/53/files#r2020113460
- DPに対するコメント
- 確かにメモリの観点から使えない枝を残しておかないようにしたいならDPは微妙
- バックトラックはDFSで枝刈りしているのと同じ
Comment on lines +71 to +72

Choose a reason for hiding this comment

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

「使えない枝を残しておかない」「DFSで枝刈りしているのと同じ」というより、「バックトラックはミュータブルな中間状態を持ち回すことでメモリ効率が良い」ということでしょうか。(言語化の仕方が違うだけかもしれませんが。)

確かにバックトラックはそういうメモリ効率や実行速度の良さがある一方で、再帰に伴うスタック領域の消費やデバックがしづらくなる側面もあり、トレードオフですね。

スタックの深さ自体は、Step 2で書かれているようなループ実装と同じですが、再帰特有のオーバーヘッド、例えば引数、戻りアドレスなどが定数倍で若干増します。

また、Step 2を書かれてわかったと思いますが、ループで書くとミュータブルを持ち回すことが難しくなり(逐次コピーが必要になり)メリットが失われます。

Copy link
Owner Author

Choose a reason for hiding this comment

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

確かにそういう見方をすると比較がしやすいですね
トレードオフも整理していただきありがとうございます!

- https://github.com/Yoshiki-Iwasa/Arai60/pull/57/files#r1741307179
- 計算量の話
- 上手く評価する方法はないまであるか?
Copy link

Choose a reason for hiding this comment

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

私は、平易な抑え方は思いつきませんでした。

緩めた抑え方でもいいのですが、時間内に終わるかどうかの見積もりにならないのでは困ったものであると思います。

- 自分のやり方だと(targetを超えない組み合わせの数)×(candidate数)として表せる
- この問題の解の個数を target Kを用いて F(K), candidate数 N とすると

$$
計算量 = \Sigma_{i=2}^{K-1}F(i)*N
$$

- F(K)<計算量 かつ F(2) ≤ 1とすると

$$
計算量<N*(1+N+N^2+…+N^{K-1})\sim O(N^K)
$$

- ソートすると横方向にも枝刈りできる

## Step 3. Final Solution

- 方針はあまり変わらず自分がしっくり来たhelper関数の再帰で実装

```python
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
target_combinations = []

def combination_sum_helper(prefix: list[int], target: int, candidate_index: int) -> None:
for i in range(candidate_index, len(candidates)):
combination = prefix + [candidates[i]]

Choose a reason for hiding this comment

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

これだとバックトラックの一番の強みである「ミュータブルな中間状態を持ち回すことでメモリ効率が良い」点が消えてしまう気がします。

Choose a reason for hiding this comment

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

自分なら本問のバックトラックはこう書きます。

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        sorted_candidates = sorted(candidates)
        results = []
        combination = []

        def traverse(remaining, start_index):
            for i in range(start_index, len(sorted_candidates)):
                candidate = sorted_candidates[i]

                if candidate > remaining:
                    return
                
                if candidate == remaining:
                    results.append(combination + [candidate])
                    return
                
                combination.append(candidate)
                traverse(remaining - candidate, i)
                combination.pop()
        
        traverse(target, 0)
        return results

combinationをresultsに保存するときに初めてコピーしている点に注目していただければ幸いです。

candidatesをソートしての枝刈りなどは本筋を逸れます。

Choose a reason for hiding this comment

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

頑張ってiterativeでも書いてみました。

from dataclasses import dataclass


@dataclass
class Frame:
    remaining: int
    next_index: int


class Solution:
    def combinationSum(self, candidates: list[int], target: int) -> list[list[int]]:
        candidates = sorted(candidates)

        results = []
        combination = []
        stack = [Frame(remaining=target, next_index=0)]

        def backtrack_one_level() -> None:
            """Pop current frame and move parent's loop to the next candidate."""
            stack.pop()
            if not stack:
                return
            combination.pop()
            stack[-1].next_index += 1

        while stack:
            frame = stack[-1]

            if frame.next_index >= len(candidates):
                backtrack_one_level()
                continue

            candidate = candidates[frame.next_index]
            
            if candidate > frame.remaining:
                backtrack_one_level()
                continue

            if candidate == frame.remaining:
                results.append(combination + [candidate])
                frame.next_index += 1
                continue

            combination.append(candidate)
            stack.append(
                Frame(
                    remaining=frame.remaining - candidate,
                    next_index=frame.next_index  # reuse allowed
                )
            )

        return results

Frameはスタックフレームのことです。

Copy link
Owner Author

Choose a reason for hiding this comment

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

確かにこう書くと新しくリストを作る回数は大きく減りますね

if candidates[i] == target:
target_combinations.append(combination)
elif candidates[i] < target:
combination_sum_helper(combination, target - candidates[i], i)

combination_sum_helper([], target, 0)
return target_combinations
```