-
Notifications
You must be signed in to change notification settings - Fork 0
Solved Arai60/39. Combination Sum #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
| - 再帰で候補を使っていき結果を一つのリストに追加していく | ||
| - target_sumに到達したらリストに追加 | ||
| - それ以外はtargetを削って次に託す | ||
| - 特に問題文に記載はないがcandidatesが昇順ならここでbreakしたい |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
candidatesをソートしてbreakすれば良い(したほうが良い)のではないでしょうか。
全体の計算量に対してcandidatesのソートはボトルネックにならないので。
| - 確かにメモリの観点から使えない枝を残しておかないようにしたいならDPは微妙 | ||
| - バックトラックはDFSで枝刈りしているのと同じ |
There was a problem hiding this comment.
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を書かれてわかったと思いますが、ループで書くとミュータブルを持ち回すことが難しくなり(逐次コピーが必要になり)メリットが失われます。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
確かにそういう見方をすると比較がしやすいですね
トレードオフも整理していただきありがとうございます!
| if candidates[i] < target: | ||
| combination_sum_helper(prefix + [candidates[i]], target - candidates[i], i) | ||
| elif candidates[i] == target: | ||
| target_combinations.append(prefix + [candidates[i]]) |
There was a problem hiding this comment.
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という変数名は使いたくない気がした |
There was a problem hiding this comment.
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]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
これだとバックトラックの一番の強みである「ミュータブルな中間状態を持ち回すことでメモリ効率が良い」点が消えてしまう気がします。
There was a problem hiding this comment.
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 resultscombinationをresultsに保存するときに初めてコピーしている点に注目していただければ幸いです。
candidatesをソートしての枝刈りなどは本筋を逸れます。
There was a problem hiding this comment.
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 resultsFrameはスタックフレームのことです。
There was a problem hiding this comment.
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 | ||
| - 計算量の話 | ||
| - 上手く評価する方法はないまであるか? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
私は、平易な抑え方は思いつきませんでした。
緩めた抑え方でもいいのですが、時間内に終わるかどうかの見積もりにならないのでは困ったものであると思います。
問題:https://leetcode.com/problems/combination-sum/description/