今日は非常に古典アルゴリズムの問題、フロアの数についての話に、卵の数は、あなただけの壊れた床を投げていない卵を見つけるために試みの最小数を把握しましょう。国内メーカーとGoogleフェイスブックのインタビューでは、多くの場合、この質問を検討しているが、彼らはあまりにも無駄な卵を投げるには、壊れたボウルや何かを投げて、カップに投げると思います。
特定の問題は、最終的には、非常に効率的な数学的なソリューションを再びそれを言うが、この質問の解決スキルの多くを、光の動的プログラミング効率、いくつかのさまざまなアイデアのようにしてます。いつものスタイル番号に付着し、これらのスキルが全体を判断できないので、学校にも価値がある、賢いが、役に立たない、あまりにも奇妙な拒否のスキルを拒否しました。
ここでは強調されている汎用の動的プログラミングのアイデアで問題を見てです。
まず、分析タイトル
あなたが1を持って前に:質問はこれですN
、総N
床層、そしてあなたを与えるK
(卵をK
少なくとも1)。今、この建物の床の存在を決定する0 <= F <= N
、このフロアは、卵を投げされ、卵はちょうど壊れていなかった(上のF
階壊れます、より低いF
床が壊れされません)。今、あなたは、聞いて最悪のケースを、あなたは少なくともするために、いくつかの卵を投げるしたいかを決定床にF
それを?
それはあなたがレスリングは最上階に卵を破られていないご覧いただくこと、であるF
が、それをスローするように「少なくとも」数回の下の「最悪のケース」は何ですか?私たちは、理解するための例です。
さんが言ってみましょう、それは卵の数を制限するかどうか、今のあなたがその階に行くと、正確に卵を壊したか、7階建てで、?
最も原始的な方法は、リニアスキャンである:壊れていない、私はそれをスローするように二階に行き、私は一階にそれを投げてみましょう、壊れていない、私は三階に行ってきました......
この戦略では、最悪の私は、7個の卵が壊れていない層に試みるべきであるとの状況(F = 7
私は卵を投げ7倍です)、。
まず、あなたは、下の「最悪のケース」と呼ばれているものを理解する必要があります壊れた卵は間隔徹底的な検索で行わなければならないとき、あなたは壊れた卵の上の第一層でそれを投げる言わない、これはあなたの幸運ではなく、最悪のケースであります。
私たちは今、数回を投げることを意図しているかを理解しましょう「少なくとも」。まだ卵の数を制限することを検討していない、同じ7階には、我々は戦略を最適化することができます。
最善の戦略は、私が最初に行くのアイデアのバイナリ検索を使用することで(1 + 7) / 2 = 4
、それをスローするように床:
説明壊れた場合はF
4よりも、私が最初に行った(1 + 3) / 2 = 2
層のテスト......
説明が破壊されていない場合F
よりも大きい又は4に等しく、Iは最初に行っ(5 + 7) / 2 = 6
層のテスト......
この戦略では、最悪の場合は(壊れていない7個の卵を層に試行すべきであるF = 7
)、または卵(ティア1に分割されていますF = 0
)。しかし、関係なく、最悪のシナリオの種類の、ちょうど試してくださいlog7
切り上げと呼ばれる前の試行のマイナー、より7少ない3倍に等しい以上を数回を投げます。
PS:それはビットランダウの記号の計算アルゴリズムの複雑さのようなものです。
あなたは卵の数を制限しない場合は実際には、その後、明らかにバイナリ思考が試みの最小数を取得することができますが、問題はそれである今、あなたは卵の数を制限K
直接死ぬためにバイナリ思考を使用します。
たとえば、あなたに卵、7階を与えるために、あなたはそれの半分を使用してみろ?あなたは、卵は大丈夫壊れていない場合は、それをスローするように四階に直接アクセスしていますが、何も壊れた卵は、テストを継続していなければ、卵は正確にレスリングが床壊れていないと判断することはできませんF
Aを。のみスキャン線形この場合に使用される方法は、アルゴリズムの戻り結果は7であるべきです。
一部の読者は、このアイデア持っていることがあります。床は間違いなく最速で除外し、その後、彼らは最初の半分を見つけてみましょうバイナリ検索速度を、その後、一度だけの卵とをAリニアスキャンするまで、このようにして得られた結果は、少なくともではありません卵の数はそれを投げ?
残念ながら、たとえば、床が高くなっていない、100層、次の2個の卵を与え、あなたはそれが唯一の1〜49層をスキャンし、線形、最悪のケースにすることができ、壊れた、50階でそれを投げます50回を投げます。
そうしない場合は、「半分」「第5」「非常に、」かなり最悪の場合には試行回数を削減しますなります。のは、最初の投げ卵ごとに10階建て、壊れた卵スキャンリニア秒以下、20回の合計を言ってみましょう。
実際には、最適解は14回です。最適な戦略は非常に大きく、何の法律はまったくあり。
すべてのこのナンセンスは必ずタイトルの意味を理解し、この問題は確かに複雑であることを認識することです言って、でも私たちの手がそれを解決するためのアルゴリズムを使用する方法、カウントすることは容易ではないですか。
第二に、分析の考え方
問題は、「ステータス」、「選択」するもので、その後、徹底的である何:動的なプログラミングの問題は、あなたが直接、私たちは前に何度も強調してきた枠組みを設定することができます。
明らかに「ステータス」、現在所有している卵の数K
数や床をテストする必要があるとN
。テスト進行すると、検索範囲の床を減らすことができる卵の数は、状態の変化である、削減されます。
「選択肢は、」実際にどの階を選択するために卵を投げています。スキャンと半分リニア以前のアイデアを想起し、卵を投げ、そして層までリニアスキャンテストを選択する各中間階部分を選択するためのバイナリ検索。別の選択肢は、遷移状態になります。
ここで、「ステータス」および「選択」をクリアダイナミックプログラミングの基本的な考え方が形成され、二次元でなければなりません:dp
配列または2つの状態パラメータを用いてdp
状態遷移を表現する関数;加えて、すべてのオプションを介してループするループの、選択最良の選択ステータスの更新:
# 当前状态为 K 个鸡蛋,面对 N 层楼
# 返回这个状态下的最优结果
def dp(K, N):
int res
for 1 <= i <= N:
res = min(res, 这次在第 i 层楼扔鸡蛋)
return res
この擬似コード及び再帰状態遷移を示していないが、一般的なアルゴリズムのフレームワークが完了しました。
私たちは、最初に選択したi
卵を投げた後階建ての、二つのことが発生する可能性があります壊れた卵を、卵は壊れませんでした。この状態遷移時間が来ていることに注意してください:
卵が壊れている場合は、その後、卵の数は、K
必要があり、床ゾーン検索から1を引く必要があります[1..N]
なっ[1..i-1]
コi-1
階建ての建物。
卵が割れていなければ、その後、卵の数K
フロアゾーン検索から変更がなければならない[1..N]
となっ[i+1..N]
コN-i
階建ての建物。
PS:注意深い読者は壊れていない場合は、I-階建て投げ卵で上層階に探索空間の床を狭め、それはそれを構築するのi階建てが含まれていない、求めることができますか?いいえ、それは含まれていて。F 0の開始時に再帰まで、に等しく、前記第i階層実際には0相当に採取し、エラーがないようにしてもよいです。
私たちが求めているのであり、最悪の場合の最初に卵を投げるの数なので、卵i
その場合の結果に応じて、-floorなし破片より:
def dp(K, N):
for 1 <= i <= N:
# 最坏情况下的最少扔鸡蛋次数
res = min(res,
max(
dp(K - 1, i - 1), # 碎
dp(K, N - i) # 没碎
) + 1 # 在第 i 楼扔了一次
)
return res
容易に理解ベースケースを再帰的:フロアの数がときN
0に等しい、明確に卵を投げ必要はありません。卵数がときK
1で、明らかにのみ全フロアをスキャンリニアできます。
def dp(K, N):
if K == 1: return N
if N == 0: return 0
...
これまでのところ、実際には、この質問が解決されます!ただ、オーバーラップ部分問題を解消するためにメモを追加します。
def superEggDrop(K: int, N: int):
memo = dict()
def dp(K, N) -> int:
# base case
if K == 1: return N
if N == 0: return 0
# 避免重复计算
if (K, N) in memo:
return memo[(K, N)]
res = float('INF')
# 穷举所有可能的选择
for i in range(1, N + 1):
res = min(res,
max(
dp(K, N - i),
dp(K - 1, i - 1)
) + 1
)
# 记入备忘录
memo[(K, N)] = res
return res
return dp(K, N)
アルゴリズムの時間計算量はどのくらいですか?問題の動的計画アルゴリズムの時間計算量はサブ関数自体の複雑さ×数です。
関数自体の複雑さは、この再帰部分の複雑さを無視することであるdp
forループ内の関数を、関数自体の複雑さはO(N)です。
すなわち、異なる状態の組み合わせの総数のサブ問題の数、二つの状態の製品は、明らかであり、すなわちO(KN)。
したがって、アルゴリズムの総時間の複雑さはO(K * N ^ 2)、空間複雑性O(KN)です。
第三に、トラブルシューティング
この問題は非常に複雑ですが、アルゴリズムのコードは、動的プログラミングの特徴である、非常に簡単です、プラス網羅覚書/ DPテーブルの最適化は本当に新しいものではありません。
床ループスルー用とのコードは、なぜ最初に、一部の読者には理解できないことがあり[1..N]
、おそらく混乱を探索する前に、このロジックおよびリニアスキャンを置きます。まあ、それはまさに、それだけで「」を選択やっています。
レッツは、顔10階建てもし、あなたが卵2個を持っていると言うの床にそれをスローするように選択?私は、この全体の10階建てでもう一度試していることを、知りません。次の時間を選択する方法については、あなたが心配しないで、正しい状態遷移があり、再帰が各オプションのコストを計算します、我々は最適解である最高を取ります。
さらに、そのようなバイナリサーチサイクルのためのコードを変更するなど、この問題に対するより良い解決策は、時間複雑性O(K * N * logN個)を低減することができるがあり、改良された動的プログラミング溶液は、その後さらにOに還元することができる(KN )、最適な時間複雑性O(K * logN個)を解決するための数学的方法を使用して、空間の複雑さはO(1)に到達します。
溶液の半分少し誤解を招くおそれがあり、我々は投げ卵の関係でそれを議論する前に、あなたはおそらくそれの半分を考えているが、実際には、半分のダイムではありません。状態遷移方程式のモノトーンため、画像の関数である、あなたはすぐに最大の価値を見つけることができますバイナリ検索を使用してください。
簡単なバイナリは、実際には、これだけのコードの最適化で、それを見つけるために最適化されました:
def dp(K, N):
for 1 <= i <= N:
# 最坏情况下的最少扔鸡蛋次数
res = min(res,
max(
dp(K - 1, i - 1), # 碎
dp(K, N - i) # 没碎
) + 1 # 在第 i 楼扔了一次
)
return res
ループのための以下の状態遷移方程式を達成するための特定のコードです。
\ [DP(K、N)= \ MIN_ {0 <= I <= N} \ {\最大\ {DP(K - 1、I - 1)、DP(K、N - I)\} + 1 \ } \]
まず、私たちによるとdp(K, N)
(と定義配列K
の卵が直面N
階は、少なくとも数回を投げる必要がある)、知っていることは容易でK
固定したときに、この関数は単調増加でなければならない、どんなに賢いあなたの戦略は、床はテスト増加の数を増やす必要があります。
注意を払うので、dp(K - 1, i - 1)
およびdp(K, N - i)
この2つの機能、i
1からにあるN
我々が固定されている場合は、単一の成長K
とN
、上で見られるこの2つの機能i
で、元の機能i
増加にも単調に増加し、後者しなければならないi
の増加それは単調減少である必要があります。
この時間交差点まあ2つの大きい方の値を求め、その後、見つける、実際には、これらの最大のうちの最小値を求め、学生はバイナリサーチに精通しているが、バレー(谷)の値を求めることこの等価考えることは、確かに敏感ではないですさて、あなたはすぐにポイントを見つけるためにバイナリ検索を使用することができます。
それに直接コードを貼り付け、考え方はまったく同じです。
def superEggDrop(self, K: int, N: int) -> int:
memo = dict()
def dp(K, N):
if K == 1: return N
if N == 0: return 0
if (K, N) in memo:
return memo[(K, N)]
# for 1 <= i <= N:
# res = min(res,
# max(
# dp(K - 1, i - 1),
# dp(K, N - i)
# ) + 1
# )
res = float('INF')
# 用二分搜索代替线性搜索
lo, hi = 1, N
while lo <= hi:
mid = (lo + hi) // 2
broken = dp(K - 1, mid - 1) # 碎
not_broken = dp(K, N - mid) # 没碎
# res = min(max(碎,没碎) + 1)
if broken > not_broken:
hi = mid - 1
res = min(res, broken + 1)
else:
lo = mid + 1
res = min(res, not_broken + 1)
memo[(K, N)] = res
return res
return dp(K, N)
ここでの他の解決策は、次の記事残し、拡大することはない背の高い投げ卵前売を。
、明確な十分に理解するために、選択を行い、状態を見つける合理化することができ、類推によって学ぶことができます。私たちは、このソリューションが十分である必要があり、それを感じます。その後、賢いが、役に立たない遅すぎる、それらを考慮することは行く、このフレームワークを習得するための空き容量があります。
最後にそれに気づく、「ダイナミックプログラミングコメント(改訂版)」と「バックトラックコメント(改訂版)」の書き込みに行ったこと、およびので、しばらくお待ち、テンプレートの力アルゴリズムの質問の無限の多様性と戦うためにあなたを教えています。
私は最近作られた電子書籍「labuladongアルゴリズムチートシートは」、[] [] [データ構造、アルゴリズム思考] [高周波]インタビュー4章、プログラミング、動的に分割して60件の以上のオリジナルの記事の総数、絶対に大丈夫ですが!ダウンロード可能な期間限定私の公共の番号で、labuladong背景キーワードを使用すると、ダウンロードを解放することができます[PDF]を返信!
私は国民の関心番号labuladongの清流を歓迎し、公共技術の数は、オリジナルに準拠は、明らかに問題に捧げ!