自分はアルゴリズムが苦手なので、根本的な思考法から教えてほしいと思った
山の内容を表す、0から始まる整数配列
numsが与えられます。ここで、nums[0]は山の最上部の要素です。1回の操作では、以下のいずれかを行うことができます:
- 山が空でない場合、山の最上部の要素を取り除きます。
- 取り除かれた要素が1つ以上ある場合、その中から任意の1つを山に戻します。この要素が新しい最上部の要素となります。
また、実行すべき操作の総数 k を示す整数 k が与えられます。
k 回ちょうど操作を行った後に得られる、山の一番上の要素の最大値を返してください。k 回操作を行っても空でない山を得ることができない場合は、-1 を返してください。
-
答えが-1でないとき
-
k-1個分配列を左から取得して、その最大値を返せばいい? - 例にもある通り、それが「取りうる最大値」となる
- kが配列の要素数を上回っている時などに注意する
-
-
答えが-1になるのはどんなとき?
- k=1のとき(例の通り)
[注意] これは不正確。k=1でもn >= 2ならnums[1]を返せる(1回popするだけ)。
正しくは n == 1かつkが奇数 のとき。
- n=1, k=偶数: pop→push→pop→push…と繰り返し、最後にpushで元に戻る → nums[0]を返せる
- n=1, k=奇数: 最後の操作が必ずpopになるためスタックが空になる → -1
ルールをモデル化しきれないので、実際に動くコードにしてみる
- 取得した配列
obtainedを用意する -
numsの先頭をとるのをtake, numsの先頭に加えるのをaddとする -
takeとaddどちらも可能な場合takeする
[注意] ここが最大の落とし穴。「できるならtakeする」というグリーディな方針は正しくない。
例えばnums=[1,100], k=2の場合、greedy takeだと「take(1)→take(100)→…」となるが、
正解は「take(1)→add(1)」でもなく「take(1)→take(100)でnums[0]=100」となる。
つまり「何をtakeするか」「いつaddするか」の最適な組み合わせを、逐次的に決めることはできない。
手順をシミュレートしようとしているのが根本的な間違い。
-
addはmax(obtained)を追加する - 以下四つの状態がある
| 条件 | 可能な動作 |
|---|---|
len(nums)>0 && len(obtained)>0 |
take add
|
len(nums)>0 && len(obtained)==0 |
take |
len(nums)==0 && len(obtained)>0 |
add |
len(nums)==0 && len(obtained)==0 |
(なし)(有り得ない) |
- ループを回して、それぞれの条件により動作を決める
- 最後のループは必ず
addする
[注意] 最後のループで必ずaddするのは正しくない。
例えばnums=[1,2,3], k=3のとき、take→take→takeでnums[3](範囲外)ではなく
nums[2]=3がトップになる。最後がtakeのほうが良い場合もある。
- 最後に
len(nums)==0の場合-1を返し、そうでなければnums[0]を返す - なにもできなければその時点で
-1を返す(これいる?)
第一陣
class Solution:
def maximumTop(self, nums: List[int], k: int) -> int:
obtained = []
for i in range(k):
if(len(nums)>0 and i<k-1):
obtained.append(nums.pop(0))
elif(len(obtained)>0):
max_num = max(obtained)
nums.insert(0, max_num)
obtained.remove(max_num)
else:
return -1
if(len(nums)>0):
return nums[0]
else:
return -1
失敗...nums[0]は常に最大値である必要があるがnums.insert(0, max_num)だとn回目以降のaddでnums[0]がn番目に大きい数になる、単にappendがいい
[注意] ここで気づいたバグの修正は正しいが、より根本的な問題がある。
このコードは「常にtake優先、最後だけadd」という固定戦略になっている。
しかし最適解は、取り除いた要素の中の最大値を最後にトップに乗せることで得られる。
シミュレーションの戦略自体を見直す必要がある。
第二陣
class Solution:
def maximumTop(self, nums: List[int], k: int) -> int:
obtained = []
for i in range(k):
if(len(nums)>0 and i<k-1):
obtained.append(nums.pop(0))
elif(len(obtained)>0):
max_num = max(obtained)
nums.append(max_num)
obtained.remove(max_num)
else:
return -1
if(len(nums)>0):
return nums[0]
else:
return -1
失敗...単にappendではダメだった、そりゃそうだ
常にnums[0]が最大値になるようにaddしていく必要がある
max(obtained)を返せれば楽だが、-1になる場合をコードに落とし込むやり方がわからない
nums.append(max_num)を変える場合、nums[0]がobtainedから追加したものならnums[0]より小さい場合2番目に追加すればよいが、もともとnumsに入っている場合単に1番目に追加しなければならない(takeが最大値を取得しているのではないため)<-addするのはlen(nums)==0のときだけなのでその心配はない?
ここで気づいたが、最初にk > len(nums)のときaddとtakeが交互に行われることになるが、その回数が偶数の場合len(nums)==0のためreturn -1となる、しかし実際はaddし続けてもいいため間違い<-最後だけ個別の条件が必要?
こういう時addし続けるという手もあるが、この全体の決定をどうコードに落とすかわからない(決定は何に依存しているのか?)
[注意] 「決定は何に依存しているのか?」という問いが核心をついている!
ここでシミュレーションから発想の転換をするべきだった。
「どう動かすか」ではなく「最終的にどの要素がトップに来られるか」を考えるべき。
「k回操作を最適に行った結果」は、「k回操作でトップに置ける要素の集合」の最大値に等しい。
→ どの要素がトップに置けるか? という問いに変換する。
第三陣
こうやってifがたくさんあるコードは美しくないよねえ...
class Solution:
def maximumTop(self, nums: List[int], k: int) -> int:
def _take(nums, obtained):
obtained.append(nums.pop(0))
def _add(nums, obtained):
max_num = max(obtained)
if(len(nums)==0):
nums.insert(0, max_num)
return
if(max_num > nums[0]):
nums.insert(0, max_num)
else:
nums.insert(1, max_num)
obtained.remove(max_num)
obtained = []
for i in range(k):
if(len(nums)>0 and i<k-1):
_take(nums, obtained)
elif(len(obtained)>0 and i<k-1):
_add(nums, obtained)
elif(i==k-1 and len(nums)==1 and len(obtained)>0):
_add(nums, obtained)
elif(i==k-1 and len(nums)>1 and len(obtained)>0):
max_num = max(obtained)
if(nums[1] > max_num):
_take(nums, obtained)
else:
_add(nums, obtained)
else:
return -1
if(len(nums)>0):
return nums[0]
else:
return -1
失敗...nums==[17,61,5,1,44] and k==100のとき失敗する(出し入れが偶数回)
i==k-1 and len(nums)==0 and len(obtained>0)の場合が抜けてる?
第四陣
差分だけ(l24)
elif(i==k-1 and len(nums)<2 and len(obtained)>0):
失敗...最終回だけaddがlen(nums)>0の状態で実行されることを忘れていた
結局nums[0]がobtainedから追加されたものなのかどうなのか判別しなければならなくなったぞ
[注意] ここがシミュレーション戦略の限界。場合分けが際限なく増えていく。
これは「アプローチを変えるべき」というシグナルと受け取るべきだった。
以下、claudeの記述
理想的な思考の道筋
ステップ1:問いを変換する
「k回操作をどう並べるか」をシミュレートするのではなく、
「k回操作した後、どの要素をトップに置けるか」 を考える。
操作の順序には無限の組み合わせがあるが、結果として可能な最終状態は有限に絞れる。
→ 取れる最大値 = トップに置ける要素の集合の最大値
ステップ2:nums[j] をトップに置ける条件を考える
nums[j](j番目の要素)をk回操作後にトップに置くには?
前提:nums[j]は元々j段下にある。取り出すには最低j回のpopが必要。
ケース①:ちょうどj回popする(k == j)
- j回popすれば
nums[j]がトップ、操作もちょうどk回。✓ - 条件:
j < nかつj == k
ケース②:j回popした後、余りの操作(k-j回)を「空消費」する
- j回popして
nums[j]がトップ。残りk-j回を消費したい。 - 空消費の方法:
nums[j]をpopしてpushで戻す(2回1セット) - → k-jが偶数なら消費できる
ケース③:k-jが奇数のとき(ただしk-j >= 3かつnums[j+1]が存在する)
- j回pop →
nums[j]がトップ →nums[j]をpop →nums[j+1]をpop →nums[j]をpush → 残りを空消費 - この手順で「k-j(奇数)= 3 + 偶数」を消費できる ✓
ケース④:k-j == 1(つまりj == k-1)
- j回popして
nums[j]がトップ。残り1回の操作が必ず必要。 - 残り1回でpopすると
nums[j]が消える。pushすると別の要素が被さる。 -
どうやっても
nums[j]をトップに残せない → ✗
結論(n >= 2の場合):j != k-1かつj < nならnums[j]はトップに置ける
ステップ3:ボーナス候補 — nums[k]
k回ちょうどpopし続ければ、nums[k]が自然にトップに来る(余りの操作ゼロ)。
条件:k < n
ステップ4:特殊ケース — n == 1
要素が1つしかない場合、popとpushを交互に繰り返すしかない。
- k が偶数:最後がpushで
nums[0]に戻る →nums[0]を返す - k が奇数:最後がpopでスタックが空 →
-1
ステップ5:まとめてコードにする
class Solution:
def maximumTop(self, nums: List[int], k: int) -> int:
n = len(nums)
# 特殊ケース:要素が1つ
if n == 1:
return -1 if k % 2 == 1 else nums[0]
ans = -1
# 候補①②③:j < k-1 かつ j < n の範囲(j == k-1 だけ除外)
for j in range(min(k - 1, n)):
ans = max(ans, nums[j])
# 候補④:k回popした先の要素
if k < n:
ans = max(ans, nums[k])
return ans
ポイントの整理:
-
nums[k-1]だけが絶対に置けない(残り1回問題) - それ以外の
nums[0]~nums[min(k,n)-1]はすべて候補 -
nums[k](k回pop後)も候補 -
n >= 2の場合、候補が0になることはないので-1は返らない
発想の転換ポイント(振り返り)
| 最初のアプローチ | 理想のアプローチ |
|---|---|
| 「どう操作するか」をシミュレート | 「どの要素が最終トップになれるか」を考える |
逐次的に take/add を決める |
候補の集合を一発で特定する |
| 場合分けが際限なく増える | 条件が1つ(j != k-1)にまとまる |
教訓
- シミュレートで突っ走らない
- あくまで仮説検証の手段
- そこからルールを見つける
- 最終状態を考える
- k回目にどうなっていればいい?
- 操作の自由度を確認する
- 余った操作はどう使える?
- ここなら偶数回のターン
- 不変状態を探す
-
nums[0]~nums[k-1]の範囲しかtopになれないことに気づく
-
数日後再挑戦する
再挑戦
この順番でいく
- 一回の行動で何ができるか整理する
- 法則を考える
- ここで操作の自由度を見つける
- 不変状態も見つけたい
- 最終状態を考える
- 整理した通りにプログラムを書く
| 文字 | 文字コード |
|---|---|
| α | 03b1 |
| β | 03b2 |
| γ | 03b3 |
| δ | 03b4 |
| ε | 03b5 |
一回の行動でなにができるか
取りうる行動は以下二つ
- 山からとる(A)
- とったものからどれかを山に入れる(B)
この行動に関係ある条件は以下二つ - 山の要素が0個以上
len(pile) > 0:(α) - とったものの要素が0個以上
len(took) > 0:(β)
| 条件 | 行動 |
|---|---|
| α and β | なし |
| not α and β | A |
| α and not β | B |
| not α and not β | A, B |
[注意] この表はα・βの定義と行動の対応が逆転している。α(len(pile) > 0)が真ならAが可能、β(len(took) > 0)が真ならBが可能なので、正しくはα and β → A, B・not α and not β → なしとなる。表を作ったら定義と照合して一行ずつ確認すること。幸い、この後の脳内シミュレーションは実態に即した形で進んでいるが、表に頼った推論が混乱の一因になっている。
ここで、Bで何を入れるかとnot α and not βのときの行動が一意に定まらない
-> 組み合わせ爆発!? ここを詰める
法則を考える
気づいたことを書き出していく
- not 03b1 and not 03b2のターンが圧倒的に多い!
- AとBを交互にやればターンを消費できる(操作の自由度を把握)
- 答えはnumsに入っている数値以外ありえない
- 極論
return max(nums)である程度あたるかも - 正確には
return (max nums[:k])か - もっと言うと、最後に1ターン消費してpileに戻すので
nums[:k-1]のどれか
- 極論
- とって→出してなので、
k-2<0のとき行動不能return -1
[注意] k < 2で-1を返すのは早計。k=0は操作なしなのでnums[0]をそのまま返せるし、k=1かつn >= 2なら一回popしてnums[1]を返せる。-1になるケースは後で気づくn=1の特殊ケースを除けばほぼ存在しない。「できないケースを探す」より「できるケースを正確に列挙する」方が漏れが少ない。
-
len(nums) > k-1なら、k-1回とってから一番大きいものを戻せばいいのでreturn max(nums[:k-1])でOK
[注意] ここで 「k回全部takeするだけ」というパターンを見落としている。len(nums) > kなら最後にaddしなくてもk回連続popでnums[k]がトップになれる。「最後のターンは必ずaddに使う」という暗黙の前提が視野を狭めている。最終ターンの操作はtakeでもaddでも構わないことを意識すること。
-
len(nums) =< k-1なら?- この条件は
k > len(nums)+1と同値 -
k-len(num)が奇数なら、全てのnumsをとる->最後のターンまで(len(nums) - (k-1))回分のとって出してをやってをやる->最後のターンでmax(took)すなわちmax(nums)を入れればいいのでreturn max(nums) -
k-len(num)が偶数なら?- 全部とってから最後の二回で小さい順にいれればいいじゃん!
- つまり結局
return max(nums)でOK! - 本当に?
- この条件は
第一陣
自分でも信じられないが...
class Solution:
def maximumTop(self, nums: List[int], k: int) -> int:
if (k - 2 < 0):
return -1
if (len(nums) > k - 1):
return max(nums[:k-1])
return max(nums)
失敗...だが171/191のテストケースで通った!
失敗ケースでもプログラムの処理を簡単に頭で追えるのはよい
以下のケースで落ちる
nums = [35,43,23,86,23,45,84,2,18,83,79,28,54,81,12,94,14,0,0,29,94,12,13,1,48,85,22,95,24,5,73,10,96,97,72,41,52,1,91,3,20,22,41,98,70,20,52,48,91,84,16,30,27,35,69,33,67,18,4,53,86,78,26,83,13,96,29,15,34,80,16,49]
k = 15
ここから例外ケースを考える
プログラム通りに行くとk-1は14でlen(nums)は72のため答えは86のはず
しかし実際の答えは94(index=15)
気づいた...len(nums)>kなら別に最後に戻さなくてもいい!
- その場合
nums[k]を返すことになる(0-indexedに注意、nums[k+1]ではない) -
len(nums) > kのとき、最後に戻す場合の候補の中の最大値max(nums[:k-1])と全部取る場合の答えnums[k]のどちらか大きい方を返せばいい - 実はこれは
len(nums)=kでも同様、つまりlen(nums) >= kの場合
第二陣
等号不等号が嫌いなので条件の順番を変えた
最後のreturnが1行にまとまってうれしい
class Solution:
def maximumTop(self, nums: List[int], k: int) -> int:
if (k - 2 < 0):
return -1
if (k > len(nums)):
return max(nums)
return max([nums[k], max(nums[:k-1])])
失敗...なんと53/197しかあってない!? うっかりミスの予感...
またこんな時もnums[k]を返せばよかった
- いや、この場合
nums[k]しか返せない
nums = [68,76,53,73,85,87,58,24,48,59,38,80,38,65,90,38,45,22,3,28,11]
k = 1
nums[k]を返せる場合とはいつか
-
len(nums) > kのとき
nums[k]を返せない場合とはいつか -
k >= len(nums)のとき
nums[k]しか返せない場合とはいつか -
k = 1 and len(nums) > kのとき -
k = 1だとpileからとることしかできない -
k = 1 and len(nums) = 1の時はpileが空になるのでreturn -1する- 逆に
k > 1 and len(nums) = 1のときはkが偶数ならreturn nums[0]でいいが、奇数ならreturn -1となる - つまり
len(nums) = 1 and k%2=1のときreturn -1,len(nums) = 1 and k%2=0のときreturn nums[0]となる
- 逆に
len(nums) > 1のとき、結局のところmax(nums)かnums[k]かmax(nums[:k-1])のどれかが答えになる
ここでlen(nums) = kのときnums[k]はindex out of range
- これが大量の失敗の原因か!
-
len(nums) = kのとき、それまで全てtakeしていたら最後の一回は残すしかない- しかしその場合はそれまでのどこかで一番大きいのを入れておけばいい
- 結局
return max(nums)でいいのか?
len(nums) > k-1のとき、最後の一回ではpileからとってnums[k]を返すかtookから加えてmax(nums[:k-1])を返すしかない
- 0始まりと1始まりがややこしい
>>> n = [1,2,3,4,5,6,7,8,9,10]
>>> n[:3]
[1, 2, 3]
>>> n[3]
4
これはk=3のときを想定、なのでこの場合return max([nums[k], max(nums[:k-1])])で問題はない
第三陣
結局条件の順番は戻すことになった
len(nums) > 1 and k=1の場合も入れた
class Solution:
def maximumTop(self, nums: List[int], k: int) -> int:
if (len(nums) == 1):
return -1 if k % 2 == 1 else nums[0]
if (k == 1):
return nums[k]
if (len(nums) > k):
return max([nums[k], max(nums[:k-1])])
return max(nums)
失敗...191/197通った
あと少し この場合で落ちた
nums = [4,6,1,0,6,2,4]
k = 0
実はk=0の時もある...ひっかけ!?
コードパスとしてはlen(nums) > kの場合を通った模様
これもまた nums[k]を返さざるを得ない場合であった
ちなみに条件は以下
1 <= nums.length <= 10^5-
0 <= nums[i], k <= 10^9
k<0もある!!インデックスがややこしい上に意味が通っていないが...
[注意] 制約0 <= nums[i], k <= 10^9は0 <= kも含んでいるのでk < 0は存在しない。行き詰まったとき、まず問題文のConstraintsに立ち返る習慣が重要。存在しないケースへの対処を追加すると、本来のバグが見えにくくなる上にデバッグも複雑になる。
- この場合
len(nums) > abs(k)ならlen(nums)+kとすることでk>0としてindexを数えられる -
abs(k) > len(nums)だとkの値がおかしくなる - ここまで脳内でシミュレーションしながら制約をあらいだしてたのに、ここにきてそれが通用しなくなった
第四陣
結局絶対値にしてみた
0 <= kっていってないの悪問だろ!もう知らない!
[注意] 「悪問」ではなく制約の読み落としが原因。制約0 <= kは最初から明記されていた。行き詰まってフラストレーションを感じたときこそ、問題文に立ち返るべきタイミング。感情的に対処コードを追加すると、本来のバグが見えにくくなる。
class Solution:
def maximumTop(self, nums: List[int], k: int) -> int:
if (len(nums) == 1):
return -1 if k % 2 == 1 else nums[0]
if (k < 0):
k = abs(k)
if (k == 0 or k == 1):
return nums[k]
if (len(nums) > k):
return max([nums[k], max(nums[:k-1])])
return max(nums)
失敗...しかし196/197通った!!
最後の一つはこれ
nums = [0,1,2]
k = 3
分岐に入らずmax(nums)つまり2をかえしたが、実際の答えは1
k = len(nums)とk > len(nums)を同じように扱っていたのは、このように考えたから
len(nums) = kのとき、それまで全てtakeしていたら最後の一回は残すしかない
- しかしその場合はそれまでのどこかで一番大きいのを入れておけばいい
- 結局return max(nums)でいいのか?
[注意] 「どこかのターンで最大値を入れておける」という推論が崩れるのはmax(nums)がnums[k-1]の位置にあるとき。これは「理想的な思考の道筋」のj != k-1という除外条件そのもの。k == len(nums)もk < len(nums)も除外されるのは常にnums[k-1]だけという統一的な視点で見れば、別ケースとして扱う必要はなくreturn max(nums[:k-1])に一本化できる。
この場合のみこれが違った!
しかしその場合はそれまでのどこかで一番大きいのを入れておけばいい
k = len(nums)の場合
- 1ターン目は必ずとるしかない
- 最後のターンは入れるしかない
- この場合
max(nums)をどこかのターンで入れて、それ以外はすべてとることになる - しかし
max(nums)が一番最後にある場合それは不可能 - その場合
max(nums[:k-1])を返すことになる(スライスの番号指定は1始まり!)
- この場合
第五陣
集中切れたからこれで最後!
class Solution:
def maximumTop(self, nums: List[int], k: int) -> int:
if (len(nums) == 1):
return -1 if k % 2 == 1 else nums[0]
if (k < 0):
k = abs(k)
if (k == 0 or k == 1):
return nums[k]
if (len(nums) == k):
return max(nums[:k-1]) if nums.index(max(nums)) + 1 == len(nums) else max(nums)
if (len(nums) > k):
return max([nums[k], max(nums[:k-1])])
return max(nums)
[注意] if (len(nums) == k)の条件式はreturn max(nums[:k-1])と常に等価。maxが最後の位置にない場合もmax(nums) == max(nums[:k-1])が成り立つため、三項演算子は不要。複雑な条件を書いたときは「これはシンプルにまとめられないか」と一歩引いて確認する習慣が大事。
成功!!🥳 めちゃくちゃうれしいです...
感想
結局再挑戦でも4回もリテイク喰らったが、コードは見違えるほどシンプルになった!
最初のコードでシミュレーションするやり方だと正解には絶対にたどり着けなかったと思う
最初に一回の行動と条件を整理してから、それをコードに起こさず脳内でシミュレーションしつつブレスト的に隠された条件をバンバン挙げる作戦が効いたのではないか
ちなみにパフォーマンスは以下の通り
| 項目 | 結果 |
|---|---|
| Runtime | 7ms(Beats 47.37%) |
| Memory | 31.20MB(Beats 66.51%) |
まあpythonだしこんなもん?
ただエッジケースを見つけるのはなかなか苦戦した。
エッジケースが起きる条件を洗い出さなければそれこそ組み合わせ爆発してしまうし、適当に思いつきで条件を増やすのは限界があると思う。
しかしうまいやり方がわからない
[注意] エッジケースを体系的に見つけるには、各パラメータの境界値をあらかじめ列挙する方法が有効。今回なら実装前にk=0, k=1, k=n-1, k=n, k>nとn=1をリストアップしておけば、テスト失敗で気づく前に対処できる。「ブレストで出てきた条件を境界値一覧として整理してから実装に入る」という手順にすると、後から追加する場合分けを大幅に減らせる。
ちなみに、k < 0の条件分岐を消した時のコードは以下の通り
コード
class Solution:
def maximumTop(self, nums: List[int], k: int) -> int:
if (len(nums) == 1):
return -1 if k % 2 == 1 else nums[0]
# if (k < 0):
# k = abs(k)
if (k == 0 or k == 1):
return nums[k]
if (len(nums) == k):
return max(nums[:k-1]) if nums.index(max(nums)) + 1 == len(nums) else max(nums)
if (len(nums) > k):
return max([nums[k], max(nums[:k-1])])
return max(nums)
結果
成功
パフォーマンス
| 項目 | 結果 |
|---|---|
| Runtime | 0ms(Beats 100.00%) |
| Memory | 31.58MB(Beats 34.45%) |
結局kが負の時ないんかい!?!?!?
なおパフォーマンスはsubmitするごとに変わるので、あまり当てにならない
以下、claudeの記述
Claudeからのアドバイス
1. 問いを変換する習慣
最初の挑戦でも再挑戦でも、最初の一手が「操作をシミュレートしよう」になっていた。
操作の組み合わせが膨大な問題では、「k回操作した結果として何が起こり得るか」という結果の集合に問いを変換するのが有効。
「どう動かすか」ではなく「何が起こり得るか」
今回の場合、「最終的にトップに置ける要素はどれか」という問いに変換した瞬間に見通しが開けた。
シミュレーションは仮説検証には使えるが、アルゴリズムの骨格にしようとすると破綻しやすい。
2. 実装前に境界値を列挙する
エッジケースをテスト失敗で一つずつ発見していたが、それは本来、実装前にできる作業。
手順:
- 各パラメータの最小・最大・境界をリストアップする
- 今回なら
n=1、k=0、k=1、k=n-1、k=n、k>n
- 今回なら
- それぞれについて「自分のロジックは正しく動くか」を紙上で確認してから実装する
この習慣があれば、k=0・k=n・k<0(非存在)の迷走はすべて事前に防げた。
3. 制約を最初に確認する
k<0への対処に時間を使ったが、制約0 <= k <= 10^9を実装前に確認していれば完全に不要だった。
問題文のConstraintsは**「何を考えなくていいか」を教えてくれる情報**でもある。
確認のタイミング:
- 解法を考え始める前(入力の範囲を把握する)
- 行き詰まったとき(存在しないケースを追いかけていないか確認する)
4. 暗黙の前提を疑う
再挑戦で複数の見落としが「無意識の前提」から来ていた。
| 前提 | 実際 |
|---|---|
| 最後のターンは必ずaddする | takeで終わっても良い(nums[k]を見落とした原因) |
k < 2は行動不能 |
k=0はnums[0]、k=1はnums[1]を返せる |
k=nはk>nと異なる特殊ケース |
同じ論理(nums[k-1]の除外)で統一できる |
行き詰まったときは「自分が当然だと思っていることは本当に正しいか?」と問い直す。
5. 複雑になったら等価変換を探す
条件分岐が増えたり三項演算子が複雑になったとき、それはロジックをまだ正しく把握できていないサインであることが多い。
今回の例:
# 書いたコード
return max(nums[:k-1]) if nums.index(max(nums)) + 1 == len(nums) else max(nums)
# 等価なシンプルな形
return max(nums[:k-1])
「条件分岐を消せないか」「この2つの分岐はまとめられないか」と常に問う習慣が、コードの見通しと正確さを両方改善する。