Try an interactive version of this dialog: Sign up at solve.it.com, click Upload, and pass this URL.
import sys
sys.path.append('.')
from leet_utils import lc_setup, get_leetcode_problem, lc_export
lc_setup("https://leetcode.com/problems/first-bad-version/", add_cells=False)
from IPython.display import HTML
problem = get_leetcode_problem("https://leetcode.com/problems/first-bad-version/")
HTML(problem["content"])
You are a product manager and currently leading a team to develop a new product. Unfortunately, the latest version of your product fails the quality check. Since each version is developed based on the previous version, all the versions after a bad version are also bad.
Suppose you have n versions [1, 2, ..., n] and you want to find out the first bad one, which causes all the following ones to be bad.
You are given an API bool isBadVersion(version) which returns whether version is bad. Implement a function to find the first bad version. You should minimize the number of calls to the API.
Example 1:
Input: n = 5, bad = 4 Output: 4 Explanation: call isBadVersion(3) -> false call isBadVersion(5) -> true call isBadVersion(4) -> true Then 4 is the first bad version.
Example 2:
Input: n = 1, bad = 1 Output: 1
Constraints:
1 <= bad <= n <= 231 - 1
実装1
- アルゴリズムの選択
- [1, 2, ..., n] --(API)--> [False, ..., False, True, ..., True]となる。
- First Trueを見つける方法だが、単調述語なので二分探索(lower_bound, bisect_left)でいける。
- 実装
- range(1, n + 1)に対し、key=isBadversionで単調述語を作れる。
- 返り値がインデックスであり、1-indexedなversionsより1小さいことに注意。
- すべてのversionがfineなときは分かりやすく-1を返すことにする。(未来の次のバージョンを返すのは違和感がある。)
- 計算量
- Time: O(log n)
- Extra Space: O(1)
import bisect
# The isBadVersion API is already defined for you.
# def isBadVersion(version: int) -> bool:
class Solution:
def firstBadVersion(self, n: int) -> int:
first_bad_index = bisect.bisect_left(
range(1, n + 1), True, key=isBadVersion
)
first_bad_version = first_bad_index + 1
if first_bad_version == n + 1:
# all versions are fine
return -1
return first_bad_version
🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠
コードを拝見しました。いくつかレビューポイントを挙げますね。
良い点
bisect_left+keyの活用はPythonic で簡潔- 計算量の分析も正確
改善点・確認事項
-
問題の制約との整合性
- LeetCodeの問題文では通常「必ず1つ以上のbad versionが存在する」という前提があります。
-1を返すケースは実際には発生しない可能性が高いです。問題の制約を確認してみてください。
- LeetCodeの問題文では通常「必ず1つ以上のbad versionが存在する」という前提があります。
-
rangeオブジェクトのインデックスアクセスbisect_left(range(1, n+1), ...)は動作しますが、rangeへのインデックスアクセスはO(1)なので問題ありません。ただし、これを知らないと「rangeを全部展開してるのでは?」と誤解されやすいので、コメントがあると親切かも。
-
より直接的な書き方
- 0-indexedのインデックスを1-indexedのバージョンに変換するのではなく、最初から1-indexedで考える方法もあります:
Copied!def firstBadVersion(self, n: int) -> int: return bisect.bisect_left(range(1, n + 1), True, key=isBadVersion) + 1
問題の制約で必ずbad versionが存在するなら、この1行で済みます。
-
手動で二分探索を書く選択肢
- 面接やコーディング練習の観点では、
bisectに頼らず自分で二分探索を書く練習も価値があります。この問題はその練習に最適です。
- 面接やコーディング練習の観点では、
問題の制約を確認してみましょうか?
- LLMレビューの感想
- 久しぶりにbisectに頼らず書いてみてもいいかも。
- コメント集
- https://discord.com/channels/1084280443945353267/1351704260793995355/1370664386246606899
-
left, right もよく使われるのですが、本当はいい名前ではあまりないです。二分探索をする仕事をシフト制でやるとして、職場に行ってみたら「左が100で右が200」とだけ書いてある業務日誌があったら昨日働いていた人に電話しませんか。「左と右ってどういう意味なのか。」
- docstringにループ不変条件を書いておくと良いかも
-
- https://discord.com/channels/1084280443945353267/1351704260793995355/1370664386246606899
# The isBadVersion API is already defined for you.
# def isBadVersion(version: int) -> bool:
class Solution:
def firstBadVersion(self, n: int) -> int:
"""Invariant:
- for all i < left: isBadVersion(i) is False
- for all i > right: isBadVersion(i) is True
"""
left = 1
right = n
while left <= right:
mid = (left + right) // 2
if isBadVersion(mid):
right = mid - 1
else:
left = mid + 1
# right == left - 1
if left == n + 1:
return -1
return left
class Solution:
def firstBadVersion(self, n: int) -> int:
"""Invariant:
- for all i <= left: isBadVersion(i) is False
- for all i > right: isBadVersion(i) is True
"""
left = 0
right = n
while left < right:
mid = (left + right + 1) // 2
if isBadVersion(mid):
right = mid - 1
else:
left = mid
# left == right
if left + 1 == n + 1:
# all versions are fine
return -1
return left + 1
🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠
Step 2のコードを拝見しました!
良い点
- 不変条件をdocstringに明記しているのが素晴らしい(Discord のコメントを反映してますね)
- 4パターンのうち2つを意識的に選んで書いている点も学習として良い
- ループ終了時の状態(
right == left - 1など)をコメントしているのも◎
改善点・確認事項
-
2つ目の実装の
midの計算Copied!mid = (left + right + 1) // 2
この
+1が必要な理由をコメントしておくと、後で見返したときに助かります。(left = midの場合、+1がないとleft == right - 1のとき無限ループになる) -
2つ目の実装の終了条件チェック
Copied!if left + 1 == n + 1:
これは単に
if left == n:と書けますね。 -
オーバーフロー対策(面接向け)
Copied!mid = left + (right - left) // 2
Pythonでは整数オーバーフローがないので不要ですが、C++/Javaなどでは定番の話題です。面接で「他の言語だとどう?」と聞かれることがあります。
-
不変条件の言葉の統一
- 1つ目:
i < left,i > right(開区間的) - 2つ目:
i <= left,i > right(混合)
どちらも正しいですが、不変条件と初期値・更新式・終了条件の整合性を意識して書けているのが伝わります。
- 1つ目:
-
細かい点
- 問題の制約上、bad versionは必ず存在するので
-1を返すケースは不要かもしれません(前回と同様)
- 問題の制約上、bad versionは必ず存在するので
全体的に、不変条件を意識した丁寧な実装になっていると思います!何か気になる点はありますか?
🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠
{
"id": "toolu_01UibjijfxcBwKcaP2vapeMJ",
"call": {
"function": "read_url",
"arguments": {
"url": "https://github.com/huyfififi/coding-challenges/pull/14"
}
},
"result": "'Skip to content\\n\\n## Navigation Menu\\n\\nToggle navigation\\n\\n[ ](/)\\n\\n[ Sign in ](/login?return_t<TRUNCATED>"
}
{
"id": "toolu_016DFeEPnESCYQNNk8j4CVUk",
"call": {
"function": "read_url",
"arguments": {
"url": "https://github.com/rihib/leetcode/pull/33"
}
},
"result": "'Skip to content\\n\\n## Navigation Menu\\n\\nToggle navigation\\n\\n[ ](/)\\n\\n[ Sign in ](/login?return_t<TRUNCATED>"
}
{
"id": "toolu_01QAKAk7HuG9LXLx6bSbYjxc",
"call": {
"function": "read_url",
"arguments": {
"url": "https://github.com/NobukiFukui/Grind75-ProgrammingTraining/pull/25"
}
},
"result": "'Skip to content\\n\\n## Navigation Menu\\n\\nToggle navigation\\n\\n[ ](/)\\n\\n[ Sign in ](/login?return_t<TRUNCATED>"
}
各PRの内容をまとめます。
PR 1: huyfififi/coding-challenges#14 (Python)
レビューコメントのポイント:
- 変数名
left_iについて (colorbox):iがindexの略なら省略せずindexと書くか、単にleftにした方が良い left/rightの意味 (oda): 「左が100で右が200」だけでは意味が分からない。本当は「99以前はgood確認済み、100以降は未確認」という情報を表現したい。変数名を長くするか、ループ不変条件をコメントで明記すべき- オーバーフロー対策:
mid = left + (right - left) / 2の書き方を覚えておくと良い(Pythonでは不要だが他言語で有用) - エッジケース (oda):
n=1でwhileに入らないケースへの注意
PR 2: rihib/leetcode#33 (Go)
レビューコメントのポイント:
- 「半閉区画」→「半開区間」 (nodchip): 用語の訂正
- ループ不変条件の説明が不十分 (nodchip): 「rightは常にtrueの範囲に収まる」「leftはtrueに移動する」などの説明に対し、「なぜそうなるのか」「right=midにしてよい理由は?」など根本的な理由を問う質問が多数
- isBadVersion()の呼び出し回数 (oda): ループ1回で最大3回呼ばれる実装があり、APIが重い処理の場合に問題になる可能性
特徴: nodchipさんから二分探索の理解を深めるための鋭い質問が多く、ループ不変条件や境界の意味を正確に説明できるかが問われている
PR 3: NobukiFukui/Grind75-ProgrammingTraining#25 (Python)
レビューコメントのポイント:
- 整数除算 (liquo-rice):
int(left + (right - left) / 2)より//を使う方が分かりやすい - colorbox: 「良さそうに見えます」
特徴: シンプルなPRで、軽微なPythonの書き方の指摘のみ
共通して学べるポイント:
🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠
では、各トピックを深掘りしていきますね。
1. 変数名とループ不変条件の明示
二分探索の left/right は実は曖昧な名前です。odaさんの指摘が秀逸で:
「左が100で右が200」とだけ書いてある業務日誌があったら昨日働いていた人に電話しませんか
left/right が表しているのは実際には以下のような情報です:
# 悪い例:何を意味するか分からない
left = 1
right = n
# 良い例:不変条件を明示
# Invariant:
# - all versions in [1, left) are good
# - all versions in (right, n] are bad
left = 1
right = n
不変条件を書くことで:
- 初期値の妥当性 が確認できる(最初は何も確認していないので、空区間であるべき)
- 更新式の正しさ が検証できる(不変条件が維持されるか?)
- 終了条件の意味 が明確になる(何が分かった状態で終わるのか?)
また、区間の表現方法(開/閉)を統一することも重要です:
[left, right](閉区間):left <= rightで継続、left = mid + 1orright = mid - 1[left, right)(半開区間):left < rightで継続、更新式も変わる
2. APIコールの回数を意識
rihib さんのPRで指摘された点です。以下のような実装があったとします:
while left < right:
mid = (left + right) // 2
if isBadVersion(mid) and not isBadVersion(mid - 1): # 2回呼ぶ
return mid
elif isBadVersion(mid): # さらに1回
right = mid
else:
left = mid + 1
問題点:
- ループ1回で最大3回の
isBadVersion()呼び出し - このAPIがネットワーク越しや重い処理だと性能に影響
改善版:
while left < right:
mid = (left + right) // 2
if isBadVersion(mid): # 1回だけ
right = mid
else:
left = mid + 1
return left
一般的な教訓: 外部API・重い関数の呼び出しは最小限に。結果をキャッシュするか、ロジックを工夫する。
3. Pythonでは // を使う
# 避けるべき
mid = int(left + (right - left) / 2)
# 推奨
mid = left + (right - left) // 2
理由:
/は浮動小数点除算、//は整数除算int()で変換すると、意図が不明瞭になる- 浮動小数点の精度問題を避けられる(非常に大きな数の場合)
補足: オーバーフロー対策について
# 素朴な書き方(Pythonでは問題なし)
mid = (left + right) // 2
# オーバーフロー対策版
mid = left + (right - left) // 2
Pythonは任意精度整数なのでオーバーフローしませんが、C++/Java/Goなどでは left + right がオーバーフローする可能性があります。面接で「他の言語ではどうする?」と聞かれることがあるので、知っておくと良いです。
何か特定のトピックについてさらに質問はありますか?
# The isBadVersion API is already defined for you.
# def isBadVersion(version: int) -> bool:
class Solution:
def firstBadVersion(self, n: int) -> int:
"""Invariant:
- all versions in [1, left) are fine
- all versions in [right, n] are bad
"""
left = 1
right = n + 1
while left < right:
mid = left + (right - left) // 2
if isBadVersion(mid):
right = mid
else:
left = mid + 1
# left == right
if right == n + 1:
return -1
return right
- 振り返り
- first Trueを見つけるとき、右側に対する不変条件がi > rightよりi >= rightとなっていた方が、返り値が考えやすいと感じた。
- #実装3は、i < leftとi >= rightに対し不変条件を考えているため返り値がrightと分かりやすい。
- 停止条件もleft == rightと重なるため、分かりやすいと感じる。