diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7ee915 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +start_new_problem.sh +main.go +go.mod +go.sum +*.go \ No newline at end of file diff --git a/153FindMinimumInRotatedSortedArray.md b/153FindMinimumInRotatedSortedArray.md new file mode 100644 index 0000000..a2d410b --- /dev/null +++ b/153FindMinimumInRotatedSortedArray.md @@ -0,0 +1,201 @@ +問題: https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/description/ + +### Step 1 +- O(log n)時間のアルゴリズムを書けと書いてあるが、 +全く思いつかなかったので、一旦O(n)時間のコードを書いた +- データサイズも5000と小さいのでO(n)時間でも通ると思ったら案の定ACした + +```Go +func findMin(nums []int) int { + return slices.Min(nums) +} +``` + +- 以下を参考にして理解して何も見ずにコードを書く + - https://github.com/hayashi-ay/leetcode/pull/45/files#diff-856251eccb601f9962fc7fdd308675f5690975413b45fe6be0950672570bc6caR74 + - https://github.com/hayashi-ay/leetcode/pull/45/files#diff-856251eccb601f9962fc7fdd308675f5690975413b45fe6be0950672570bc6caR1 +- テストケース + - [0] -> 0 + - [0,1] -> 0 + - [1,0] -> 0 + - [0,1,2] -> 0 + - [2,0,1] -> 0 + - [1,2,0] -> 0 + - [1,2,3,0] -> 0 + +```Go +func findMin(nums []int) int { + left := 0 + right := len(nums) - 1 + for left < right { + middle := left + (right-left)/2 + if nums[middle] > nums[len(nums)-1] { + left = middle + 1 + } else { + right = middle + } + } + return nums[left] +} +``` + +### Step 2 + +#### 2a +- 閉区間の右端の値と真ん中の値を比較して狭めていく方法 +- 言語化 +1. 二分探索を、 [false, false, false, ..., false, true, true, ture, ..., true] と並んだ配列があったとき、 false と true の境界の位置を求める問題、または一番左の true の位置を求める問題と捉えているか? + - 最小値より左の要素をfalse、最小値以降の要素をtrueとして、 + 一番左のtrue、すなわち最小値を探す問題と捉える + - [3,4,0,1,2] -> [false,false,true,true,true] +2. 位置を求めるにあたり、答えが含まれる範囲を狭めていく問題と捉えているか? +3. 範囲を考えるにあたり、閉区間・開区間・半開区間の違いを理解できているか? + - 閉区間を採用 +4. 用いた区間の種類に対し、適切な初期値を、理由を理解したうえで、設定できるか? + - [left,right]=[0,len(nums)-1] + - 閉区間を採用した理由は、右端の要素を比較に用いる際に、 + index out of rangeを起こしたくないから + - (下記リンク先を見て追記)最終的に[false,false,[true],true,true]のように、 + 左端も右端も一番左のtrueを指すようにしたい + - https://github.com/seal-azarashi/leetcode/pull/39/files#r1851396321 +5. 用いた区間の種類に対し、適切なループ不変条件を、理由を理解したうえで、設定できるか? + - 終了条件はleft==right==(一番左のtrueのインデックス) + - なので不変条件はleft= (最小値), (最小値) <= nums[right] +6. 用いた区間の種類に対し、範囲を狭めるためのロジックを、理由を理解したうえで、適切に記述できるか? + - leftとrightの中間middleを取る + - 左側に区間を狭めるとき: nums[middle] < nums[right] + -> middleが最小値である可能性があるので、rightをmiddleに更新 + - 右側に区間を狭めるとき: nums[middle] > nums[right] + -> middleは最小値でないことが確定するので、leftをmiddle+1に更新 + - nums[middle] == nums[right]となる時は、 + numsの要素が全てuniqueであることとmiddleがleft寄りになることから、 + left==middle==rightの時にしか起きえない。 + この時、終了条件left==rightより、すでにループを出ている + - 停止性: left<=middle<=rightで、right<-middle, left<-middle+1のどちらかが起きる。 + middleは左に寄ることからmiddle==right>leftになることはなく、区間が必ず1以上狭まるので停止する +- 言語化してからトップダウンにアルゴリズムをコードに落とし込むとかなりスッキリした +- 整理している中で重要だと感じたのは、middle=(left+right)/2で取ると、middle nums[right]: + left = middle + 1 + case nums[middle] == nums[right]: + // This code should be unreachable because all elements of nums are unique and middlenums.back()ならnums[middle]>nums[right]なので + - 個人的には二分探索は調べるべき区間を狭めていくアルゴリズムなので、left~right間で完結させたい気持ちがあり、 + nums[right]を比較に用いる方が好み + - (以下リンク先を読んで追記) [false,...,false,true,...,true]のうち一番左のtrueを知りたいのだが、 + 一番左のtrueの要素はそれより右のものより常に小さいので、どれと比べても良い + - https://github.com/seal-azarashi/leetcode/pull/39/files#r1851140547 + - Q. right の初期値は nums.size() でもいいですか + - nums[right]にアクセスしているので、index out of rangeを起こす + - 初期値をnums.size()にするのなら区間を半開区間として、かつ比較をnums.back()で行うとできそう + - 2cでやってみよう + +#### 2b +- 2aを再帰でやってみる + +```Go +func findMin(nums []int) int { + var findMinHelper func(left int, right int) int + findMinHelper = func(left, right int) int { + if left == right { + return nums[left] + } + middle := left + (right-left)/2 + switch { + case nums[middle] < nums[right]: + return findMinHelper(left, middle) + case nums[middle] > nums[right]: + return findMinHelper(middle+1, right) + default: + log.Fatal("Something went wrong.") + return -1 // unreachable + } + } + + return findMinHelper(0, len(nums)-1) +} +``` + +#### 2c +- 半開区間を用いるとどうなるかを試してみる +1. 二分探索を、 [false, false, false, ..., false, true, true, ture, ..., true] と並んだ配列があったとき、 false と true の境界の位置を求める問題、または一番左の true の位置を求める問題と捉えているか? + - 2aと同じ +2. 位置を求めるにあたり、答えが含まれる範囲を狭めていく問題と捉えているか? +3. 範囲を考えるにあたり、閉区間・開区間・半開区間の違いを理解できているか? + - 半開区間を採用 +4. 用いた区間の種類に対し、適切な初期値を、理由を理解したうえで、設定できるか? + - [left,right)=[0,len(nums)) +5. 用いた区間の種類に対し、適切なループ不変条件を、理由を理解したうえで、設定できるか? + - 終了条件はleft==right==(一番左のtrueのインデックス) + - ここまで考えて半開区間を採用することの辻褄を合わせるのが嫌になった +6. 用いた区間の種類に対し、範囲を狭めるためのロジックを、理由を理解したうえで、適切に記述できるか? + +### Step 3 + +```Go +func findMin(nums []int) int { + left := 0 + right := len(nums) - 1 + for left < right { + middle := left + (right-left)/2 + switch { + case nums[middle] < nums[right]: + right = middle + continue + case nums[middle] > nums[right]: + left = middle + 1 + continue + default: + log.Fatal("nums might contain duplicates.") + } + } + return nums[left] +} +```