Skip to content

139. Word Break#8

Open
philip82148 wants to merge 1 commit into
mainfrom
solve/8.Word-Break
Open

139. Word Break#8
philip82148 wants to merge 1 commit into
mainfrom
solve/8.Word-Break

Conversation

@philip82148

Copy link
Copy Markdown
Owner

注意

ファイルが複数ある場合、<番号>.cppの<番号>が最も大きいものが最新版です!
最新版のみレビューお願いします!(もちろん過去のものをレビューしていただいても構いませんmm)

問題

https://leetcode.com/problems/word-break/description/?envType=problem-list-v2&envId=xo2bgr0r

Comment thread 01-15/8.Word Break/1.cpp
}

private:
bool WordBreakAux(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auxという言葉が見慣れない気がします(チームメンバーと合意が取れているならいい気がします)
Auxiliaryのことだったら, WordBreakHelperとかならDiscordの方はよく使っている気がします。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

あ、申し訳ないです経験不足でした

Comment thread 01-15/8.Word Break/2.cpp
// なお以下で出てくるコメントはプロダクト版にも書くつもりのコメント
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

他の方法は何か思いつきましたか?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この問題、基本的にはsi文字目から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の方が良かったなと思っています)
どうでしょうか?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

一応、私が書いてみたのを貼っておきます。

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;
    };
};

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なるほど、Trie木による解法ですね!
計算量はこれが一番少なそうですね
ありがとうございます!
(あとこういう講師の方のコードを見るのが一番助かりますmm)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この問題、基本的にはsのi文字目からj文字目までの部分文字列(i,j)がwordDictの要素で分割できるかというDPだと思っています。それをBFSやDFS、あるいは愚直にDPという形に落とし込めると思っていて。

  • この説明だと2次元DPですが、実際には1次元DPで十分で、@philip82148 さんのコードもそうなっています。
  • DPの問題は、トップダウンかボトムアップにやるかで2つの方法があって、 @philip82148 さんは今回トップダウンで解いています。両方で書けるようにすれば良いでしょう。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • この説明だと2次元DPですが、実際には1次元DPで十分で、@philip82148 さんのコードもそうなっています。

O(NM) (N=s.size()、M=wordDict.size())ということですね!

  • DPの問題は、トップダウンかボトムアップにやるかで2つの方法があって、 @philip82148 さんは今回トップダウンで解いています。両方で書けるようにすれば良いでしょう。

なるほどです!ボトムアップでも解いてみます!

Comment thread 01-15/8.Word Break/1.cpp
if (start == s.size()) return true;
if (checked[start]) return false;
for (auto& word : wordDict) {
if (s.substr(start).starts_with(word)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

substr() によって string のコピーが作られるため、無駄な処理が走るのが気になりました。 string のコピーを作らずに処理を書けますか?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちら、sはstring_viewですのでコピーは作られません。
こういう柔軟な書き方が出来るところがstring_viewのいい所だと個人的には思っているのですが、避けた方が良いでしょうか?
ちなみにstring_viewではないのですがchromiumでは24件ありました
なお、これ以外の書き方だとs.compare(start, word.size(), word) == 0となります。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちら読み間違えました。失礼いたしました。

Comment thread 01-15/8.Word Break/1.cpp
) {
if (start == s.size()) return true;
if (checked[start]) return false;
for (auto& word : wordDict) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

個人的には参照で受けるとき、かつ内容を変更しない場合は、 const auto& で受けることが多いです。この辺りはチームの平均的な書き方に合わせるのがよいと思います。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

勉強になります!
ありがとうございます!

@Yoshiki-Iwasa

Copy link
Copy Markdown

この問題、文字列消しながら探索ノードを広げるイメージで解いていく方法や、Trie木を使った方法など色々考えられるのでDiscordを漁ってみると良さそうです

Comment thread 01-15/8.Word Break/2.cpp

if (index == s.size()) return true;

if (is_checked[index]) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このへんのチェックとフラグ更新は、pushするまえに行うとpush/popする回数を減らせますか?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

はい、減らせます!ただ、今回の場合は良いのですが、問題によってはis_checkedのフラグはqueueの処理途中で変わることがあるので、僕の中では一旦pushしてpop時に処理するとバグが少ないイメージです。
でも、BFSのパターン(書き方)としてpushする前にis_checkedのフラグを見るというのも一般的であればその方法もよいと思います。
そちらの書き方でも認知負荷は高くないですかね?
(他の方のレビューを見ていてBFSにはいくつかの書き方のパターンがある認識で、一般的なものに合わせた方が認知負荷+バグの低減につながると考えています)
どうでしょうか?

Comment thread 01-15/8.Word Break/2.cpp
// <時間>
// 7分
// <コメント>
// 1から着想を得て書いた

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

queueを使っているので、はじめの方法と深さ優先/幅優先の探索順序が変わりますね。
それを考慮したうえで選んだ、ということが思考過程にあるとよいと思いました。(解法に問題があるということではありません)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます!
再帰の方がDFSなので、stackを使うとまたDFSになってしまう、という意識と、今回の場合BFSの方が空間計算量が少なくなるのかなと思いながらBFSに変えたのですが、後者に関しては今全然そんなことなさそうなことに気づきました。
(発見がありました、ありがとうございますmm)

Comment thread 01-15/8.Word Break/2.cpp
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
queue<int> indexes;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_checked(に相当するもの)を使用する場合、indexesを使用しない方法もあると思いますが、そのメリット・デメリットを挙げられますか?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

すみません…、分かりません…。

is_checked(に相当するもの)を使用する場合、indexesを使用しない方法

こちらの方法のヒントを頂けないでしょうか?(質問には自分でこたえたいです)

Comment thread 01-15/8.Word Break/2.cpp
public:
bool wordBreak(string s, vector<string>& wordDict) {
queue<int> indexes;
vector<bool> is_checked(s.size());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

空間計算量をもっと少なくする方法はありますか?

@philip82148 philip82148 Nov 30, 2024

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indexespriority_queue<int, vector<int>, greater<int>>にして、1回のpopで同じ値がなくなるまでpopするようにすると、is_checkedが要らなくなることに気づきました

Comment thread 01-15/8.Word Break/2.cpp
// 1から着想を得て書いた
// <疑問・不安点(・個人的な感想、違う意見があれば教えてほしいもの)>
// - indexの他の命名候補はstart_index
// - is_checkedの他の命名候補はindex_is_checked

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当初はis_checkedで問題ないと思っていましたが、もし最適化を検討する場合は、is_checkedという名前よりはis_reachedみたいな到達したか否かを表す名前とか、あるいは実装に寄せるならqueueに入れたかみたいな名前もあり得るかもしれないですね。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なるほどです!
競プロの癖でis_checkedという名前に慣れてしまっていたのでもっと工夫できることに気づきました!
ありがとうございます!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants