Week 9. 第167-188题

167. 两数之和 II - 输入有序数组

分析

只是说同一个下标上的位置上的数只能用1次, 但是如果有2个2, 在不同下标, 可以用2次

code(hash)

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> hash;
        for (int i = 0; i < nums.size(); i ++ ){
            int r = target - nums[i];
            if (hash.count(r)) return {hash[r] + 1, i + 1};
            hash[nums[i]] = i;
        }
        return {};
    }
};

code(双指针) = 寒假每日一题(1532. 找硬币 (1月19日))

注意

一定要i < j 因为每个变量只能用1次
while后需要判断是否== target

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        for (int i = 0, j = nums.size() - 1; i < j; i ++ ){
            while (i < j && nums[i] + nums[j] > target) j --;
            if (i < j && nums[i] + nums[j] == target ) return {i + 1, j + 1};
        }
        return {};
    }
};

168. Excel表列名称

分析

当位数固定的时候, 是26进制, 但实际上位数不固定, 并不是
为了简化问题, 先确定第n个数是有几位

1位数 有26种选择
2位数 有 2 6 2 26^2 262种选择
3位数 有 2 6 3 26^3 263种选择
n > 26的话 说明一位数用完了
n > 2 6 2 26^2 262 说明 两位数用完了
以此类推, 直到找到第k位数
2 6 k ≥ n 26^k \geq n 26kn

图中做了偏移 0-A, 所以上面的推论 n > 26改成 n >= 26, 依次偏移
在这里插入图片描述
举个例子
在这里插入图片描述

code(yxc的做法)

class Solution {
public:
    string convertToTitle(int n) {
        int k = 1; // k表示位数
        for (long long p = 26; n > p; p *= 26) {
            n -= p; // -p -p
            k ++ ;
        }
        
        n --; // 因为要从0->A, 所以n --
        string res;
        while (k -- ){ 
            res += n % 26 + 'A';
            n /= 26;
        }

        reverse(res.begin(), res.end());
        return res;
    }
};

自己优化(比yxc的代码更好)

class Solution {
public:
    string convertToTitle(int n) {
        string res;
        while (n){
            n --; // 0-->A
            res += n % 26  +'A';
            n /= 26;
        }
        reverse(res.begin(), res.end());
        return res;
    }
};

171. Excel表列序号

分析

code

class Solution {
public:
    int titleToNumber(string s) {
        int res = 0;
        for (int i = 0; i < s.size(); i ++ )
            res = res * 26 + (s[i] - 'A' + 1); // A ---> 1所以要+1
        return res;
    }
};

169. 多数元素

分析

很多面试地方都出现过

从前扫描每个数, 扫描过程中维护两个变量

r:当前存的数(库存)
c:当前数的数量(库存的数量)
只用两个变量空间O(1)

扫描到的数是x的话, 如果x == r, 那么 c = c + 1, 表示当前的数多了一个(库存多1个)
如果x != r, 那么让c = c - 1, 表示我拿当前存的数, 把x消耗掉(拿出来一个库存, 把x干掉, 然后c = c - 1)

有一个边界 如果r != c, 并且c = 0, 表示当前存的数没有库存了, 就令r = x, 把x拿来当库存

时间O(n), 空间O(1)

做法的正确性:

比方说数组中有一个t, t出现的次数比其他数加起来都要多, 这样的话, 如果最后算出来答案不是t的话, 一定不对; 因为如果最后存的不是t, 那么t一定被其他数消耗掉了(因为只有两个数不同的时候才会消耗库存), 每消耗一个t都会消耗一个与t不同的数, 但是除了t1以外, 其他数加在一块都不如t多, 不可能把t都消耗掉, 所以矛盾了
因此剩下来的数只能是t

code

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        int r, c = 0;
        for (auto x : nums){
            if (r == x) c ++; // 先跟仓库比较
            else if (!c) r = x, c ++; // 如果仓库空了的话
            else c --;// 其余情况
        }
        return r;
    }
};

172. 阶乘后的零

分析

小学数奥题
yxc: 我们来试一下
10! (末尾2个0)
20! (末尾4个0)
25! (末尾6个0)

