-
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| ## Step 1. Initial Solution | ||
|
|
||
| - 再帰で候補を使っていき結果を一つのリストに追加していく | ||
| - target_sumに到達したらリストに追加 | ||
| - それ以外はtargetを削って次に託す | ||
| - 特に問題文に記載はないがcandidatesが昇順ならここでbreakしたい | ||
| - 同じ組み合わせを入れないようにしたい | ||
| - 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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という変数名は使いたくない気がした | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 「使えない枝を残しておかない」「DFSで枝刈りしているのと同じ」というより、「バックトラックはミュータブルな中間状態を持ち回すことでメモリ効率が良い」ということでしょうか。(言語化の仕方が違うだけかもしれませんが。) 確かにバックトラックはそういうメモリ効率や実行速度の良さがある一方で、再帰に伴うスタック領域の消費やデバックがしづらくなる側面もあり、トレードオフですね。 スタックの深さ自体は、Step 2で書かれているようなループ実装と同じですが、再帰特有のオーバーヘッド、例えば引数、戻りアドレスなどが定数倍で若干増します。 また、Step 2を書かれてわかったと思いますが、ループで書くとミュータブルを持ち回すことが難しくなり(逐次コピーが必要になり)メリットが失われます。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 確かにそういう見方をすると比較がしやすいですね |
||
| - https://github.com/Yoshiki-Iwasa/Arai60/pull/57/files#r1741307179 | ||
| - 計算量の話 | ||
| - 上手く評価する方法はないまであるか? | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 commentThe 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はスタックフレームのことです。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| ``` | ||
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のソートはボトルネックにならないので。