# Greedy (8) > 紀錄 NeetCode-150 與 LeetCode 的解題想法、刷題過程與錯誤,以 Python 或 C++ 兩種語言為主 > > 其他 NeetCode-150 或 LeetCode 的主題請見 [Here](https://hackmd.io/-ovTFlzLRWaSgzDGhbSwsA) <!-- 常用色碼紀錄 Easy: <font color="#00ad5f">**Easy**</font> Medium: <font color="#ffbb00">**Medium**</font> Hard: <font color="#ee2f56">**Hard**</font> --> Greedy 的題目重點在於透過適合的 greedy rule 將問題分解,找到每個 subproblem 的局部最佳解後,再從每一次的局部最佳解構成最後的解集合。 ## 1. Maximum Subarray <font color="#ffbb00">**Medium**</font> > Given an array of integers `nums`, find the subarray with the largest sum and return the sum. > > A **subarray** is a contiguous non-empty sequence of elements within an array. ### Example 1: ```java Input: nums = [2,-3,4,-2,2,1,-1,4] Output: 8 ``` Explanation: The subarray [4,-2,2,1,-1,4] has the largest sum 8. ### Example 2: ```java Input: nums = [-1] Output: -1 ``` ### Constraints * `1 <= nums.length <= 1000` * `-1000 <= nums[i] <= 1000` ### Recommended complexity * Time complexity: $O(n)$ * Space complexity: $O(1)$ ### Solution **Greedy rule:** 對於 index = i 的元素,分析要不要將他加入至目前的 subArray 之中使它對目前的最大和(local optimal)有貢獻。 迭代輸入陣列 nums 並維護一個當前最大和 surrSum 以及全局最大和 maxSum,每個元素考慮以下狀況: * 加上 nums[i] 後總和 >= nums[i] * 表示 nums[i] 對 currSum 有貢獻 * 可加入 subarray 之中 * 加上 nums[i] 後總和 < nums[i] * 表示不需要前面的 subarray,因為前面的元素會拖垮後面的表現 * 更新 currSum 與 maxSum * 並從 nums[i] 開始做為第一個元素,往後找新的 subarray ```python= class Solution: def maxSubArray(self, nums: List[int]) -> int: maxSum = nums[0] currSum = 0 for n in nums: if currSum + n < n: currSum = 0 currSum += n maxSum = max(currSum, maxSum) return maxSum ``` ## 2. Jump Game <font color="#ffbb00">**Medium**</font> > You are given an integer array `nums` where each element `nums[i]` indicates your maximum jump length at that position. > > Return `true` if you can reach the last index starting from index `0`, or `false` otherwise. ### Example 1: ```java Input: nums = [1,2,0,1,0] Output: true ``` Explanation: First jump from index 0 to 1, then from index 1 to 3, and lastly from index 3 to 4. ### Example 2: ```java Input: nums = [1,2,1,0,1] Output: false ``` ### Constraints * `1 <= nums.length <= 1000` * `0 <= nums[i] <= 1000` ### Recommended complexity * Time complexity: $O(n)$ * Space complexity: $O(1)$ ### Solution **Greedy rule:** 找到每個能抵達局部終點的局部最佳解 (1) 能不能抵達終點最快的看法是從終點往回逆推,如果回推的某個點能夠抵達目前的終點,表示此點是一個局部最佳解 (2) 此時將終點更新為這個局部最佳解的點,並繼續逆推找下一個最佳解 (3) 直到找到起點為止,表示能夠從起點透過上述的每個局部最佳解走到終點 ```python= class Solution: def canJump(self, nums: List[int]) -> bool: n = len(nums) dst = n - 1 for i in range(n - 2, -1, -1): if nums[i] + i >= dst: dst = i return True if dst == 0 else False ``` ## 3. Jump Game ll <font color="#ffbb00">**Medium**</font> > You are given an array of integers `nums`, where `nums[i]` represents the maximum length of a jump towards the right from index `i`. For example, if you are at `nums[i]`, you can jump to any index `i + j` where: > > * `j <= nums[i]` > * `i + j < nums.length` > > You are initially positioned at `nums[0]`. > > Return the minimum number of jumps to reach the last position in the array (index `nums.length - 1`). You may assume there is always a valid answer. ### Example 1: ```java Input: nums = [2,4,1,1,1,1] Output: 2 ``` Explanation: Jump from index 0 to index 1, then jump from index 1 to the last index. ### Example 2: ```java Input: nums = [2,1,2,1,0] Output: 2 ``` ### Constraints * `1 <= nums.length <= 1000` * `0 <= nums[i] <= 100` ### Recommended complexity * Time complexity: $O(n)$ * Space complexity: $O(1)$ ### Solution **Greedy rule:** 每次尋找能夠走最遠距離的點作為局部最佳解 * 維護兩個指標 `leftPtr` 與 `rightPtr`,這兩個指標間的點表示目前可以抵達的點 * 初始化,兩個指標都從 index = 0 的起點開始 * 透過迭代可以抵達的點尋找最遠距離 * 迭代完並找到最遠距離後,更新下一段的指標 * leftPtr = rightPtr + 1 * rightPtr = 距離最遠的點 * 當右指標(最遠點)超過終點位置,表示抵達最後終點 每次在找最遠距離時,每更新一次指標就表示選擇一個點前進 $\Rightarrow$ 步數 + 1 ```python= class Solution: def jump(self, nums: List[int]) -> int: leftPtr, rightPtr = 0, 0 res = 0 while rightPtr < len(nums) - 1: # go through the reachable index farIdx = rightPtr for i in range(leftPtr, rightPtr + 1): farIdx = max(farIdx, i + nums[i]) # update left and right pointer leftPtr = rightPtr + 1 rightPtr = farIdx res += 1 return res ``` ## 4. Gas Station <font color="#ffbb00">**Medium**</font> > There are `n` gas stations along a circular route. You are given two integer arrays `gas` and `cost` where: > > `gas[i]` is the amount of gas at the `ith` station. > `cost[i]` is the amount of gas needed to travel from the `ith` station to the `(i + 1)th` station. (The last station is connected to the first station) > > You have a car that can store an unlimited amount of gas, but you begin the journey with an empty tank at one of the gas stations. > > Return the starting gas station's index such that you can travel around the circuit once in the clockwise direction. If it's impossible, then return `-1`. > > It's guaranteed that at most one solution exists. ### Example 1: ```java Input: gas = [1,2,3,4], cost = [2,2,4,1] Output: 3 ``` Explanation: Start at station 3 (index 3) and fill up with 4 unit of gas. Your tank = 0 + 4 = 4 Travel to station 0. Your tank = 4 - 1 + 1 = 3 Travel to station 1. Your tank = 3 - 2 + 2 = 3 Travel to station 2. Your tank = 3 - 2 + 3 = 4 Travel to station 3. Your tank = 2 - 4 + 4 = 2 ### Example 2: ```java Input: gas = [1,2,3], cost = [2,3,2] Output: -1 ``` Explanation: You can't start at station 0 or 1, since there isn't enough gas to travel to the next station. If you start at station 2, you can move to station 0, and then station 1. At station 1 your tank = 0 + 3 - 2 + 1 - 2 = 0. You're stuck at station 1, so you can't travel around the circuit. ### Constraints * `1 <= gas.length == cost.length <= 1000` * `0 <= gas[i], cost[i] <= 1000` ### Recommended complexity * Time complexity: $O(n)$ * n is the size of input array * Space complexity: $O(1)$ ### Solution **Greedy rule:** 選定起點後開始走,如果在某個中間站點的油量 < 0 表示該點不能做為起點 當發現某個點不能做為起點,意味著從這個起點開始到油量 < 0 的點,瓦斯補給(`gas`)的總和 < 消耗成本(`cost`)的總和,因此中間路途的點都不能作為起點。Step-by-step 如下: * 設總油量 `total = 0`,起點為 index = 0 * 以單一站點來看,離開每個站點後所剩的油量為 `remains = gas[i] - cost[i]`(先補給再消耗) * 若 `total + remains < 0` 表示油量不夠 * 以下一個點 index = `i + 1` 作為新的起點 * 更新總油量 `total = 0` ```python= class Solution: def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int: if sum(gas) < sum(cost): return -1 totalGas = 0 n = len(gas) startIdx = 0 for i in range(n): remains = gas[i] - cost[i] totalGas += remains if totalGas < 0: totalGas = 0 startIdx = i + 1 return startIdx ``` ## 5. Hamd of Straights <font color="#ffbb00">**Medium**</font> > You are given an integer array `hand` where `hand[i]` is the value written on the `ith` card and an integer `groupSize`. > > You want to rearrange the cards into groups so that each group is of size `groupSize`, and card values are consecutively increasing by `1`. > > Return `true` if it's possible to rearrange the cards in this way, otherwise, return `false`. ### Example 1: ```java Input: hand = [1,2,4,2,3,5,3,4], groupSize = 4 Output: true ``` Explanation: The cards can be rearranged as `[1,2,3,4]` and `[2,3,4,5]`. ### Example 2: ```java Input: hand = [1,2,3,3,4,5,6,7], groupSize = 4 Output: false ``` Explanation: The closest we can get is `[1,2,3,4]` and `[3,5,6,7]`, but the cards in the second group are not consecutive. ### Constraints: * `1 <= hand.length <= 1000` * `0 <= hand[i] <= 1000` * `1 <= groupSize <= hand.length` ### Recommended complexity * Time complexity: $O(n \cdot \log n)$ * Spce complexity: $O(n)$ ### Solution **Greedy rule:** 每次從最小的數字開始形成一個 group (局部最佳解) * 每個數字可能提供給多個 group 使用,所以要先計算/紀錄每個數字出現的頻率 * 將給定的 array 排序,從最小的數字開始建立 group * 選定最小的數字(假設為 n)後,因為 group 內的數字是每次遞增 1,所以直接把 n, n+1, n+2, n+3 這四個數字的頻率 - 1 表示用這四個數字形成一個 group * 如果某數字不存在或頻率不夠(=0),表示不能建立這組 group > [!Tip] > * 選定最小數字作為每個 group 的起點,後面 groupSize - 1 個數字直接以迭代遞增的方式找出來然後把頻率/次數 - 1 即可,不用真的創建 group 放入數字 > * 完成一個 group 後(local optimal)再完成下一個 group,這樣的目的是符合 greedy 的概念 ```python= class Solution: def isNStraightHand(self, hand: List[int], groupSize: int) -> bool: if len(hand) % groupSize: return False count = {} for i in hand: count[i] = 1 + count.get(i, 0) hand.sort() for i in hand: if count[i] != 0: for j in range(i, i + groupSize): if (j in count) and (count[j] > 0): count[j] -= 1 else: return False return True ``` ## 6. Merge Triplets to Form Target <font color="#ffbb00">**Medium**</font> > You are given a 2D array of integers `triplets`, where `triplets[i] = [ai, bi, ci]` represents the `ith` triplet. You are also given an array of integers `target = [x, y, z]` which is the triplet we want to obtain. > > To obtain `target`, you may apply the following operation on `triplets` zero or more times: > > Choose two different triplets `triplets[i]` and `triplets[j]` and update `triplets[j]` to become `[max(ai, aj), max(bi, bj), max(ci, cj)]`. > > * E.g. if `triplets[i] = [1, 3, 1]` and `triplets[j] = [2, 1, 2]`, `triplets[j]` will be updated to `[max(1, 2), max(3, 1), max(1, 2)] = [2, 3, 2]`. > > Return `true` if it is possible to obtain `target` as an **element of** `triplets`, or `false` otherwise. ### Example 1: ```java Input: triplets = [[1,2,3],[7,1,1]], target = [7,2,3] Output: true ``` Explanation: Choose the first and second triplets, update the second triplet to be [max(1, 7), max(2, 1), max(3, 1)] = [7, 2, 3]. ### Example 2: ```java Input: triplets = [[2,5,6],[1,4,4],[5,7,5]], target = [5,4,6] Output: false ``` ### Constraints: * `1 <= triplets.length <= 1000` * `1 <= triplets.length <= 1000` ### Recommended complexity * Time complexity: $O(n)$ * n is the size of the input array * Space complexity: $O(1)$ ### Solution **Greedy rule:** 刪除不合理的組別,每次挑選可以符合 target[i] 的位置(local optimal) (1) 刪除不合理 只要某個組別(`tri`)中的三個數字有一個(`tri[i]`)大於對應的 target 值(`target[i]`),表示該組不可能對 target 有貢獻。 (2) 挑選剩餘組別 剩餘組別的值必定 <= 對應的 target 值,只有剩餘組別中有一個數字 = 對應的 target 值,則該組必定可以透過交換保留它並湊出最後的 target。 ```python= class Solution: def mergeTriplets(self, triplets: List[List[int]], target: List[int]) -> bool: candidate = set() for tri in triplets: if tri[0] > target[0] or tri[1] > target[1] or tri[2] > target[2]: continue for idx, value in enumerate(tri): if value == target[idx]: candidate.add(idx) return len(candidate) == 3 ``` ## 7. Partition Labels <font color="#ffbb00">**Medium**</font> > You are given a string `s` consisting of lowercase english letters. > > We want to split the string into as many substrings as possible, while ensuring that each letter appears in at most one substring. > > Return a list of integers representing the size of these substrings in the order they appear in the string. ### Example 1: ```java Input: s = "xyxxyzbzbbisl" Output: [5, 5, 1, 1, 1] ``` Explanation: The string can be split into `["xyxxy", "zbzbb", "i", "s", "l"]`. ### Example 2: ```java Input: s = "abcabc" Output: [6] ``` ### Constraints * `1 <= s.length <= 100` ### Recommended complexity * Time complexity: $O(n)$ * n is the length og the string `s` * Space complexity: $O(m)$ * m is the number of unique characters in the string `s` ### Solution (1) 原本的想法 **Greedy rule:** 每次完成一個字母做為局部最佳解 * 根據題意所說,每個字母只能出現在最多一個 substring 中,所以需要計算每個字母剩餘的出現次數/頻率 * 以 two pointer 做搜索,左指標指向目前正在處理的字元,右指標歷遍 substring 字串 * 右指標歷遍字串的過程中,每碰到一個字元就將它的出現次數 - 1,直到左指標指向的字元的頻率 = 0 時表示該字元處理完 * 左指標 + 1 處理下一個字元 * 直到左指標 = 右指標,表示找到 substring 的分界點,再計算 sustring 的長度 ```python= class Solution: def partitionLabels(self, s: str) -> List[int]: count = {} for ch in s: count[ch] = 1 + count.get(ch, 0) LP, RP = 0, 0 start = 0 res = [] while LP < len(s): ch = s[LP] while count[ch] != 0: count[s[RP]] -= 1 RP += 1 LP += 1 if LP == RP: res.append(RP - start) start = RP return res ``` (2) Improvement **Greedy rule:** 每次完成一個字母做為局部最佳解 * 要完成一個字母最快的方式就是直接找到它出現的最後位置,就是該字母需要對應的 substring 長度 * 從頭開始歷遍字串 `s` * 尋找每個字母最後出現的位置並取最大值 * 當最大位置 = 目前正在處理的位置,表示找到 substring 的分界點 * 再計算 substring 長度 ```python= class Solution: def partitionLabels(self, s: str) -> List[int]: lastIndex = {} for idx, ch in enumerate(s): lastIndex[ch] = idx res = [] size = 0 end = 0 for idx, ch in enumerate(s): size += 1 end = max(end, lastIndex[ch]) if idx == end: res.append(size) size = 0 return res ``` ## 8. Valid Parenthesis String <font color="#ffbb00">**Medium**</font> > You are given a string `s` which contains only three types of characters: `'('`, `')'` and `'*'`. > > Return `true` if `s` is **valid**, otherwise return `false`. > > A string is valid if it follows all of the following rules: > > * Every left parenthesis `'('` must have a corresponding right parenthesis `')'`. > * Every right parenthesis `')'` must have a corresponding left parenthesis `'('`. > * Left parenthesis `'('` must go before the corresponding right parenthesis `')'`. > * A `'*'` could be treated as a right parenthesis `')'` character or a left parenthesis `'('` character, or as an empty string `""`. ### Example 1: ```java Input: s = "((**)" Output: true ``` Explanation: One of the `'*'` could be a `')'` and the other could be an empty string. ### Example 2: ```java Input: s = "(((*)" Output: false ``` Explanation: The string is not valid because there is an extra `'('` at the beginning, regardless of the extra `'*'`. ### Constraints * `1 <= s.length <= 100` ### Recommended complexity * Time complexity: as good or better than $O(n)$ * Space complexity: as good or better than $O(n)$ ### Soluton **Greedy rule**: 維護一個可能的左括的出現次數的範圍 每一個 ( 都需要一個 ) 配對才能組成一組括號,因此當發現一個 ( 時左括號的出現次數 + 1,反之出現 ) 時左括號出現次數 - 1。又因為 `*` 符號可以作為 ( 與 ) 使用,所以遇到 `*` 時可以同時 + 1 或 - 1。因此最後計算出的結果 ( 出現次數應該是一個範圍,具有最大值 leftMax(未匹配的 `(` 的最大數量) 與最小值 leftMin(未匹配的 `(` 的最小數量)。 只有 leftMin = 0 的狀況才是合理的,因為每組括號都可以正確閉合。 * 當 leftMax < 0 表示 ) 太多,無法形成合理的括號對 * 當 leftMin < 0 表示出現了多餘的 ),無法形成合理的括號對 ```python= class Solution: def checkValidString(self, s: str) -> bool: leftMin, leftMax = 0, 0 for ch in s: if ch == ')': leftMin, leftMax = leftMin - 1, leftMax - 1 elif ch == '(': leftMin, leftMax = leftMin + 1, leftMax + 1 else: # when * appear leftMin, leftMax = leftMin - 1, leftMax + 1 if leftMax < 0: return False if leftMin < 0: leftMin = 0 return leftMin == 0 ```