-
Notifications
You must be signed in to change notification settings - Fork 0
300. Longest Increasing Subsequence #33
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: main
Are you sure you want to change the base?
Conversation
| ``` | ||
| 思考ログ: | ||
| - この問題に対してoverkillなのは分かっているが、皆さんが結構実装されているのでこの機会にお勉強してみた | ||
| - シンプルなセグ木の理解自体はそんなにかからなかったが、寧ろこの問題での使い方の部分で躓いた(それはセグ木を理解できていな(ry) |
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.
うーん、この overkill という表現にとても違和感があります。
「必要よりも高度な道具を使って解決する」くらいの意図だと思うのですが、ここでいう高度とはどういうことでしょうか。
実装の複雑さだったら、C++ の std::map 典型的には赤黒木のほうがはるかに複雑ですよ。Priority Queue さえも複雑じゃないでしょうか。
これは、アルゴリズムやデータ構造が、なんらかの高度さの順に並んでいるという感覚がなければ出てこない表現です。
セグメントツリーは、確かに「使えることに気が付きにくいが、計算量を落とせる場合が極めて稀にあり、短時間で書けるくらい単純である」という意味で、プログラミングコンテストに向いているため、競技プログラミング同好会時代にも出てきていました。
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.
ありがとうございます。
overkillという表現は、ロジックを理解しようとして色々調べていた際に見かけたものを深く考えず引用したものなのですが、少し思考を整理してみました。
今回私が「overkill」と感じたのは、セグ木のような方法を用いずとも、もっと素直で万人が思いつくであろう方法が他にあるだろう、さらにそれを使うことで劇的に計算量が改善されるならまだしも、そうでないならoverkill(やり過ぎ)だろう、という感覚だったのだと思います。
ご指摘のアルゴリズムやデータ構造に順序が入っているのでは、という点に関してはなんとも言えないところがあり、こんなの誰かから聞かないと一生思い付かないな、みたいなアイデアに関しては高度なもの、という感覚は少なからずあります。ただそのようなものを知っていることが偉い、という感覚はない(はず)と思っています。
| compressed_num = [num_to_compressed_num[num] for num in nums] | ||
| return compressed_num | ||
|
|
||
| compressed_num = compress(nums) |
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.
compressed_numsとかの方が良いかなと思います
| n = len(set(compressed_num)) | ||
| st = SegTree(n) | ||
| for i, num in enumerate(compressed_num): | ||
| res = st.query(0, num) |
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.
resより良い命名があると思います。ものとしてはnumより小さい値で終わるLISのうち最長のものですね。
|
|
||
| 講師役目線でのセルフツッコミポイント: | ||
| - 命名関連の選択肢は再度考えた方がいい | ||
| - 名前を工夫する |
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.
step3で書いたつもりでした。
| # get parent_index | ||
| i = (i - 1) // 2 | ||
| while i >= 0: | ||
| self.tree[i] = max(self.tree[2 * i + 1], self.tree[2 * i + 2]) | ||
| i = (i - 1) // 2 |
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.
while (i := (i - 1) // 2) >= 0:
self.tree[i] = max(self.tree[2 * i + 1], self.tree[2 * i + 2])一応、こうも書けそうですね。見づらくなるギリギリのラインな気もしますが
| l = self._convert_to_tree_index(l) | ||
| r = self._convert_to_tree_index(r) | ||
|
|
||
| result = IDENTITY_ELEMENT |
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.
IDENTITY_ELEMENT, 定数 (本当は変数ですが) にする必要ありますかね?自分はそのまま result = 0 と書いた方が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.
演算によって単位元が変わるので、こんな形式にしたのですが、このSegTreeクラスは汎用的に設計ができてる訳でもないので、なんだか中途半端なことになっていると思います。
| while l < r: | ||
| # if l is left child | ||
| if l % 2 == 0: | ||
| result = max(result, self.tree[l]) | ||
| # if r-1 is right child | ||
| if r % 2 == 0: | ||
| result = max(result, self.tree[r - 1]) | ||
| l = (l - 1) // 2 | ||
| r = (r - 1) // 2 |
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.
自分がセグメント木をあまり理解していないからでしょうが、何をやっているのか分かりませんでした...
(if l is left child とありますが、lが偶数ならlは右の子のような?)
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.
配列の0番目の要素がrootになるようにしているので、奇数が左の子になると思います。
嘘つきました。コメントがおかしいかも。。
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.
lが偶数ならlは右の子のような?
右ですね。。
やっていることは、、説明がややこしいですね。。何か具体例を考えるのがいいかもです。
配列の長さが7のケース(葉が4つ)で考えます。
0[1, 2, 3, 4]
1[1, 2] 2[3, 4]
3[1] 4[2], 5[3] 6[4]
のように配列の要素[圧縮した要素の集合]が対応しています。
numsから4(圧縮後)が出てきたとします。
今まで出てきた1 or 2 or 3で終わる最長の部分列の長さが知りたいです(それ+1を6[4]にセットすれば良いので)
この時、3[1] 4[2], 5[3]の各々について確認するのではなく、1[1, 2] , 5[3]だけ確認すれば効率がいいということで、そのような候補の探索を始めるのですが、例えば左端の3[1]をこの候補に入れるかどうかを判定するのに、これが右の子かどうかを確認しています。左の子の場合は親を確認すれば良いのでスキップします。右の子の場合は親に左の子の情報が含まれてしまっているのでこれを採用する必要があります。
区間の右端から考える場合も考え方は同じです(ただ候補に含めるかどうかの判定の左右が逆になります)
親が同じになったら探索を終了します。
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.
圧縮した要素の集合は1-indexedになってますが0-indexedですかね?(じゃないとコードにある木のindexへの変換が上手くいかない気がします)
これって一般にはqueryの引数としてlが0以外も来ることを想定していると思いますが、lが0より大きい場合って変なことになったりしませんか?上の具体例だとたとえば st.query(1,3) (0-indexedで書いています) みたいなやつです。
たとえば下の図に書いたような [1,3)の区間に対するクエリ (st.query(1,3)) を行うと、rが偶数のときr-1の場所を見ながら親を辿るので[0,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.
このコード、n = 4 で r = 4 が来るとどこまでループが回りますかね。l = 2 とでもしましょうか。
はじめに、(5, 7) になります。その後、(2, 3), (0, 1), (-1, 0) で (-1, -1) で停止であってますか。
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.
あ、これ、まずなんで訳が分からなくなるかというと、l, r という整数に2つの情報、つまり、「どこ」から「どこ」までのセグメントであるか、という情報を載せていて、(segment - 1) // 2 という整数の上での操作と、セグメントの上での意味が遠く見えるからです。
さて、segment(l, width=1) で、セグメントの範囲の値が手に入るとしましょう。
ただ、width は l が2で割り切る回数まで倍に広げた場合は効率的に計算できます。
効率を考えないと
result = 0
for i in range(l, r):
result = max(result, segment(i, 1))が求めたいものですね。
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.
ところで、これだと効率が悪いので速くしたいのですよね。
大まかには次のような構造です。
result = 0
width = 1
while l < r:
# segment(f(l), width) と segment(g(r), width) で result 更新
# l, r を微修正
width *= 2次、l と r の不変条件は、なんでしょうか。[l, r) の範囲は未計算ということですね。
つまり、このプログラムは width *= 2 をしなくても大まかには動くものです。
result = 0
while l < r:
result = max(result, segment(l, 1))
l += 1
result = max(result, segment(r - 1, 1))
r -= 1
# 最後一回は2回行われることがあるので本当は判定必要だけれども今回は max だからまあいいでしょう。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.
さて、l が width * 2 で割れるときには、segment の計算を width *= 2 してからしたいわけです。しかし、割れない場合には、その場で計算するしかありませんね。r も同じで r が奇数ならば width を広げたら計算ができなくなるので、計算してから次にいくしかありませんね。
これくらいの情報が陽にあれば、読めるコードになると思います。
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.
@oda
補足説明もありがとうございます!
少し考えてみたいと思います。
以前にも”意図と操作の距離”についてコメントを頂いていました。
面倒くさがって、結果として遠回りになってしまっているのですね。
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.
| if not minimum_last_val_subseq \ | ||
| or minimum_last_val_subseq[-1] < num: | ||
| minimum_last_val_subseq.append(num) | ||
|
|
||
| i = bisect.bisect_left(minimum_last_val_subseq, num) | ||
| minimum_last_val_subseq[i] = num |
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.
continue忘れですかね?numをappendしてからnumを二分探索してその位置にnumを挿入していて無駄な気がします。
個人的には常に二分探索する方が好みです。
i = bisect_left(minimum_last_val_subseq, num)
if i >= len(minimum_last_val_subseq):
minimum_last_val_subseq.append(num)
else:
minimum_last_val_subseq[i] = numThere 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.
ありがとうございます。
スキップし忘れました。
セグ木のバグを修正
| # 長さがindex+1になるような任意の増加部分列を考えた時 | ||
| # それらの最後の要素のうち最小のものを記録するための配列 | ||
| # ex) [4,6], [3,5], [1,2]ならmin(6, 5, 2) = 2が入る | ||
| minimum_last_val_subseq = [] |
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.
個人的には length_to_min_last_vals という名前にすると思います。配列の添え字に何を指定すると、どのような値が返ってくるかを表そうとしています。ただ、 length と添え字が 1 ずれている点に注意が必要です。
| > min_ends_of_increasing_subsequencesが適切です。この名前は要素が増加部分列の最終要素の最小値であることを明確に示し、長さも扱いやすいです。 | ||
| > もし簡潔さを重視する場合は、increasing_subseq_min_endsも良い選択です。 | ||
| - なお、質問は以下 | ||
| > 今pythonでコードを書いているのですが、変数名の命名で困っています。 |
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.
プロンプトエンジニアリングですね。
ここら辺の事情もなかなか追えてませんが、面白そうな領域ですよね。
| res = st.query(0, num) | ||
| st.update(num, res + 1) | ||
|
|
||
| return st.get_maximum_val_of_all_segments() |
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.
st.query(0, n) で十分な気がしています。
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.
st.query(0, n)でもう一度計算するのも無駄かなと思い、この処理を加えました。
| assert 0 <= seg_i < self.leaf_n, f'seg_i is out of index. seg_i: {seg_i}' | ||
|
|
||
| tree_i = seg_i + self.leaf_n |
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.
下の query にもあるし、別のメソッドに切り出した方が見通しが良い気がしました。
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.
1行で済むような処理なので関数に切り出さない選択をしましたが、このインデックス変換の計算は、何をしているかぱっと見では分かりにくいので、関数として切り出すのも良いなと思いました。
| n = len(set(compressed_num)) | ||
| st = SegTree(n) | ||
| for i, num in enumerate(compressed_num): | ||
| res = st.query(0, num) |
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.
resとは何でしょうか?
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.
resultの略で横着しました。。
| tree_l = start_seg_i + self.leaf_n | ||
| tree_r = end_seg_i + self.leaf_n |
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.
tree_lやtree_rという変数名を見てindexだとは思えない気がします。
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.
いい名前が思いつきませんでした。。
seg_iに合わせて、start_tree_iとかにするか、tree_i, tree_jとかにしてしまうとかですかね。。
https://leetcode.com/problems/longest-increasing-subsequence/description/