Conversation
a8810bc to
d9dccdc
Compare
| # この書き方は正式な書き方ではなく、自分が考えるときのイメージです。 | ||
| # [ は区間の左端で、 true の左側にあります。 ) は区間の右端で、 true の右側にあります。 | ||
| # ただし、右側は開区間のため、インデックスの値は true の位置と同じで、 true を区間には含めない。 | ||
| # よってループの不変条件は left < right とする。 |
There was a problem hiding this comment.
ループ不変条件(ループ不変式)の意図するところと理解が異なるように思います。
というのも、ループ不変条件はループ前・ループ内の各反復における処理の前後・ループ後、のどのタイミングでも真となるような条件のことを指します。
また、ループ不変式はアルゴリズムの正当性を容易に理解・証明するのに用いられます。詳しくはアルゴリズムイントロダクションやWikipediaなどをご覧になってみてはいかがでしょうか。
There was a problem hiding this comment.
個人的にはwikipediaの説明はあっさりしててややありがたみが分かりにくいので、アルゴリズムイントロダクションをお勧めしたいです。お送りしたリンクは4版ですが、自分が読んだ3版なら最初の20ページほどを読めばループ不変条件(ループ不変式)の説明にあたれるはずで、wikiよりわかりやすかったように思います
There was a problem hiding this comment.
リンクの紹介までありがとうございます。
ループ不変式はループの後にも成り立つ条件なので、この表現は正式ではないなとは自分も感じました。
コードを書くときにループ不変式を意識できるようになりたいです。
英語の第三版に目を通してみました。Loop invarianは最初の20ページほどで現れますね。勉強になりました。
https://www.cs.mcgill.ca/~akroit/math/compsci/Cormen%20Introduction%20to%20Algorithms.pdf
There was a problem hiding this comment.
こちら自分がループ不変条件の意味を誤って理解していたようでした。失礼いたしました。
| ``` | ||
|
|
||
|
|
||
| 保証している条件 |
There was a problem hiding this comment.
それはそれとして、こちらの条件の記述は(言いたいことはわかるんですが)ちょっと細かいところが違いますね
5ky7/arai60#41 (comment)
ループ不変式が初期化時、ループブロック内の更新前後、およびループ終了時の全てで成り立っていることがループ不変条件を用いたアルゴリズムの正当性の証明には肝要ですので、丁寧に記述してあげた方が良いかと思います
There was a problem hiding this comment.
コメントを受けて追記しました。
先ほど紹介していただいたアルゴリズムイントロダクションでもループ不変条件が成立していることの説明が丁寧に行われていましたね。
| left = 0 | ||
| right = len(nums) - 1 | ||
| while right - left > 1: | ||
| mid = left + (right - left) // 2 |
There was a problem hiding this comment.
オーバーフロー回避のための記述ですが、pythonのintは多倍長整数ですので、直感的な記述
mid = (left + right) // 2
で良いかと思います。
(numpyとかは裏でCが走っていて64bit integerを扱うのでこのオーバーフロー回避が必要になりうるんですが。)
There was a problem hiding this comment.
ここは個人的な好みですがこのままにしておきます。
numpyではオーバーフローが発生すること、知らなかったです。関連しそうな記事があったので読んでみました。
| right = len(nums) - 1 | ||
| while right - left > 1: | ||
| mid = left + (right - left) // 2 | ||
| if nums[mid] == target: |
There was a problem hiding this comment.
numsにtargetが複数含まれていた場合に、bisect_leftとして返したい「target以上の要素の最小のインデックス」が返らないですね。
There was a problem hiding this comment.
その通りでこのコードには問題が色々とありますね(sol3.pyで改善しています。)
There was a problem hiding this comment.
何を問題視してどう変えたかとかはメモに書いておくと,時間を経てご自身で読み返したとき・他の人が参照するとき・コメントをもらうとき,などで色々学びがあると思います.
ご自身の言葉とある程度の粒度で思考を整理すれば記憶の定着にもよいでしょうし.
ま〜忙しいときは割ける時間との兼ね合いなんですが.
There was a problem hiding this comment.
- 問題点1: right = len(nums) - 1と初期化しているため探索範囲が(煩雑なロジックを経ないと)len(nums)をカバーできていない
- 問題点2: while right - left > 1がループ条件のためleft==rightではない場合があり得る。どちらを返すか固定されていない。
- 問題点3: nums[mid] == target で即座にreturnしてしまっているため、target が重複している場合にもっとも左を返す保証がない
| @@ -0,0 +1,9 @@ | |||
| class Solution: | |||
| def searchInsert(self, nums: List[int], target: int) -> int: | |||
There was a problem hiding this comment.
こちらのsol4.py の計算量って見積もりましたか?
sol1 から3と比較するとどうでしょうか
There was a problem hiding this comment.
二分探索でO(log N)だった計算量がO(N)になってしまっていますね。
配列のスライス操作で合計 N/2 + N/4 + ... 1=O(N)の時間計算量となってしまいます。
同様に空間計算量もリストのスライス操作でデータのコピーが走るのでO(N)となります。
itertools.isliceを用いる場合も考えてみると空間計算量はO(log N)に改善しますが、時間計算量は変わりませんね。
| right = len(nums) | ||
| mid = right // 2 | ||
| if nums[mid] < target: | ||
| return mid + 1 + self.searchInsert(nums[mid + 1 : right], target) |
There was a problem hiding this comment.
スライスの終了インデックス [~:right] はなくても良いですね。
ない方がpythonicでしょう
| left = mid | ||
| else: | ||
| right = mid | ||
| if target <= nums[left]: |
There was a problem hiding this comment.
sol1.pyやsol3.pyと比べてreturn までに処理が増えたのは、bisect_leftとして返しうる値の範囲(0以上len(nums)以下)をleft, right の初期化時に覆わなかったのが原因ですかね。
There was a problem hiding this comment.
なるほど、targetがすべての要素より大きい場合を考慮できていないということですね。答えは 0~NのN+1通りですから。
(二分探索をしばらく書いておらず、記憶を頼りに空で書いたらこのようなコードになったという次第です。改めて勉強しなおそうと思います。)
| # この書き方は正式な書き方ではなく、自分が考えるときのイメージです。 | ||
| # [ は区間の左端で、 true の左側にあります。 ) は区間の右端で、 true の右側にあります。 | ||
| # ただし、右側は開区間のため、インデックスの値は true の位置と同じで、 true を区間には含めない。 | ||
| # よってループの不変条件は left < right とする。 |
There was a problem hiding this comment.
こちら自分がループ不変条件の意味を誤って理解していたようでした。失礼いたしました。
| while left < right: | ||
| middle = (left + right) // 2 | ||
| # target より小さい値は false、 target 以上の値は true とする。 | ||
| if (numbers[middle] < target): |
There was a problem hiding this comment.
() を付けないほうが多いと思います。
if numbers[middle] < target:| # 5. ループの終了状態で false, false, ..., false, [true), true, ..., true となって欲しい。 | ||
| # この書き方は正式な書き方ではなく、自分が考えるときのイメージです。 | ||
| # [ は区間の左端で、 true の左側にあります。 ) は区間の右端で、 true の右側にあります。 | ||
| # ただし、右側は開区間のため、インデックスの値は true の位置と同じで、 true を区間には含めない。 |
There was a problem hiding this comment.
自分の書いた表記と説明です。読み直してみたのですが、自分でもあまり良く分からなかったです。
大幅に書き直してみたのですが、これだと分かりますでしょうか。
# 1. false, false, false, ..., false, true, true, true, ..., true と並んだ配列があったとき、
# 一番左側の true の位置を求める問題だと捉える。
# ここでは、 target より小さい値は false、 target 以上の値は true とする。
# 2. 位置を求めるにあたり、答えが含まれる範囲を狭めていく問題と捉える。
# 3. 範囲を考えるにあたり、区間を閉区間で捉える。
# (区間の左端の要素と右端の要素は区間に含まれると捉える。)
# 4. 初期値は 0, len(numbers) - 1 とする。
def lower_bound(numbers, left, right, target):
# 5. ループの終了時に false, false, ..., false, [true], true, ..., true となって欲しい。
# (この書き方は正式な書き方ではなく、自分が考えるときのイメージです。)
# [ は区間の左端で、 true のすぐ左側にあるイメージ。
# ] は区間の右端で、 true のすぐ右側にあるイメージ。
# left == right となったときに、区間の中に含まれる要素が 1 つだけになり、
# 一番左側の true の位置が求まる。
# よって while 文の条件式は left < right とする。
while left < right:
middle = (left + right) // 2
# target より小さい値は false、 target 以上の値は true とする。
if numbers[middle] < target:
# 6. 右に狭める場合。
# numbers[middle] < target のため、
# middle の位置の要素を、狭めたあとの区間に含めたくない。
# middle の位置の要素を区間に含めないようにするには、
# left の位置を middle + 1 にすればよい。
left = middle + 1
else:
# 左に狭める場合。
# numbers[middle] == target かもしれないため、
# middle の位置の要素を、狭めたあとの区間に含めたい。
# middle の位置の要素を区間に含めるには、
# right の位置を middle にすればよい。
right = middle
return lefthttps://discord.com/channels/1084280443945353267/1196498607977799853/1269560324818731028
There was a problem hiding this comment.
初期値は 0, len(numbers) - 1 とする。
だと、配列の最大値より大きいtargetの時、std::lower_boundと振る舞いが異なりませんか?
長さnの配列numsの挿入位置は[0, 1, .., n - 1, n]のn + 1個(挿入位置iは、要素iの左側を指す)あり、これをnums[i] >= target (nums[n] = INFとする)で投影すると、[false, false, ..., true, true]というbool型のソートされた仮想配列になるので、この上で二分探索するイメージで考えていました。この仮想配列上で一番左のtrueを探す問題で、必ずtrueは少なくとも一つ存在することは分かっているので、閉区間を最後の一個になるまで狭めると良いですよね。
[ は区間の左端で、 true のすぐ左側にあるイメージ。
個人的には、leftが区間の一番左側の要素、rightが区間の一番右側の要素を指すインデックスと考える方が、インデックスとの対応が取れて、直感的に思いました。
There was a problem hiding this comment.
ありがとうございます。おっしゃる通りだと思います。以下のように書き換えました。
# 1. false, false, false, ..., false, true, true, true, ..., true と並んだ配列があったとき、
# 一番左側の true の位置を求める問題だと捉える。
# ここでは、 target より小さい値は false、 target 以上の値は true とする。
# 2. 位置を求めるにあたり、答えが含まれる範囲を狭めていく問題と捉える。
# 3. 範囲を考えるにあたり、 left を区間の左端の要素の位置、 right を区間の右端の要素の位置とする。
# (閉区間)
# 4. 初期値は left = 0, right = len(numbers) とする。
# ただし、 numbers 右端の要素の右側に、番兵 INF が置かれているものとみなす。
def lower_bound(numbers, left, right, target):
# 5. ループの終了時に区間の中に含まれる要素が 1 つだけになるようにし、それを答えとしたい。
# 区間の中に含まれる要素が 1 つだけになるのは left == right のときである。
# よって while 文の条件式は left < right とする。
while left < right:
middle = (left + right) // 2
# target より小さい値は false、 target 以上の値は true とする。
if numbers[middle] < target:
# 6. 右に狭める場合。
# numbers[middle] < target のため、
# middle の位置の要素を、狭めたあとの区間に含めたくない。
# middle の位置の要素を区間に含めないようにするには、
# left の位置を middle + 1 にすればよい。
left = middle + 1
else:
# 左に狭める場合。
# numbers[middle] == target かもしれないため、
# middle の位置の要素を、狭めたあとの区間に含めたい。
# middle の位置の要素を区間に含めるには、
# right の位置を middle にすればよい。
right = middle
return left
def upper_bound(numbers, left, right, target):
while left < right:
middle = (left + right) // 2
if (numbers[middle] <= target):
left = middle + 1
else:
right = middle
return left
if __name__ == '__main__':
print(lower_bound([0, 1, 2, 3, 4, 5], 0, 6, 3))
print(upper_bound([0, 1, 2, 3, 4, 5], 0, 6, 3))There was a problem hiding this comment.
自明かもしれませんが、left = 0, right = nの示す範囲を、閉区間だと考えると、最終的に知りたい答えのある領域を指していて、半閉区間だと考えると、未探索の領域ですね。世間一般で、二分探索の初期値・whileの条件式・更新式を議論するとき、後者の方を指しているのだと、arahi10さんのコメントを見てようやく気づきました。
ソート状態を維持したまま挿入できる位置の左端を考える問題の場合だと、探索済みの領域について以下のことが言えますね。こちらが、loop invariantとなり、未探索の領域を空にすることで、すべての範囲についての情報を得ることができます。
閉区間: nums[i] < target (i < left). nums[i] >= target (i > right)
半閉区間: nums[i] < target (i < left). nums[i] >= target (i >= right)
開区間: nums[i] < target (i <= left). nums[i] >= target (i >= right)
(iが範囲外の時は、条件が常に成り立つと仮定)
There was a problem hiding this comment.
なるほど。「未探索領域をどう定義するか」という視点で整理すると、初期値や更新式が分かりやすくなりますね。
https://leetcode.com/problems/search-insert-position/