寫在前面
今天的LC打卡雖然是easy,但是用到了dp的思路來解,對於dp思維的練習我覺得很有幫助,再加上很多大佬在題解裏面列了不少類似的題目,所以就一併寫了並且整理一下。(打家劫舍系列+醜數系列)
面試題 17.16. 按摩師
一個有名的按摩師會收到源源不斷的預約請求,每個預約都可以選擇接或不接。在每次預約服務之間要有休息時間,因此她不能接受相鄰的預約。給定一個預約請求序列,替按摩師找到最優的預約集合(總預約時間最長),返回總的分鐘數。
示例 1:
輸入: [1,2,3,1]
輸出: 4
解釋: 選擇 1 號預約和 3 號預約,總時長 = 1 + 3 = 4。
解法:前一位的狀態只有選和不選,然後當前位置可以根據上一位的狀態來找到當前位選和不選的最大值。
遞推方程:dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
這題不需要用dp數組,兩個dp變量就行了。
代碼:
class Solution {
public:
int massage(vector<int>& nums) {
if(nums.empty()) return 0;
int dp0 = 0;
int dp1 = nums[0];
int i=1;
while(i<nums.size()){
int t0 = max(dp0,dp1);
int t1 = max(dp0+nums[i],dp1);
dp0 = t0;
dp1 = t1;
++i;
}
return max(dp0,dp1);
}
};
198. 打家劫舍
你是一個專業的小偷,計劃偷竊沿街的房屋。每間房內都藏有一定的現金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。
給定一個代表每個房屋存放金額的非負整數數組,計算你在不觸動警報裝置的情況下,能夠偷竊到的最高金額。
上一題的原型其實就是這道經典的小偷問題,完全一模一樣,所以這裏就不寫了。
213. 打家劫舍 II
你是一個專業的小偷,計劃偷竊沿街的房屋,每間房內都藏有一定的現金。這個地方所有的房屋都圍成一圈,這意味着第一個房屋和最後一個房屋是緊挨着的。同時,相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。
給定一個代表每個房屋存放金額的非負整數數組,計算你在不觸動警報裝置的情況下,能夠偷竊到的最高金額。
解法:其實就是變成了環形的,頭尾需要特判,所以分成不選頭和不選尾,範圍兩個答案的較大值。
代碼:
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.empty()) return 0;
if(nums.size()==1) return nums[0];
int dp0 = 0;
int dp1 = nums[0];
int i=1;
while(i<nums.size()-1){
int t0 = max(dp0,dp1);
int t1 = max(dp0+nums[i],dp1);
dp0 = t0;
dp1 = t1;
++i;
}
int res1 = max(dp0,dp1);
dp0 = 0;
dp1 = nums[1];
i=2;
while(i<nums.size()){
int t0 = max(dp0,dp1);
int t1 = max(dp0+nums[i],dp1);
dp0 = t0;
dp1 = t1;
++i;
}
int res2 = max(dp0,dp1);
return max(res1,res2);
}
};
337. 打家劫舍 III
在上次打劫完一條街道之後和一圈房屋後,小偷又發現了一個新的可行竊的地區。這個地區只有一個入口,我們稱之爲“根”。 除了“根”之外,每棟房子有且只有一個“父“房子與之相連。一番偵察之後,聰明的小偷意識到“這個地方的所有房屋的排列類似於一棵二叉樹”。 如果兩個直接相連的房子在同一天晚上被打劫,房屋將自動報警。
計算在不觸動警報的情況下,小偷一晚能夠盜取的最高金額。
解法:思路不變,但是因爲變成了二叉樹存放,所以變成了一道樹形dp。
對於一個子樹來說,有兩種情況:
- 包含當前根節點
- 不包含當前根節點
情況1:包含根節點
由於包含了根節點,所以不能選擇左右兒子節點,這種情況的最大值爲:
當前節點 + 左兒子情況2 + 右二子情況2
情況2:不包含根節點
這種情況,可以選擇左右兒子節點,所以有四種可能:
- 左兒子情況1 + 右兒子情況1
- 左兒子情況1 + 右兒子情況2
- 左兒子情況2 + 右兒子情況1
- 左兒子情況2 + 右兒子情況2
綜合來說就是,max(左兒子情況1, 左兒子情況2) + max(右兒子情況1, 右兒子情況2)。
結合這兩種情況,dfs遍歷二叉樹即可。
來源:https://leetcode-cn.com/problems/house-robber-iii/solution/cdong-tai-gui-hua-si-xiang-shi-xian-xiang-xi-shuo-/
代碼:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
pair<int, int> dfs(TreeNode *root) {
if (root == nullptr) {
return { 0, 0 };
}
auto left_pair = dfs(root->left);
auto right_pair = dfs(root->right);
return {root->val + left_pair.second + right_pair.second,
max(left_pair.first, left_pair.second) + max(right_pair.first, right_pair.second)};
}
int rob(TreeNode* root) {
auto p = dfs(root);
return max(p.first, p.second);
}
};
做完打家劫舍系列,不如一併練習一下類似的簡單dp題目。
1262. 可被三整除的最大和
給你一個整數數組 nums,請你找出並返回能被三整除的元素最大和。
示例 1:
輸入:nums = [3,6,5,1,8]
輸出:18
解釋:選出數字 3, 6, 1 和 8,它們的和是 18(可被 3 整除的最大和)。
解法:
不妨設dp[i] 代表 選取的數字累加和 模3 = i 的數字和
假定nums[i] % 3 = 1 ,那麼,和 前面選取的數字和模 3 = 2 的數相加,就可以模3爲 0 ,表達起來就是 dp[0] = max(dp[0], nums[i] + dp[2])
依次類推,只要不斷更新 dp 數組即可,注意一點,更新的時候要保存上一個狀態的值,避免後續更新的時候重複影響。
代碼:
class Solution {
public:
int maxSumDivThree(vector<int>& nums) {
vector<int> dp={0,0,0};
for(int i=0; i<nums.size(); i++){
int mod = nums[i] % 3;
int a = dp[(3+0-mod)%3];
int b = dp[(3+1-mod)%3];
int c = dp[(3+2-mod)%3];
if( a || mod==0) dp[0] = max(dp[0],a+nums[i]);
if( b || mod==1) dp[1] = max(dp[1],b+nums[i]);
if( c || mod==2) dp[2] = max(dp[2],c+nums[i]);
}
return dp[0];
}
};
801. 使序列遞增的最小交換次數
我們有兩個長度相等且不爲空的整型數組 A 和 B 。
我們可以交換 A[i] 和 B[i] 的元素。注意這兩個元素在各自的序列中應該處於相同的位置。
在交換過一些元素之後,數組 A 和 B 都應該是嚴格遞增的(數組嚴格遞增的條件僅爲A[0] < A[1] < A[2] < … < A[A.length - 1])。
給定數組 A 和 B ,請返回使得兩個數組均保持嚴格遞增狀態的最小交換次數。假設給定的輸入總是有效的。
示例:
輸入: A = [1,3,5,4], B = [1,2,3,7]
輸出: 1
解釋:
交換 A[3] 和 B[3] 後,兩個數組如下:
A = [1, 3, 5, 7] , B = [1, 2, 3, 4]
兩個數組均爲嚴格遞增的。
注意:
- A, B 兩個數組的長度總是相等的,且長度的範圍爲 [1, 1000]。
- A[i], B[i] 均爲 [0, 2000]區間內的整數。
解法:
對於每一列的數,都有兩種狀態,換或者不換,所以我們定義兩個dp變量來儲存狀態。
初始化:如果只有一列:那麼dp = [0, 1]
dp[0] = 0, 不做交換,總交換數爲0.
dp[1] = 1, 做交換,總交換數爲1,同時也滿足最後的要求,是一個遞增數列,因爲數組只有一個元素。
如果有兩列, 假設 a1 = A[i - 1], b1 = B[i - 1] 以及 a2 = A[i], b2 = B[i], i = 1
相對的有4中情況
(1)a1 < a2 並且 b1 < b2
表明,當前列相對前一列,可以不做交換,因爲條件已經滿足了。
(2)a1 < b2 並且 b1 < a2
表明,當前列相對前一列,可以做交換,因爲交換了條件可以滿足。
(3) not a1 < a2 或者 not b1 < b2
是(1)取反,當前列相對前一列必須交換,這裏的交換是相對的,如果前一列已經交換了,當前列可以不動。
(4)not a1 < b2 或者 not b1 < a2
是(2)取反,當前列相對前一列必須不能換。
對於(3)
new_dp[0] = dp[1],要想當前列不動,那麼前一列必須是換過的。
new_dp[1] = dp[0] + 1,要想當前列換,前一列必須沒動過。總之,對於情況(3),與前一列操作相反就對了。
對於(4)
new_dp[0] = dp[0],要想當前列不動,那麼前一列必須也不動。
new_dp[1] = dp[1] + 1,要想當前列換,前一列必須也換過。總之,對於情況(4),與前一列操作相同就對了。
但是(3)(4)並沒有包括全部的情況。對於(1)(2),可換可不換,按照貪心原則,那就是能不換就不換。
new_dp[0] = min(dp[0], dp[1]),管你前面換沒換過,我是取小的那個數,然後就不換了。
new_dp[1] = min(dp[0], dp[1]) + 1,管你前面換沒換過,我是取小的那個數,然後換一下,因爲dp[1]對應的就是當前列交換的情況,所以不管怎麼樣,這個數值必須對應當前列交換這個操作。
代碼:
class Solution {
public:
int minSwap(vector<int>& a,vector<int>& b) {
vector<int> dp = {0,1};
for(int i=1; i<a.size(); i++){
int a1 = a[i-1];
int a2 = a[i];
int b1 = b[i-1];
int b2 = b[i];
if(a1>=a2 || b1>=b2){
int t = dp[0];
dp[0] = dp[1];
dp[1] = t + 1;
}
else if(a1>=b2 || b1>=a2){
++dp[1];
}
else{
int m = min(dp[0],dp[1]);
dp[0] = m;
dp[1] = m+1;
}
}
return min(dp[0], dp[1]);
}
};
練完以上這些題目,我們再做一個經典系列:醜數
263. 醜數
編寫一個程序判斷給定的數是否爲醜數。
醜數就是隻包含質因數 2, 3, 5 的正整數。
解法:第一道沒什麼好說的,普通的暴力題目。
代碼:
class Solution {
public:
bool isUgly(int num) {
if(num==0) return false;
while(num%2==0) num /= 2;
while(num%3==0) num /= 3;
while(num%5==0) num /= 5;
return num==1;
}
};
264. 醜數 II
編寫一個程序,找出第 n 個醜數。
醜數就是隻包含質因數 2, 3, 5 的正整數。
示例:
輸入: n = 10
輸出: 12
解釋: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 個醜數。
解法:使用三個指針,分別代表2,3,5,然後記錄指針位置對應的數乘對應的倍數的最小值,選中的指針+1,一直算到第n個就行了。
代碼:
class Solution {
public:
int nthUglyNumber(int k) {
if(k==1) return 1;
int a(0),b(0),c(0);//記錄2,3,5對應的位置
vector<int> dp;
dp.push_back(1);
int s = 1;
while(dp.size()<k){
int next = min(2*dp[a],min(3*dp[b],5*dp[c]));
if(next==2*dp[a]) a++;//選中的指針要+1
if(next==3*dp[b]) b++;
if(next==5*dp[c]) c++;
dp.push_back(next);
}
return dp.back();
}
};
面試題 17.09. 第 k 個數
有些數的素因子只有 3,5,7,請設計一個算法找出第 k 個數。注意,不是必須有這些素因子,而是必須不包含其他的素因子。例如,前幾個數按順序應該是 1,3,5,7,9,15,21。
和上一題完全一樣,除了三個因子的變化,只要是互質的,上一個解法就是可行的。
313. 超級醜數
編寫一段程序來查找第 n 個超級醜數。
超級醜數是指其所有質因數都是長度爲 k 的質數列表 primes 中的正整數。
示例:
輸入: n = 12, primes = [2,7,13,19]
輸出: 32
解釋: 給定長度爲 4 的質數列表 primes = [2,7,13,19],前 12 個超級醜數序列爲:[1,2,4,7,8,13,14,16,19,26,28,32] 。
說明:
- 1 是任何給定 primes 的超級醜數。
- 給定 primes 中的數字以升序排列。
- 0 < k ≤ 100, 0 < n ≤ 106, 0 < primes[i] < 1000 。
- 第 n 個超級醜數確保在 32 位有符整數範圍內。
解法:和上一道醜數的dp思路是一樣的,只不過這裏的因數變成了一個數組,所以我們也需要一個數組才存放對應的指針位置,值得一提的是,我本來是用數字來記錄位置的,但是由於會出現重複的問題(由於primes個數變多,會出現多個因數可以乘出同一個next值,此時這些指針都要+1),如果用vector判重會超時,所以就想到了set,但是set的下標不能用整數來記錄,所以就要用到iterator,這裏也算是鞏固了一下指針的概念。
代碼:
class Solution {
public:
int nthSuperUglyNumber(int n, vector<int>& primes) {
if(primes.empty()) return 0;
int l = primes.size();
set<int> dp;
dp.insert(1);
vector<set<int>::iterator> points;//point數組記錄dp集合的迭代位置
for(int i=0; i<l; i++)
points.push_back(dp.begin());
while(dp.size()<n){
int minv = INT_MAX;
int mini = 0;
for(int i=0; i<l; i++)
minv = min(minv,*points[i]*primes[i]);//算出next值
dp.insert(minv);
for(int i=0; i<l; i++)
if(*points[i]*primes[i]==minv) points[i]++;//迭代位置+1
}
return *dp.rbegin();//set的最後一位索引,取值。
}
};
set用的不是特別多,這題算是讓我徹底搞明白了set的遍歷以及迭代器的用法,受益匪淺。
1201. 醜數 III
請你幫忙設計一個程序,用來找出第 n 個醜數。
醜數是可以被 a 或 b 或 c 整除的 正整數。
示例:
輸入:n = 3, a = 2, b = 3, c = 5
輸出:4
解釋:醜數序列爲 2, 3, 4, 5, 6, 8, 9, 10… 其中第 3 個是 4。
提示:
- 1 <= n, a, b, c <= 10^9
- 1 <= a * b * c <= 10^18
- 本題結果在 [1, 2 * 10^9] 的範圍內
解法:數據範圍10^9,已經和dp沒關係了,雖然名字還是醜數=.=
這裏直接參考了一位大神的題解,其實就是一道數學題,用到了經典的容斥原理。
鏈接:https://leetcode-cn.com/problems/ugly-number-iii/solution/er-fen-fa-si-lu-pou-xi-by-alfeim/
代碼:
class Solution {
public:
using LL = long long;
int nthUglyNumber(int n, int a, int b, int c) {
//看到n的範圍應該馬上聯想到是,典型的二分思路
LL low = min(min(a,b),c); //下邊界顯然是a、b、c中最小者
LL high = static_cast<LL>(low) * n; //上邊界是這個最小者的n倍
LL res = Binary_Search(low,high,a,b,c,n);
LL left_a = res%a;
LL left_b = res%b;
LL left_c = res%c;
return res - min(left_a,min(left_b,left_c));
}
//二分搜索
LL Binary_Search(LL low,LL high,int a,int b,int c,LL n){
if(low >= high) return low;
LL mid = (low + high)>>1;
LL MCM_a_b = MCM(a,b);
LL MCM_a_c = MCM(a,c);
LL MCM_b_c = MCM(b,c);
LL MCM_a_b_c = MCM(MCM_a_b,c);
//獨立的醜數個數爲,當前數分別除以a、b、c的和,減去當前數除以a、b、c兩兩間最小公倍數的和,再加上當前數除以 a、b、c三者的最小公倍數
LL count_n = mid/a + mid/b + mid/c - mid/MCM_a_b - mid/MCM_b_c - mid/MCM_a_c + mid/MCM_a_b_c;
if(count_n == n) return mid;
if(count_n < n) return Binary_Search(mid + 1,high,a,b,c,n);
return Binary_Search(low,mid-1,a,b,c,n);
}
//求最小公倍數:兩數乘積除以最大公約數
LL MCM(LL a,LL b){
LL Multi = a * b;
while(b > 0){
LL tmp = a % b;
a = b;
b = tmp;
}
return Multi/a;
}
};