leetcode 121-130

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}) PjPi=(Pi+1Pi)+(Pi+2Pi+1)+...+(PjPj1)
由于这些交易段是没有交集的, 题目要求我们选择某些段, 求收益的最大值, 每一天最多只能选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 piminp, 还有另外一种情况, 因为是在前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)是往下走的最值, 不是要求解的答案):

  1. u->val(没有左右儿子, 或者说往下走是负数)
  2. u->val + f(a)
  3. 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
建图方式:

  1. 两两枚举下, 看看两个字符能不能连边, O(n^2L), 因为需要用O(L)时间判断两个单词是否不同
  2. 枚举下每个单词的每个字母nL, 再枚举下这个字母可以变成哪些字母, 一共26种情况, 26nL, 然后再用变完的字母去hash表种查询, 看看是否出现过O(L)的时间去查询, 26nL^2

因为没有数据范围, 只能自己推, 考虑什么时候 n 2 L ≥ 26 n L 2 n^2 L \geq 26nL^2 n2L26nL2
n ≥ 26 L n \geq 26L n26L的时候, 第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);
        }
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值