两天速通力扣HOT100[DAY2] (55~100)
本题解旨在以最简单的语言总结hot100各题思路,为每一题提供一个思考入口,但想要手撕出来,需要自己认真推理细节。
目录
55、全排列
思路
回溯基本思想:DFS + 状态还原
面对前方n种选择的时候,循环选择其中一种,做出对应的改变并进入下一层,退出时恢复原状。
code
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
dfs(nums, 0);
return res;
}
private:
vector<vector<int>> res;
void dfs(vector<int> nums, int x) {
if (x == nums.size() - 1) {
res.push_back(nums); // 添加排列方案
return;
}
for (int i = x; i < nums.size(); i++) {
swap(nums[i], nums[x]); // 交换,将 nums[i] 固定在第 x 位
dfs(nums, x + 1); // 开启固定第 x + 1 位元素
swap(nums[i], nums[x]); // 恢复交换
}
}
};
作者:Krahets
链接:https://leetcode.cn/problems/permutations/solutions/2363882/46-quan-pai-lie-hui-su-qing-xi-tu-jie-by-6o7h/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
56、子集
思路
回溯暴力
code
class Solution {
public:
vector<int> t;
vector<vector<int>> ans;
void dfs(int cur, vector<int>& nums) {
if (cur == nums.size()) { // 递归终点
ans.push_back(t);
return;
}
t.push_back(nums[cur]); // 选择当前元素
dfs(cur + 1, nums); // 进入下一层
t.pop_back(); // 不选择当前元素
dfs(cur + 1, nums); // 进入下一层
}
vector<vector<int>> subsets(vector<int>& nums) {
dfs(0, nums);
return ans;
}
};
57、电话号码的字母组合
思路
回溯暴力
每个键遍历选择某个字母,进入下一个键
code
class Solution {
private:
vector<string> m = {
"abc","def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
};
string digits;
public:
void dfs(vector<string>& res, string& one, int index) {
if(index == digits.size()) {
res.push_back(one);
return ;
}
int t = digits[index] - '0' - 2;
for(int i = 0; i < m[t].size(); i++) {
one += m[t][i];
dfs(res, one, index + 1);
one.pop_back();
}
}
vector<string> letterCombinations(string digits) {
vector<string> res;
if(digits.empty()) return res;
string one = "";
this->digits = digits;
dfs(res, one, 0);
return res;
}
};
58、组合总和
思路
回溯:对每个数字有选or不选两种选择,维护好每种情况下剩余target,回溯遍历即可。
剪枝:由于每个数字都是正数,所以当target < 0时无需继续遍历,直接return即可。
code
class Solution {
public:
void dfs(vector<int>& candidates, int target, vector<vector<int>>& ans, vector<int>& combine, int idx) {
if (idx == candidates.size() || target < 0) {
return;
}
if (target == 0) {
ans.emplace_back(combine);
return;
}
// 直接跳过
dfs(candidates, target, ans, combine, idx + 1);
// 选择当前数
if (target - candidates[idx] >= 0) {
combine.emplace_back(candidates[idx]);
dfs(candidates, target - candidates[idx], ans, combine, idx);
combine.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> ans;
vector<int> combine;
dfs(candidates, target, ans, combine, 0);
return ans;
}
};
59、括号生成
思路
记录已放置左右括号的数量,当左括号数量小于n时可以放左括号,当右括号数量小于左括号数量时可以放右括号,根据数量来判断有几种选择,对每种选择回溯遍历即可。
code
class Solution {
void backtrack(vector<string>& ans, string& cur, int open, int close, int n) {
if (cur.size() == n * 2) {
ans.push_back(cur);
return;
}
if (open < n) {
cur.push_back('(');
backtrack(ans, cur, open + 1, close, n);
cur.pop_back();
}
if (close < open) {
cur.push_back(')');
backtrack(ans, cur, open, close + 1, n);
cur.pop_back();
}
}
public:
vector<string> generateParenthesis(int n) {
vector<string> result;
string current;
backtrack(result, current, 0, 0, n);
return result;
}
};
60、单词搜索
思路
从多个入口出发,回溯遍历
code
class Solution {
public:
int m, n;
vector<vector<int>> used;
bool inBoard(int i, int j) {
return 0 <= i && i < m && 0 <= j && j < n;
}
bool dfs(vector<vector<char>>& board, string word, int index, int i, int j) {
if(index == word.size() - 1) return true;
bool res = false;
if(inBoard(i - 1, j) && board[i-1][j] == word[index + 1] && used[i-1][j] == 0) {
used[i-1][j] = 1;
res |= dfs(board, word, index + 1, i - 1, j);
used[i-1][j] = 0;
}
if(inBoard(i, j - 1) && board[i][j-1] == word[index + 1] && used[i][j-1] == 0) {
used[i][j-1] = 1;
res |= dfs(board, word, index + 1, i, j - 1);
used[i][j-1] = 0;
}
if(inBoard(i + 1, j) && board[i+1][j] == word[index + 1] && used[i+1][j] == 0) {
used[i+1][j] = 1;
res |= dfs(board, word, index + 1, i + 1, j);
used[i+1][j] = 0;
}
if(inBoard(i, j + 1) && board[i][j+1] == word[index + 1] && used[i][j+1] == 0) {
used[i][j+1] = 1;
res |= dfs(board, word, index + 1, i, j + 1);
used[i][j+1] = 0;
}
return res;
}
bool exist(vector<vector<char>>& board, string word) {
this->m = board.size();
this->n = board[0].size();
vector<int> line(n);
used.resize(m, line);
bool res = false;
for(int i = 0; i < m; ++i) {
for(int j = 0; j < n; ++j) {
if(board[i][j] == word[0]){
used[i][j] = 1;
res |= dfs(board, word, 0, i, j);
used[i][j] = 0;
}
}
}
return res;
}
};
61、分割回文串
思路
回溯 + 动态规划预处理
为求出所有可能,需要回溯遍历,思路如下:
当遍历到i
位置时,认为前0~i-1
的字符串已分割完毕,从i
出发寻找j
,使得s[i:j]
也是回文串,这时有两种选择:
- 将
s[i:j]
加入结果集,跳到j+1
位置继续遍历 - 忽略,继续
j++
寻找新的回文串右边界
如此便形成标准回溯模式。
优化:每次判断s[i:j]
是否为回文串耗时,可使用动态规划预处理,将判断某一段是否为回文串提前计算出来,后续直接取用即可。
dp[i][j]
表示字符串s[i:j]
是否为回文串,是取1
,否取0
,递推式如下:
d
p
[
i
]
[
j
]
=
{
t
r
u
e
i
≥
j
d
p
[
i
+
1
,
j
−
1
]
&
(
s
[
i
]
=
=
s
[
j
]
)
i
<
j
\begin{equation} dp[i][j] =\left\{ \begin{array}{ll} true & i \geq j \\ dp[i+1, j-1] \& (s[i] == s[j]) & i < j \end{array}\right. \end{equation}
dp[i][j]={truedp[i+1,j−1]&(s[i]==s[j])i≥ji<j
code
class Solution {
private:
vector<vector<int>> f;
vector<vector<string>> ret;
vector<string> ans;
int n;
public:
void dfs(const string& s, int i) {
if (i == n) {
ret.push_back(ans);
return;
}
for (int j = i; j < n; ++j) {
if (f[i][j]) {
ans.push_back(s.substr(i, j - i + 1));
dfs(s, j + 1);
ans.pop_back();
}
}
}
vector<vector<string>> partition(string s) {
n = s.size();
f.assign(n, vector<int>(n, true));
for (int i = n - 1; i >= 0; --i) {
for (int j = i + 1; j < n; ++j) {
f[i][j] = (s[i] == s[j]) && f[i + 1][j - 1];
}
}
dfs(s, 0);
return ret;
}
};
62、N皇后
思路
N
个皇后需要放置在N
行N
列中,可以将结果保存为一个长度为N
的数组,记录第i
行的皇后放置在哪一列。对第一行来说,有N
种选择,第二行有N-1
种,第三行有N-3
种,回溯遍历,遍历途中判断是否违背了斜线规则。
code
class Solution {
public:
vector<vector<string>> solveNQueens(int n) {
auto solutions = vector<vector<string>>();
auto queens = vector<int>(n, -1);
auto columns = unordered_set<int>();
auto diagonals1 = unordered_set<int>();
auto diagonals2 = unordered_set<int>();
backtrack(solutions, queens, n, 0, columns, diagonals1, diagonals2);
return solutions;
}
void backtrack(vector<vector<string>> &solutions, vector<int> &queens, int n, int row, unordered_set<int> &columns, unordered_set<int> &diagonals1, unordered_set<int> &diagonals2) {
if (row == n) {
vector<string> board = generateBoard(queens, n);
solutions.push_back(board);
} else {
for (int i = 0; i < n; i++) {
if (columns.find(i) != columns.end()) {
continue;
}
int diagonal1 = row - i;
if (diagonals1.find(diagonal1) != diagonals1.end()) {
continue;
}
int diagonal2 = row + i;
if (diagonals2.find(diagonal2) != diagonals2.end()) {
continue;
}
queens[row] = i;
columns.insert(i);
diagonals1.insert(diagonal1);
diagonals2.insert(diagonal2);
backtrack(solutions, queens, n, row + 1, columns, diagonals1, diagonals2);
queens[row] = -1;
columns.erase(i);
diagonals1.erase(diagonal1);
diagonals2.erase(diagonal2);
}
}
}
vector<string> generateBoard(vector<int> &queens, int n) {
auto board = vector<string>();
for (int i = 0; i < n; i++) {
string row = string(n, '.');
row[queens[i]] = 'Q';
board.push_back(row);
}
return board;
}
};
63、搜索插入位置
思路
二分查找,个人感觉左闭右闭的边界控制好写
code
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while(left <= right) {
int mid = (left + right) >> 1;
if(nums[mid] == target) {
return mid;
} else if(nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
}
};
64、搜索二维矩阵
思路
- 一次二分查找定位所在行,一次二分查找定位到所在列
- 该二维矩阵每行头尾拼接可以降为一维数组,按一维的二分查找即可
code
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size(), n = matrix[0].size();
int left = 0, right = m * n - 1;
while(left <= right) {
int mid = (left + right) >> 1;
int r = mid / n, c = mid % n;
if(matrix[r][c] == target) {
return true;
} else if(matrix[r][c] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return false;
}
};
65、在排序数组中查找元素的第一个和最后一个位置
思路
二分查找在找到目标元素时,是直接返回;若想找到第一个和位置,只需在mid找到时,将mid排除出查找区间继续查找即可,直到left == right == mid
code
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
int first = -1, last = -1;
while(left <= right) {
int mid = (left + right) >> 1;
if(nums[mid] == target) {
first = mid;
right = mid - 1; // 将mid排除出查找区间继续查找
} else if(nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
left = 0; right = nums.size() - 1;
while(left <= right) {
int mid = (left + right) >> 1;
if(nums[mid] == target) {
last = mid;
left = mid + 1; // 将mid排除出查找区间继续查找
} else if(nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return {first, last};
}
};
66、搜索旋转排序数组
思路
- 一次二分找到最小的位置,也就是旋转点;再按逻辑二分查找,将旋转数组映射到升序数组
- 根据二分位置的值判断哪边是右序的,再判断target位于有序的一边还是无序的一边。有序的就正常二分,无序的就去碰碰运气。
code
class Solution {
public:
int search(vector<int>& nums, int target) {
int min = 0, n = nums.size();
for(int l = 1, r = n - 1; l <= r; ) {
int mid = (l + r) >> 1;
if(nums[0] < nums[mid]) l = mid + 1;
else {
r = mid - 1;
min = mid;
}
}
for(int l = min, r = l + n - 1; l <= r; ) {
int mid = (l + r) >> 1;
int i = mid % n;
if(target < nums[i]) r = mid - 1;
else if(target > nums[i]) l = mid + 1;
else return i;
}
return -1;
}
};
67、寻找旋转排序数组中的最小值
思路
同上题
code
class Solution {
public:
int findMin(vector<int>& nums) {
if(nums[nums.size() - 1] > nums[0]) return nums[0];
int res = 0;
int left = 1, right = nums.size() - 1; // left 从 1 开始
while(left <= right) {
int mid = (right + left) >> 1;
if(nums[mid] > nums[0]) { // nums[mid] > nums[0] 说明mid在有序一侧,要往无序一侧寻找
left = mid + 1;
} else { // mid在无序一侧,最小值一定在mid左边
res = mid;
right = mid - 1;
}
}
return nums[res];
}
};
68、寻找两个正序数组的中位数
思路
两个数组共m + n
个数,寻找合并起来的中位数,代表在数组1中找到一个位置i,数组2中找到一个位置j,使得A[i] < B[j + 1] && B[j] < A[i + 1]
,其中,i, j
满足关系i + j = (m + n + 1) / 2
但是关于数组越界等细节比较繁琐,需要仔细想想。
code
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
if (nums1.size() > nums2.size()) {
return findMedianSortedArrays(nums2, nums1);
}
int m = nums1.size();
int n = nums2.size();
int left = 0, right = m;
// median1:前一部分的最大值
// median2:后一部分的最小值
int median1 = 0, median2 = 0;
while (left <= right) {
// 前一部分包含 nums1[0 .. i-1] 和 nums2[0 .. j-1]
// 后一部分包含 nums1[i .. m-1] 和 nums2[j .. n-1]
int i = (left + right) / 2;
int j = (m + n + 1) / 2 - i;
// nums_im1, nums_i, nums_jm1, nums_j 分别表示 nums1[i-1], nums1[i], nums2[j-1], nums2[j]
int nums_im1 = (i == 0 ? INT_MIN : nums1[i - 1]);
int nums_i = (i == m ? INT_MAX : nums1[i]);
int nums_jm1 = (j == 0 ? INT_MIN : nums2[j - 1]);
int nums_j = (j == n ? INT_MAX : nums2[j]);
if (nums_im1 <= nums_j) {
median1 = max(nums_im1, nums_jm1);
median2 = min(nums_i, nums_j);
left = i + 1;
} else {
right = i - 1;
}
}
return (m + n) % 2 == 0 ? (median1 + median2) / 2.0 : median1;
}
};
作者:力扣官方题解
链接:https://leetcode.cn/problems/median-of-two-sorted-arrays/solutions/258842/xun-zhao-liang-ge-you-xu-shu-zu-de-zhong-wei-s-114/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
69、有效的括号
思路
左括号入栈,右括号判断与栈顶是否匹配,匹配则出栈,不匹配则失败。
code
class Solution {
public:
bool isValid(string s) {
char stack[10001];
int top =0;
for (int i = 0; s[i]; ++i) {
if (s[i] == '(' || s[i] == '[' || s[i] == '{') stack[top++] = s[i];
else {
if ((--top) < 0) return false;//先减减,让top指向栈顶元素
if (s[i] == ')' && stack[top] != '(') return false;
if (s[i] == ']' && stack[top] != '[') return false;
if (s[i] == '}' && stack[top] != '{') return false;
}
}
return (!top);//防止“【”这种类似情况
}
};
70、最小栈
思路
一个栈存已有的数据,一个栈存目前的最小值
code
class MinStack {
public:
stack<int> stack1, stack2;
MinStack() {
}
void push(int val) {
stack2.push(stack1.empty() ? val : min(stack2.top(), val));
stack1.push(val);
}
void pop() {
stack1.pop();
stack2.pop();
}
int top() {
return stack1.top();
}
int getMin() {
return stack2.top();
}
};
/**
* Your MinStack object will be instantiated and called as such:
* MinStack* obj = new MinStack();
* obj->push(val);
* obj->pop();
* int param_3 = obj->top();
* int param_4 = obj->getMin();
*/
71、字符串解码
思路
栈保存一个二元组(int count, string str)
,遇到数字则计算count,遇到左括号则取str入栈,遇到右括号则出栈
code
class Solution {
public:
string decodeString(string s) {
stack<pair<string,int>> st;
string res="";
int mul=0;
for (auto c:s){
if(c=='['){
st.emplace(res,mul);
res="";
mul=0;
}else if(c==']'){
auto [last_res,cur_mul]=st.top();
st.pop();
string tmp=last_res;
for(int i=0;i<cur_mul;i++) tmp+=res;
res=tmp;
}else if('0'<=c && c<='9'){
mul=mul*10+(c-'0');
}else{
res += c;
}
}
return res;
}
};
72、每日温度
思路
单调栈
code
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = temperatures.size();
vector<int> ans(n);
stack<int> s;
for (int i = 0; i < n; ++i) {
while (!s.empty() && temperatures[i] > temperatures[s.top()]) {
int previousIndex = s.top();
ans[previousIndex] = i - previousIndex;
s.pop();
}
s.push(i);
}
return ans;
}
};
作者:力扣官方题解
链接:https://leetcode.cn/problems/daily-temperatures/solutions/283196/mei-ri-wen-du-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
73、柱状图中最大的矩形
思路
- 对于一个高度,如果能得到向左和向右的边界
- 那么就能对每个高度求一次面积
- 遍历所有高度,即可得出最大面积
- 使用单调栈,在出栈操作时得到前后边界并计算面积
code
class Solution {
public:
int largestRectangleArea(vector<int>& heights)
{
int ans = 0;
vector<int> st;
heights.insert(heights.begin(), 0);
heights.push_back(0);
for (int i = 0; i < heights.size(); i++)
{
while (!st.empty() && heights[st.back()] > heights[i])
{
int cur = st.back();
st.pop_back();
int left = st.back() + 1;
int right = i - 1;
ans = max(ans, (right - left + 1) * heights[cur]);
}
st.push_back(i);
}
return ans;
}
};
74、数组中的第k个最大元素
思路
- 大根堆
- 基于快排的选择方法
code
class Solution {
public:
int quickselect(vector<int> &nums, int l, int r, int k) {
if(l == r) return nums[k];
int partition = nums[l], i = l - 1, j = r + 1;
while(i < j) {
do i++; while(nums[i] < partition);
do j--; while(nums[j] > partition);
if(i < j) swap(nums[i], nums[j]);
}
if(k <= j) return quickselect(nums, l, j, k);
else return quickselect(nums, j + 1, r, k);
}
int findKthLargest(vector<int> &nums, int k) {
int n = nums.size();
return quickselect(nums, 0, n - 1, n - k);
}
};
75、前K个高频元素
思路
哈希表 + 小根堆
code
class Solution {
public:
static bool cmp(pair<int, int>& m, pair<int, int>& n) {
return m.second > n.second;
}
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> mp;
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(&cmp)> pq(cmp); // 优先队列自定义排序的写法
vector<int> res;
for(int num : nums) {
mp[num]++;
}
for(auto& [num, count] : mp) {
if(pq.size() == k) {
if(pq.top().second < count) {
pq.pop();
pq.emplace(num, count);
}
} else {
pq.emplace(num, count);
}
}
while(k--) {
res.push_back(pq.top().first);
pq.pop();
}
return res;
}
};
76、数据流的中位数
思路
一个大根堆保存较小的一半元素,一个小根堆保存较大的一半元素,两个堆顶即为中位数。
code
class MedianFinder {
public:
priority_queue<int, vector<int>, less<int>> queMin;
priority_queue<int, vector<int>, greater<int>> queMax;
MedianFinder() {}
void addNum(int num) {
if (queMin.empty() || num <= queMin.top()) {
queMin.push(num);
if (queMax.size() + 1 < queMin.size()) {
queMax.push(queMin.top());
queMin.pop();
}
} else {
queMax.push(num);
if (queMax.size() > queMin.size()) {
queMin.push(queMax.top());
queMax.pop();
}
}
}
double findMedian() {
if (queMin.size() > queMax.size()) {
return queMin.top();
}
return (queMin.top() + queMax.top()) / 2.0;
}
};
77、买卖股票的最佳时机
思路
维护一个整数当前可获得的最大利润,维护一个正整数表示当前见过的最小值,遍历即可
code
class Solution {
public:
int maxProfit(vector<int>& prices) {
int res = 0, min_price = prices[0];
for(int i = 1; i < prices.size(); ++i) {
if(prices[i] - min_price > res) res = prices[i] - min_price;
if(prices[i] < min_price) min_price = prices[i];
}
return res;
}
};
78、跳跃游戏
思路
维护当前能跳到的最远位置
code
class Solution {
public:
bool canJump(vector<int>& nums) {
int end = nums[0];
for(int i = 1; i < nums.size(); ++i) {
if(i > end) return false;
end = max(end, i + nums[i]);
}
return true;
}
};
79、跳跃游戏II
思路
维护当前能跳到的最远位置,和上次的最远位置(边界),当到达边界时,更新边界为当前能到达的最远位置,跳跃次数+1
code
class Solution {
public:
int jump(vector<int>& nums) {
int maxPos = 0, n = nums.size(), end = 0, step = 0;
for (int i = 0; i < n - 1; ++i) {
if (maxPos >= i) {
maxPos = max(maxPos, i + nums[i]);
if (i == end) {
end = maxPos;
++step;
}
}
}
return step;
}
};
作者:力扣官方题解
链接:https://leetcode.cn/problems/jump-game-ii/solutions/230241/tiao-yue-you-xi-ii-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
80、划分字母区间
思路
记录每个字母最后一次出现的位置,遍历字符串更新当前区间右边界,抵达时划分区间
code
class Solution {
public:
vector<int> partitionLabels(string s) {
int last[26];
int length = s.size();
for (int i = 0; i < length; i++) {
last[s[i] - 'a'] = i;
}
vector<int> partition;
int start = 0, end = 0;
for (int i = 0; i < length; i++) {
end = max(end, last[s[i] - 'a']);
if (i == end) {
partition.push_back(end - start + 1);
start = end + 1;
}
}
return partition;
}
};
作者:力扣官方题解
链接:https://leetcode.cn/problems/partition-labels/solutions/455703/hua-fen-zi-mu-qu-jian-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
81、爬楼梯
思路
- 动规
d p [ i ] = d p [ i − 1 ] + d p [ i − 2 ] ; dp[i] = dp[i - 1] + dp[i - 2]; dp[i]=dp[i−1]+dp[i−2];
- 矩阵快速幂。妙不可言
code
递推的代码没什么好说的,这是矩阵快速幂的代码。
class Solution {
public:
vector<vector<long long>> multiply(vector<vector<long long>> &a, vector<vector<long long>> &b) {
vector<vector<long long>> c(2, vector<long long>(2));
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j];
}
}
return c;
}
vector<vector<long long>> matrixPow(vector<vector<long long>> a, int n) {
vector<vector<long long>> ret = {{1, 0}, {0, 1}};
while (n > 0) {
if ((n & 1) == 1) {
ret = multiply(ret, a);
}
n >>= 1;
a = multiply(a, a);
}
return ret;
}
int climbStairs(int n) {
vector<vector<long long>> ret = {{1, 1}, {1, 0}};
vector<vector<long long>> res = matrixPow(ret, n);
return res[0][0];
}
};
作者:力扣官方题解
链接:https://leetcode.cn/problems/climbing-stairs/solutions/286022/pa-lou-ti-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
82、杨辉三角
思路
模拟
code
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> ret(numRows);
for (int i = 0; i < numRows; ++i) {
ret[i].resize(i + 1);
ret[i][0] = ret[i][i] = 1;
for (int j = 1; j < i; ++j) {
ret[i][j] = ret[i - 1][j] + ret[i - 1][j - 1];
}
}
return ret;
}
};
83、打家劫舍
思路
d p [ i ] = m a x ( d p [ i − 1 ] , d p [ i − 2 ] + n u m s [ i ] ) dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]) dp[i]=max(dp[i−1],dp[i−2]+nums[i])
code
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n == 0) return 0;
if(n == 1) return nums[0];
vector<int> dp(n, 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for(int i = 2; i < n; ++i) {
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[n - 1];
}
};
84、完全平方数
思路
动态规划
f
[
i
]
=
1
+
m
i
n
j
=
1
i
f
[
i
−
j
2
]
f[i] = 1 + min _{j = 1} ^{\sqrt{i}} f[i - j^2]
f[i]=1+minj=1if[i−j2]
code
class Solution {
public:
int numSquares(int n) {
vector<int> f(n + 1);
for (int i = 1; i <= n; i++) {
int minn = INT_MAX;
for (int j = 1; j * j <= i; j++) {
minn = min(minn, f[i - j * j]);
}
f[i] = minn + 1;
}
return f[n];
}
};
作者:力扣官方题解
链接:https://leetcode.cn/problems/perfect-squares/solutions/822940/wan-quan-ping-fang-shu-by-leetcode-solut-t99c/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
85、零钱兑换
思路
动态规划
f
[
i
]
=
m
i
n
j
=
0
n
−
1
f
(
i
−
c
j
)
+
1
f[i] = min _{j=0} ^{n-1} \ f(i - c_j) + 1
f[i]=minj=0n−1 f(i−cj)+1
code
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int Max = amount + 1;
vector<int> dp(amount + 1, Max);
dp[0] = 0;
for (int i = 1; i <= amount; ++i) {
for (int j = 0; j < (int)coins.size(); ++j) {
if (coins[j] <= i) {
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
};
作者:力扣官方题解
链接:https://leetcode.cn/problems/coin-change/solutions/132979/322-ling-qian-dui-huan-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
86、单词拆分
思路
dp[i]
表示字符串s
的前i
个字符能否被拆分
d
p
[
i
]
=
d
p
[
j
]
&
&
c
h
e
c
k
(
s
[
j
:
i
−
1
]
)
dp[i] = dp[j]\ \&\& \ check(s[j:i-1])
dp[i]=dp[j] && check(s[j:i−1])
code
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
auto wordDictSet = unordered_set <string> ();
for (auto word: wordDict) {
wordDictSet.insert(word);
}
auto dp = vector <bool> (s.size() + 1);
dp[0] = true;
for (int i = 1; i <= s.size(); ++i) {
for (int j = 0; j < i; ++j) {
if (dp[j] && wordDictSet.find(s.substr(j, i - j)) != wordDictSet.end()) {
dp[i] = true;
break;
}
}
}
return dp[s.size()];
}
};
87、最长递增子序列
思路
- 动态规划
dp[i]
表示以i
结尾的最长递增子序列的长度。 - 贪心 + 二分查找,看题解吧300. 最长递增子序列 - 力扣(LeetCode)
code
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n, 1);
int res = 1;
for(int i = 1; i < n; ++i) {
for(int j = i - 1; j >= 0; --j) {
if(nums[i] > nums[j]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
res = max(res, dp[i]);
}
return res;
}
};
88、乘积最大子数组
思路
若采用上题一样的思路,很容易推导出这样的递推式 f [ i ] = m a x i = 1 n { f ( i − 1 ) ∗ a i , a i } f[i] = max_{i=1} ^n \{f(i-1) * a_i, a_i\} f[i]=maxi=1n{f(i−1)∗ai,ai}。表示当前元素要么累乘在上一个元素的结果上,要么独自开启一段新区间。这样忽略了负负可以得正的情况,当遇到负数的时候会默认放弃。
因此,可以维护两个dp数组,一个计算最大值,一个计算最小值(负的越多越好),这样当再次遇到一个负数,可以分别与最大值相乘得到最小值,与最小值相乘得到最大值。
code
class Solution {
public:
int maxProduct(vector<int>& nums) {
vector <long> maxF(nums.begin(),nums.end()), minF(nums.begin(), nums.end());
for (int i = 1; i < nums.size(); ++i) {
maxF[i] = max(maxF[i - 1] * nums[i], max((long)nums[i], minF[i - 1] * nums[i]));
minF[i] = min(minF[i - 1] * nums[i], min((long)nums[i], maxF[i - 1] * nums[i]));
if(minF[i]<INT_MIN) {
minF[i]=nums[i];
}
}
return *max_element(maxF.begin(), maxF.end());
}
};
89、分割等和子集
思路
一个相对普适的动态规划思路:DFS --> 记忆化搜索 --> 递推关系式 --> 空间优化。
对于动态规划的题目来说,一般都有一种暴力搜索的方法可以解,且是基于DFS/BFS/普通递归的,这种解法会超时,但是暗含了大问题与子问题的依赖关系。借助这种关系可以写出递推关系式,而递归解法的递归终点判断,也恰好对应了动态规划的初始状态定义。
因此,如果能写出递归解法,提交却超时的问题,一般可以考虑动态规划解法。
例如跳台阶问题。
int func(int n) {
if(n == 1) return 1;
if(n == 2) return 2;
return func(n - 1) + func(n - 2); // 从小问题的解合并递推出大问题的解
}
最简单暴力的方法是递归,自顶向下分解问题,再从底向上合并问题得到结果。而仔细观察会发现存在重复计算,如计算
f
[
10
]
f[10]
f[10]需要先计算
f
[
8
]
,
f
[
9
]
f[8], f[9]
f[8],f[9],可是在计算
f
[
9
]
f[9]
f[9]时,已经计算过
f
[
8
]
f[8]
f[8]了,因此,如果将
f
[
8
]
f[8]
f[8]保存下来,
f
[
10
]
f[10]
f[10]就无需再算。这就是记忆化搜索,保存下来的数组就是dp
数组。至此时间复杂度已经降为
O
(
n
)
O(n)
O(n)了。
再分析,发现自顶向下分解问题的终点是
f
[
1
]
f[1]
f[1] 和 $f[2], $只有两个,所以可以直接从底向上计算就好了,也就是for(int i = 3; i < n; ++i) f[i] = f[i - 1] + f[i - 2]
一次遍历。这就是动态规划解法。
至此,时间已经无可再优化了,但空间还有。因为设计并维护了一个 int
型数组,占用空间O(n)
,但递推过程只依赖前面两个元素的结果,1 ~ i - 3
的空间完全浪费。所以采用滚动数组。对做题来说,这一步一般无关紧要。
code
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for(int num : nums)sum+=num;
int n=nums.size();
bool dp[sum+1];
memset(dp,0,sizeof(dp));
dp[0]=true;
for(int i=0;i<n;i++){
for(int j=sum;j>=nums[i]*2;j--){
dp[j]|=dp[j-nums[i]*2];
}
}
return dp[sum];
}
};
90、最长有效括号
思路
dp[i]
表示以i
结尾的最长有效括号长度
如果str[i] == '('
,自然不可能是有效括号
如果str[i] == ')'
,两种情况
- 如果
str[i - 1] == '('
,则dp[i] = dp[i - 2] + 2
- 如果
str[i - 1] == ')'
,则去找前一个有效括号的最前端的前一个是否为(
,是的话dp[i] = dp[i - 1] + dp[i - dp[i - 1] - 2] + 2
code
class Solution {
public:
int longestValidParentheses(string s) {
int maxans = 0, n = s.length();
vector<int> dp(n, 0);
for (int i = 1; i < n; i++) {
if (s[i] == ')') {
if (s[i - 1] == '(') {
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
} else if (i - dp[i - 1] > 0 && s[i - dp[i - 1] - 1] == '(') {
dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
}
maxans = max(maxans, dp[i]);
}
}
return maxans;
}
};
作者:力扣官方题解
链接:https://leetcode.cn/problems/longest-valid-parentheses/solutions/314683/zui-chang-you-xiao-gua-hao-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
91、不同路径
思路
C m + n − 2 m − 1 C _{m + n - 2} ^ {m-1} Cm+n−2m−1
code
class Solution {
public:
int C(int m, int n) {
long long res = 1;
for(int i = 0; i < n; ++i) {
res = res * (m - i) / (i + 1);
}
return res;
}
int uniquePaths(int m, int n) {
return C(m + n - 2, min(m, n) - 1);
}
};
92、最小路径和
思路
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
code
class Solution {
public:
int C(int m, int n) {
long long res = 1;
for(int i = 0; i < n; ++i) {
res = res * (m - i) / (i + 1);
}
return res;
}
int uniquePaths(int m, int n) {
return C(m + n - 2, min(m, n) - 1);
}
};
93、最长回文子串
思路
d p [ i ] [ j ] = d p [ i + 1 ] [ j − 1 ] ∧ ( s i = = s j ) dp[i][j] = dp[i + 1][j - 1] \land (s_i == s_j) dp[i][j]=dp[i+1][j−1]∧(si==sj)
注意遍历顺序
code
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
if (n < 2) {
return s;
}
int maxLen = 1;
int begin = 0;
// dp[i][j] 表示 s[i..j] 是否是回文串
vector<vector<int>> dp(n, vector<int>(n));
// 初始化:所有长度为 1 的子串都是回文串
for (int i = 0; i < n; i++) {
dp[i][i] = true;
}
// 递推开始
// 先枚举子串长度
for (int L = 2; L <= n; L++) {
// 枚举左边界,左边界的上限设置可以宽松一些
for (int i = 0; i < n; i++) {
// 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
int j = L + i - 1;
// 如果右边界越界,就可以退出当前循环
if (j >= n) {
break;
}
if (s[i] != s[j]) {
dp[i][j] = false;
} else {
if (j - i < 3) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
}
// 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
if (dp[i][j] && j - i + 1 > maxLen) {
maxLen = j - i + 1;
begin = i;
}
}
}
return s.substr(begin, maxLen);
}
};
作者:力扣官方题解
链接:https://leetcode.cn/problems/longest-palindromic-substring/solutions/255195/zui-chang-hui-wen-zi-chuan-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
94、最长公共子序列
思路
dp[i][j]
表示str1[:i]
和str2[:j]
的最长公共子序列长度
d
p
[
i
]
[
j
]
=
{
d
p
[
i
−
1
]
[
j
−
1
]
+
1
,
s
t
r
1
[
i
−
1
]
=
s
t
r
2
[
j
−
1
]
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
]
[
j
−
1
]
)
,
s
t
r
1
[
i
−
1
]
≠
s
t
r
2
[
j
−
1
]
dp[i][j] =\{ \begin{matrix} dp[i-1][j-1] + 1, & str1[i-1] = str2[j-1] \\ max(dp[i-1][j],dp[i][j-1]), & str1[i-1] \neq str2[j-1] \end{matrix}
dp[i][j]={dp[i−1][j−1]+1,max(dp[i−1][j],dp[i][j−1]),str1[i−1]=str2[j−1]str1[i−1]=str2[j−1]
code
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.length(), n = text2.length();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
for (int i = 1; i <= m; i++) {
char c1 = text1.at(i - 1);
for (int j = 1; j <= n; j++) {
char c2 = text2.at(j - 1);
if (c1 == c2) {
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];
}
};
95、编辑距离
思路
这题好好看官解 72. 编辑距离 - 力扣(LeetCode)吧,一般人真想不出来,这题会了别的题也不一定会。
code
class Solution {
public:
int minDistance(string word1, string word2) {
int n = word1.length();
int m = word2.length();
// 有一个字符串为空串
if (n * m == 0) return n + m;
// DP 数组
vector<vector<int>> D(n + 1, vector<int>(m + 1));
// 边界状态初始化
for (int i = 0; i < n + 1; i++) {
D[i][0] = i;
}
for (int j = 0; j < m + 1; j++) {
D[0][j] = j;
}
// 计算所有 DP 值
for (int i = 1; i < n + 1; i++) {
for (int j = 1; j < m + 1; j++) {
int left = D[i - 1][j] + 1;
int down = D[i][j - 1] + 1;
int left_down = D[i - 1][j - 1];
if (word1[i - 1] != word2[j - 1]) left_down += 1;
D[i][j] = min(left, min(down, left_down));
}
}
return D[n][m];
}
};
96、只出现一次的数字
思路
异或运算
code
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ret = 0;
for (auto e: nums) ret ^= e;
return ret;
}
};
97、多数元素
思路
摩尔投票法:随变立一个候选者,遍历遇到和它相等的则票数加一,不等则票数减一,若减为负数,改立新的候选者。
code
class Solution {
public:
int majorityElement(vector<int>& nums) {
int candidate = nums[0];
int cnt = 1;
for(int i = 1; i < nums.size(); ++i) {
if(nums[i] == candidate) cnt++;
else {
cnt--;
if(cnt < 0) {
candidate = nums[i];
cnt = 1;
}
}
}
return candidate;
}
};
98、颜色分类
思路
双指针一个指向下一个0
应该在的位置,一个指向下一个2
应该在的位置,从两头向中间移动。
i
从0
向后移动,遇到0
就和第一个指针交换,遇到2
就和第二个交换,遇到1
就不理会。
code
class Solution {
public:
void sortColors(vector<int>& nums) {
int n = nums.size();
int p0 = 0, p2 = n - 1;
for (int i = 0; i <= p2; ++i) {
while (i <= p2 && nums[i] == 2) {
swap(nums[i], nums[p2]);
--p2;
}
if (nums[i] == 0) {
swap(nums[i], nums[p0]);
++p0;
}
}
}
};
99、下一个排列
思路
- 在 尽可能靠右的低位 进行交换,需要 从后向前 查找
- 将一个 尽可能小的「大数」 与前面的「小数」交换。比如 123465,下一个排列应该把 5 和 4 交换而不是把 6 和 4 交换
- 将「大数」换到前面后,需要将「大数」后面的所有数 重置为升序,升序排列就是最小的排列。
code
class Solution {
public:
void nextPermutation(vector<int>& nums) {
next_permutation(nums.begin(), nums.end());
}
};
不要学,还是老老实实看题解吧!
100、寻找重复数
思路
把该序列看做一个图,nums[i]
表示 从 i
到 nums[i]
有一条边。由于nums.length == n + 1
且 1 <= nums[i] <= n
,所以如果没有重复数,该图应该是一个链表,有重复数,该图应该是一个有环的链表。
题目变为:返回链表第一个入环节点
参考 26.环形链表II。妙不可言!
code
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int slow = 0, fast = 0;
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while (slow != fast); // 相遇
slow = 0; // 一个指针回到首部重新走
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
};