diff --git a/src/bin/step1.rs b/src/bin/step1.rs index d640da2..d2e676d 100644 --- a/src/bin/step1.rs +++ b/src/bin/step1.rs @@ -8,26 +8,154 @@ // 正解したら終わり /* + 問題の理解 + - 文字列sと文字列を含む配列word_dictが与えられる。sを1つ以上のword_dictの単語のスペース区切りのシーケンスに分割できる場合にtrueを返す。 + 何がわからなかったか - - + - まず問題の内容(何が求められているのか)が理解できなかったのでChatGPT(GPT-5.1)に聞いて理解を進めた。 何を考えて解いていたか - - + - s="leetcode" word_dict=["leet", "code"] output=true + - s="applepenapple" word_dict=["apple", "pen"] output=true + - s="catsandog" word_dict=["cats", "dog", "sand", "and", "cat"] output=false + 文字列sからword_dictの単語に一致するワードを全て取り出せたらtrueという感じだろうか。 + 文字列sからword_dictの単語に一致するワードを取り出していって、文字列sに取り出せない文字が残ったらfalseという方向で考える。 + 文字列のメソッドに疎いのでRustの公式ドキュメントを読む。指定した文字列から指定した文字列を除いて残りを返すようなメソッドがあると良さそう。 + str::replaceメソッドでword_dictに含まれる単語をsで見つけたら空文字で置換して、最後にs.is_empty()を返せば実現できそう。 + https://doc.rust-lang.org/std/primitive.str.html#method.replace + str::replaceメソッドは文字を全部見る必要があるので、線形探索O(s.len())だと見積もる。 + word_dictも線形探索するので、二重ループになり全体の時間計算量はO(s.len() * word_list.len())になる。 + 入力の制約からs.len() <= 300, wordDict.len() <= 1000 なので300_000 ナイーブな実装でも大丈夫そう。 + 手元のテストケースは通ったので、別のテストケースを考えてからLeetCode採点システムに提出する。 + エッジケースのようなものは、catsandogのケースでカバーされているように見えるので特に別のケースを思いつかないのでLeetCode採点システムに提出する。 + ```rust + pub fn word_break(s: String, word_dict: Vec) -> bool { + let mut s = s; + for word in word_dict { + s = s.replace(&word, ""); + } + s.is_empty() + } + ``` + s="cars" word_dict=["car", "ca", "rs"] output=true このテストケースでWrong Answerとなった。 + s="catsandog" word_dict=["cats", "dog", "sand", "and", "cat"] output=false このテストケースもtrueになるべきでは?と思いよく分からない。 + 問題を理解できていないのでChatGPT(GPT-5.1)に聞いてみる。 + + 問題の理解(Chat-GPT(GPT-5.1)に問題の解説を聞いた後) + - 文字列sと文字列から構成される配列word_listが与えられる。 + - word_listに含まれる単語を使って、文字列sを分割したときに、文字列sに余計な文字列が残らないように分割できればtrueを返す。 + - s="cars" word_dict=["car", "ca", "rs"] + - 単語"car"で区切ると文字列sに"s"が残り分割しきれない。単語"ca"で区切ると文字列sに"rs"が残り単語"rs"と一致するので最終的に文字列sに余分な文字列が残らないのでtrue + - s="catsandog" word_dict=["cats", "dog", "sand", "and", "cat"] + - 単語"cats"で分割すると残りの文字列は"andog"となり、word_listに含まれる単語で分割できるものはない。どの単語で分割しても文字列sが空になるように分割しきれないのでfalse + + 何を考えて解いていたか(Chat-GPT(GPT-5.1)に問題の解説を聞いた後) + - 手が止まったので解答を見て理解する 想定ユースケース - - + - 英語のスペルチェックとかで使えそうだなと思った。存在する英単語をword_listとして、文章をsとする。文章sを単語で分割しきれるかでスペルミスを検出する。 + + 解答の理解 + - LeetCodeのSolutionsのRust実装を見たが、一次元DP配列に値を突っ込んでいくコードばかりでどういう気持ちでこのコードを書いているのか理解できなかった。 + NeetCodeの解説動画を見て、再帰による実装を行ってみる + https://www.youtube.com/watch?v=Sx9NNgInc3A + 再帰処理 + - 基本ケース + - s.len() <= i であればtrueを返す + - 再帰ケース + - word_listのword.len()と等しい長さ分の文字列をs[i..i+word.len()]から取り出した時に一致するかをword_listのword全てで確認する。 + - 一致した時 i を i+word.len() に更新して再帰処理に入る。 + - 一致しない時 は falseとして扱う 再帰処理に入らない + or で結果をまとめる。1つでもtrueになるパスがあれば、word_listのwordでsが分割できるため。 + + この考え方で再帰+memo化のコードを実装してAccept 正解してから気づいたこと - - + - メソッド命名 is_splitable_word はもう少し良い命名がある気がするものの思いつかない + - 文字列の区間を表すs_start_index,s_end_indexは少し冗長な気がするが、許容範囲かなという感覚。短くしたところで得られるメリットもあまりなさそう。 + - 再帰+メモ化をする前に計算量見積もりをするべきだった。 */ +use std::collections::HashMap; + pub struct Solution {} -impl Solution {} +impl Solution { + pub fn word_break(s: String, word_dict: Vec) -> bool { + let mut splitable_word_cache: HashMap = HashMap::new(); + Self::is_splitable_word(&s, 0, &word_dict, &mut splitable_word_cache) + } + + fn is_splitable_word( + s: &str, + s_start_index: usize, + word_list: &[String], + splitable_word_cache: &mut HashMap, + ) -> bool { + if s.len() <= s_start_index { + return true; + } + + if let Some(cache) = splitable_word_cache.get(&s_start_index) { + return *cache; + } + + word_list.iter().any(|word| { + let s_end_index = s_start_index + word.len(); + let Some(splited_s) = s.get(s_start_index..s_end_index) else { + return false; + }; + + if splited_s != word { + return false; + } + + let is_splitable = + Self::is_splitable_word(s, s_end_index, word_list, splitable_word_cache); + splitable_word_cache.insert(s_start_index, is_splitable); + + return is_splitable; + }) + } +} #[cfg(test)] mod tests { use super::*; #[test] - fn step1_test() {} + fn playground() { + let s = "leetcode"; + assert_eq!(&s[4..8], "code"); + } + + #[test] + fn step1_test() { + let s = "leetcode".to_string(); + let word_list = vec!["leet", "code"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "applepenapple".to_string(); + let word_list = vec!["apple", "pen"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "cars".to_string(); + let word_list = vec!["car", "ca", "rs"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "catsandog".to_string(); + let word_list = vec!["cats", "dog", "sand", "and", "cat"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), false); + } } diff --git a/src/bin/step2.rs b/src/bin/step2.rs index e92520d..1391991 100644 --- a/src/bin/step2.rs +++ b/src/bin/step2.rs @@ -12,26 +12,147 @@ // 改善する時に考えたこと /* - 講師陣はどのようなコメントを残すだろうか? - - - 他の人のコードを読んで考えたこと - - + - is_splitable_word の命名で悩んでいたが、いくつか選択肢の幅があった。 + is_segmentable,can_be_segmented など + can_segment,can_split が良いかなと思った。 + https://github.com/olsen-blue/Arai60/pull/39#discussion_r1983043320 + + - step1のキャッシュ変数の命名について。 + 確かにindex_to_breakable,index_to_segmentableとかのほうが分かりやすいと思った。 + https://github.com/ryosuketc/leetcode_arai60/pull/52/files#r2225024995 + + - 入力チェックをすっかり忘れていことに気付いた。 + https://github.com/docto-rin/leetcode/pull/44/files#diff-9942a679b9535897a1d10eaaf67b9243ebbfa70786f9252cefa19d384314c8c9R133-R136 + + 文字列sが空文字の時にtrueを返しているのがよく分からなのでGPT-5.1に聞いてみる。 + > 空文字列は「単語を 0 個使うことで構成できる」と解釈するため、標準的には + 空文字列は構成可能 → true + です。 + + 上記の理由からDP実装で空文字を表すdp[0] = trueと設定しているとのこと。制約上あり得ない入力なのでそこまで重要ではないと思っていたがそんなことはなかった。 + DP的な考え方をするうえで大切な部分だと思った。 + + - 「Trie木」という単語を初めて聞いた。ソフトウェアエンジニアの常識には含まれていないとのことで深追いせずメモのみとする。 + https://github.com/hayashi-ay/leetcode/pull/61#discussion_r1536822342 + Wikiで少し説明を見たがよく分からない。 + https://ja.wikipedia.org/wiki/%E3%83%88%E3%83%A9%E3%82%A4_(%E3%83%87%E3%83%BC%E3%82%BF%E6%A7%8B%E9%80%A0) - 他の想定ユースケース - - + - step1で問題の理解と、どういう気持で処理を行っているのかは分かったと思うのでDPテーブルによる解法も練習する。 + > というわけで、先頭から DP が"模範解答"だろうな、とは思います。 + https://discord.com/channels/1084280443945353267/1200089668901937312/1221781262109380699 改善する時に考えたこと - - + - 文字列の内、ある区間の文字列を表す変数名はsplited_sよりもpeeked_sの方が良さそう。 + - これに付随してs_start_index,s_end_indexよりはpeeked_start,peeked_endの方が良さそう + - 入力チェックを行う。テストケースを追加する。 + - is_splitable_word -> can_segment + - splitable_word_cache -> index_to_segmentable + - anyのクロージャ内でキャッシュの登録を行っていたが、早期リターンした場合にキャッシュされないことに気付いたので修正した。 + チェックしようとしている文字列区間の開始位置から分割可能であるかの結果をキャッシュしているので、区間ごとに見るために外側でキャッシュするのが正しい。 + - word_dict.into_iter()はword_dict.iter()にするべき。共有参照(&T)しか使わないのが分かっているので。 + + 所感 + - 最近コメント集を見に行くのを忘れていることに気付いたので忘れないようにする。 + レビュー依頼するときの癖で、直近のレビュー依頼のPull Requestを見ていたがコメント集から見ていくようにする。 + - 正直この解法が一番自然に感じるが、DP実装も練習のために実装する。step2a_dp.rs */ +use std::collections::HashMap; + pub struct Solution {} -impl Solution {} +impl Solution { + pub fn word_break(s: String, word_dict: Vec) -> bool { + if s.is_empty() { + return true; + } + if word_dict.is_empty() { + return false; + } + + let mut index_to_segmentable = HashMap::new(); + Self::can_segment(&s, 0, &word_dict, &mut index_to_segmentable) + } + + fn can_segment( + s: &str, + peeked_start: usize, + word_dict: &[String], + index_to_segmentable: &mut HashMap, + ) -> bool { + if s.len() <= peeked_start { + return true; + } + + if let Some(segmentable) = index_to_segmentable.get(&peeked_start) { + return *segmentable; + } + + let segmentable = word_dict.iter().any(|word| { + let peeked_end = peeked_start + word.len(); + let Some(peeked_s) = s.get(peeked_start..peeked_end) else { + return false; + }; + + if peeked_s != word { + return false; + } + + Self::can_segment(s, peeked_end, word_dict, index_to_segmentable) + }); + index_to_segmentable.insert(peeked_start, segmentable); + + segmentable + } +} #[cfg(test)] mod tests { use super::*; #[test] - fn step2_test() {} + fn step2_test() { + let s = "leetcode".to_string(); + let word_list = vec!["leet", "code"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "applepenapple".to_string(); + let word_list = vec!["apple", "pen"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "cars".to_string(); + let word_list = vec!["car", "ca", "rs"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "catsandog".to_string(); + let word_list = vec!["cats", "dog", "sand", "and", "cat"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), false); + + let s = "".to_string(); + let word_list = vec!["cats", "dog", "sand", "and", "cat"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "abc".to_string(); + let word_list = Vec::new(); + assert_eq!(Solution::word_break(s, word_list), false); + + let s = "".to_string(); + let word_list = Vec::new(); + assert_eq!(Solution::word_break(s, word_list), true); + } } diff --git a/src/bin/step2a_dp.rs b/src/bin/step2a_dp.rs new file mode 100644 index 0000000..c5dc854 --- /dev/null +++ b/src/bin/step2a_dp.rs @@ -0,0 +1,106 @@ +// Step2a_dp +// 目的: DP実装を練習する + +/* + DP実装の理解 + NeetCodeの解説動画は最終的にDP実装をしている。 + https://www.youtube.com/watch?v=Sx9NNgInc3A + s="leetcode" word_list=["leet", "code"] + s=文字列 + - 再帰の基本ケースである、sの最後に到達したら分割できたということをdp[s.len() + 1] = true で表現している。 + - sを後ろから先頭に向けて走査する。 + - 一致する単語を見つけた時 + - 今見ている文字位置iから一致した文字長さ分進めた時に余りの文字が無いかの情報を格納する。 + - 単語末尾(余り無し)がtrueなので、単語末尾までピッタリ分割できる(余りが無い)とtrueと判定される。 + - 入力のword_dictに含まれる単語を使って分割できるかを確認すれば良いので、ある文字位置で分割できる場合は、その文字位置で分割できないことを確認する必要はない。 + - dp[i]がtrueであれば、そのループを抜ける + - dp[0]まで到達した時に全部分割できたかの情報が入る。 + - 自然言語で表すのがだいぶ難しいと感じた。 + - これはdpテーブルの末尾から先頭に向けて見ているのでトップダウンだろうか。 + GPT-5.1に聞いたところボトムアップアプローチとのこと。 + 再帰+メモ化: トップダウンアプローチ + dp配列: 先頭、末尾どちらから開始するかに関わらずボトムアップアプローチ + + 所感 + - breakするときのif分をブロック節のように見せるためにループ先頭に持ってくるか、値のすぐ後にもってくるか迷った。 + ループ先頭に持って行くとsegmentable_words[i]の出現場所がばらばらになり、文脈が分断され読み手の負荷が増えると考えたので、先頭ではなく末尾に置いた。 + - コメント集からdiscordのやり取り見た時に逆順で書けますかみたいなコメント見かけたのでdpを逆順(head -> tail)で見るバージョンも書く。 step2b_dp_head_to_tail.rs +*/ + +pub struct Solution {} +impl Solution { + pub fn word_break(s: String, word_dict: Vec) -> bool { + let mut segmentable_words = vec![false; s.len() + 1]; + segmentable_words[s.len()] = true; + + for i in (0..s.len()).rev() { + for word in &word_dict { + let Some(peeked_s) = s.get(i..i + word.len()) else { + continue; + }; + + if peeked_s != word { + continue; + } + + segmentable_words[i] = segmentable_words[i + word.len()]; + if segmentable_words[i] { + break; + } + } + } + + segmentable_words[0] + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step2a_test() { + let s = "leetcode".to_string(); + let word_list = vec!["leet", "code"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "applepenapple".to_string(); + let word_list = vec!["apple", "pen"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "cars".to_string(); + let word_list = vec!["car", "ca", "rs"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "catsandog".to_string(); + let word_list = vec!["cats", "dog", "sand", "and", "cat"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), false); + + let s = "".to_string(); + let word_list = vec!["cats", "dog", "sand", "and", "cat"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "abc".to_string(); + let word_list = Vec::new(); + assert_eq!(Solution::word_break(s, word_list), false); + + let s = "".to_string(); + let word_list = Vec::new(); + assert_eq!(Solution::word_break(s, word_list), true); + } +} diff --git a/src/bin/step2b_dp_head_to_tail.rs b/src/bin/step2b_dp_head_to_tail.rs new file mode 100644 index 0000000..5aa4ae8 --- /dev/null +++ b/src/bin/step2b_dp_head_to_tail.rs @@ -0,0 +1,90 @@ +// Step2a_dp +// 目的: DP実装を練習する。DP配列を先頭から末尾に書けて見るバージョン + +/* + 所感 + - DP配列を先頭から末尾に向けて走査する方法だと手が止まって解けなかった。時間切れなのでGPT-5.1に聞く + - 空文字をtrueとしてdp[0]をtrueとする。<- 直感に反するので腹落ちしない + - dp[0]をtrueとした時、一致する単語の長さdp[i+word.len()] = true をする前に、それまでに出現した文字列[i+word.len()]がtrueであることを確認する必要がある。 + dp[i+word.len()] がtrueでない時単語を分割しきれていない。 <- ここもよく分からない。 + 時間切れなので写経のみとする +*/ + +pub struct Solution {} +impl Solution { + pub fn word_break(s: String, word_dict: Vec) -> bool { + let mut segmentable_words = vec![false; s.len() + 1]; + segmentable_words[0] = true; + + for i in 0..s.len() { + for word in &word_dict { + let Some(peeked_word) = s.get(i..i + word.len()) else { + continue; + }; + + if peeked_word != word { + continue; + } + + if !segmentable_words[i] { + break; + } + + segmentable_words[i + word.len()] = true; + } + } + + *segmentable_words.last().unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step2b_test() { + let s = "leetcode".to_string(); + let word_list = vec!["leet", "code"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "applepenapple".to_string(); + let word_list = vec!["apple", "pen"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "cars".to_string(); + let word_list = vec!["car", "ca", "rs"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "catsandog".to_string(); + let word_list = vec!["cats", "dog", "sand", "and", "cat"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), false); + + let s = "".to_string(); + let word_list = vec!["cats", "dog", "sand", "and", "cat"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "abc".to_string(); + let word_list = Vec::new(); + assert_eq!(Solution::word_break(s, word_list), false); + + let s = "".to_string(); + let word_list = Vec::new(); + assert_eq!(Solution::word_break(s, word_list), true); + } +} diff --git a/src/bin/step3.rs b/src/bin/step3.rs index 0af0a4a..ce61dd4 100644 --- a/src/bin/step3.rs +++ b/src/bin/step3.rs @@ -9,23 +9,98 @@ // 作れないデータ構造があった場合は別途自作すること /* - 時間計算量: - 空間計算量: + n = s.len() + m = word_dict.len() + L = max(word_dict[i].len()) + 時間計算量: O(m * n * L) <- O(n * m)だと思ったが文字列同士の比較の線形探索の時間計算量を見落としていた。 + 空間計算量: O(n) */ /* - 1回目: 分秒 - 2回目: 分秒 - 3回目: 分秒 + 1回目: 3分51秒 + 2回目: 3分6秒 + 3回目: 2分1秒 +*/ + +/* + 所感 + - 暗記で書いている感じが強い。こうするとこうなるからこうという感じ。 */ pub struct Solution {} -impl Solution {} +impl Solution { + pub fn word_break(s: String, word_dict: Vec) -> bool { + let mut segmentable_words = vec![false; s.len() + 1]; + segmentable_words[s.len()] = true; + + for i in (0..s.len()).rev() { + for word in &word_dict { + let Some(peeked_s) = s.get(i..i + word.len()) else { + continue; + }; + + if peeked_s != word { + continue; + } + + segmentable_words[i] = segmentable_words[i + word.len()]; + if segmentable_words[i] { + break; + } + } + } + + segmentable_words[0] + } +} #[cfg(test)] mod tests { use super::*; #[test] - fn step3_test() {} + fn step3_test() { + let s = "leetcode".to_string(); + let word_list = vec!["leet", "code"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "applepenapple".to_string(); + let word_list = vec!["apple", "pen"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "cars".to_string(); + let word_list = vec!["car", "ca", "rs"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "catsandog".to_string(); + let word_list = vec!["cats", "dog", "sand", "and", "cat"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), false); + + let s = "".to_string(); + let word_list = vec!["cats", "dog", "sand", "and", "cat"] + .into_iter() + .map(str::to_string) + .collect(); + assert_eq!(Solution::word_break(s, word_list), true); + + let s = "abc".to_string(); + let word_list = Vec::new(); + assert_eq!(Solution::word_break(s, word_list), false); + + let s = "".to_string(); + let word_list = Vec::new(); + assert_eq!(Solution::word_break(s, word_list), true); + } }