怎么考虑n!, 时间O(logn)
yxc : 也就是说n! 在 long long范围内的数都要处理掉(没明白为什么long long范围内的数都要处理掉)

long long最大范围 2 63 − 1 2^{63} - 1 2631, 大概算成 2 60 2^{60} 260 = ( 2 10 ) 6 (2^{10})^6 (210)6,
然后 2 10 2^{10} 210 = 1024 ~= 1 0 3 10^3 103
( 2 10 ) 6 (2^{10})^6 (210)6 = ( 1 0 3 ) 6 (10^3)^6 (103)6 = 1 0 18 10^{18} 1018
O(logn)的算法,当 n ≤ 1 0 18 n \leq 10^{18} n1018可以使用
因此yxc说了这句话, y老师太强了

考虑下一个数末尾0的个数取决于什么
n! = k取决于什么, 取决于k末尾能分解出来多少个10, 因为你每有一个10, 就能分解出一个0

末尾能分解出来多少个0的意思是, 因为k是乘积的形式, 所以一个10, 就一个0, 两个10, 10 * 10, 就是100, 2个0

有多少个10, 意味着10 = 2 * 5, 因此末尾有多少个10, 意味着有多少对<2, 5>

比方说 k = 2 a ∗ 5 b k = 2^a * 5^b k=2a5b, 因此末尾有多少个0取决于 min(a, b)

所以本质核心是求n! 因式分解 有多少个2, 有多少个5

怎么计算 n ! n! n!有多少2, 有多少个5呢
考虑一般的情况, n ! n! n!有多少个质因子 p p p

  1. 考虑1~n中p的倍数有多少个 , p, 2p, … 4p, … 所以有 ⌊ n p ⌋ \lfloor \frac{n}{p} \rfloor pn
  2. 1 ~ n中 p 2 p^2 p2的倍数, p 2 , 2 p 2 , 3 p 2 p^2, 2p^2, 3p^2 p2,2p2,3p2, 所以有 ⌊ n p 2 ⌋ \lfloor \frac{n}{p^2} \rfloor p2n
  3. … p^3

上述正确性:

看了5次, 现在好像觉得显然了
比如 x = p k x = p^k x=pk, 会在p的倍数中计算1次, p^k 计算1次, 所以总共k次

回到本题:

n!中2的个数 ⌊ n 2 ⌋ + ⌊ n 2 2 ⌋ + ⌊ n 2 3 ⌋ + . . . . \lfloor \frac{n}{2} \rfloor + \lfloor \frac{n}{2^2} \rfloor + \lfloor \frac{n}{2^3} \rfloor + .... 2n+22n+23n+....
n!中5的个数 ⌊ n 5 ⌋ + ⌊ n 5 2 ⌋ + ⌊ n 5 3 ⌋ + . . . . \lfloor \frac{n}{5} \rfloor + \lfloor \frac{n}{5^2} \rfloor + \lfloor \frac{n}{5^3} \rfloor + .... 5n+52n+53n+....

可以发现下面这个式子小于上面这个式子

因此最终只需要算下第2个式子就可以了

code

class Solution {
public:
    int trailingZeroes(int n) {
        int res = 0;
        while (n) res += n / 5, n /= 5;
        return res;
    }
};

173. 二叉搜索树迭代器

分析

就是将leetcode 94. 拆开来

由于

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> stk;
        while (root || stk.size()){
            while (root){
                stk.push(root);
                root = root->left;
            }
            // if (stk.size()) 可以不用加, 因为如果root非空,那么stk必定有元素,如果root空,能进循环,那么必定第二个循环条件满足,不空
            root = stk.top(); stk.pop();
            res.push_back(root->val);
            root = root->right;   
        }
        return res;
    }
};

因为在root = root->right;执行完毕后, 会回到前面, 执行while(root)部分, 因此我们需要将迭代写到Next(), 保证每次调用next(), 会执行root = stk.top();以下的四句话

题目时间复杂度另外要求

next() 操作平均下来是O(1)

虽然next()里有一个while循环, 但是树里面的每一个点只会进栈一次, 所以每次调用next, 所有的next次数加到一块, 最多只会循环n次, 那么next()一共会调用n - 1次, n n − 1 \frac{n}{n - 1} n1n均摊下来是O(1)

