121. 买卖股票的最佳时机
分析
只有1支股票, 最多完成一笔交易
样例模拟
7 1 5 3 6 4
发现可以在第2天买入, 第5天卖出, 获利5
扫描一遍就可以了
具体:
当前第i天, 记录下[1, i-1]中的最小值, 然后在第i天卖出的话, 只需要在最小值的地方买入, 这样对每天取max, 然后[1, i - 1]的最小值, 可以边扫描边维护
整个算法时间复杂度O(n)
code
class Solution {
public:
int maxProfit(vector<int>& prices) {
int res = 0;
for (int i = 0, minp = INT_MAX; i < prices.size(); i ++ ){
res = max(res, prices[i] - minp); // 更新最大值
minp = min(minp, prices[i]); // 维护最小值
}
return res;
}
};
122. 买卖股票的最佳时机 II
分析
可以进行多次交易, 但是交易不能重叠
不能出现如下有交集的交易
但是这样是可以的, 先买入, 再卖出
所有交易一定是如下图
交易分解
可以将交易进行拆分(
i:买入, j卖出
将交易拆分成连续每一天的交易
P
j
−
P
i
=
(
P
i
+
1
−
P
i
)
+
(
P
i
+
2
−
P
i
+
1
)
+
.
.
.
+
(
P
j
−
P
j
−
1
)
P_j - P_i = (P_{i + 1} - P_i ) + (P_{i + 2} - P_{i + 1}) + ... + (P_j - P_{j - 1})
Pj−Pi=(Pi+1−Pi)+(Pi+2−Pi+1)+...+(Pj−Pj−1)
由于这些交易段是没有交集的, 题目要求我们选择某些段, 求收益的最大值, 每一天最多只能选1次
所有可以购买的股票的方式, 从若干天里面选一些进行单天的交易, 为了收益最大, 可以提前算下每天收益是多少, 从里面选一些使得总和最大, 那么应该选择和为正的值加起来
如果当天买入, 后一天卖出, 可以获得收益的话, 就操作
联动题
联动交易分解思想
1163.纪念品(NOIP, CCF-2019)
若能想到交易分解, 该题就变成了简单的背包问题
code
class Solution {
public:
int maxProfit(vector<int>& prices) {
int res = 0;
for (int i = 0; i + 1 < prices.size(); i ++ ){
res += max(-prices[i] + prices[i + 1], 0);
}
return res;
}
};
123. 买卖股票的最佳时机 III
分析
yxc:可以用dp, 但是介绍另外一种思路
另外思路: 凡是交易2次的问题, 在枚举的时候, 可以枚举两次交易的分界点
前后缀分解:
可以枚举第2次交易买入的时间, 当枚举完第2次交易买入的时间后, 比方说第2次交易买入的时间为第i天, 怎么求这一类方案的最值?
这样分段后, 第1次交易必然在[1, i - 1], 第2次交易在[i, n], 想要让和最大, 因为两段是独立的, 只需要前面取最值, 后面取最值
后面取最值, 可以用第1题的思路, 扫描的时候记录下[i + 1, n]里的最大值, 因为第 i 天买入已经确定了, 因此需要找个最大值卖出
前面的最值:
f[i] : 表示[1, i]天操作1次, 取得的最大值
那么前面区间的最值就是f[i - 1]
然后总和就是f[i - 1] + maxp - i
预处理f数组也和第1题一样
从前往后扫描一遍, 枚举下哪一天卖出, 第i天卖出的话,
p
i
−
m
i
n
p
p_i - minp
pi−minp, 还有另外一种情况, 因为是在前i天卖出, 不一定非得在第i天卖出, 即在[1, i -1]天卖出f[i - 1]
时间复杂度: 预处理f数组O(n), 后面段扫描一遍O(n), 总O(n)
联动题
AcWing题库搜“前后缀分解”
3道题
code
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<int> f(n + 2);// 下标从1开始, 方便计算
for (int i = 1, minp = INT_MAX; i <= n; i ++ ){ // 预处理f数组, 来保存前一段的最大值
f[i] = max(f[i - 1], prices[i - 1] - minp); // 下标1开始, 当前的i对应原数组的i - 1, i = 1对应 price[0]
minp = min(minp, prices[i - 1]);
}
int res = 0;
for (int i = n, maxp = 0; i; i -- ){ // 计算后一段最大值, 同时求答案, i >= 1, 可以简写成i
res = max(res, maxp - prices[i - 1] + f[i - 1]);
maxp = max(maxp, prices[i - 1]);
}
return res;
}
};
124. 二叉树中的最大路径和
分析
求和最大的路径和是多少, 路径的个数, 起点n个, 终点n个, O(n^2), 路径是有向的, 虽然有些路径会算两次
需要想一种方法, 将所有路径都枚举出来, 才可以求最值
树里面枚举路径, 一般枚举路径的最高点, 这个最高点一般叫做LCA(最近公共祖先)
求以LCA为最高点的路径最大值, 由于左子树和右子树是独立的, 因此只要让左边取最大值, 右边取最大值
只需要递归的时候, 让左子树返回最大的和
dp和贪心的本质区别
dp一般是把原问题分成若干个子问题, dp是用dp的方式求每个子问题的最值, 然后再取max
贪心问题, 可以将原问题分解成若干个子问题, 可以通过推理的方式, 推断出来最优解一定在某一类里, 或者某一类一定不是最优解, 这样某些类就不用计算了
如果求出来了左儿子最大值f(a), 右儿子最大值f(b), f(u)怎么求
那么从u节点往下走的最值f(u)分为三种情况:(注意f(u)是往下走的最值, 不是要求解的答案):
- u->val(没有左右儿子, 或者说往下走是负数)
- u->val + f(a)
- u->val + f(b)
以u为最高点路径和就是:u的值 + 左边往下走的最值 + 右边往下走的最值
f(a) < 0的话, 就不走
每个点遍历1次, 时间复杂度 O(n)
code
/**
* 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 {
public:
int ans;
int maxPathSum(TreeNode* root) {
ans = INT_MIN;
dfs(root);
return ans;
}
int dfs(TreeNode* u){
if (!u) return 0;
int left = max(0, dfs(u->left)); // 左子树的递归最大值
int right = max(0, dfs(u->right)); // 右子树的递归最大值
ans = max(ans, u->val + left + right);
return u->val + max(left, right);
}
};
125. 验证回文串
分析
直接扫描
tolower函数可以将字母转换成小写
toupper转换成大写
code
class Solution {
public:
bool check(char c){
return c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z';
}
bool isPalindrome(string s) {
for (int i = 0, j = s.size() - 1; i < j; i ++, j -- ){
while (i < j && !check(s[i])) i ++;
while (i < j && !check(s[j])) j --;
if (i < j && tolower(s[i]) != to_lower(s[j]))) return false;
}
return true;
}
};
126. 单词接龙 II
分析
本质想是最短路的问题, 可以把每个单词看成图论中的一个点, 如果每个单词可以在一步以内变成另外一个单词的话, 就连边
这样就可以构造图
然后题目要求某两个点之间的最短距离
由于边权都是1, 通过bfs来求
估计下答案的数量, 最坏情况下有多少种
每组里有3个点, 如果有n个点话, 那么有n / 3组
每组可以往上走, 也可以往下走, 那么总共有 2 n 3 2^{\frac{n}{3}} 23n种不同情况
最坏情况下指数级别
所以发现答案是指数级别, 所以没有必要考虑优化, 直接将答案爆搜出来就可以了
优化(dist数组的使用)
搜索的不能直接爆搜, 要加优化
先求dist数组, dist[i]: 起点到i的最短距离, 求完之后可以利用dist做优化, 保证我们不会搜索没有意义的方案
假设已经求出来dist数组, 倒着来搜, 每次从终点出发, 比如有3条路end->a, end->b, end->c, 然后用dist数组判断下每个分支有没有可能沿着最短路走到起点, 比方说如果从a这条路能到达起点, 那么dist[a] + 1 = dist[end]; 如果dist[a] + 1 > dist[end] 说明没有最短路
建图
单词个数:n
单词长度:L
建图方式:
- 两两枚举下, 看看两个字符能不能连边, O(n^2L), 因为需要用O(L)时间判断两个单词是否不同
- 枚举下每个单词的每个字母nL, 再枚举下这个字母可以变成哪些字母, 一共26种情况, 26nL, 然后再用变完的字母去hash表种查询, 看看是否出现过O(L)的时间去查询, 26nL^2
因为没有数据范围, 只能自己推, 考虑什么时候
n
2
L
≥
26
n
L
2
n^2 L \geq 26nL^2
n2L≥26nL2
n
≥
26
L
n \geq 26L
n≥26L的时候, 第1种建图方式比较差, 采用第2种建图; 否则用n < 26L
yxc:以前写的代码采用第1种方式, 后来leetcode改了数据, 第1种建图被卡了, 代码采用第2种方式建图
课程相关评论
178 6个月前 回复
126真难
yxc 5个月前 回复
是的hh 这个思路在提高课线段树中讲到过。
code
class Solution {
public:
unordered_set<string> S;
unordered_map<string, int> dist;
queue<string> q;
vector<vector<string>> ans;
vector<string> path;
string beginWord;
vector<vector<string>> findLadders(string _beginWord, string endWord, vector<string>& wordList) {
beginWord = _beginWord;
for (auto word : wordList) S.insert(word);
dist[beginWord] = 0;
q.push(beginWord);
while (q.size()){
auto t = q.front(); q.pop();
string r = t;
for (int i = 0; i < t.size(); i ++ ){
string t = r;
for (char j = 'a'; j <= 'z'; j ++ ){
t[i] = j;
if (S.count(t) && dist.count(t) == 0){ // 当单词t在字典中, 并且当前单词t的到起点的最短路没有被计算过
dist[t] = dist[r] + 1;
if (t == endWord) break; // 当前t到达终点, 如果距离为k, 说明长度为k - 1层已经全部遍历过(说明往队伍里插入的距离是k, 不可能是k - 1的, 所以k - 1已经搜完), 后面的单词不用计算距离, 因为距离都比当前层数大
q.push(t);
}
}
}
}
if (dist.count(beginWord) == 0) return ans;
path.push_back(endWord);
dfs(endWord);
return ans;
}
void dfs(string t){
if (t == beginWord){
reverse(path.begin(), path.end()); // 因为存的时候是倒着存, 翻转以下push到答案中
ans.push_back(path);
reverse(path.begin(), path.end()); // 恢复现场, 因为答案不止1条path
}else { // 从endWord 往beginWord寻找路径
string r = t;
for (int i = 0; i < t.size(); i ++ ) {
t = r;
for (char j = 'a'; j <= 'z'; j ++ ){
t[i] = j;
if (dist.count(t) && dist[r] == dist[t] + 1){ // 当单词t出现在dist数组中, 并且距离和最短路匹配
path.push_back(t);
dfs(t);
path.pop_back(); // 恢复现场
}
}
}
}
}
};
code(O(n^2L)建图)(TLE)
class Solution {
public:
unordered_set<string> S;
unordered_map<string, int> dist;
queue<string> q;
vector<vector<string>> ans;
vector<string> path;
string beginWord;
vector<vector<string>> findLadders(string _beginWord, string endWord, vector<string>& wordList) {
beginWord = _beginWord;
for (auto word : wordList) S.insert(word);
dist[beginWord] = 0;
q.push(beginWord);
while (q.size()){
auto t = q.front(); q.pop();
int tot;
for (auto x : wordList){ // 寻找wordList每个与当前字符只差1个字母的字符
tot = 0;
for (int i = 0; i < t.size(); i ++ ){
if (t[i] != x[i]) tot ++;
if (tot > 1) break;
}
if (dist.count(x) == 0 && tot == 1) {
dist[x] = dist[t] + 1;
q.push(x);
if (x == endWord) break;
}
}
}
if (dist.count(beginWord) == 0) return ans;
path.push_back(endWord);
dfs(endWord);
return ans;
}
void dfs(string t){
if (t == beginWord){
reverse(path.begin(), path.end()); // 因为存的时候是倒着存, 翻转以下push到答案中
ans.push_back(path);
reverse(path.begin(), path.end()); // 恢复现场, 因为答案不止1条path
}else { // 从endWord 往beginWord寻找路径
string r = t;
for (int i = 0; i < t.size(); i ++ ) {
t = r;
for (char j = 'a'; j <= 'z'; j ++ ){
t[i] = j;
if (dist.count(t) && dist[r] == dist[t] + 1){ // 当单词t出现在dist数组中, 并且距离和最短路匹配
path.push_back(t);
dfs(t);
path.pop_back(); // 恢复现场
}
}
}
}
}
};
127. 单词接龙
分析
上一题的简化版, 只需要计算最短路, 不需要输出方案, 因此只需要将上一题的第1步拿过来即可
但是需要注意距离算上起点, n + 1
code
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
unordered_set<string> S;
for(auto word : wordList) S.insert(word);
unordered_map<string, int> dist;
queue<string> q;
dist[beginWord] = 0;
q.push(beginWord);
while (q.size()){
auto t = q.front(); q.pop();
string r = t;
for (int i = 0; i < t.size(); i ++ ){
t = r;
for (char j = 'a'; j <= 'z'; j ++ ){
t[i] = j;
if (S.count(t) && !dist.count(t)){
dist[t] = dist[r] + 1;
if (t == endWord) return dist[t] + 1;
q.push(t);
}
}
}
}
return 0;
}
};
128. 最长连续序列
分析
并查集一般写的时候, 只写路径压缩, 因为已经很快了, 但是其实 还有按秩合并, 只有这两个方法结合, 并查集每步才是O(1), 如果只有其中1步, 时间复杂度是O(logn)的
这题有一个看着像并查集的做法, 是不能按秩合并的, 只能写路径压缩
所以用看着像并查集的做法, 其实时间复杂度是O(nlogn)的, 不满足要求的
先用hash表存每个数
枚举的时候, 可以按任意顺序来枚举, 比方说枚举到x, 从x看下连续的段, 最右边的界限: x + 1, x + 2, … 最后到y.
为了避免重复枚举, 每次枚举x, 需要看下x是否是起点, 如果x - 1存在, 那么x不枚举, 只枚举每一段的第1个数, 所在在枚举所有情况的时候, 而且要判重, 枚举过的数要删掉, 这样保证每个数只枚举1次, O(n)
一定要注意删除, 比如某个起点出现10000次, 然后长度是10000, 那么会在hash表中找了10000次, 又枚举了同一个起点10000次, 那么时间复杂度O(n^2)了
code
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> S;
for (auto num : nums) S.insert(num);
int res = 0;
for (auto x : nums){
if (S.count(x) && !S.count(x - 1)){
int y = x;
S.erase(x);
while (S.count(y + 1)){
y ++;
S.erase(y);
}
res = max(res, y - x + 1);
}
}
return res;
}
};
129. 求根节点到叶节点数字之和
分析
如果从根节点下来的时候, 没加当前点k, 和是number的话. 加上当前的数的话, 应该是number * 10 + k
用以上公式维护下, 根节点到当前节点的值是多少
总结:
递归结束条件: 没有左右子树
如果有左子树, 往左子树继续计算number
如果有右子树, 往右子树继续计算number
code
/**
* 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 {
public:
int ans = 0;
int sumNumbers(TreeNode* root) {
if (root) dfs(root, 0);
return ans;
}
void dfs(TreeNode* root, int number){
number = number * 10 + root->val;
if (!root->left && !root->right) ans += number;
if (root->left) dfs(root->left, number);
if (root->right) dfs(root->right, number);
}
};
130. 被围绕的区域
分析
当然可以硬找, 找每个O的连通块, 看看是不是到边界了, 就表示没有被包围
但有一种简单的方式, 看下哪些O是没有被包围的, 就是O可以走到边界上, 先将没有被包围的O找出来, 然后将剩余的O变成X, 就是从四周去找O的连通块, 然后标记下, 再把没有被标记的变成X
不需要恢复现场
谋杀柠檬 26天前 回复
dfs中board不用复原现场吗
谋杀柠檬 26天前 回复
明白了,因为需要保留所有被灌溉区域,所以不用恢复现场
code
注意在全局变量中改变了board, 要赋值给函数参数_board
class Solution {
public:
vector<vector<char>> board;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
void solve(vector<vector<char>>& _board) {
board = _board;
int n = board.size();
if (n == 0) return ;
int m = board[0].size();
if (m == 0) return ;
for (int i = 0; i < n; i ++ ){
if (board[i][0] == 'O') dfs(i, 0);
if (board[i][m - 1] == 'O') dfs(i, m - 1);
}
for (int i = 0; i < m; i ++ ){
if (board[0][i] == 'O') dfs(0, i);
if (board[n - 1][i] == 'O') dfs(n - 1, i);
}
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ ){
if (board[i][j] == '#') board[i][j] = 'O';
else if (board[i][j] == 'O') board[i][j] = 'X';
}
_board = board; // 注意要赋值
}
void dfs(int x, int y){
board[x][y] = '#';
for (int i = 0; i < 4; i ++ ){
int a = x + dx[i], b = y + dy[i];
if (a >= 0 && a < board.size() && b >= 0 && b < board[0].size() && board[a][b] == 'O') dfs(a, b);
}
}
};