-
Notifications
You must be signed in to change notification settings - Fork 0
solved word ladder #45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: arai60
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # 単純にBFSをしてtime limit overしたもの, そりゃそうだろうなという感じの時間計算量 | ||
| class Solution: | ||
| def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: | ||
| for word in wordList: | ||
| if word == endWord: # ここ, if endWord not in wordListで効率化できるな | ||
| break | ||
| else: | ||
| return 0 # もしwordListになければreturnする | ||
|
|
||
| def calc_word_diff(s1: str, s2: str) -> int: | ||
| word_diff_count = 0 | ||
| for i in range(len(s1)): | ||
| if s1[i] != s2[i]: | ||
| word_diff_count += 1 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. この問題で必要なのって、単語が隣接しているかどうかなので、diff_countについて2以上を数える必要がなくなり、そのようなロジックを組み込めるとそのぶんシンプルにできそうです。 |
||
| return word_diff_count | ||
| queue = deque([(beginWord, wordList, 1)]) | ||
| while queue: | ||
| tail_word, available_words, sequence_length = queue.popleft() | ||
| if tail_word == endWord: | ||
| return sequence_length | ||
| for word in available_words: | ||
| if calc_word_diff(tail_word, word) == 1: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ネストが深すぎるので、ここを反転させてearly continueみたいな感じでやれると読みやすくできそうです。 |
||
| next_available_words = available_words.copy() | ||
| next_available_words.remove(word) | ||
| queue.append((word, next_available_words, sequence_length + 1)) | ||
|
|
||
| return 0 | ||
|
|
||
| # 前もってグラフを作っておいてBFS, seenで管理しているので最大でもO(5000^2)なはず。通りはしたがなんか遅い, 何か間違えている気がする | ||
| # seenで管理するのは今回はshortest pathなのでBFSで最初についたルートでseenに追加したとしても問題ないからだと考えている | ||
| class Solution: | ||
| def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: | ||
| for word in wordList: | ||
| if word == endWord: # ここ, if endWord not in wordListで効率化できるな | ||
| break | ||
| else: | ||
| return 0 # もしwordListになければreturnする | ||
|
|
||
| def calc_word_diff(s1: str, s2: str) -> int: | ||
| word_diff_count = 0 | ||
| for i in range(len(s1)): | ||
| if s1[i] != s2[i]: | ||
| word_diff_count += 1 | ||
| return word_diff_count | ||
|
|
||
| word_graph = defaultdict(list) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. グラフの中に入れる要素を単語本体ではなく wordList 内のインデックスとすることで、処理を少し軽くすることができるかもしれません。 |
||
| for word in wordList: | ||
| if calc_word_diff(beginWord, word) == 1: | ||
| word_graph[beginWord].append(word) | ||
|
|
||
| for i in range(len(wordList) - 1): | ||
| for j in range(i + 1, len(wordList)): | ||
| if calc_word_diff(wordList[i], wordList[j]) == 1: | ||
| word_graph[wordList[i]].append(wordList[j]) | ||
| word_graph[wordList[j]].append(wordList[i]) | ||
|
|
||
| seen = set() | ||
| queue = deque([(beginWord, 1)]) | ||
| while queue: | ||
| tail_word, sequence_length = queue.popleft() | ||
| for next_word in word_graph[tail_word]: | ||
| if next_word == endWord: | ||
| return sequence_length + 1 | ||
| if next_word in seen: | ||
| continue | ||
| seen.add(next_word) | ||
| queue.append((next_word, sequence_length + 1)) | ||
|
|
||
| return 0 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| # @hardjuice @tohsumi @colorbox @pompom0x0 @takkatao | ||
| # https://github.com/olsen-blue/Arai60/pull/20/files | ||
| # - 遷移のさせ方、には一工夫必要。ある単語(例:hot)から、変化のさせ方としては、3パターン存在する。 | ||
| # - 具体的には、「*ot」「h*t」「ho*」の3パターンである。これらをキーとして、対応する単語を、辞書管理すれば良さそう。 | ||
| # - 「hot」 ->「*ot」「h*t」「ho*」という変換が、必要そう。 | ||
| # このまとめ方が参考になった, 文字列の結合でなくてtupleをkeyにするというのも確かにと思った | ||
| # get_keys_from_word(word)で結局O(n)かかるので自分と同じような実行時間になるのかしら...? | ||
| # https://github.com/colorbox/leetcode/pull/34/files | ||
| # https://github.com/Hurukawa2121/leetcode/pull/20#pullrequestreview-2569671996 | ||
| # https://github.com/t0hsumi/leetcode/pull/20 | ||
| # https://github.com/tshimosake/arai60/pull/11#discussion_r1944112516 | ||
| # ordを一応確認 https://github.com/t0hsumi/leetcode/pull/12#discussion_r1886759238 | ||
|
|
||
| # 最初にグラフを作らなくてもよかった | ||
| class Solution: | ||
| def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: | ||
| for word in wordList: | ||
| if word == endWord: # ここ, if endWord not in wordListで効率化できるな | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pythonのリストのin, not inについてはネイティブコードで実装されているので、速度的にも入力文字数という意味でもnot inとした方が良さそうです。 |
||
| break | ||
| else: | ||
| return 0 # もしwordListになければreturnする | ||
|
|
||
| def calc_word_diff(s1: str, s2: str) -> int: | ||
| word_diff_count = 0 | ||
| for i in range(len(s1)): | ||
| if s1[i] != s2[i]: | ||
| word_diff_count += 1 | ||
| return word_diff_count | ||
|
|
||
| seen = set() | ||
| queue = deque([(beginWord, 1)]) | ||
| while queue: | ||
| tail_word, sequence_length = queue.popleft() | ||
| for word in wordList: | ||
| if word not in seen and calc_word_diff(tail_word, word) == 1: | ||
| if word == endWord: | ||
| return sequence_length + 1 | ||
| queue.append((word, sequence_length + 1)) | ||
| seen.add(word) | ||
| return 0 | ||
| # 書いてから思ったけど1に等しいかどうかで判断する方が綺麗かも | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 僕もそう思います |
||
| # def is_connectable(s1: str, s2: str) -> bool: | ||
| # count = 0 | ||
| # for i in range(len(s1)): | ||
| # if s1[i] != s2[i]: | ||
| # count += 1 | ||
| # return count == 1 | ||
|
|
||
| # 1文字違う←→1文字だけ入れ替えたものがhitするかで判断, ただまあ早くはなったんですが文字の読みにくくなった気がする。calc_word_diffみたいに関数名にした方が好きかも | ||
| # あとはこのコードだと英語小文字以外で対応ができない, 拡張がだるい, 文字コード全部対象だとlower_characters = 'abcdefghijklmnopqrstuvwxyz'のところが長くてつらいなあという感じでしょうか | ||
| # このコードがボトルネックで, サービスに影響を与えるなら使うか程度? | ||
| class Solution(object): | ||
| def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: | ||
| wordList = set(wordList) | ||
| queue = deque([[beginWord, 1]]) | ||
| lower_characters = 'abcdefghijklmnopqrstuvwxyz' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. string.ascii_lowercaseを使うのもありかと思います。字数はそれほど変わりませんが、タイプミスや入れ忘れは減ると思います。 |
||
| while queue: | ||
| word, length = queue.popleft() | ||
| if word == endWord: | ||
| return length | ||
| for i in range(len(word)): | ||
| for c in lower_characters: | ||
| next_word = word[:i] + c + word[i + 1:] # chr(ord('a') + [0:26]の数)でも良い | ||
| if next_word in wordList: | ||
| wordList.remove(next_word) | ||
| queue.append([next_word, length + 1]) | ||
| return 0 | ||
|
|
||
| # key_to_words, word_to_keysを作ってwhile文内部の処理を早くする放法 | ||
| # key = (beginWord[:i], beginWord[i + 1:])という処理, 早くはなるんだけどやはりコメントを付与してあげないと一見してわからないコードかも? | ||
| class Solution(object): | ||
| def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: | ||
| key_to_words = defaultdict(list) | ||
| word_to_keys = defaultdict(list) | ||
|
Comment on lines
+73
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 両方とも用意するのわかりやすくていいですね |
||
| for i in range(len(beginWord)): | ||
| key = (beginWord[:i], beginWord[i + 1:]) # 1文字違うことを示す ex. hot -> *ot, h*t, ho*のようにおこなう | ||
| key_to_words[key].append(beginWord) | ||
| word_to_keys[beginWord].append(key) | ||
|
|
||
| for word in wordList: | ||
| for i in range(len(word)): | ||
| key = (word[:i], word[i + 1:]) | ||
| key_to_words[key].append(word) | ||
| word_to_keys[word].append(key) | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 私は初期化の部分で関数を分けるかもしれません。 |
||
| if endWord not in word_to_keys: | ||
| return 0 | ||
|
|
||
| seen = set([beginWord]) | ||
| queue = deque([(beginWord, 1)]) | ||
| while queue: | ||
| tail_word, sequence_length = queue.popleft() | ||
| for key in word_to_keys[tail_word]: | ||
| for word in key_to_words[key]: | ||
| if word not in seen: | ||
| if word == endWord: | ||
| return sequence_length + 1 | ||
|
Comment on lines
+92
to
+97
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ここのネストがかなり深くなっていくので、tail_wordを引数として、次のwordを生成する関数があってもいいのではと感じました。以下みたいな感じでしょうか? def generate_next_word(word: str, seen: set[str]) -> Iterator[str]:
for key in word_to_keys[word]:
for next_word in key_to_words[key]:
if next_word in seen:
continue
yield next_wordThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 同意見です。(ちょっと驚きました。)
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wordがendWordの時すぐreturnすればネストを一つ消せることに気づきましたが, それでもまだ長い気がするのでyieldを使うことにします。 while queue:
tail_word, sequence_length = queue.popleft()
for key in word_to_keys[tail_word]:
for word in key_to_words[key]:
if word == endWord:
return sequence_length + 1
if word not in seen:
queue.append((word, sequence_length + 1))
seen.add(word) |
||
| queue.append((word, sequence_length + 1)) | ||
| seen.add(word) | ||
| return 0 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| class Solution: | ||
| def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: | ||
| if endWord not in wordList: | ||
| return 0 | ||
|
|
||
| def is_connectable(s1: str, s2: str) -> bool: | ||
| count = 0 | ||
| for i in range(len(s1)): | ||
| if s1[i] != s2[i]: | ||
| count += 1 | ||
| return count == 1 | ||
|
Comment on lines
+6
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. countが1を超えた段階でFalseを返すと、少しだけ早くできそうです。 |
||
|
|
||
| seen = set([beginWord]) | ||
| queue = deque([(beginWord, 1)]) | ||
| while queue: | ||
| tail_word, sequence_length = queue.popleft() | ||
| for word in wordList: | ||
| if word not in seen and is_connectable(word, tail_word): | ||
| if word == endWord: | ||
| return sequence_length + 1 | ||
| queue.append((word, sequence_length + 1)) | ||
| seen.add(word) | ||
| return 0 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
時間計算量を求めることはできますか?また、そこからおおよその実行時間を推定できますか?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
この解答だとqueue.append((word, next_available_words, sequence_length + 1))で使える単語も保持していて遠回りも含めた全部のルートを考えてしまっているので, 大雑把には最悪の場合n = wordList.lengthとした時n×(n-1)×(n-2)×...×1となってしまい, O(n!)かかります。Pythonは1秒10^6stepほどだった気がするので今回wordList.length <=5000であり, スターリング公式でn!≒n^(n+1/2) / e^nとして考えるとn=5000を代入して1831^5000 × 70 / 10^6≒(2000)^5000×70/10^6=7×10^16495 sec程度かかる気がします。(2^10≒10^3を用いて計算)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
あ、いやcalc_word_diffにword.length分かかるので今回word.length<=10なので7×10^16496 secでしょうか
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ここの部分に違和感を感じました。単語 S1 → S2 → S3 → ... → Sn と遷移して endWord にたどり着く場合、 endWord に近づく遷移と遠ざかる遷移があります。遷移は n 回行われますので、多めに見積もって 2^n に比例した個数の状態が保持されます。また wordList のコピーを毎回作っていますので、 2^n * n になると思います。 n = 5000 を代入すると、 log_{10}2 = 0.3 として、 2^5000 * 5000 ≒ 10^1500 * 5000 = 5.0 * 10^1503 くらいになると思います。
単語 S1 → S2 → S3 → ... → Sn と遷移して endWord にたどり着くような入力を意図して作れるかは未検討です。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
こういう感じでしょうか

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
はい、そのようにイメージしました。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ちょっと実験してみました。
begin: hot, wordList: [lot, got, cot, cog], end: cogで少し樹形図書いてみたら9ルート出てきて確かに2^4のほうが4!よりは抑えられている気がしました。ありがとうございます。