空间深搜的时候取决于栈的高度, 栈里面存的是一条链, 链最长的话, 表示树最高, 因此树最长就是树的高度O(h)

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 BSTIterator {
public:
    stack<TreeNode*> stk;
    BSTIterator(TreeNode* root) {
        while (root){
            stk.push(root);
            root = root->left;
        }
    }
    
    int next() {
        auto root = stk.top(); stk.pop();
        int val = root->val;
        root = root->right; // 遍历完后, 当根指向右子树
        while (root){
            stk.push(root);
            root = root->left;
        }
        return val;
    }
    
    bool hasNext() {
        return stk.size();
    }
};

/**
 * Your BSTIterator object will be instantiated and called as such:
 * BSTIterator* obj = new BSTIterator(root);
 * int param_1 = obj->next();
 * bool param_2 = obj->hasNext();
 */

174. 地下城游戏

分析

左上走到右下, 保证每时每刻骑士的血量不能掉到0
然后问我们从左上到右下(确保安全到达的情况下)最少的初始血量是多少
骑士生命没有上限

因为路径一旦确定以后, 你每一步掉的血量是知道的, 这样的话, 你就能反推出来你的初始血量是多少

所以路径一旦确定, 你的初始血量就能确定

那么对于每条路径都能求最小值, 那么我们的f[i][j], 应该在所有f[i][j]中取个最小的最小值

可以将f[i][j]最后一步的状态划分, ➡️, 或者⬇️

➡️: (i, j) ->(i, j + 1)
⬇️: (i, j) ->(i + 1, j)

往右的情况分析:
假设当前(i, j)最少的血量是x, 那么走完(i,j)(即已经统计完x(i,j)的消耗/补充的血量), 那么血量应该是x + w[i][j], 为了保证f[i][j + 1]能够安全抵达终点,
那么 x + w[i][j] >= f[i][j + 1] (因为f[i][j + 1]的含义是从(i, j + 1)到达终点的最小值)

x + w [ i ] [ j ] ≥ f [ i ] [ j + 1 ] x + w[i][j] \geq f[i][j + 1] x+w[i][j]f[i][j+1] 转化下 x ≥ f [ i ] [ j + 1 ] − w [ i ] [ j ] x \geq f[i][j + 1] - w[i][j] xf[i][j+1]w[i][j]
所以 x x x最小取的是 f [ i ] [ j + 1 ] − w [ i ] [ j ] f[i][j + 1] - w[i][j] f[i][j+1]w[i][j]

同理第2种情况最小值是 f [ i + 1 ] [ j ] − w [ i ] [ j ] f[i + 1][j] - w[i][j] f[i+1][j]w[i][j]
那么f[i][j] 在这两种情况中取一个最小值

在这里插入图片描述

联动题

leetcode 62 63
跟路径形的dp非常像

注意1

从终点倒推, 因为前面的状态会用到后面的状态, 因此需要先计算后面的状态
~i 等价于 i >= 0

i == n - 1, j == m - 1的时候需要保证 f[i][j] + w[i][j] >= 1, 即终点处血量>= 1
化简下 f[i][j] >= 1 - w[i][j]
当然还需要保证进入i, j之前, f[i][j]不能是负数, 进入之前f[i][j] >= 1;
合并下两式子
f[i][j] = max(1, 1 - w[i][j]);

注意2

向左边走, 和向右边走 是独立的, 所以用两个if

code

class Solution {
public:
    int calculateMinimumHP(vector<vector<int>>& w) {
        int n = w.size(), m = w[0].size();
        vector<vector<int>> f(n, vector<int>(m, 1e8));

        for (int i = n - 1; i >= 0; i -- )
            for (int j = m - 1; j >= 0; j -- )
            	// 因为最后终点格子上w[i][j]也是有值的, 所以也需要判断
                if (i == n - 1 && j == m - 1) f[i][j] = max(1, 1 - w[i][j]);
                else {
                  	// 往右边走, 和往下走是独立的, 所以两个if
                    if (i + 1 < n) f[i][j] = min(f[i][j], f[i + 1][j] - w[i][j]);
                    if (j + 1 < m) f[i][j] = min(f[i][j], f[i][j + 1] - w[i][j]);
                    f[i][j] = max(1, f[i][j]); // 最终需要保证f[i][j] >= 1
                }
    
    return f[0][0];
    }
};

