01揹包
劍指 Offer II 101. 分割等和子集
給定一個非空的正整數數組 nums ,請判斷能否將這些數字分成元素和相等的兩部分。
示例 1:
輸入:nums = [1,5,11,5]
輸出:true
解釋:nums 可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
輸入:nums = [1,2,3,5]
輸出:false
解釋:nums 不可以分爲和相等的兩部分
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 100
class Solution {
public:
bool canPartition(vector<int>& nums) {
//nums = [1,5,11,5] -> [1, 11],[5,5] -> true
int nlen = nums.size();
if (nlen < 2) {
return false;
}
int total_sum = accumulate(nums.begin(), nums.end(), 0);
// 和爲奇數,肯定不可以拆分
if (total_sum % 2) {
return false;
}
//找最大值
int max_sum = *max_element(nums.begin(), nums.end());
int part_sum = total_sum / 2;
if (max_sum > part_sum) {
return false;
}
//1. dp代表部分和爲i
// vector<vector<int> > dp(nlen, vector<int>(part_sum + 1, 0));
// for (int i = 0; i < nlen; ++i) {
// dp[i][0] = true;
// }
// //第0個數可以構成num[0]的部分和
// dp[0][nums[0]] = true;
// for (int i = 1; i < nlen; ++i)
// {
// int num = nums[i];
// for (int j = 1; j <= part_sum; ++j)
// {
// //當前數小於部分和, 不加 | 加當前數
// if (nums[i] <= j) {
// dp[i][j] = dp[i - 1][j] | dp[i - 1][j - num];
// } else {
// dp[i][j] = dp[i - 1][j];
// }
// }
// }
// return dp[nlen - 1][part_sum];
//2. 優化
vector<int> dp(part_sum + 1, 0);
dp[0] = true;
for (int i = 0; i < nlen; ++i)
{
int num = nums[i];
for (int j = part_sum; j >= num; --j)
{
//不加 | 加
//如果是遍歷 [num, part_sum],之前得到的 j,會被之後的 j-num破壞掉
//而逆序遍歷,則總是先遍歷 尚未計算的j
dp[j] = dp[j] | dp[j - num];
}
}
return dp[part_sum];
}
};
時間複雜度:\(O(n^2)\);空間複雜度:O(n)
劍指 Offer II 102. 加減的目標值
給定一個正整數數組 nums 和一個整數 target 。
向數組中的每個整數前添加 '+' 或 '-' ,然後串聯起所有整數,可以構造一個 表達式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然後串聯起來得到表達式 "+2-1" 。
返回可以通過上述方法構造的、運算結果等於 target 的不同 表達式 的數目。
示例 1:
輸入:nums = [1,1,1,1,1], target = 3
輸出:5
解釋:一共有 5 種方法讓最終目標和爲 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
示例 2:
輸入:nums = [1], target = 1
輸出:1
提示:
1 <= nums.length <= 20
0 <= nums[i] <= 1000
0 <= sum(nums[i]) <= 1000
-1000 <= target <= 1000
法一:DFS
class Solution {
int ans;
public:
void dfs(vector<int>& nums, int target, int cur, int sum, int nlen)
{
if (cur == nlen) {
if (target == sum) {
ans++;
}
}
else {
dfs(nums, target, cur + 1, sum + nums[cur], nlen);
dfs(nums, target, cur + 1, sum - nums[cur], nlen);
}
}
int findTargetSumWays(vector<int>& nums, int target) {
int nlen = nums.size();
if (nlen == 1 && nums[0] == target) {
return 1;
}
ans = 0;
dfs(nums, target, 0, 0, nlen);
return ans;
}
};
法二:動態規劃
class Solution {
// int ans;
public:
// void dfs(vector<int>& nums, int target, int cur, int sum, int nlen)
// {
// if (cur == nlen) {
// if (target == sum) {
// ans++;
// }
// }
// else {
// dfs(nums, target, cur + 1, sum + nums[cur], nlen);
// dfs(nums, target, cur + 1, sum - nums[cur], nlen);
// }
// }
int findTargetSumWays(vector<int>& nums, int target) {
// int nlen = nums.size();
// if (nlen == 1 && nums[0] == target) {
// return 1;
// }
// 回溯
// ans = 0;
// dfs(nums, target, 0, 0, nlen);
// return ans;
//2. 動態規劃(01揹包)
// 記數組的元素和爲 sum,添加 - 號的元素之和爲neg,則其餘添加+的元素之和爲sum−neg,得到的表達式的結果爲
// (sum - neg) - neg = sum - 2 * neg = target
// neg = (sum - target) / 2
// 由於nums中元素都是非負整數,neg也得是非負整數,所以sum-taget是 非負偶數。不符合條件直接返回0
int nlen = nums.size();
if (nlen == 1 && nums[0] == target) {
return 1;
}
int sum_n = accumulate(nums.begin(), nums.end(), 0);
int diff = sum_n - target;
if (diff < 0 || diff % 2 != 0) {
return 0;
}
int neg = diff / 2;
//滿足上述條件,問題轉變爲在nums的前i個數中選取元素,使得這些元素之和等於j的方案數
//定義二維數據dp,dp[i][j]表示nums的前i個數中選取元素,使得這些元素之和等於j的方案數
//dp[nlen][neg]
// vector<vector<int> > dp(nlen + 1, vector<int>(neg + 1, 0));
//當i=0,沒有元素可以選時,元素和只能爲0,對應方案數爲1
// dp[0][0] = 1;
//1<=i<=n時,對於數組nums中第i個元素num,遍歷0<=j<=neg,計算dp[i][j]的值
//對於num > j, 則不能選num, 此時 dp[i][j] = dp[i - 1][j];
// num <= j, 則方案數爲 dp[i][j] = dp[i - 1][j] + dp[i - 1][j - num]
// for (int i = 1; i <= nlen; ++i)
// {
// int num = nums[i - 1];
// for (int j = 0; j <= neg; ++j)
// {
// dp[i][j] = dp[i - 1][j];
// if (j >= num) {
// dp[i][j] = dp[i][j] + dp[i - 1][j - num];
// }
// }
// }
// return dp[nlen][neg];
//2. 優化
vector<int> dp(neg + 1, 0);
dp[0] = 1;
for (int i = 0; i < nlen; ++i)
{
int num = nums[i];
for (int j = neg; j >= num; --j)
{
dp[j] = dp[j] + dp[j - num];
}
}
return dp[neg];
}
};
完全揹包
322. 零錢兌換
給你一個整數數組 coins ,表示不同面額的硬幣;以及一個整數 amount ,表示總金額。
計算並返回可以湊成總金額所需的 最少的硬幣個數 。如果沒有任何一種硬幣組合能組成總金額,返回 -1 。
你可以認爲每種硬幣的數量是無限的。
示例 1:
輸入:coins = [1, 2, 5], amount = 11
輸出:3
解釋:11 = 5 + 5 + 1
示例 2:
輸入:coins = [2], amount = 3
輸出:-1
示例 3:
輸入:coins = [1], amount = 0
輸出:0
提示:
1 <= coins.length <= 12
1 <= coins[i] <= 231 - 1
0 <= amount <= 104
法一:動態規劃
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
if amount == 0:
return 0
clen = len(coins)
Max = amount + 1
dp = [Max for _ in range(amount + 1)]
dp[0] = 0
for i in range(1, amount + 1):
for j in range(0, clen):
if coins[j] <= i:
dp[i] = min(dp[i], dp[i - coins[j]] + 1)
return dp[amount] if dp[amount] <= amount else -1
法二:記憶化搜索
class Solution {
vector<int> count;
int dp(vector<int>& coins, int rest_amount)
{
if (rest_amount < 0) return -1;
if (rest_amount == 0) return 0;
//如果下次還要計算這個問題的值直接從數組中取出返回即可,這樣能保證每個子問題最多隻被計算一次
if (count[rest_amount - 1] != 0) return count[rest_amount - 1];
int Min = INT_MAX;
for (int coin : coins)
{
//重複選
int res = dp(coins, rest_amount - coin);
if (res >= 0 && res < Min) {
Min = res + 1;
}
}
count[rest_amount - 1] = (Min == INT_MAX ? -1 : Min);
return count[rest_amount - 1];
}
public:
int coinChange(vector<int>& coins, int amount) {
if (amount < 1) return 0;
//count爲0~amount所需要的最小硬幣數
count.resize(amount);
return dp(coins, amount);
}
};
python寫法:使用 @functools.lru_cache(max_len)
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
# 2. 記憶化搜索
@functools.lru_cache(amount)
def dp(rest_amount):
if rest_amount < 0: return -1
if rest_amount == 0: return 0
mini = int(1e9)
for coin in coins:
res = dp(rest_amount - coin)
if res >= 0 and res < mini:
mini = res + 1
return mini if mini < int(1e9) else -1
if amount < 1: return 0
return dp(amount)
279. 完全平方數
給你一個整數 n ,返回 和爲 n 的完全平方數的最少數量 。
完全平方數 是一個整數,其值等於另一個整數的平方;換句話說,其值等於一個整數自乘的積。例如,1、4、9 和 16 都是完全平方數,而 3 和 11 不是。
示例 1:
輸入:n = 12
輸出:3
解釋:12 = 4 + 4 + 4
示例 2:
輸入:n = 13
輸出:2
解釋:13 = 4 + 9
提示:
1 <= n <= 104
class Solution {
public:
int numSquares(int n) {
if (n == 1) {
return 1;
}
//1, 4, 9
vector<int> dp(n + 1, n);
dp[0] = 0;
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j*j <= i; ++j)
{
if (j*j <= i) {
dp[i] = min(dp[i], dp[i - j*j] + 1);
}
}
}
//必然存在解,因爲有1
return dp[n];
}
};
一、簡單DP
62. 不同路徑
一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記爲 “Start” )。
機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記爲 “Finish” )。
問總共有多少條不同的路徑?
示例 1:
輸入:m = 3, n = 7
輸出:28
示例 2:
輸入:m = 3, n = 2
輸出:3
解釋:
從左上角開始,總共有 3 條路徑可以到達右下角。
- 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右
- 向下 -> 向右 -> 向下
示例 3:
輸入:m = 7, n = 3
輸出:28
示例 4:
輸入:m = 3, n = 3
輸出:6
提示:
1 <= m, n <= 100
題目數據保證答案小於等於 2 * 109
class Solution {
int ans;
public:
// 1. dfs:超時
// void dfs(int i, int j, int m, int n)
// {
// if (i < 0 || i == m || j < 0 || j == n) {
// return;
// }
// if (i == m - 1 && j == n - 1) {
// ans += 1;
// return;
// }
// int dirs[][2] = {{0, 1}, {1, 0}};
// for (int k = 0; k < 2; k++)
// {
// int x = i + dirs[k][0], y = j + dirs[k][1];
// if (x < 0 || x == m || y < 0 || y == n) {
// continue;
// }
// dfs(x, y, m, n);
// }
// }
// int uniquePaths(int m, int n) {
// dfs(0, 0, m, n);
// return ans;
// }
//2. 動態規劃: O(n^2)
// int uniquePaths(int m, int n)
// {
// vector<vector<int> > dp(m + 1, vector<int>(n + 1));
// for (int i = 0; i < m; ++i) {
// dp[i][0] = 1;
// }
// for (int j = 0; j < n; ++j) {
// dp[0][j] = 1;
// }
// for (int i = 1; i < m; ++i)
// {
// for (int j = 1; j < n; ++j)
// {
// dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
// }
// }
// return dp[m - 1][n - 1];
// }
// O(m)
int uniquePaths(int m, int n)
{
// C(m + n - 2, m - 1): m + n - 2次移動,有m-1次向下,n-1次向右。因此路徑總數
long long ans = 1;
for (int x = n, y = 1; y < m; ++x, ++y)
{
ans = ans * x / y;
}
return ans;
}
};
動態規劃:時間複雜度O(n2),空間複雜度O(n2)
組合數:時間複雜度O(m), 空間O(1)
85. 最大矩形
給定一個僅包含 0 和 1 、大小爲 rows x cols 的二維二進制矩陣,找出只包含 1 的最大矩形,並返回其面積。
示例 1:
輸入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
輸出:6
解釋:最大矩形如上圖所示。
示例 2:
輸入:matrix = []
輸出:0
示例 3:
輸入:matrix = [["0"]]
輸出:0
示例 4:
輸入:matrix = [["1"]]
輸出:1
示例 5:
輸入:matrix = [["0","0"]]
輸出:0
提示:
rows == matrix.length
cols == matrix[0].length
1 <= row, cols <= 200
matrix[i][j] 爲 '0' 或 '1'
class Solution {
public:
int maximalRectangle(vector<vector<char>>& matrix) {
int row = matrix.size();
if (row == 0) {
return 0;
}
int col = matrix[0].size();
// left[i][j] 爲矩陣第 i 行第 j 列元素的左邊連續 1 的數量。
vector<vector<int> > left(row, vector<int>(col));
for (int i = 0; i < row; ++i)
{
for (int j = 0; j < col; ++j)
{
if (matrix[i][j] == '1') {
left[i][j] = (j == 0 ? 0 : left[i][j - 1]) + 1;
}
}
}
int ret = 0;
for (int i = 0; i < row; ++i)
{
for (int j = 0; j < col; ++j)
{
if (matrix[i][j] == '0') {
continue;
}
int width = left[i][j];
int area = width;
for (int k = i - 1; k >= 0; k--) {
width = min(width, left[k][j]);
area = max(area, (i - k + 1) * width);
}
ret = max(ret, area);
}
}
return ret;
}
};
class Solution {
public:
int maximalRectangle(vector<vector<char>>& matrix) {
int row = matrix.size();
if (row == 0) {
return 0;
}
int col = matrix[0].size();
// left[i][j] 爲矩陣第 i 行第 j 列元素的左邊連續 1 的數量。
vector<vector<int> > left(row, vector<int>(col, 0));
for (int i = 0; i < row; ++i)
{
for (int j = 0; j < col; ++j)
{
if (matrix[i][j] == '1') {
left[i][j] = (j == 0 ? 0 : left[i][j - 1]) + 1;
}
}
}
int ret = 0;
//單調棧
//對於每一列,使用基於柱狀圖的方法
for (int j = 0; j < col; ++j)
{
vector<int> up(row, 0), down(row, 0);
stack<int> stk;
for (int i = 0; i < row; ++i)
{
while (!stk.empty() && left[stk.top()][j] >= left[i][j]) {
stk.pop();
}
up[i] = stk.empty() ? -1 : stk.top(); //
stk.push(i);
}
stk = stack<int>();
for (int i = row - 1; i >= 0; --i)
{
while (!stk.empty() && left[stk.top()][j] >= left[i][j]) {
stk.pop();
}
down[i] = stk.empty() ? row : stk.top(); //
stk.push(i);
}
for (int i = 0; i < row; ++i)
{
int height = down[i] - up[i] - 1;
int area = height * left[i][j];
ret = max(ret, area);
}
}
return ret;
//使用柱狀圖的優化暴力方法
// for (int i = 0; i < row; ++i)
// {
// for (int j = 0; j < col; ++j)
// {
// if (matrix[i][j] == '0') {
// continue;
// }
// int width = left[i][j];
// int area = width;
// for (int k = i - 1; k >= 0; k--) {
// width = min(width, left[k][j]);
// area = max(area, (i - k + 1) * width);
// }
// ret = max(ret, area);
// }
// }
// return ret;
}
};
禮物最大價值(矩陣貪心類題目)劍指 Offer 47
class Solution {
public:
int maxValue(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
for(int i = 0; i < m; i++)
{
for (int j = 0; j < n; j++)
{
if (i == 0 && j ==0) {
continue;
}
if (i == 0) {
grid[i][j] += grid[i][j - 1];
}
else if (j == 0) {
grid[i][j] += grid[i - 1][j];
}
else {
grid[i][j] += max(grid[i][j - 1], grid[i - 1][j]);
}
}
}
return grid[m - 1][n - 1];
}
};
時間複雜度 O(MN):M, N分別爲矩陣行高、列寬;動態規劃需遍歷整個grid矩陣,使用 O(MN)時間。
空間複雜度 O(1):原地修改使用常數大小的額外空間。
爬樓梯 劍指 Offer 10- II.
class Solution {
public:
int numWays(int n) {
if (n == 0 || n == 1)
{
return 1;
}
if (n == 2)
{
return 2;
}
int a = 1, b = 2;
int res = 0, MOD = 1000000007;
for (int i = 3; i <= n; i++)
{
res = (a + b) % MOD;
a = b;
b = res;
}
return res ;
}
};
時間複雜度O(n),空間複雜度O(1)
152. 乘積最大子數組
給你一個整數數組 nums ,請你找出數組中乘積最大的非空連續子數組(該子數組中至少包含一個數字),並返回該子數組所對應的乘積。
測試用例的答案是一個 32-位 整數。子數組 是數組的連續子列。
示例 1:
輸入: nums = [2,3,-2,4]
輸出: 6
解釋: 子數組 [2,3] 有最大乘積 6。
示例 2:
輸入: nums = [-2,0,-1]
輸出: 0
解釋: 結果不能爲 2, 因爲 [-2,-1] 不是子數組。
提示:
1 <= nums.length <= 2 * 104
-10 <= nums[i] <= 10
nums 的任何前綴或後綴的乘積都 保證 是一個 32-位 整數
class Solution {
public:
int maxProduct(vector<int>& nums) {
int nlen = nums.size();
if (nlen == 1) {
return nums[0];
}
int min_pro = nums[0], max_pro = nums[0];
int ans = nums[0];
for (int i = 1; i < nlen; ++i)
{
int t_max = max_pro, t_min = min_pro;
max_pro = max(t_max * nums[i], max(t_min * nums[i], nums[i]));
min_pro = min(t_min * nums[i], min(t_max * nums[i], nums[i]));
ans = max(ans, max_pro);
}
return ans;
}
};
1186. 刪除一次得到子數組最大和
給你一個整數數組,返回它的某個 非空 子數組(連續元素)在執行一次可選的刪除操作後,所能得到的最大元素總和。換句話說,你可以從原數組中選出一個子數組,並可以決定要不要從中刪除一個元素(只能刪一次哦),(刪除後)子數組中至少應當有一個元素,然後該子數組(剩下)的元素總和是所有子數組之中最大的。
注意,刪除一個元素後,子數組 不能爲空。
示例 1:
輸入:arr = [1,-2,0,3]
輸出:4
解釋:我們可以選出 [1, -2, 0, 3],然後刪掉 -2,這樣得到 [1, 0, 3],和最大。
示例 2:
輸入:arr = [1,-2,-2,3]
輸出:3
解釋:我們直接選出 [3],這就是最大和。
示例 3:
輸入:arr = [-1,-1,-1,-1]
輸出:-1
解釋:最後得到的子數組不能爲空,所以我們不能選擇 [-1] 並從中刪去 -1 來得到 0。
我們應該直接選擇 [-1],或者選擇 [-1, -1] 再從中刪去一個 -1。
提示:
1 <= arr.length <= 105
-104 <= arr[i] <= 104
class Solution {
public:
int maximumSum(vector<int>& arr) {
int alen = arr.size();
if (alen == 1) {
return arr[0];
}
//a: 未執行任何刪除操作得到的當前子數組最大和
//b: 已經執行刪除後得到的當前子數組最大和
int a = arr[0], b = 0, ans = arr[0];
for (int i = 1; i < alen; ++i)
{
b = max(b + arr[i], a); // 考慮執行刪除,max(之前已執行刪除現在只能加上當前值,之前最大子數組未執行刪除-故當前位置可刪除)
a = max(a + arr[i], arr[i]); // 不考慮執行刪除,直接更新a=max(a + nums[i], nums[i])
ans = max(ans, max(a, b));
}
return ans;
}
};
198. 打家劫舍
你是一個專業的小偷,計劃偷竊沿街的房屋。每間房內都藏有一定的現金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。給定一個代表每個房屋存放金額的非負整數數組,計算你 不觸動警報裝置的情況下 ,一夜之內能夠偷竊到的最高金額。
示例 1:
輸入:[1,2,3,1]
輸出:4
解釋:偷竊 1 號房屋 (金額 = 1) ,然後偷竊 3 號房屋 (金額 = 3)。
偷竊到的最高金額 = 1 + 3 = 4 。
示例 2:
輸入:[2,7,9,3,1]
輸出:12
解釋:偷竊 1 號房屋 (金額 = 2), 偷竊 3 號房屋 (金額 = 9),接着偷竊 5 號房屋 (金額 = 1)。
偷竊到的最高金額 = 2 + 9 + 1 = 12 。
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 400
class Solution:
def rob(self, nums: List[int]) -> int:
nlen = len(nums)
if nlen == 1:
return nums[0]
if nlen == 2:
return max(nums[0], nums[1])
# dp = [0 for _ in range(nlen)]
# dp[0], dp[1] = nums[0], max(nums[0], nums[1])
# for i in range(2, nlen):
# dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
# return dp[nlen - 1]
# 動態數組
first, second = nums[0], max(nums[0], nums[1])
for i in range(2, nlen):
temp = second
second = max(second, first + nums[i])
first = temp
return second
213. 打家劫舍 II
你是一個專業的小偷,計劃偷竊沿街的房屋,每間房內都藏有一定的現金。這個地方所有的房屋都 圍成一圈 ,這意味着第一個房屋和最後一個房屋是緊挨着的。同時,相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警 。給定一個代表每個房屋存放金額的非負整數數組,計算你 在不觸動警報裝置的情況下 ,今晚能夠偷竊到的最高金額。
示例 1:
輸入:nums = [2,3,2]
輸出:3
解釋:你不能先偷竊 1 號房屋(金額 = 2),然後偷竊 3 號房屋(金額 = 2), 因爲他們是相鄰的。
示例 2:
輸入:nums = [1,2,3,1]
輸出:4
解釋:你可以先偷竊 1 號房屋(金額 = 1),然後偷竊 3 號房屋(金額 = 3)。
偷竊到的最高金額 = 1 + 3 = 4 。
示例 3:
輸入:nums = [1,2,3]
輸出:3
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 1000
class Solution {
public:
int rob(vector<int>& nums) {
int nlen = nums.size();
if (nlen == 0)
{
return 0;
}
if (nlen == 1)
{
return nums[0];
}
return max(my_rob(nums, 0, nlen-1), my_rob(nums, 1, nlen));
}
int my_rob(vector<int>& nums, int start, int end)
{
int cur = 0, pre = 0;
for (int i = start; i < end; i++)
{
int t = cur;
cur = max(pre + nums[i], cur);
pre = t;
}
return cur;
}
};
337. 打家劫舍 III
小偷又發現了一個新的可行竊的地區。這個地區只有一個入口,我們稱之爲 root 。
除了 root 之外,每棟房子有且只有一個“父“房子與之相連。一番偵察之後,聰明的小偷意識到“這個地方的所有房屋的排列類似於一棵二叉樹”。 如果 兩個直接相連的房子在同一天晚上被打劫 ,房屋將自動報警。
給定二叉樹的 root 。返回 在不觸動警報的情況下 ,小偷能夠盜取的最高金額 。
示例 1:
輸入: root = [3,2,3,null,3,null,1]
輸出: 7
解釋: 小偷一晚能夠盜取的最高金額 3 + 3 + 1 = 7
示例 2:
輸入: root = [3,4,5,1,3,null,1]
輸出: 9
解釋: 小偷一晚能夠盜取的最高金額 4 + 5 = 9
提示:
樹的節點數在 [1, 104] 範圍內
0 <= Node.val <= 104
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
unordered_map<TreeNode*, int> f, g; // f拿,g不拿
public:
void dfs(TreeNode* node)
{
if (!node) {
return;
}
dfs(node->left);
dfs(node->right);
f[node] = node->val + g[node->left] + g[node->right]; // 拿當前節點,則不拿子節點
g[node] = max(f[node->left], g[node->left]) + max(f[node->right], g[node->right]);
}
int rob(TreeNode* root) {
dfs(root);
return max(f[root], g[root]);
}
};
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
struct SubTreeStatus {
int selected;
int notSelected;
};
public:
SubTreeStatus dfs(TreeNode* node) {
if (!node) {
return {0, 0};
}
auto left = dfs(node->left);
auto right = dfs(node->right);
int selected = node->val + left.notSelected + right.notSelected;
int notSelected = max(left.selected, left.notSelected) + max(right.selected, right.notSelected);
return {selected, notSelected};
}
int rob(TreeNode* root) {
auto rootStatus = dfs(root);
return max(rootStatus.selected, rootStatus.notSelected);
}
};
二、字符串+DP
139. 單詞拆分
給你一個字符串 s 和一個字符串列表 wordDict 作爲字典。請你判斷是否可以利用字典中出現的單詞拼接出 s 。
注意:不要求字典中出現的單詞全部都使用,並且字典中的單詞可以重複使用。
示例 1:
輸入: s = "leetcode", wordDict = ["leet", "code"]
輸出: true
解釋: 返回 true 因爲 "leetcode" 可以由 "leet" 和 "code" 拼接成。
示例 2:
輸入: s = "applepenapple", wordDict = ["apple", "pen"]
輸出: true
解釋: 返回 true 因爲 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
注意,你可以重複使用字典中的單詞。
示例 3:
輸入:
s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
輸出: false
提示:
1 <= s.length <= 300
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 20
s 和 wordDict[i] 僅有小寫英文字母組成
wordDict 中的所有字符串 互不相同
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
int slen = s.length(), wlen = wordDict.size();
unordered_set<string> dict;
for (const auto& e : wordDict) {
dict.insert(e);
}
//dp[i] 表示字符串s前i個字符組成的字符串s[0..i−1]
//是否能被空格拆分成若干個字典中出現的單詞
vector<bool> dp(slen + 1, 0);
dp[0] = true;
for (int i = 1; i <= slen; ++i)
{
//分割 [0, j - 1], [j, i - 1]
for (int j = 0; j < i; ++j)
{
if (dp[j] && dict.count(s.substr(j, i - j))) {
dp[i] = true;
break;
}
}
}
return dp[slen];
}
};
編輯距離 leetcode 72
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.length(), n = word2.length();
if (m * n == 0) {
return m + n;
}
int dp[m + 1][n + 1];
for (int i = 0; i <= m; i++)
{
dp[i][0] = i; // 當word2爲空時, word1需要刪掉i個字符
}
for (int j = 0; j <= n; j++)
{
dp[0][j] = j;
}
for (int i = 1; i <= m; ++i)
{
for (int j = 1; j <= n; ++j)
{
int flag = (word1[i - 1] == word2[j - 1] ? 0 : 1);
dp[i][j] = min(dp[i - 1][j] + 1, min(dp[i][j - 1] + 1, dp[i - 1][j - 1] + flag));
}
}
return dp[m][n];
}
};
最長公共子序列 leetcode 1143
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.length(), n = text2.length();
if (m * n == 0) {
return 0;
}
int dp[m + 1][n + 1];
memset(dp, 0, sizeof(dp));
// vector<vector<int> > dp(m + 1, vector<int>(n + 1, 0));
for (int i = 1; i <= m; ++i)
{
for (int j = 1; j <= n; ++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]);
}
}
}
return dp[m][n];
}
};
516. 最長迴文子序列
給你一個字符串 s ,找出其中最長的迴文子序列,並返回該序列的長度。
子序列定義爲:不改變剩餘字符順序的情況下,刪除某些字符或者不刪除任何字符形成的一個序列。
示例 1:
輸入:s = "bbbab"
輸出:4
解釋:一個可能的最長迴文子序列爲 "bbbb" 。
示例 2:
輸入:s = "cbbd"
輸出:2
解釋:一個可能的最長迴文子序列爲 "bb" 。
提示:
1 <= s.length <= 1000
s 僅由小寫英文字母組成
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
slen = len(s)
# i,j範圍內最長迴文子串長度
dp = [[0]*(slen) for _ in range(slen)]
for i in range(slen - 1, -1, -1):
dp[i][i] = 1
for j in range(i + 1, slen):
if s[i] == s[j]:
# [i + 1, j - 1], 兩邊符合,則向中間字符串判斷
dp[i][j] = dp[i + 1][j - 1] + 2
else:
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
return dp[0][slen - 1]
# 轉換成兩個字符串最長公共子序列
# dp = [[0]*(slen + 1) for _ in range(slen + 1)]
# ss = s[::-1]
# for i in range(1, slen+1):
# for j in range(1, slen+1):
# if s[i - 1] == ss[j - 1]:
# dp[i][j] = dp[i - 1][j - 1] + 1
# else:
# dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
# return dp[slen][slen]
時間複雜度、空間複雜度:\(O(n^2)\)
最長迴文字串 leetcode 5
中心拓展算法
class Solution {
public:
pair<int, int> expandAroundCenter(const string& s, int left, int right)
{
while (left >= 0 && right < s.size() && s[left] == s[right])
{
--left;
++right;
}
return {left + 1, right - 1};
}
string longestPalindrome(string s) {
int slen = s.length();
if (slen == 1)
{
return s;
}
int start = 0, end = 0;
for (int i = 0; i < s.size(); ++i)
{
auto [left1, right1] = expandAroundCenter(s, i, i);
auto [left2, right2] = expandAroundCenter(s, i, i + 1);
if (right1 - left1 > end - start) // 相當於
{
start = left1;
end = right1;
}
if (right2 - left2 > end - start)
{
start = left2;
end = right2;
}
}
return s.substr(start, end - start + 1);
}
};
能看懂的寫法
class Solution {
public:
string longestPalindrome(string s) {
int slen = s.length();
if (slen == 1)
{
return s;
}
int max_left = 0, max_right = -1;
for (int i = 0; i < slen; ++i)
{
int left = i, right = i;
while (left >= 0 && s[left] == s[i]) --left;
while (right < slen && s[right] == s[i]) ++right;
while (left >= 0 && right < slen && s[left] == s[right])
{
--left; ++right;
}
if (max_right - max_left < right - left)
{
max_left = left;
max_right = right;
}
}
return s.substr(max_left + 1, max_right - max_left - 1);
}
};
最長不重複子串 leetcode 3
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
occ = set()
slen = len(s)
# 右指針,初始值爲-1,相當於我們在字符串的左邊界的左側,還沒有開始移動
rk, ans = -1, 0
for i in range(slen):
if i != 0:
# 左指針向右移動一格,移除一個字符
occ.remove(s[i - 1])
while rk + 1 < slen and s[rk + 1] not in occ:
# 不斷的移動右指針
occ.add(s[rk + 1])
rk += 1
# 第 i 到 rk個字符是一個極長的無重複字符串子串
ans = max(ans, rk - i + 1)
return ans
10. 正則表達式匹配
給你一個字符串 s 和一個字符規律 p,請你來實現一個支持 '.' 和 '*' 的正則表達式匹配。
'.' 匹配任意單個字符
'*' 匹配零個或多個前面的那一個元素
所謂匹配,是要涵蓋 整個 字符串 s的,而不是部分字符串。
示例 1:
輸入:s = "aa", p = "a"
輸出:false
解釋:"a" 無法匹配 "aa" 整個字符串。
示例 2:
輸入:s = "aa", p = "a"
輸出:true
解釋:因爲 '' 代表可以匹配零個或多個前面的那一個元素, 在這裏前面的元素就是 'a'。因此,字符串 "aa" 可被視爲 'a' 重複了一次。
示例 3:
輸入:s = "ab", p = "."
輸出:true
解釋:"." 表示可匹配零個或多個('*')任意字符('.')。
提示:
1 <= s.length <= 20
1 <= p.length <= 30
s 只包含從 a-z 的小寫字母。
p 只包含從 a-z 的小寫字母,以及字符 . 和 *。
保證每次出現字符 * 時,前面都匹配到有效的字符
class Solution {
public:
bool isMatch(string s, string p) {
int slen = s.length(), plen = p.length();
auto matches = [&](int i, int j) {
if (i == 0) {
return false;
}
if (p[j - 1] == '.') {
return true;
}
return s[i - 1] == p[j - 1];
};
// dp的i範圍: 1~slen, j範圍: 1~plen
vector<vector<int> > dp(slen + 1, vector<int>(plen + 1));
dp[0][0] = true;
for (int i = 0; i <= slen; ++i)
{
//把 a* 組合看成整體
for (int j = 1; j <= plen; ++j)
{
//p的第j個字符是*
if (p[j - 1] != '*') {
if (matches(i, j)) {
dp[i][j] = dp[i][j] | dp[i - 1][j - 1];
}
}
else {
dp[i][j] = dp[i][j] | dp[i][j - 2];
//比較s第i個字符和p第j-1個字符
if (matches(i, j - 1)) {
//dp[i-1][j]:匹配s末尾的一個字符,將該字符扔掉,而該組合還可以繼續進行匹配;
//dp[i][j-2]:不匹配字符,將該組合扔掉,不再進行匹配。
dp[i][j] = dp[i][j] | dp[i - 1][j];
}
}
}
}
return dp[slen][plen];
}
};
22. 括號生成
數字 n 代表生成括號的對數,請你設計一個函數,用於能夠生成所有可能的並且 有效的 括號組合。
示例 1:
輸入:n = 3
輸出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
輸入:n = 1
輸出:["()"]
提示:
1 <= n <= 8
class Solution {
public:
void backtrack(vector<string>& ans, string& current, int open, int close, int n)
{
if (current.size() == n * 2) {
ans.emplace_back(current);
return;
}
if (open < n) {
current.push_back('(');
backtrack(ans, current, open + 1, close, n);
current.pop_back();
}
if (close < open) {
current.push_back(')');
backtrack(ans, current, open, close + 1, n);
current.pop_back();
}
}
vector<string> generateParenthesis(int n) {
if (n == 1) {
return {"()"};
}
vector<string> ans;
string current;
backtrack(ans, current, 0, 0, n);
return ans;
}
};
三、貪心算法
55. 跳躍遊戲
給定一個非負整數數組 nums ,你最初位於數組的 第一個下標 。
數組中的每個元素代表你在該位置可以跳躍的最大長度。
判斷你是否能夠到達最後一個下標。
示例 1:
輸入:nums = [2,3,1,1,4]
輸出:true
解釋:可以先跳 1 步,從下標 0 到達下標 1, 然後再從下標 1 跳 3 步到達最後一個下標。
示例 2:
輸入:nums = [3,2,1,0,4]
輸出:false
解釋:無論怎樣,總會到達下標爲 3 的位置。但該下標的最大跳躍長度是 0 , 所以永遠不可能到達最後一個下標。
提示:
1 <= nums.length <= 3 * 104
0 <= nums[i] <= 105
class Solution {
public:
bool canJump(vector<int>& nums) {
int nlen = nums.size();
if (nlen == 1 || nums[0] >= nlen - 1) {
return true;
}
//每次記錄當前位置能到達的最遠下標,如果當前位置小於上一次記錄能到達的最遠下標,則fasle
int fastLength = nums[0];
for (int i = 1; i < nlen; i++)
{
if (fastLength < i) {
return false;
}
fastLength = max(i + nums[i], fastLength);
if (fastLength >= nlen - 1) {
return true;
}
}
return false;
}
};
時間複雜度:O(n); 空間複雜度:O(1)
300. 最長遞增子序列
給你一個整數數組 nums ,找到其中最長嚴格遞增子序列的長度。子序列 是由數組派生而來的序列,刪除(或不刪除)數組中的元素而不改變其餘元素的順序。例如,[3,6,2,7] 是數組 [0,3,1,6,2,2,7] 的子序列。
示例 1:
輸入:nums = [10,9,2,5,3,7,101,18]
輸出:4
解釋:最長遞增子序列是 [2,3,7,101],因此長度爲 4 。
示例 2:
輸入:nums = [0,1,0,3,2,3]
輸出:4
示例 3:
輸入:nums = [7,7,7,7,7,7,7]
輸出:1
提示:
1 <= nums.length <= 2500
-104 <= nums[i] <= 104
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int nlen = nums.size();
if (nlen == 0) {
return 0;
}
//1. dp: O(n^2)
//nums = [10,9,2,5,3,7,101,18]
// vector<int> dp(nlen, 0);
// int ans = 0;
// for (int i = 0; i < nlen; ++i)
// {
// dp[i] = 1;
// for (int j = 0; j < i; ++j)
// {
// if (nums[j] < nums[i]) {
// dp[i] = max(dp[i], dp[j] + 1);
// }
// }
// if (ans < dp[i]) {
// ans = dp[i];
// }
// }
// return ans;
//2. 貪心+二分查找 O(nlogn)
//我們維護一個數組 d[i] ,表示長度爲 i 的最長上升子序列的末尾元素的最小值
//用 len 記錄目前最長上升子序列的長度,起始時 len 爲 1, d[1] = nums[0]。
int len = 1;
vector<int> d(nlen + 1, 0);
d[len] = nums[0];
//d[i]關於i單調遞增的
//依次遍歷nums中每個元素,更新d和len的值,如果nums[i] > d[len]則更新len=len+1
//否則d[1..len]中找到 d[i-1] < nums[j] <d[i],更新d[i] = nums[j]
//流程:
//1. 設已求出最大子序列長度爲len,從前向後遍歷nums,遍歷到nums[i]時:
// - 如果nums[i] > d[len], 則之間加入d數組末尾,並更新len++
// - 否則,在d數組中二分查找,找到第一個比nums[i]小的數d[k],並更新d[k+1] = nums[i]
for (int i = 1; i < nlen; ++i)
{
if (nums[i] > d[len]) {
d[++len] = nums[i];
} else {
// 如果找不到說明所有的數都比 nums[i] 大,此時要更新 d[1],所以這裏將 pos 設爲 0
int left = 1, right = len, pos = 0;
while (left <= right) {
int mid = (left + right) >> 1;
if (d[mid] < nums[i]) {
pos = mid;
left = mid + 1;
} else {
right = mid - 1;
}
}
d[pos + 1] = nums[i];
cout << pos + 1 << " " << nums[i] << endl;
}
}
return len;