diff --git a/src/bin/step1.rs b/src/bin/step1.rs new file mode 100644 index 0000000..c4f66a4 --- /dev/null +++ b/src/bin/step1.rs @@ -0,0 +1,151 @@ +// Step1 +// 目的: 方法を思いつく + +// 方法 +// 5分考えてわからなかったら答えをみる +// 答えを見て理解したと思ったら全部消して答えを隠して書く +// 5分筆が止まったらもう一回みて全部消す +// 正解したら終わり + +/* + 問題の理解 + - 整数からなる配列candidates,整数targetが与えられる。 + 合計値がtargetとなるような重複のないcandidates[i]の組み合わせを全て返す。 + candidates[i]は同じ値を何度使っても良い。 + candidates=[2,3,5], target=8のとき、 + out=[[2,2,2,2],[2,3,3],[3,5]] + + 何を考えて解いていたか + - 何がしたいのかはわかるので、手作業でやることを考える。 + 補数(complement) = target - candidates[i] を求める。 + 同じ値candidates[i]は何度でも使えるので、候補から除くことはできない。 + 決定木として考えると、complementが2以上のときは更に決定木を辿る必要がある。 + candidates=[2,3,5], target=8を決定木の図として書いて見ると、3 -> 5のパスと 5 -> 3 のパスでノードが同じなので重複する結果が得られる。 + ナイーブな対応方法は、sort + setによる重複管理だがこのあたりでbacktrackingが使えそうな感じがする。 + backtrackingではcadidates[i]をpopした残りを次に引き継いで、すぐ元に戻すイメージだがここでpopしてしまうと、何度でも同じ値を使えるという制約に反しているのでおかしなことになる。 + 他の方法は思いつかないのでナイーブな実装方針とする。 + - complement = target - candidates[i] を求めながら再帰処理を行う。 + - base_case + complement < 2 then return + complement == 0 then combination.push(candidates[i]) + + - recursive_case + complement = target - candidates[i] + f(candidates, complement, combination) + combinations.push(combination) + + combinations.foreach(combination => combination.sort()) + HashSet::from_iter(combinations.into_iter()) + + コードを書いてからになってしまったがLeet Code採点システム提出前に時間計算量と実行時間の概算見積もりを行う。 + n = candidates.len() + m = target + f(n, m) = cost(combination.clone()) + n * f(n ,m - 2) のような計算式になると思うが、ここからBig-O記法の時間計算量にする方法がよく分からない。 + f(n) = n * f(n-1) のときは n - 1, n - 2, n - 3となり階乗になるのが感覚として分かるが、n * (m - 2) のような場合はよく分からない。 + 実際に値を当てはめると答えを最後まで求めるまでもなく、n * mにはならないのでn ^ mとして考える。 + f(m - 2) としたのは問題の制約からcandidates[i]の最小値が2であるため、最悪ケースとして2を利用した。 + 時間計算量: O(n ^ m) + 空間計算量: O(n ^ m) + + 問題の制約: + 1 <= candidates.len <= 30 + 2 <= candidates[i] <= 40 + 1 <= target <= 40 + candidatesの要素は重複していない + + 時間計算量だけ見ると 40 ^ 30 の時点で計算量が爆発しているので、LeetCode採点システムでTime Limit Exceededになりそう。 + Acceptedとなった。Acceptedになったということは明らかに時間計算量の見積もりが間違っていることは分かる。 + LeetCode採点システムでは以下のように表示された。 + 時間計算量: O(N(T / M+1)) + 空間計算量: O(T / M) + + GPT-5.2に聞いてみる。 + n = candidates.len() + t = target + m = min(candidates) 入力の制約では2。 + d = t / m 木の深さ。 ここが分かっていなかった。target < 2で底を枝刈りしているが計算量としては無視している。 + candidates=[2,3,5] target=8のとき、最悪計算量ではcandidates[0]である2で考える。一番深い位置まで木の枝が伸びるイメージ。 + d = t / m = 8 / 2 = 4となる。緒間的にもtarget=8のとき、candidates[i]との補数を求めながら単調減少していくと4回処理を行うことが分かる。(target < 2の枝刈りは無視) + f(n, t) = cost(combination.clone()) + n * f(n, t - m) のような感じになり、 f(n, t - m)の再帰がどこまで深くなるかの見積もり方が分かっていなかった。 + O(n ^ (t / m))になると思うので、LeetCode採点システムが表示している計算量(O(N(T / M+1)))が間違ってそう。 + 補助空間計算量も自分のコードでは重複を含んだ配列をcombinationsで保持しているので、LeetCode採点システムが表示している O(T / M)にはならないと思った。 + 重複排除前にcombinationsにパスを保持する部分が支配的なので、O(n ^ (t / m) * (t / m))が正しいと思った。 + 時間計算量 O(n ^ (t / m))による実行時間の概算見積を行う。 + 30 ^ (40 / 2) / 10 ^ 8 = 3.486784401E21 となり計算量が爆発している。 + 改めてLeetCode問題文を見るとテストケースの制約として、答えとなるような一意の組み合わせの数が150以下になるとある。ただ、この制限を適用してどう実行時間の概算見積をすればよいか分からない。 + GPT-5.2に聞いてみたが、重複したパスも全て見る実装なので組み合わせの数が150になる制限を適用できないとのことだった。 + 折角なので概算見積を行いたかったが、LeetCode採点システムのテストケースはAcceptedしたときのものは見られなさそうなので諦める。 + + 何がわからなかったか + - backtrackingによる重複を発生させないアルゴリズム + + 所感 + - backtrackingアルゴリズムを思い通りに扱えないので、少し問題設定が変わると対応できない。慣れの問題ではあるので気にせず進める。 +*/ + +use std::collections::HashSet; + +pub struct Solution {} +impl Solution { + pub fn combination_sum(candidates: Vec, target: i32) -> Vec> { + let mut combination = Vec::new(); + let mut combinations = Vec::new(); + + Self::make_combination_sum(&candidates, target, &mut combination, &mut combinations); + combinations.iter_mut().for_each(|c| c.sort()); + let unique_combinations: HashSet<_> = HashSet::from_iter(combinations.into_iter()); + + unique_combinations.into_iter().collect() + } + + fn make_combination_sum( + candidates: &[i32], + target: i32, + combination: &mut Vec, + combinations: &mut Vec>, + ) { + if target == 0 { + combinations.push(combination.to_vec()); + return; + } + if target < 2 { + return; + } + + for i in 0..candidates.len() { + let complement = target - candidates[i]; + + combination.push(candidates[i]); + Self::make_combination_sum(candidates, complement, combination, combinations); + combination.pop(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step1_test() { + let mut expect = vec![vec![2, 2, 3], vec![7]]; + let mut actual = Solution::combination_sum(vec![2, 3, 6, 7], 7); + expect.iter_mut().for_each(|x| x.sort()); + expect.sort(); + actual.iter_mut().for_each(|x| x.sort()); + actual.sort(); + assert_eq!(expect, actual); + + let mut expect = vec![vec![2, 2, 2, 2], vec![2, 3, 3], vec![3, 5]]; + let mut actual = Solution::combination_sum(vec![2, 3, 5], 8); + expect.iter_mut().for_each(|x| x.sort()); + expect.sort(); + actual.iter_mut().for_each(|x| x.sort()); + actual.sort(); + assert_eq!(expect, actual); + + let expect = Vec::>::new(); + let actual = Solution::combination_sum(vec![2], 1); + assert_eq!(expect, actual); + } +} diff --git a/src/bin/step2.rs b/src/bin/step2.rs new file mode 100644 index 0000000..809c71a --- /dev/null +++ b/src/bin/step2.rs @@ -0,0 +1,123 @@ +// Step2 +// 目的: 自然な書き方を考えて整理する + +// 方法 +// Step1のコードを読みやすくしてみる +// 他の人のコードを2つは読んでみること +// 正解したら終わり + +// 以下をメモに残すこと +// 講師陣はどのようなコメントを残すだろうか? +// 他の人のコードを読んで考えたこと +// 改善する時に考えたこと + +/* + 他の人のコードを読んで考えたこと + https://discord.com/channels/1084280443945353267/1233295449985650688/1242146721094570034 + - step1で時間計算量の見積もりに躓いてかなり時間使ったが、簡単に理解できるものではないという知識があれば避けられたなと思った。 + candidates[i]の最小が1の場合は、分割数の極限の振る舞いというものを知っている必要がある。ただし、ソフトウェアエンジニアの常識には含まれていなさそう。 + 今回の問題では制約によりcandidates[i]の最小が2であるので、分割数の極限の振る舞いは関係ないことに注意が必要。 + + https://github.com/Yoshiki-Iwasa/Arai60/pull/57/changes + - Rust実装で解法に幅がある。 + + https://github.com/Yoshiki-Iwasa/Arai60/pull/57/changes#diff-d1aabe64d3b8ecf58a421e7472d87dea56ba3ec30721950fd3a93272eed8fa2aR60 + - candidates.split_first()で何をしているのか最初分からなかったが、candidate_indexの代わりになっていると理解した。この方法は思いつかなかった。 + + 写経した解法 + https://github.com/hayashi-ay/leetcode/pull/65/changes#diff-f084bff8e4dbd771bf8a202d43b499bc30bffb7c10d4c5ccd2102f021910fd19R147 + - 写経してみたが、書きながらcandidate_index + 1 している部分が直感に反すると感じた。 + - 同じ値(同じcandidate_index)はいくつ使っても良いのに、+1するときに特別な判定をしているようには見えないため。 + - candidate_index + 1によって次のcandidatesを見るといったforループのような動きをさせていると理解した。 + - この時点ではsumを変更しておらず、candidate_indexを1つ進めて次に引き継いでいるだけのように見える。 + - candidates[i] + sumがtarget以下のときcandidates[i]を選べて、さらに次でも同じ値(candidates[i])を使える可能性があるのでcandidate_indexはそのまま引き継いでいると理解した。 + + 改善する時に考えたこと + - 参考にしたコードはPythonで書かれており、インナーファンクションで1つ外側の変数を見に行けるので再帰関数の引数部分がすっきりとしている。 + - Rustでそのまま書くと引数の数が6つになり、流石に気になるので改善できるか考えてみる。(step2a.rs) +*/ + +pub struct Solution {} +impl Solution { + pub fn combination_sum(candidates: Vec, target: i32) -> Vec> { + let mut combination = Vec::new(); + let mut all_combinations = Vec::new(); + Self::make_combinations( + &candidates, + 0, + 0, + target, + &mut combination, + &mut all_combinations, + ); + + all_combinations + } + + fn make_combinations( + candidates: &[i32], + candidate_index: usize, + sum: i32, + target: i32, + combination: &mut Vec, + all_combinations: &mut Vec>, + ) { + if sum == target { + all_combinations.push(combination.to_vec()); + return; + } + if candidates.len() <= candidate_index { + return; + } + + Self::make_combinations( + candidates, + candidate_index + 1, + sum, + target, + combination, + all_combinations, + ); + + if sum + candidates[candidate_index] <= target { + combination.push(candidates[candidate_index]); + Self::make_combinations( + candidates, + candidate_index, + sum + candidates[candidate_index], + target, + combination, + all_combinations, + ); + combination.pop(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step2_test() { + let mut expect = vec![vec![2, 2, 3], vec![7]]; + let mut actual = Solution::combination_sum(vec![2, 3, 6, 7], 7); + expect.iter_mut().for_each(|x| x.sort()); + expect.sort(); + actual.iter_mut().for_each(|x| x.sort()); + actual.sort(); + assert_eq!(expect, actual); + + let mut expect = vec![vec![2, 2, 2, 2], vec![2, 3, 3], vec![3, 5]]; + let mut actual = Solution::combination_sum(vec![2, 3, 5], 8); + expect.iter_mut().for_each(|x| x.sort()); + expect.sort(); + actual.iter_mut().for_each(|x| x.sort()); + actual.sort(); + assert_eq!(expect, actual); + + let expect = Vec::>::new(); + let actual = Solution::combination_sum(vec![2], 1); + assert_eq!(expect, actual); + } +} diff --git a/src/bin/step2a.rs b/src/bin/step2a.rs new file mode 100644 index 0000000..7f201ee --- /dev/null +++ b/src/bin/step2a.rs @@ -0,0 +1,87 @@ +// Step2a +// 目的: step2.rsのコードを改善する + +/* + 改善する時に考えたこと + - 引数6つは多すぎるので減らしたい。 + - sumがtargetに達するか見るのではなく、targetから初めてちょうど0になるかを見るようにすればsumが要らなくなる + - candidates.split_first()により、candidate_indexの引き継ぎをなくして引数を減らす + https://github.com/Yoshiki-Iwasa/Arai60/pull/57/changes#diff-d1aabe64d3b8ecf58a421e7472d87dea56ba3ec30721950fd3a93272eed8fa2aR60 + + 所感 + - candidates.split_first()の部分が自分の考えの外にあったので感心した。 +*/ + +pub struct Solution {} +impl Solution { + const MIN_CANDIDATE: i32 = 2; + + pub fn combination_sum(candidates: Vec, target: i32) -> Vec> { + let mut combination = Vec::new(); + let mut all_combinations = Vec::new(); + + Self::make_combinations(&candidates, target, &mut combination, &mut all_combinations); + + all_combinations + } + + fn make_combinations( + candidates: &[i32], + complement: i32, + combination: &mut Vec, + all_combinations: &mut Vec>, + ) { + if complement == 0 { + all_combinations.push(combination.to_vec()); + return; + } + if complement < Self::MIN_CANDIDATE { + return; + } + + let Some((&candidate, rest_candidates)) = candidates.split_first() else { + return; + }; + + Self::make_combinations(rest_candidates, complement, combination, all_combinations); + + if candidate <= complement { + combination.push(candidate); + Self::make_combinations( + candidates, + complement - candidate, + combination, + all_combinations, + ); + combination.pop(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step2a_test() { + let mut expect = vec![vec![2, 2, 3], vec![7]]; + let mut actual = Solution::combination_sum(vec![2, 3, 6, 7], 7); + expect.iter_mut().for_each(|x| x.sort()); + expect.sort(); + actual.iter_mut().for_each(|x| x.sort()); + actual.sort(); + assert_eq!(expect, actual); + + let mut expect = vec![vec![2, 2, 2, 2], vec![2, 3, 3], vec![3, 5]]; + let mut actual = Solution::combination_sum(vec![2, 3, 5], 8); + expect.iter_mut().for_each(|x| x.sort()); + expect.sort(); + actual.iter_mut().for_each(|x| x.sort()); + actual.sort(); + assert_eq!(expect, actual); + + let expect = Vec::>::new(); + let actual = Solution::combination_sum(vec![2], 1); + assert_eq!(expect, actual); + } +} diff --git a/src/bin/step2b.rs b/src/bin/step2b.rs new file mode 100644 index 0000000..76683b8 --- /dev/null +++ b/src/bin/step2b.rs @@ -0,0 +1,71 @@ +// Step2b +// 目的: 別の解法を練習する。再帰->ループ + +/* + 所感 + - 再帰よりもすっきりと書けるなと思った。 + - combinationをスタックに積む時にclone()するのをためらってどうにかできないか迷ったが、どうにもならないことに気付くのに少し時間を使った。具体的には可変参照を取り回してどうにかならないかと考えた。 + コーディング練習ではコピーは避けられないか?といったことを考えながら実装するのは大切だが、一方で最適化に囚われすぎて動くものを作るといった本来の目的から逸脱しないように気をつける必要があるななどと考えていた。 + + > Rustでは超クールなメモリ確保なしのゼロコピーアルゴリズムが安全に書けるからと言って、すべてのアルゴリズムを超クールにゼロコピーでメモリ確保なしに書く必要はない + https://users.rust-lang.org/t/feeling-rust-is-so-difficult/29962/15 + 「Effective Rust 項目20: 過剰な最適化の誘惑を退けよう」より引用 +*/ + +pub struct Solution {} +impl Solution { + pub fn combination_sum(candidates: Vec, target: i32) -> Vec> { + let mut all_combinations = Vec::new(); + let mut frontier = Vec::new(); + + frontier.push((0, 0, Vec::new())); + while let Some((sum, candidate_index, mut combination)) = frontier.pop() { + if sum == target { + all_combinations.push(combination.to_vec()); + continue; + } + if candidate_index == candidates.len() { + continue; + } + + let candidate_sum = candidates[candidate_index] + sum; + if candidate_sum <= target { + combination.push(candidates[candidate_index]); + frontier.push((candidate_sum, candidate_index, combination.clone())); + combination.pop(); + } + + frontier.push((sum, candidate_index + 1, combination)); + } + + all_combinations + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step2b_test() { + let mut expect = vec![vec![2, 2, 3], vec![7]]; + let mut actual = Solution::combination_sum(vec![2, 3, 6, 7], 7); + expect.iter_mut().for_each(|x| x.sort()); + expect.sort(); + actual.iter_mut().for_each(|x| x.sort()); + actual.sort(); + assert_eq!(expect, actual); + + let mut expect = vec![vec![2, 2, 2, 2], vec![2, 3, 3], vec![3, 5]]; + let mut actual = Solution::combination_sum(vec![2, 3, 5], 8); + expect.iter_mut().for_each(|x| x.sort()); + expect.sort(); + actual.iter_mut().for_each(|x| x.sort()); + actual.sort(); + assert_eq!(expect, actual); + + let expect = Vec::>::new(); + let actual = Solution::combination_sum(vec![2], 1); + assert_eq!(expect, actual); + } +} diff --git a/src/bin/step3.rs b/src/bin/step3.rs new file mode 100644 index 0000000..cf37055 --- /dev/null +++ b/src/bin/step3.rs @@ -0,0 +1,115 @@ +// Step3 +// 目的: 覚えられないのは、なんか素直じゃないはずなので、そこを探し、ゴールに到達する + +// 方法 +// 時間を測りながらもう一度解く +// 10分以内に一度もエラーを吐かず正解 +// これを3回連続でできたら終わり +// レビューを受ける +// 作れないデータ構造があった場合は別途自作すること + +/* + n = candidates.len() + m = candidates.min() + t = target + 時間計算量: O(n ^ (t / m)) + 空間計算量: O((t / m)) +*/ + +/* + 1回目: 7分44秒 + 2回目: 5分50秒 + 3回目: 5分05秒 +*/ + +/* + 所感: + - target ~ 0 までの補数を計算する方向で一度目は書いたが、0 ~ targetとしたほうが素直な感じがしたので、この書き方に落ち着いた。 + 具体的には、make_combinationsの引数sum,target_sumの変数名で値の関係を表せるのでより良いと思った。 + - 入力の制約としてあり得ないが、candidates[i]にマイナスの値があると、target_sumに到達せず(sumが単調増加しなくなる)に無限ループになる。 + LeetCode採点システムに通らなくなるが、関数のシグネチャを変更して自然数のみを扱うようにすることで、型レベルでこれを防ぐことができると思った。step4.rsで書いておく。 +*/ + +pub struct Solution {} +impl Solution { + pub fn combination_sum(candidates: Vec, target: i32) -> Vec> { + let mut all_combinations = Vec::new(); + let mut combination = Vec::new(); + + Self::make_combinations( + &candidates, + 0, + target, + &mut combination, + &mut all_combinations, + ); + + all_combinations + } + + fn make_combinations( + candidates: &[i32], + sum: i32, + target_sum: i32, + combination: &mut Vec, + all_combinations: &mut Vec>, + ) { + if sum == target_sum { + all_combinations.push(combination.to_vec()); + return; + } + + let Some((&candidate, rest_candidates)) = candidates.split_first() else { + return; + }; + let candidate_sum = candidate + sum; + + if candidate_sum <= target_sum { + combination.push(candidate); + Self::make_combinations( + candidates, + candidate_sum, + target_sum, + combination, + all_combinations, + ); + combination.pop(); + } + + Self::make_combinations( + rest_candidates, + sum, + target_sum, + combination, + all_combinations, + ); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step3_test() { + let mut expect = vec![vec![2, 2, 3], vec![7]]; + let mut actual = Solution::combination_sum(vec![2, 3, 6, 7], 7); + expect.iter_mut().for_each(|x| x.sort()); + expect.sort(); + actual.iter_mut().for_each(|x| x.sort()); + actual.sort(); + assert_eq!(expect, actual); + + let mut expect = vec![vec![2, 2, 2, 2], vec![2, 3, 3], vec![3, 5]]; + let mut actual = Solution::combination_sum(vec![2, 3, 5], 8); + expect.iter_mut().for_each(|x| x.sort()); + expect.sort(); + actual.iter_mut().for_each(|x| x.sort()); + actual.sort(); + assert_eq!(expect, actual); + + let expect = Vec::>::new(); + let actual = Solution::combination_sum(vec![2], 1); + assert_eq!(expect, actual); + } +} diff --git a/src/bin/step4.rs b/src/bin/step4.rs new file mode 100644 index 0000000..01c28ae --- /dev/null +++ b/src/bin/step4.rs @@ -0,0 +1,125 @@ +// Step4 +// 目的: シグネチャの改善 + +/* + 入力の制約上はありえないが、candidates[i]に0又は負数が入ると無限ループするのが気になるので、シグネチャを変更して型レベルで無限ループを防止する。 + + 所感: + - 呼び出し側(テストコード)が少し煩雑になるものの、combination_sumの引数candidatesのシグネチャがVecであるにも関わらず、負数が入ると無限ループするよりは良いと思った。 + 問題なさそうに動きつつある日突然バグが顕在化するよりは良いという感覚。 + - 引数チェックとしてcandidatesをfileterしても良いが、型で防げるならこちらのほうが計算量的、仕様の明確さが優れているという感覚。 +*/ + +use std::num::NonZeroU32; + +pub struct Solution {} +impl Solution { + /* + このコードはLeet Code採点システムを通りません。 + 関数のシグネチャが採点システムと一致しないためです。 + */ + pub fn combination_sum(candidates: Vec, target: u32) -> Vec> { + let mut all_combinations = Vec::new(); + let mut combination = Vec::new(); + + Self::make_combinations( + &candidates, + 0, + target, + &mut combination, + &mut all_combinations, + ); + + all_combinations + } + + fn make_combinations( + candidates: &[NonZeroU32], + sum: u32, + target_sum: u32, + combination: &mut Vec, + all_combinations: &mut Vec>, + ) { + if sum == target_sum { + all_combinations.push(combination.to_vec()); + return; + } + + let Some((&candidate, rest_candidates)) = candidates.split_first() else { + return; + }; + let candidate_sum = candidate.get() + sum; + + if candidate_sum <= target_sum { + combination.push(candidate); + Self::make_combinations( + candidates, + candidate_sum, + target_sum, + combination, + all_combinations, + ); + combination.pop(); + } + + Self::make_combinations( + rest_candidates, + sum, + target_sum, + combination, + all_combinations, + ); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn non_zero_u32_vec(values: Vec) -> Vec { + values + .into_iter() + .map(|v| NonZeroU32::new(v).unwrap()) + .collect() + } + + #[test] + fn step4_test() { + let mut expect = vec![vec![2, 2, 3], vec![7]] + .into_iter() + .map(non_zero_u32_vec) + .collect::>>(); + let candidates = non_zero_u32_vec(vec![2, 3, 6, 7]); + let mut actual = Solution::combination_sum(candidates, 7); + expect.iter_mut().for_each(|x| x.sort()); + expect.sort(); + actual.iter_mut().for_each(|x| x.sort()); + actual.sort(); + assert_eq!(expect, actual); + + let mut expect = vec![vec![2, 2, 2, 2], vec![2, 3, 3], vec![3, 5]] + .into_iter() + .map(non_zero_u32_vec) + .collect::>>(); + let candidates = non_zero_u32_vec(vec![2, 3, 5]); + let mut actual = Solution::combination_sum(candidates, 8); + expect.iter_mut().for_each(|x| x.sort()); + expect.sort(); + actual.iter_mut().for_each(|x| x.sort()); + actual.sort(); + assert_eq!(expect, actual); + + let expect = Vec::>::new() + .into_iter() + .map(non_zero_u32_vec) + .collect::>>(); + let candidates = non_zero_u32_vec(vec![2]); + let actual = Solution::combination_sum(candidates, 1); + assert_eq!(expect, actual); + + let expect = Vec::>::from_iter(vec![vec![]]); + let candidates = non_zero_u32_vec(vec![2]); + let actual = Solution::combination_sum(candidates, 0); + assert_eq!(expect, actual); + } +}