139. Word Break#8
Conversation
| } | ||
|
|
||
| private: | ||
| bool WordBreakAux( |
There was a problem hiding this comment.
Auxという言葉が見慣れない気がします(チームメンバーと合意が取れているならいい気がします)
Auxiliaryのことだったら, WordBreakHelperとかならDiscordの方はよく使っている気がします。
There was a problem hiding this comment.
| // なお以下で出てくるコメントはプロダクト版にも書くつもりのコメント | ||
| class Solution { | ||
| public: | ||
| bool wordBreak(string s, vector<string>& wordDict) { |
There was a problem hiding this comment.
この問題、基本的にはsのi文字目からj文字目までの部分文字列(i,j)がwordDictの要素で分割できるかというDPだと思っています。それをBFSやDFS、あるいは愚直にDPという形に落とし込めると思っていて。
ただ、今回の場合0<=i,j<s.size()をすべて走査する必要はなく、wordDictの内容を利用することで解くべき部分問題(i,j)の数を減らすことが出来て、それをコード化すると、1.cppの再帰DFS(をもう少しわかりやすくしたもの)が一番いいのかなと思っています。
ただ、キャッシュ変数を作ってそれを渡し続けるのが気に入っていなくて(僕の気にしすぎならいいのですが)、
次点繰り上げで2.cppのiterative DFSが分かりやすいかなと思っています。(2.cppはBFSですがDFSの方が良かったなと思っています)
どうでしょうか?
There was a problem hiding this comment.
一応、私が書いてみたのを貼っておきます。
class Solution {
public:
bool wordBreak(string s, vector<string>& words) {
TrieNode root;
for (const auto& word : words) {
TrieNode* node = &root;
for (char c : word) {
auto [it, inserted] = node->next.try_emplace(c, make_unique<TrieNode>());
node = it->second.get();
}
node->is_word_end = true;
}
// is_breakable[i]: s.substr(0, i) is breakable.
vector<bool> is_breakable(s.size() + 1);
is_breakable[0] = true;
for (int i = 0; i < s.size(); ++i) {
if (!is_breakable[i]) {
continue;
}
TrieNode* node = &root;
for (int j = 0; i + j < s.size(); ++j) {
auto it = node->next.find(s[i + j]);
if (it == node->next.end()) {
break;
}
node = it->second.get();
if (node->is_word_end) {
is_breakable[i + j + 1] = true;
}
}
}
return is_breakable.back();
}
private:
struct TrieNode {
unordered_map<char, unique_ptr<TrieNode>> next;
bool is_word_end = false;
};
};There was a problem hiding this comment.
なるほど、Trie木による解法ですね!
計算量はこれが一番少なそうですね
ありがとうございます!
(あとこういう講師の方のコードを見るのが一番助かりますmm)
There was a problem hiding this comment.
この問題、基本的にはsのi文字目からj文字目までの部分文字列(i,j)がwordDictの要素で分割できるかというDPだと思っています。それをBFSやDFS、あるいは愚直にDPという形に落とし込めると思っていて。
- この説明だと2次元DPですが、実際には1次元DPで十分で、@philip82148 さんのコードもそうなっています。
- DPの問題は、トップダウンかボトムアップにやるかで2つの方法があって、 @philip82148 さんは今回トップダウンで解いています。両方で書けるようにすれば良いでしょう。
There was a problem hiding this comment.
- この説明だと2次元DPですが、実際には1次元DPで十分で、@philip82148 さんのコードもそうなっています。
O(NM) (N=s.size()、M=wordDict.size())ということですね!
- DPの問題は、トップダウンかボトムアップにやるかで2つの方法があって、 @philip82148 さんは今回トップダウンで解いています。両方で書けるようにすれば良いでしょう。
なるほどです!ボトムアップでも解いてみます!
| if (start == s.size()) return true; | ||
| if (checked[start]) return false; | ||
| for (auto& word : wordDict) { | ||
| if (s.substr(start).starts_with(word) |
There was a problem hiding this comment.
substr() によって string のコピーが作られるため、無駄な処理が走るのが気になりました。 string のコピーを作らずに処理を書けますか?
There was a problem hiding this comment.
こちら、sはstring_viewですのでコピーは作られません。
こういう柔軟な書き方が出来るところがstring_viewのいい所だと個人的には思っているのですが、避けた方が良いでしょうか?
ちなみにstring_viewではないのですがchromiumでは24件ありました。
なお、これ以外の書き方だとs.compare(start, word.size(), word) == 0となります。
| ) { | ||
| if (start == s.size()) return true; | ||
| if (checked[start]) return false; | ||
| for (auto& word : wordDict) { |
There was a problem hiding this comment.
個人的には参照で受けるとき、かつ内容を変更しない場合は、 const auto& で受けることが多いです。この辺りはチームの平均的な書き方に合わせるのがよいと思います。
|
この問題、文字列消しながら探索ノードを広げるイメージで解いていく方法や、Trie木を使った方法など色々考えられるのでDiscordを漁ってみると良さそうです |
|
|
||
| if (index == s.size()) return true; | ||
|
|
||
| if (is_checked[index]) continue; |
There was a problem hiding this comment.
このへんのチェックとフラグ更新は、pushするまえに行うとpush/popする回数を減らせますか?
There was a problem hiding this comment.
はい、減らせます!ただ、今回の場合は良いのですが、問題によってはis_checkedのフラグはqueueの処理途中で変わることがあるので、僕の中では一旦pushしてpop時に処理するとバグが少ないイメージです。
でも、BFSのパターン(書き方)としてpushする前にis_checkedのフラグを見るというのも一般的であればその方法もよいと思います。
そちらの書き方でも認知負荷は高くないですかね?
(他の方のレビューを見ていてBFSにはいくつかの書き方のパターンがある認識で、一般的なものに合わせた方が認知負荷+バグの低減につながると考えています)
どうでしょうか?
| // <時間> | ||
| // 7分 | ||
| // <コメント> | ||
| // 1から着想を得て書いた |
There was a problem hiding this comment.
queueを使っているので、はじめの方法と深さ優先/幅優先の探索順序が変わりますね。
それを考慮したうえで選んだ、ということが思考過程にあるとよいと思いました。(解法に問題があるということではありません)
There was a problem hiding this comment.
ありがとうございます!
再帰の方がDFSなので、stackを使うとまたDFSになってしまう、という意識と、今回の場合BFSの方が空間計算量が少なくなるのかなと思いながらBFSに変えたのですが、後者に関しては今全然そんなことなさそうなことに気づきました。
(発見がありました、ありがとうございますmm)
| class Solution { | ||
| public: | ||
| bool wordBreak(string s, vector<string>& wordDict) { | ||
| queue<int> indexes; |
There was a problem hiding this comment.
is_checked(に相当するもの)を使用する場合、indexesを使用しない方法もあると思いますが、そのメリット・デメリットを挙げられますか?
There was a problem hiding this comment.
すみません…、分かりません…。
is_checked(に相当するもの)を使用する場合、indexesを使用しない方法
こちらの方法のヒントを頂けないでしょうか?(質問には自分でこたえたいです)
| public: | ||
| bool wordBreak(string s, vector<string>& wordDict) { | ||
| queue<int> indexes; | ||
| vector<bool> is_checked(s.size()); |
There was a problem hiding this comment.
indexesをpriority_queue<int, vector<int>, greater<int>>にして、1回のpopで同じ値がなくなるまでpopするようにすると、is_checkedが要らなくなることに気づきました
| // 1から着想を得て書いた | ||
| // <疑問・不安点(・個人的な感想、違う意見があれば教えてほしいもの)> | ||
| // - indexの他の命名候補はstart_index | ||
| // - is_checkedの他の命名候補はindex_is_checked |
There was a problem hiding this comment.
当初はis_checkedで問題ないと思っていましたが、もし最適化を検討する場合は、is_checkedという名前よりはis_reachedみたいな到達したか否かを表す名前とか、あるいは実装に寄せるならqueueに入れたかみたいな名前もあり得るかもしれないですね。
There was a problem hiding this comment.
なるほどです!
競プロの癖でis_checkedという名前に慣れてしまっていたのでもっと工夫できることに気づきました!
ありがとうございます!
注意
ファイルが複数ある場合、<番号>.cppの<番号>が最も大きいものが最新版です!
最新版のみレビューお願いします!(もちろん過去のものをレビューしていただいても構いませんmm)
問題
https://leetcode.com/problems/word-break/description/?envType=problem-list-v2&envId=xo2bgr0r