179. 最大数

分析

将所有给出的整数连在一起, 让这个数按字符串连在一起, 使得数字最大

定义了一种新的比较运算
假设a, b是字符串, 因为整数本质是字符串
定义 a = b 等价于ab(字符串) = ba(字符串)
在这里插入图片描述
定义完有什么用呢

假设最优解是 a 1 a 2 a 3 . . . a n a_1a_2a_3... a_n a1a2a3...an, 按照刚才的比较运算, 必然有 a 1 ≥ a 2 ≥ a 3 ≥ . . . ≥ a n a_1 \geq a_2 \geq a_3 \geq ... \geq a_n a1a2a3...an

这里>= 是刚才定义的类似<=, 对调一下

证明:

最优解为什么会满足这样的形式呢?
可以证明下

反证法:
假如最优解不是如上的形式, 那么中间必然会有 a i < a i + 1 a_i < a_{i + 1} ai<ai+1
我们找到第1个不满足这样形式的位置, 一定会有 a i < a i + 1 a_i < a_{i + 1} ai<ai+1, 这是第一个, (前面都是满足>=), 交换下 a i + 1 , a i a_{i + 1}, a_{i} ai+1,ai

那么交换完 . . . a i + 1 a i . . . ...a_{i + 1}a_i... ...ai+1ai...会变得更大, 与 . . . a i a i + 1 . . . ... a_ia_{i + 1}... ...aiai+1...是最优解, 矛盾

所以如果最优解是a1a2a3 … an的排列的话, 必然有 a 1 ≥ a 2 ≥ a 3 ≥ . . . ≥ a n a_1 \geq a_2 \geq a_3 \geq ... \geq a_n a1a2a3...an

有了这个结论, 把所有的数字按照比较运算符降序排序

还没结束, 因为随便定义的比较符号, 不一定能排序, 比如定义了 a < b, b < c, c < a, 那么就不能排序

继续证明: 定义的比较符号能够排序

什么样的比较顺序可以排序, 必须要满足全序关系

  1. 反对称性 a <= b, b <= a, a = b;
  2. 传递性 a <= b, b <= c, a <= c;
  3. 完全性, a <= b 或 b <= a (意思是任何两个元素必定满足其中1种关系)

1 a <= b, b <= a, 看看能否推出a = b. a <= b意味着 ab(字符串) <= ba(字符串), b <= a意味着 ba(字符串) <= ab(字符串), 因此ab = ba

3 任意两个字符串a, b是否可以比较呢? 因为字符串ab, ba可以比较

2 这里x表示 a的有x位, 同理b 有y位
b有y位的话, 那么ab = a * 10^y + b, 因为b有y位, a需要左移y位

在这里插入图片描述

code

要去掉前导0
但是如果全是0的话, 要需要留1个0, 因此k + 1 < res.size()

class Solution {
public:
    string largestNumber(vector<int>& nums) {
        sort(nums.begin(), nums.end(), [](int x, int y){
            string a = to_string(x), b = to_string(y);
            return a + b > b + a;
        });

        string res;
        for (auto x : nums) res += to_string(x);
        int k = 0;
        while (k + 1 < res.size() && res[k] == '0') k ++ ; // 去掉前导0, 当然只有1个0, 不用去, 所以是 k + 1 < res.size(), 表示当前位数k 再+ 1, k + 1位还有数字存在, 说明当前的0肯定是多余的0, 所以用 k + 1 < res.size()
        return res.substr(k);
    }
};

187. 重复的DNA序列

分析

非常简单, hash下
统计下次数 > 1的

在这里插入图片描述

code

时间复杂度是10*字符串长度n

class Solution {
public:
    vector<string> findRepeatedDnaSequences(string s) {
        unordered_map<string, int> cnt;
        for (int i = 0; i + 10 <= s.size(); i ++ ) // i + 10是当前10个单词的末尾迭代器,是最后一个字符的后一个位置
            cnt[s.substr(i, 10)] ++ ;
        vector<string> res;
        for (auto [s, c] : cnt) // 记住要在cnt中遍历, 在原string里遍历会有重复的字符串
            if (c > 1)
                res.push_back(s);
        return res;
    }
};

