Skip to content

Conversation

@Satorien
Copy link
Owner

- 再帰で候補を使っていき結果を一つのリストに追加していく
- 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のソートはボトルネックにならないので。

Comment on lines +71 to +72
- 確かにメモリの観点から使えない枝を残しておかないようにしたいならDPは微妙
- バックトラックはDFSで枝刈りしているのと同じ

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.

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

Comment on lines +17 to +20
if candidates[i] < target:
combination_sum_helper(prefix + [candidates[i]], target - candidates[i], i)
elif candidates[i] == target:
target_combinations.append(prefix + [candidates[i]])

Choose a reason for hiding this comment

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

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

- 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に限れば趣味の範囲だと考えています。普段は意味的につけたいですね。


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.

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

- バックトラックはDFSで枝刈りしているのと同じ
- 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.

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants