# Leetcode刷題學習筆記--Dynamic Programming ## Introduction + 可以從前一個狀態推展到目前的狀態? + 起始狀態是什麼? ## Problems ### [123. Best Time to Buy and Sell Stock III](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/) > 1. 一開始我使用暴力破解,但是時間效率不是很好。 ```cpp= class Solution { enum st{buy, sell}; int helper(vector<int>& prices, int idx, int tr, int prev) { if(idx == prices.size() || tr == 2) return 0; if(~mem[idx][tr][prev]) return mem[idx][tr][prev]; int donothing = helper(prices, idx + 1, tr, prev); int dotrans; if(prev == sell) dotrans = helper(prices, idx + 1, tr, buy) - prices[idx]; // buy else dotrans = helper(prices, idx + 1, tr + 1, sell) + prices[idx]; // sell return mem[idx][tr][prev] = max(dotrans, donothing); } vector<vector<vector<int>>> mem; public: int maxProfit(vector<int>& prices) { int sz = prices.size(); mem.resize(sz, vector<vector<int>>(2, vector<int>(2, -1))); return helper(prices, 0, 0, sell); } }; ``` > 2. 根據官方的解答。這個題目會限制最多兩次,是因為我們可以退化成一次。 > 3. 如果是一次的話,就是一直找自己前面的最小值。 > 4. 兩次的意思是,你可以把vector切成前半部和後半部,分別找出最大的profit。 ```cpp= class Solution { public: int maxProfit(vector<int>& prices) { int sz = prices.size(); if(sz <= 1) return 0; int leftMin = prices[0]; int rightMax = prices[sz - 1]; vector<int> bwd(sz), fwd(sz + 1); // 站在i的位置,往後看,會得到的最大profit for(int i = 1; i < sz; ++i) { bwd[i] = max(bwd[i - 1], prices[i] - leftMin); leftMin = min(leftMin, prices[i]); } // 站在j的位置,往前看,會得到的最大profit for(int j = sz - 2; j >= 0; --j) { fwd[j] = max(fwd[j + 1], rightMax - prices[j]); rightMax = max(rightMax, prices[j]); } int ans{0}; for(int i = 0; i < sz; ++i) ans = max(ans, bwd[i] + fwd[i + 1]); return ans; } }; ``` > 5. 官方的另一種更optimal的想法是,把第一次得到的profit當成是下一次price價格減少。 ```cpp= class Solution { public: int maxProfit(vector<int>& prices) { int t1min = 1e5, t1prof{0}, t2min = 1e5, t2prof{0}; for(auto& p : prices) { t1min = min(t1min, p); t1prof = max(t1prof, p - t1min); // 因為從t1賺的錢,等於是第二次交易,買入價錢減少 t2min = min(t2min, p - t1prof); t2prof = max(t2prof, p - t2min); } return t2prof; } }; ``` ### [1125. Smallest Sufficient Team](https://leetcode.com/problems/smallest-sufficient-team/description/) > 1. 參考[lee215大的解答](https://leetcode.com/problems/smallest-sufficient-team/solutions/334572/java-c-python-dp-solution/?orderBy=most_votes)。 > 2. 這題很棒,對DP會有更深的理解。必做。 > 3. 當選中第i個people的時候,如果把第i個人加入任一個team都會使整體的skill發生變化。 > 4. 但是第i個人加入的前提是,加入後的人數比加入前的人數還要少。 ```cpp= class Solution { public: vector<int> smallestSufficientTeam(vector<string>& req_skills, vector<vector<string>>& people) { int n = req_skills.size(); unordered_map<string, int> shift; for(int i = 0; i < req_skills.size(); ++i) shift[req_skills[i]] = i; // 選中的team所有的skill,最小的team unordered_map<int, vector<int>> m; // 避免hash collision m.reserve(1 << n); for(int i = 0; i < people.size(); ++i) { int skill{0}; for(auto& s : people[i]) skill |= 1 << shift[s]; for(auto& ref : m) { int comb = ref.first | skill; if(!m.count(comb) || m[comb].size() > m[ref.first].size() + 1) { m[comb] = m[ref.first]; m[comb].push_back(i); } } m[skill] = {i}; } return m[(1 << n) - 1]; } }; ``` ### [1908. Game of Nim](https://leetcode.com/problems/game-of-nim/description/) 給你一個vector<int> piles,讓alice和bob輪流玩這個遊戲,每次可以從任意非零的stack中移除最少1個最多全部piles[i],不能移動的那個palyer就是輸。返回alice使否會贏。這邊的前提是player都是optimal,也就是palyer會選對自己最有利的狀態。 > 1. 這題算是萬用題。怎麼說。因為任何題目都可以使用暴力破解,也就是列舉所有的可能性。最後再透過memorization來降低複雜度。 > 2. 這邊的暴力破解就是列舉所以可能的state,一個一個去嘗試,直到有結果。 > 3. 因為player是optimal的,如果你是player一定會選一個對自己最有利的狀況,也就是會下一手讓對手沒機會贏的狀態。也就是這個nextState對手沒機會贏。 > 4. isNextPlayerWin(vector<int>& nextState)定義為,使用nextState這個狀態,對方是否有機會贏。只要一有機會就是return true。因為這樣才能判斷是否是一定可以贏的狀態。 ```cpp= class Solution { string getKey(const vector<int>& piles) { ostringstream oss; for(auto& n : piles) oss << n << "-"; return oss.str(); } bool isNextPlayerWin(vector<int>& piles) { string key = getKey(piles); if(m.count(key)) return m[key]; for(int i = 0; i < piles.size(); ++i) { for(int j = 1; j <= piles[i]; ++j) { vector<int> nextState(piles); nextState[i] -= j; sort(nextState.begin(), nextState.end()); // 排序很重要,避免重複的計算 if(!isNextPlayerWin(nextState)) { //nextState 對方絕對不會贏,就會採取這個策略, // 因為選手是optimal return m[key] = true; } } } // 沒有一種狀態是"一定"會贏的,所以return false return m[key] = false; } unordered_map<string, bool> m; public: bool nimGame(vector<int>& piles) { m[getKey(vector<int>(piles.size()))] = 0; return isNextPlayerWin(piles); // the first Next Player is Alice } }; ``` ### [96. Unique Binary Search Trees(Medium)](https://leetcode.com/problems/unique-binary-search-trees/) 給定一個數字n,則1~n之間可以組成多少個Binary Search Tree.首先Binary search tree的定義是: > 左邊的BTS一定都比root還小。 > 右邊的BTS一定都比root還大。 n = 1時,結果為1。 n = 2時,有兩個數字[1,2],當1為root時,2只能為右邊node。當 2為root時,1只能為左邊node,所以結果為2。 n = 3時,有三個數字[1,2,3], 當1為root時,[]左邊子樹沒有,[2,3]的排列為右邊子樹。 當2為root時,[1]為左邊子樹, [3]為右邊子樹。 當3為root時,[1,2]的排列左邊子樹,[]右邊子樹沒有。 可以看出規律,[左邊子樹個數,右邊子樹個數] dp[3] = [0, 2] + [1, 1] + [2, 0] 因為左右兩邊相乘的關係,所以dp[0] = 1。 ![](https://i.imgur.com/6jAydf0.png) ```cpp= int numTrees(int n) { vector<int> dp(n + 1, 0); dp[0] = 1; for(int i = 1; i < n + 1; ++i) { for(int j = 0; j < i; ++j) dp[i] += dp[j] * dp[i - 1 - j]; } return dp.back(); } ``` ### [368. Largest Divisible Subset(Medium)](https://leetcode.com/problems/largest-divisible-subset/) 給你一個vector<int> nums,找出最大的subset滿足,任兩個數都是倍數關係。並且輸出這個subset。 例如 : nums = [1, 2, 3];可以得到兩個subset[1, 2]和[1, 3],其中1和3為倍數關係,1和2為倍數關係。 解題思路: > 1. 先排序,這樣只需要找比自己小的數字即可。 > 2. 使用DP,可以算出最大的subset。 > 3. 如果還要輸出subset內容,就要另外一個vector來記錄前一個節點。 ![](https://i.imgur.com/gAYRy21.png) ```cpp= vector<int> largestDivisibleSubset(vector<int>& nums) { auto n = nums.size(); if (n == 1) return nums; // 只有一個不用計算直接return sort(nums.begin(), nums.end()); // 排序 vector<int> dp(n, 1); vector<int> idx(n, -1); int maxidx{0}; int maxlength{0}; for(int i = 1; i < n; ++i) { for(int j = 0; j < i; ++j) { // 只找比i還小的數 if(nums[i] % nums[j] == 0) { // 倍數關係 if(dp[j] + 1 > dp[i]) { // 比現在還大 dp[i] = dp[j] + 1; idx[i] = j; // 紀錄前一個node } } } if(dp[i] > maxlength) { //找出最大subset maxlength = dp[i]; maxidx = i; } } vector<int> ans; ans.push_back(nums[maxidx]); int p = idx[maxidx]; while(p != -1) { ans.push_back(nums[p]); p = idx[p]; } return ans; } ``` ### [152. Maximum Product Subarray(Medium)](https://leetcode.com/problems/maximum-product-subarray/) 給你一個vector<int>找出,subarray中相乘數字最大的值。 > 1. subarray為連續的element. > 2. 最大值 = (負數) * (負最大值) or (正數) * (正最大值) > 3. 必須同時記錄"負最大值"和"正最大值"。 ```cpp= int maxProduct(vector<int>& nums) { auto n = nums.size(); int curr_max, curr_min, maxval; maxval = curr_max = curr_min = nums[0]; for(int i = 1; i < n; ++i) { int tmp = curr_max; curr_max = max(nums[i], max(nums[i] * curr_max, nums[i] * curr_min)); curr_min = min(nums[i], min(nums[i] * tmp, nums[i] * curr_min)); maxval = max(maxval , curr_max); } return maxval; } ``` ### [213. House Robber II(Medium)](https://leetcode.com/problems/house-robber-ii/) 這題是[198. House Robber](https://leetcode.com/problems/house-robber/)的延伸。除了相鄰點不可以偷之外,還多了頭尾相接的條件。 > 1. 解法和House Robber一樣,但是最後的node必須根據node-0有沒有偷來決定,所以使用兩個array來記錄。 > 2. 一個array紀錄有偷node-0,一個array來記錄沒偷node-0。 > 3. ==初始化array就變的很重要,因為偷了node-0,所以rob0[1]就不能偷。== ```cpp= int rob(vector<int>& nums) { auto n = nums.size(); if(n == 1) return nums[0]; if(n == 2) return max(nums[0], nums[1]); vector<int> rob0(n, 0); vector<int> rob1(n, 0); rob0[0] = nums[0]; rob0[1] = rob0[0]; // important rob1[1] = nums[1]; for(int i = 2; i < n; ++i) { if(i == n - 1) { rob0[i] = rob0[i - 1]; rob1[i] = max(rob1[i - 2] + nums[i], rob1[i - 1]); } else { rob0[i] = max(rob0[i - 2] + nums[i], rob0[i - 1]); rob1[i] = max(rob1[i - 2] + nums[i], rob1[i - 1]); } } return max(rob0[n - 1], rob1[n - 1]); } ``` ### [337. House Robber III(Medium)](https://leetcode.com/problems/house-robber-iii/) 給一個binary tree,規則一樣如果偷相鄰兩個node就會通報警察。求出在不通報警察的情況下,可以得到的最佳金額。 > 1. 一開始我用house robber的想法去解,從root推到leaf會遇到沒辦法求出最佳報酬的問題。因為會有加入重複數字的問題。 > 2. 最好的解髮是從leaf推展到root,這樣就不會有加入重複數字的問題。 > 3. ==關鍵在於"不打劫"這個節點,則子節點也不一定要打劫。== ```cpp= class Solution { int rub = 0; int norub = 1; public: int rob(TreeNode* root) { vector<int> rtn = dfs(root); return max(rtn[rub], rtn[norub]); } vector<int> dfs(TreeNode *root) { if(!root) return vector<int>(2, 0); vector<int> left = dfs(root->left); vector<int> right = dfs(root->right); vector<int> rtn(2, 0); // 打劫這個node,則兩個子節點就不能打劫。 rtn[rub] = left[norub] + right[norub] + root->val; // 不打劫這個node,則可打劫子節點,也可以不打劫子節點 // 因為是要最大的報酬,所以取最大值。 rtn[norub] = max(left[norub], left[rub]) + max(right[norub], right[rub]); return rtn; } }; ``` ### [740. Delete and Earn(Medium)](https://leetcode.com/problems/delete-and-earn/) 題目是想從array中找出最大的points,但是每次取一個數num,就必須把前後的數砍掉(num - 1, num + 1)。 > 1. 這個問題其實也是House Robber,因為相鄰的數字不能取。 > 2. 解題的重點是使用了一個vector<int>(10001, 0),因為題目跟我們說數字的值域從1到10000。 > 3. 可以使用map但是相對的判斷和前後取值會麻煩很多。 ```cpp= int deleteAndEarn(vector<int>& nums) { vector<int> sums(10001, 0); for (int num : nums) sums[num] += num; for (int i = 2; i < 10001; ++i) { sums[i] = max(sums[i - 1], sums[i - 2] + sums[i]); } return sums[10000]; } ``` > 1. 因為數字分散,所以先使用map來統計每個數字有幾個。 > 2. traverse整個map,如果目前和前一個差1就只能選目前,或是前兩個。 ```cpp= int deleteAndEarn(vector<int>& nums) { map<int, int> m; //統計每個數字出現的次數 for(auto& n : nums) m[n]++; auto n = m.size(); vector<int> dp(n); // 取第一個最小的數字 auto it = m.begin(); dp[0] = it->first * it->second; it++; for(int i = 1; i < n; ++i, ++it) { int cur = it->first * it->second; // 如果和前一個數字差一 if(it->first == (prev(it)->first + 1)) { // special case,只有兩個數字,就是看要取哪一個最大 if(i == 1) dp[i] = max(dp[i - 1], cur); else dp[i] = max(dp[i - 1], cur + dp[i - 2]); } else dp[i] = dp[i - 1] + cur; // 和前面數字不是差一,所以可以拿 } return dp.back(); } ``` ### [5. Longest Palindromic Substring(Medium)](https://leetcode.com/problems/longest-palindromic-substring/) 給你一個字串s,找出當中最長的回文子字串。 > 1. 每個字母都是長度1的回文。 > 2. 長度2的回文,兩個字母都一樣。 > 3. 長度3以上的回文,去掉頭尾也是回文。 ``` string longestPalindrome(string s) { auto n = s.length(); if(n == 1) return s; int len{1}, idx{0}; // declare an array int dp[n][n]; for(int i = 0; i < n; ++i) for(int j = 0; j < n; ++j) dp[i][j] = 0; for(int right = 0; right < n; ++right) { dp[right][right] = 1; for(int left = 0; left < right; ++left) { if(s[left] != s[right]) continue; int ll = right - left + 1; // 長度3以上的回文,去掉頭尾也是回文。 if(ll > 2 && dp[left + 1][right - 1]) dp[left][right] = max(dp[left + 1][right - 1] + 2, dp[left][right]); // 如果長度只有2 if(ll == 2) dp[left][right] = max(2, dp[left][right]); // 如果是回文,也長度(ll)比len還大,更新len和idx if(dp[left][right] && ll > len) { len = ll; idx = left; } } } return s.substr(idx, len); } ``` 2022/1/31 > 1. 因為迴文是從string中選出left和right,所以一個迴文用兩個變數來表示,使用2D DP。 ```cpp= string longestPalindrome(string s) { auto n = s.length(); vector<vector<int>> dp(n, vector<int>(n)); int idx{0}, maxlen{1}, right, left; for(right = 0; right < n; ++right) { // 只有一個char一定是回文 dp[right][right] = 1; for(left = 0; left < right; ++left) { // 當左右都是一樣,且長度小於等於3或是left, right縮小一個也是回文 dp[left][right] = (s[left] == s[right] && (right - left <= 2 || dp[left + 1][right - 1])); // 紀錄最長的情況 if(dp[left][right] && right - left + 1 > maxlen) { maxlen = right - left + 1; idx = left; } } } return s.substr(idx, maxlen); } ``` 2022/06/14 用一個vector<vector<int>> dp來記錄,從i到j的substring是不是palindrome。簡單不容易出錯的方法就是好方法。 ```cpp= string longestPalindrome(string s) { int sz = s.size(); if(sz == 1) return s; vector<vector<int>> dp(sz, vector<int>(sz)); int maxlen = 1, idx = 0; // dp[i][j] = (s[i] == s[j] && dp[i + 1][j - 1]) // 因為dp[i + 1][j - 1] 所以i由大到小,j由小到大 for(int i = sz - 1; i >= 0; --i) { for(int j = i; j < sz; ++j) { if(i == j) dp[i][j] = 1; else if(i + 1 == j) { if(s[i] == s[j]) dp[i][j] = 1; } else { dp[i][j] = (s[i] == s[j] && dp[i + 1][j - 1]); } if(dp[i][j] && j - i + 1 > maxlen) { maxlen = j - i + 1; idx = i; } } } // time complexity : O(N^2) // space complexity : O(N^2) return s.substr(idx, maxlen); } ``` ### [91. Decode Ways(Medium)](https://leetcode.com/problems/decode-ways/) A->1, B->2, C->3,以此類推到Z->26,可以編碼成一串數字。試問這串數字有幾種解碼方式。 > 1. 自己可以解碼(s[i] != 0),則解碼的方式和dp[i - 1]一樣。 > 2. 自己可以和前面合併起來解碼(10~26),則解碼的方式和dp[i - 2]一樣。 > 3. 把這兩種相加就是從0~i的解碼方式dp[i]。 ```cpp= int numDecodings(string s) { auto n = s.length(); if(n == 0 || s[0] == '0') return 0; vector<int> dp(n); dp[0] = 1; for(int i = 1; i < n; ++i) { // s[i]可以自己一個解碼,所以有dp[i - 1]種組合 dp[i] += (s[i] == '0' ? 0 : dp[i - 1]); // s[i]可以和前面合併一起解碼,所以有dp[i - 2]種組合 if(s[i - 1] == '1' || (s[i - 1] =='2' && s[i] <= '6')) dp[i] += (i>= 2 ? dp[i - 2] : 1); } return dp[n - 1]; } ``` ### [1143. Longest Common Subsequence(Medium)](https://leetcode.com/problems/longest-common-subsequence/) 給你兩個字串text1和text2,求出兩個共同的subsequence最大值。 subsequence定義就是char出現的順序不變,但是在text中不用連續。 ![](https://i.imgur.com/mZj7m1J.png) > 1. dp[i][j]的定義是text1[0-i]和text2[0-j]共同的subsequence的最大長度。 > 2. case 1 : 如果text1[i] == text2[j],則dp[i][j] = dp[i - 1][j - 1] + 1; > 3. case 2 : 如果text1[i] != text2[j],則dp[i - 1][j]和dp[i][j - 1]選一個最大的。 > 4. 因為都會存取i - 1或是j - 1,所以vector的大小給大一點(n + 1)。 ```cpp= int longestCommonSubsequence(string text1, string text2) { auto n1 = text1.length(); auto n2 = text2.length(); vector<vector<int>> dp(n1 + 1, vector<int>(n2 + 1, 0)); int rtn{0}; for(int i = 1; i < n1 + 1; ++i) { for(int j = 1; j < n2 + 1; ++j) { if(text1[i - 1] == text2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1; else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); rtn = max(rtn, dp[i][j]); } } return rtn; } ``` ### [72. Edit Distance(Hard)](https://leetcode.com/problems/edit-distance/) 給你兩個字串word1和word2,一次可以replace, insert和delete一個字元,求最小的次數可以把word1變成word2。 ```cpp= int minDistance(string word1, string word2) { int m = word1.size(), n = word2.size(); vector<vector<int>> dp(m + 1, vector<int>(n + 1)); for (int i = 0; i <= m; ++i) dp[i][0] = i; for (int i = 0; i <= n; ++i) dp[0][i] = i; for (int i = 1; i <= m; ++i) { for (int j = 1; j <= n; ++j) { // 字元相同,則不用作任何操作。 if (word1[i - 1] == word2[j - 1]) { dp[i][j] = dp[i - 1][j - 1]; } else { //字元不同,則為了讓word1[0-i]等於word2[0-j], //使用了 replace, delete,insert的最小值 dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1; } } } return dp[m][n]; } ``` ### [983. Minimum Cost For Tickets(Medium)](https://leetcode.com/problems/minimum-cost-for-tickets/) 給你一個vector<int> days來表示一年中1~365哪一天會去旅行。另外vector<int> costs來表示1日卷,7日卷,和30日卷的價錢,求出旅行完days規定的天數後,最小的cost。 > 1. 因為需要檢查days的天數,所以放入set可以快速檢視是否在days裡面。 > 2. 這一天(day)的旅費是 day - 1使用1日卷,或是day-7使用7日卷,或是day-30使用30日卷的最小值。 > 3. 如果這一天不旅行,則旅費和前一天一樣。 ```cpp= int mincostTickets(vector<int>& days, vector<int>& costs) { // 放入set就不用一直檢查是否有重複的數字 unordered_set<int> s(days.begin(), days.end()); int lastday = days.back(); vector<int> dp(lastday + 1); for(int i = 1; i <= lastday; ++i) { if(s.count(i)) { // 檢查買1日, 7日或是30日比較划算 // 使用max(0, i - 7) 來避免取到負數 dp[i] = min(dp[max(0, i - 1)] + costs[0], min(dp[max(0, i - 7)] + costs[1], dp[max(0, i - 30)] + costs[2])); } else dp[i] = dp[i - 1]; // 和前一日一樣 } return dp.back(); } ``` ### [518. Coin Change 2](https://leetcode.com/problems/coin-change-2/) 給你數個coins,求出可以排列成target的組合數目。 ```cpp= int change(int amount, vector<int>& coins) { vector<int> dp(amount + 1, 0); dp[0] = 1; for (int coin : coins) { // 這邊從最前面找到最後面,因為可以重複使用coins中的數字 for (int i = coin; i <= amount; ++i) { dp[i] += dp[i - coin]; } } return dp[amount]; } ``` 另一種top-down的做法,每一個coins可以選擇要不要取。所以有兩個選項。 ```cpp= class Solution { int helper(int amount, vector<int>& coins, int idx) { if(amount == 0) return 1; if(idx == -1) return 0; if(mem[amount][idx] > -1) return mem[amount][idx]; int rtn; int nochange = helper(amount, coins, idx - 1); int dochange = 0; if(coins[idx] <= amount) dochange = helper(amount - coins[idx], coins, idx); return mem[amount][idx] = nochange + dochange; } vector<vector<int>> mem; public: int change(int amount, vector<int>& coins) { int sz = coins.size(); mem.resize(amount + 1, vector<int>(sz, -1)); return helper(amount, coins, sz - 1); } }; ``` ==不可以重複使用的情況,請參考[Target Sum](/FbvqYdi1SsCc-R96j0jVAA#494-Target-Sum)== ### [2218. Maximum Value of K Coins From Piles](https://leetcode.com/problems/maximum-value-of-k-coins-from-piles/) 給你k個piles,每個piles像一個stack一樣,只能依序從第一個開始拿,你只能拿k個coins,試問可以拿到的最大值。 > 1. 因為最多可以拿k個, > 2. 所以對第一個piles來說有k+1個選項,不拿(0),拿一個(1),...拿k個(k)。 > 3. 當第一個piles拿了n個,則後面piles只能分配到k - n個。 > 4. 所以helper的兩個input argument,第一個為目前在的piles,第二個為還剩下多少個k > 5. 和[Coin change2](https://hackmd.io/474VEENxSVSGMRRRtfvetQ#518-Coin-Change-2)類似,只是現在每一個有k+1個選項。 ```cpp= class Solution { int sz; vector<vector<int>> mem; int helper(vector<vector<int>>& piles, int from, int k) { if(from == sz || k == 0) return 0; if(mem[from][k] > 0) return mem[from][k]; int res = helper(piles, from + 1, k); // 不取 int cur = 0; //從piles from取1個,2個...N個 for(int i = 0; i < min((int)piles[from].size(), k); ++i) { cur += piles[from][i]; //改取下一個piles,且剩下k - i - 1 coins res = max(res, helper(piles, from + 1, k - i - 1) + cur); } return mem[from][k] = res; } public: int maxValueOfCoins(vector<vector<int>>& piles, int k) { this->sz = piles.size(); mem.resize(sz, vector<int>(k + 1)); return helper(piles, 0, k); } }; ``` 2023/04/15 daily challenge但是寫出了TLE的解答。 > 1. 當初的想法是,每個piles有兩種可能,拿或是不拿 > 2. 因為只用了拿和不拿,所以必須知道拿到第幾個。 > 3. 所以先把piles反轉,從後面決定拿與不拿。 > 4. 這樣其實條件比較複雜,因為每次會跟當下的piles相關。 ```cpp= class Solution { int helper(vector<vector<int>>& piles, int k) { if(k == 0) return 0; int ans{0}; for(auto& p : piles) { if(p.size() == 0) continue; int back = p.back(); p.pop_back(); int take = helper(piles, k - 1) + back; p.push_back(back); ans = max(ans, take); } return ans; } public: int maxValueOfCoins(vector<vector<int>>& piles, int k) { for(auto& p : piles) reverse(p.begin(), p.end()); return helper(piles, k); } }; ``` 參考了[lee215的解答](https://leetcode.com/problems/maximum-value-of-k-coins-from-piles/solutions/1887010/java-c-python-top-down-dp-solution/?orderBy=most_votes)。 > 1. 其實想法和上面每個piles取與不取類似。 > 2. 從一個piles[i]的角度來看所有的組合就是,取0個, 取1個 到 取min(piles[i].size(), k) > 3. 所以從上面所有的組合中再看helper(piles, idx + 1, rem - j - 1) 的所有組合。 ```cpp= class Solution { int n; int helper(vector<vector<int>>& piles, int idx, int rem) { if(idx == n || rem == 0) return 0; if(mem[idx][rem]) return mem[idx][rem]; // ans 從取0個開始 int cur{0}, ans = helper(piles, idx + 1, rem); // 取1個,取2個,...,取min(piles[idx], rem)個 for(int j = 0; j < rem && j < piles[idx].size(); ++j) { cur += piles[idx][j]; ans = max(ans, helper(piles, idx + 1, rem - j - 1) + cur); } return mem[idx][rem] = ans; } vector<vector<int>> mem; public: int maxValueOfCoins(vector<vector<int>>& piles, int k) { n = piles.size(); mem.resize(n, vector<int>(k + 1)); return helper(piles, 0, k); } }; ``` ### [2420. Find All Good Indices](https://leetcode.com/problems/find-all-good-indices/) 判斷是否為good indices。條件是在第i個的前k個是遞減數列,後k個是遞增數列。 > 1. 只會使用brust force結果就TLE。 > 2. 再來想到用2D DP(vector<vector<bool>>)來記錄開始和結束是遞增或是遞減,但是這樣做沒什麼效率。可能是想到判斷回文的關係。 > 3. 使用1D DP來記錄到目前為止,遞增或是遞減的數列長度。 > 4. 最後只要判斷i的前後是否大於等於k即可。 ```cpp vector<int> goodIndices(vector<int>& nums, int k) { int sz = nums.size(); vector<int> fwd(sz, 1), bwd(sz, 1); for(int i = 1, j = sz - 2; i < sz; ++i, --j) if(nums[i] <= nums[i - 1]) fwd[i] = fwd[i - 1] + 1; for(int i = sz - 2; i >= 0; --i) if(nums[i] <= nums[i + 1]) bwd[i] = bwd[i + 1] + 1; vector<int> ans; for(int i = k; i < sz - k; ++i) { if(fwd[i - 1] >= k && bwd[i + 1] >= k) ans.push_back(i); } return ans; } ``` ### [1031. Maximum Sum of Two Non-Overlapping Subarrays](https://leetcode.com/problems/maximum-sum-of-two-non-overlapping-subarrays/description/) > + 因為fistLen和secondLen可以在任意位置,不好思考。 > + 先假設firstLen一定在secondLen的前面。 > + 則在 idx = i 的時候,後面subarray的firstLen最大值 + 前面subarray的secondLen的最大值和, > + 即為 idx = i 的時候的最好情況,所以我們要找出所有index的情況 > + 在考慮 secondLen和firstLen的情況。 > + 為什麼是dynamic programming? 因為dp1[i] = max(dp[i - 1], sum);每次都紀錄從i往前/往後看的最大值。 ```cpp= class Solution { public: int helper(vector<int>&nums,int x,int y){ int n=nums.size(); vector<int>dp1(n,0),dp2(n,0); int sum=0; for(int i=0;i<n;i++){ if(i<x){//when we haven't considered x-size array sum+=nums[i]; dp1[i]=sum; } else{ //when we have a window of size x sum=sum+nums[i]-nums[i-x]; dp1[i]=max(dp1[i-1],sum); } } sum=0; for(int i=n-1;i>=0;i--){ if(i+y>n-1) { //case when we haven't encountered a window of size secondlen ie. y sum+=nums[i]; dp2[i]=sum; } else{//when we have a window of size secondLen sum=sum+nums[i]-nums[i+y]; dp2[i]=max(dp2[i+1],sum); } } int ans=0; //our ans window will be from x-1 to n-y for(int i=x-1;i<n-y;i++){ ans=max(ans,dp1[i]+dp2[i+1]); } return ans; } int maxSumTwoNoOverlap(vector<int>& nums, int firstLen, int secondLen){ return max(helper(nums, firstLen, secondLen),helper(nums, secondLen, firstLen)); } }; ``` ### [2466. Count Ways To Build Good Strings](https://leetcode.com/problems/count-ways-to-build-good-strings/) > 1. why dynamic programming? > 因為string長度為len的字串,可以從string長度為len - zero或是len - one來推得。 > 2. 建立string的條件就是在string後面增加某些zero或是某些one。 > 3. 為什麼要走訪全部的lenght直到high? > 因為長度為len的string是由更短的字串所建立而成,所以必須要從長度為1開始走訪。 > 4. 從前一個狀態推展到目前狀態? > ```cpp= > if(i - zero > 0) dp[i] += dp[i - zero]; > if(i - one >= 0) dp[i] += dp[i - one]; > ``` > 5. 起始狀態,dp[0] = 1,因為當長度跟one或是zero一樣的時候,就是使用這個來建立字串。 ```cpp= class Solution { // dynamic programming // 從上一個狀態,推展到這個狀態 // 這個狀態就是長度i的string, 會得到這個長度的string有四種可能 // 1. 從 i - zero長度的string, append zero // 2. 從 i - one 長度的string, append one // 3. zero自己本身是長度i // 4. one自己本身是長度i unordered_map<int, int> m; // len, count int mod = 1e9 + 7; public: int countGoodStrings(int low, int high, int zero, int one) { /* int ans = 0; for(int i = 1; i <= high; ++i) { if(m.count(i - zero)) m[i] = m[i - zero]; if(m.count(i - one)) m[i] = (m[i] + m[i - one]) % mod; if(i == zero) m[i]++; if(i == one) m[i]++; //cout << i << "," << m[i] << endl; if(i >= low) ans = (ans + m[i]) % mod; } return ans; */ vector<int> dp(high + 1); // len dp[0] = 1; int ans{0}; for(int i = 1; i <= high; ++i) { if(i - zero >= 0) dp[i] = (dp[i] + dp[i - zero]) % mod; if(i - one >= 0) dp[i] = (dp[i] + dp[i - one]) % mod; if(i >= low) ans = (ans + dp[i]) % mod; } return ans; } }; // low = 2, high = 3, zero = 1, one = 2 // len = 1, 2, 3 // len = 1 -> 0 // len = 2 -> 00, 11 // len = 3 -> 000, 110, 011 ``` ### [1130. Minimum Cost Tree From Leaf Values](https://leetcode.com/problems/minimum-cost-tree-from-leaf-values/description/) > + 一開始解不出來是因為題目沒弄懂。 > + left node是從array依序放入,重點是依序。 > + 往上傳是left或是right的最大值。 > + 因為需要同時知道sum和left/right的最大值,所以使用pair來當return值。 ```cpp= class Solution { //sum, max(left, right) pair<int, int> helper(vector<int>& arr, int i, int j) { pair<int, int> rtn = {INT_MAX, INT_MAX}; if(~mem[i][j].first) return mem[i][j]; if(i == j) rtn = {0, arr[i]}; else { for(int k = i; k < j; ++k) { auto l = helper(arr, i, k); auto r = helper(arr, k + 1, j); // sum即為兩邊做大值的product加上兩邊的和 int sum = l.second * r.second + l.first + r.first; if(rtn.first > sum) { rtn.first = sum; // 題意是說left或是right的最大值 rtn.second = max(l.second, r.second); } } } return mem[i][j] = rtn; } vector<vector<pair<int, int>>> mem; public: int mctFromLeafValues(vector<int>& arr) { int sz = arr.size(); mem.resize(sz, vector<pair<int, int>>(sz, make_pair(-1, -1))); return helper(arr, 0, sz - 1).first; } }; ``` ### [1262. Greatest Sum Divisible by Three](https://leetcode.com/problems/greatest-sum-divisible-by-three/description/) 給你一個vector<int>,返回最大的element sum,可以被3整除。 > 1. [Greedy的解法](https://hackmd.io/p9hSX0fpSQGwCUL3QF7cLA#1262-Greatest-Sum-Divisible-by-Three)是,全部加起來後,看餘數是多少減掉餘數最小的值。 > 2. 參考[lee215](https://leetcode.com/problems/greatest-sum-divisible-by-three/solutions/431077/java-c-python-one-pass-o-1-space/)的答案,發現DP才是最漂亮的解法。 > 3. 從第n個數值的角度來看,他可以和前面三種狀態(remain = 0, 1, 2),組合出新的sum,並且更新vector<int> dp。 ```cpp= int maxSumDivThree(vector<int>& nums) { vector<int> dp(3); for(auto& n : nums) { // n和每個可能組合成一個新的 for(auto& i : vector<int>(dp)) { // copy constructor 因為我們取用的資料不能被update dp[(i + n) % 3] = max(dp[(i + n) % 3], i + n); } } return dp[0]; } ``` ### [1911. Maximum Alternating Subsequence Sum](https://leetcode.com/problems/maximum-alternating-subsequence-sum/description/) ```cpp= class Solution { vector<vector<long long>> mem; long helper(vector<int>& nums, int idx, int mul) { if(idx == nums.size()) return 0; int i = mul != -1; if(mem[idx][i] != LONG_MIN) return mem[idx][i]; long long take = helper(nums, idx + 1, -mul) + nums[idx] * mul; long long notake = helper(nums, idx + 1, mul); return mem[idx][i] = max(take, notake); // mem[idx][0] = max(mem[idx + 1][1] + nums[idx], mem[idx + 1][0]); // mem[idx][1] = max(mem[idx + 1][0] - nums[idx], mem[idx + 1][1]) } public: long long maxAlternatingSum(vector<int>& nums) { /* top-down : memorization mem.resize(nums.size(), vector<long long>(2, LONG_MIN)); return helper(nums, 0, 1); */ // bottom-up : tabulation /*enum st{even, odd}; vector<vector<long long>> dp(2, vector<long long>(nums.size() + 1)); for(int i = nums.size() - 1; i >= 0; --i) { dp[even][i] = max(dp[even][i + 1], dp[odd][i + 1] + nums[i]); dp[odd][i] = max(dp[odd][i + 1], dp[even][i + 1] - nums[i]); } return max(dp[even][0], dp[odd][0]); // time complexity : O(N) // space complexity : O(N) */ // reduce the space long long even{}, odd{}; for(int i = nums.size() - 1; i >= 0; --i) { even = max(even, odd + nums[i]); odd = max(odd, even - nums[i]); } return max(even, odd); // time complexity : O(N) // space complexity : O(1) } }; ``` ### [1751. Maximum Number of Events That Can Be Attended II](https://leetcode.com/problems/maximum-number-of-events-that-can-be-attended-ii/description/) 給你一個vector<vector<int>> events,其中有start, end和value, 選的events不能重疊,返回最大的value sum。 > + 因為選這個event必須和上一個不重疊 > 1. 一開始會用prev, current來表示,但是這樣就會多一個變數 > 2. 如果確定要選current,那下一個可以直接跳到**可以選擇**的next,而不是一個一個跳,這樣就可以避免使用兩個變數。 ```cpp= class Solution { int n; vector<vector<int>> dp; enum st{start, end, value}; int helper(vector<vector<int>>& events, int cur, int k) { if(cur == n || k == 0) return 0; if(~dp[cur][k]) return dp[cur][k]; int next; // 找出如果選擇cur的下一個可以選擇的event for(next = cur + 1; next < n; ++next) if(events[cur][end] < events[next][start]) break; return dp[cur][k] = max(helper(events, cur + 1, k), helper(events, next, k - 1) + events[cur][value]); // dp[cur][k] = max(dp[cur + 1][k], dp[next][k - 1] + events[cur][value]); // cur // /|\ // k -->| } public: int maxValue(vector<vector<int>>& events, int k) { n = events.size(); dp.resize(n, vector<int>(k + 1, -1));// O(N) sort(events.begin(), events.end()); // O(NlogN) // 因為使用memorizationation把n, k的組合都找一次 // 所以是O(NK),但是中間有找next的for loop // 所以是O(NK*N) // 如果找next改成binary search,則O(NKlogN) // time : O(N + NlogN + NKN) = O(N^2K) // space : O(NK) return helper(events, 0, k); } }; ``` ### [1444. Number of Ways of Cutting a Pizza](https://leetcode.com/problems/number-of-ways-of-cutting-a-pizza/description/) 給你一個MxN的pizza,試問切成k等分,每個pizza都要有一個以上的蘋果,試問有多少種的切法。 > 1. 先定義切法,不管垂直切或是水平切,切下去之後的上方或是左方給出去,剩下的再繼續切。 > 2. 所以helper(y, x, k),表示剩下的pizza從(y, x)開始到(m - 1, n - 1)。 > 3. 使用apples來統計從(y, x)往下看剩下的apples方便以後的計算。 > 4. dynamic programming其實也是一種暴力破解,只是有記憶功能。 ```cpp= class Solution { int m, n, mod; vector<vector<int>> apples; int helper(int y, int x, int k) { int& ans = mem[y][x][k]; if(~ans) return ans; if(k == 1) return ans = apples[y][x] > 0; else { int ways{}; //cut horizontal for(int i = y + 1; i < m; ++i) { if(apples[y][x] - apples[i][x] > 0) ways = (ways + helper(i, x, k - 1)) % mod; } // cur vertical for(int i = x + 1; i < n; ++i) { if(apples[y][x] - apples[y][i] > 0) ways = (ways + helper(y, i, k - 1)) % mod; } return ans = ways; } } vector<vector<vector<int>>> mem; public: int ways(vector<string>& pizza, int k) { m = pizza.size(); n = pizza[0].size(); mod = 1e9 + 7; apples.resize(m + 1, vector<int>(n + 1)); for(int y = m - 1; y >= 0; --y) // O(MN) for(int x = n - 1; x >= 0; --x) apples[y][x] = apples[y + 1][x] + apples[y][x + 1] - apples[y + 1][x + 1] + (pizza[y][x] == 'A'); mem.resize(m, vector<vector<int>>(n, vector<int>(k + 1, -1))); return helper(0, 0, k); // time : O(MNK) // space : O(MN + MNK) } }; ``` ### [1048. Longest String Chain](https://leetcode.com/problems/longest-string-chain/description/?envType=daily-question&envId=2023-09-23) 找出最長的string chain,string chain的定義是,字串刪掉一個char會等於前一個字串。 > 1. 因為string chain是字數少的在前面,所以對words針對size排序。 > 2. 嘗試所有刪掉一個char的可能,來找出前一個string。 ```cpp= int longestStrChain(vector<string>& words) { sort(begin(words), end(words), [](auto& str1, auto& str2){ // O(NlogN) return str1.size() < str2.size(); }); unordered_map<string, int> m; int ans{}; for(auto& w : words) { // O(NM) for(int i = 0; i < w.size(); ++i) { string prev = w.substr(0, i) + w.substr(i + 1); m[w] = max(m[w], m[prev] + 1); } ans = max(ans, m[w]); } return ans; // time : O(NM + NlogN) = O(NM) // space : O(logN + N) = O(N) } ``` ### [2646. Minimize the Total Price of Trips](https://leetcode.com/problems/minimize-the-total-price-of-the-trips/description/) 給你一個undirected graph,每個node都有不同的price。給你一個vector<vector<int>> Trips,求所有路徑經過node的和為最小。在開始前可以==把不相鄰的node價錢砍半==。 > 1. 統計每個node經過的次數。 > 2. 不相鄰的node價錢砍半,和house robber一樣, 如果parent node價錢砍半,則child node價錢一定不能砍半。 如果parent node價錢沒砍半,則child node可以砍半,也可以不砍半。 > 3. 使用dynamic programming來找出最小值。 > 4. 為什麼memory vector不需要紀錄parent? 因為parent在之後的f()用不到。 ```cpp= class Solution { unordered_map<int, vector<int>> adj; vector<int> freq; vector<vector<int>> mem; bool dfs(int p, int s, int e) { if(s == e) return true; for(auto& nxt : adj[s]) { if(nxt == p) continue; freq[nxt]++; if(dfs(s, nxt, e)) return true; freq[nxt]--; } return false; } // 為什麼mem不需要紀錄parent? // helper(node, parent, parentHalved) = f(helper(nxt, node, 0), helper(nxt, node, 1)) // f() 中不需要使用parent,parent是由node取代,nxt也是由node得到 // 所以mem不須紀錄parent int helper(vector<int>& price, int node, int parent = -1, int parentHalved = 0) { if(~mem[node][parentHalved]) return mem[node][parentHalved]; int res1 = freq[node] * price[node] / 2; int res2 = freq[node] * price[node]; for(auto& nxt : adj[node]) { if(nxt == parent) continue; res2 += helper(price, nxt, node, 0); } if(parentHalved) return mem[node][parentHalved] = res2; // 如果parent價錢砍半,則目前的node不能砍半 for(auto& nxt : adj[node]) { if(nxt == parent) continue; res1 += helper(price, nxt, node, 1); } return mem[node][parentHalved] = min(res1, res2); // 如果parent價錢沒砍半,則目前的node可以砍半,或是不用砍半 } public: int minimumTotalPrice(int n, vector<vector<int>>& edges, vector<int>& price, vector<vector<int>>& trips) { for(auto& e : edges) { adj[e[0]].push_back(e[1]); adj[e[1]].push_back(e[0]); } freq.resize(n); for(auto& t : trips) { freq[t[0]]++; dfs(-1, t[0], t[1]); } mem.resize(n, vector<int>(2, -1)); return helper(price, 0); } }; ``` ### [1239. Maximum Length of A Concatenated String with Unique Characters](https://leetcode.com/problems/maximum-length-of-a-concatenated-string-with-unique-characters/description) > 這題可以使用不同形式的DP來求解,重點是如何快速判斷string之間有unique char,使用bit manipulation。 ```cpp= class Solution { vector<int> alpha, unique; int helper(int val, int idx) { if(idx == alpha.size()) return __builtin_popcount(val); else if(!unique[idx]) return helper(val, idx + 1); else { int notake = helper(val, idx + 1); int take{}; if((val & alpha[idx]) == 0) take = helper(val | alpha[idx], idx + 1); return max(take, notake); } } public: int maxLength(vector<string>& arr) { int sz = arr.size(); alpha.resize(sz); unique.resize(sz, true); for(int i = 0; i < sz; ++i) { for(auto& c : arr[i]) { int shift = 1 << (c - 'a'); if(alpha[i] & shift) // duplicate in this word unique[i] = false; // never take this word alpha[i] |= 1 << (c - 'a'); } } int val{}, idx{}; return helper(val, idx); } }; ``` > 參考lee215的解答 ```cpp= class Solution { public: int maxLength(vector<string>& arr) { vector<bitset<26>> dp = {bitset<26>()}; // default : put one emoty bitset int res{}; for(auto& s : arr) { bitset<26> a; for(auto& c : s) a.set(c - 'a'); if(a.count() < s.size()) continue; for(int i = dp.size() - 1; i >= 0; --i) { // 從後面開始取就不會有重複問題 bitset c = dp[i]; if((c & a).any()) continue; // c 和 a有交集 dp.push_back(c | a); res = max(res, (int)c.count() + (int)a.count()); } } return res; } }; ``` ### [1043. Partition Array for Maximum Sum](https://leetcode.com/problems/partition-array-for-maximum-sum/) ```cpp= class Solution { int sz, k; // 從idx往後看有那些可能? // subarray size = 1, 2, 3, ... k // 嘗試每一種可能的subarray。 int helper(vector<int>& arr, int idx) { if(~mem[idx]) return mem[idx]; int ans{}, maxv{}; for(int i = idx; i < idx + k && i < arr.size(); ++i) { maxv = max(maxv, arr[i]); ans = max(ans, maxv * (i - idx + 1) + helper(arr, i + 1)); } return mem[idx] = ans; } vector<int> mem; public: int maxSumAfterPartitioning(vector<int>& arr, int k) { sz = arr.size(); this->k = k; mem.resize(sz + 1, -1); mem.back() = 0; return helper(arr, 0); } }; ``` ### [1696. Jump Game VI](https://leetcode.com/problems/jump-game-vi/) top down + memory -> TLE ```cpp= class Solution { int helper(vector<int>& nums, int k, int idx) { if(mem[idx] != INT_MIN) return mem[idx]; int ans = INT_MIN; for(int nxt = idx + 1; nxt <= min(idx + k, (int)nums.size() - 1); ++nxt) { ans = max(ans, nums[idx] + helper(nums, k, nxt)); } return mem[idx] = ans; } vector<int> mem; public: int maxResult(vector<int>& nums, int k) { //因為答案有可為-1,所以改成INT_MIN mem.resize(nums.size(), INT_MIN); mem.back() = nums.back(); return helper(nums, k, 0); } // time : O(NK) // space : O(N) }; ``` 改成bottom-up tabulation 也是TLE,因為time complexity一樣。 ```cpp= class Solution { public: int maxResult(vector<int>& nums, int k) { vector<int> dp(nums.size(), INT_MIN); dp.back() = nums.back(); for(int i = nums.size() - 2; i >= 0; --i) { for(int nxt = i + 1; nxt <= min(i + k, (int)nums.size() - 1); ++nxt) dp[i] = max(dp[i], nums[i] + dp[nxt]); } return dp.front(); // time : O(NK) // space : O(N) } } ``` 因為是找後k個最大值,所以可以使用multiset,可以降低複雜度 ```cpp= class Solution { public: int maxResult(vector<int>& nums, int k) { vector<int> dp(nums.size()); multiset<int> ms{dp.back() = nums.back()}; for(int i = nums.size() - 2; i >= 0; --i) { if(i + k + 1 < nums.size()) ms.erase(ms.find(dp[i + k + 1])); dp[i] = nums[i] + *ms.rbegin(); ms.insert(dp[i]); } return dp.front(); // time : O(NlogK) // space : O(N + K) } }; ``` 因為是slind window maximum可以使用deque ```cpp= class Solution { public: int maxResult(vector<int>& nums, int k) { vector<int> dp(nums.size()); dp.back() = nums.back(); deque<int> q{(int)nums.size() - 1}; for(int i = nums.size() - 2; i >= 0; --i) { while(!q.empty() && q.front() > i + k) q.pop_front(); dp[i] = dp[q.front()] + nums[i]; while(!q.empty() && dp[i] > dp[q.back()]) q.pop_back(); q.push_back(i); } return dp.front(); // time : O(N) } }; ``` ###### tags: `leetcode` `刷題`