如果想优化成O(n)的话, 直接用字符串哈希

188. 买卖股票的最佳时机 IV

分析

交易1次的话, 扫描一遍就可以了
交易2次的话, 前后缀分解
交易k次的话, 没办法取巧了

需要特判下, k >= n / 2, 可以交易无限次, 因为n支股票的话, 最多交易n / 2次, 所以可以直接用交易无限次, 所有相邻两天, 所有相邻两天, 如果后天比前天高的话, 就把差值加上

原因呢是交易的分解

如何处理交易k次
状态机模型dp

1.我刚刚买完, 还没卖的时候一个状态
2.我卖完之后的一个状态

假设从下标从1开始, 第1天之前, 手里是没有股票的,
起点是0,(无股票)
可以选择不买, 维持原状
可以选择买, 状态转换到1(手里有股票), − w i -w_i wi

状态1(有股票)
不卖, 维持原状
卖掉, 转换到状态0, (手里无股票), w i w_i wi

最多交易k次, 意味着我们在这两个状态之间最多只能转k次, 问题转化为**经过n天(每个股票价格对应一条边), 即经过n条边的情况下, 最多转k圈的情况下, 最多可以赚多少钱

f[i][j] 表示状态0, f[i][j] = max(f[i - 1][j], g[i - 1][j] + w[i])

要么原地等待, 没有交易(没转圈)f[i - 1][j],
要么从1状态转换过来, g[i - 1][j] + w[i](表示正在转第j圈, 到f[i][j]正好转完第j圈)

g[i][j] = max(g[i - 1][j], f[i - 1][j - 1])

原地等待, (没转圈) g[i - 1][j]
要么从0状态转移过来, 因为g的含义是正在转第j圈, 那么前一个状态f必然是已经转完 j - 1圈 f[i - 1][j - 1] - w[i]

在这里插入图片描述

code

int f[100001], g[100001];
class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int n = prices.size();
        if (k > n / 2){
            int res = 0;
            for (int i = 0; i + 1 < prices.size(); i ++ )
                if (prices[i + 1] > prices[i]) res += prices[i + 1] - prices[i];
            return res;
        }

        memset(f, -0x3f, sizeof f); // 一定要初始化
        memset(g, -0x3f, sizeof g); // 一定要初始化
        f[0] = 0;
        int res = 0;
        for (int i = 1; i <= n; i ++ )
            for (int j = k; j >= 0; j -- ){
                f[j] = max(f[j], g[j] + prices[i - 1]); // 优化了j, 因为i从1开始算第1天, prices是0开始的, 所以i - 1
                if (j) g[j] = max(g[j], f[j - 1] - prices[i - 1]);
                res = max(res, f[j]);
            }
        return res;

    }
};

调试debug(心得)

写的时候忘记初始化了

         memset(f, -0x3f, sizeof f);
         memset(g, -0x3f, sizeof g);
         f[0] = 0;

写成如下形式

int f[100001], g[100001];

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int n = prices.size();
        if (k > n / 2){
            int res = 0;
            for (int i = 0; i < prices.size(); i ++ )
                if (prices[i + 1] > prices[i])
                    res += prices[i + 1] - prices[i];
            return res;
        }
        // memset(f, -0x3f, sizeof f);
        // memset(g, -0x3f, sizeof g);
        // f[0] = 0;

        int res = 0;
        for (int i = 1; i <= n; i ++ )
            for (int j = k; j >= 0; j -- ){
                f[j] = max(f[j], g[j] + prices[i - 1]);
                if (j) g[j] = max(g[j], f[j - 1] - prices[i - 1]);
                res = max(res, f[j]);
            }

        cout << f[1] << endl;

        return res;

    }
};

样例:

2
[3,2,6,5,0,3]

输出: 10
预期(正确结果): 7
调试stdout : 9

f[1] = 9??? 考虑下f的定义, 转1圈, 买卖1次能得到收益9???

通读下代码, 初始化落下了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值