Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 134 additions & 6 deletions src/bin/step1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>) -> 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<String>) -> bool {
let mut splitable_word_cache: HashMap<usize, bool> = 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<usize, bool>,
) -> 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);
}
}
139 changes: 130 additions & 9 deletions src/bin/step2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>) -> 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<usize, bool>,
) -> 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);
}
}
Loading