Leetcode刷题笔记: 二分、搜索、动规复习

1. Leetcode 1760: 袋子里最少数目的球(二分)

题目链接
题目解析:涉及最大最小化问题或者最小最大化或者最大最小化问题,一般可以用答案二分法解决。
注意到最大的袋子中球最少的数值x, 和操作次数ops是单调递减的关系。
比如初始只有一个袋子,袋子数量为9,最大次数为1,当最大球最小的数量x分别为【1, 2, 3, 4, 5, 6, 7, 8, 9】时(注意最大最小球的数值范围只能是[1, nums.max()]), 可以消耗的次数ops分别为【8, 4, 2, 2, 1, 1, 1, 1, 0】, 当ops=1时, x 最小为5, 此即答案。
在这里插入图片描述
所以问题可以转化为求当ops <= max_ops时, 求最小的x值。
又因为ops和x的单调递减关系,可以进一步转化为二分问题,对最大球最小的数量x做二分,最小值 l = 1 l = 1 l=1, 最大值 r = n u m s . m a x ( ) r = nums.max() r=nums.max(), 当x小于目标值时,ops 均满足大于max_ops (可记作0), 当x大于等于目标值时,ops均满足小于等于ops (可记作1)。
即前面一堆0, 后面一堆1,要找第一个1。
二分算法代码演示:

class Solution {
public:
    int get_ops(vector<int> &nums, int mid) {
    	//mid为最大球的数量,对nums个袋子进行ops次划分,使得每个划分结果的球都小于等于mid
        int ops = 0;
        for (auto x : nums) {
            if (x % mid == 0) ops += (x / mid - 1);
            else ops += (x / mid);
            /*
			以上用了简便计算,找规律即可。写成如下代码逻辑上也可以通过,但是会超时:
			while (x > mid) {
				x -= mid;
				ops++;
			}
			*/
        }
        return ops;
    }
    int minimumSize(vector<int>& nums, int maxOperations) {
        int l = 1, r = 0;
        for (auto x: nums){
            r = max(r, x);
        }
        //以下为前面一堆0,后面一堆1,要找第一个出现的1 经典写法。
        while(l < r) {
            int mid = l + (r - l) / 2;
            int ops = get_ops(nums, mid);
            //nums=[9], max_ops=2
            //x范围:【1, 2, 3, 4, 5, 6, 7, 8, 9】
            //ops范围:【8, 4, 2, 2, 1, 1, 1, 1, 0】
            //假设max_ops=2,则ops 则ops=2后面的均可以记为1, 其余的记为0
            //x和ops一一对应;
            //ops>=2第一个出现的位置对应x=3即为答案。
            if (ops <= maxOperations) {
                r = mid;
            }else {
                l = mid + 1;
            }
        }
        return l;

    }
};

总结:最大最小问题,或最小最大问题,可以用答案二分思路解决。

2. Leetcode 46: 全排列(搜索)

题目链接
题目解析:抽象为N叉树遍历,用深搜回溯的方法。

class Solution {
public:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<vector<int>> res;
        vector<int> path;
        vector<int> visited(nums.size());
        for (int i = 0; i < nums.size(); i++) {
            visited[i] = 0;
        }
        dfs(nums, 0, path, visited, res);

        return res;
    }
    void dfs(vector<int> &nums, int start, vector<int> &path, vector<int> &visited, vector<vector<int>> &res) {
    	//start代表要给path[start]赋值元素。
        if (start == nums.size()) {
        	//将遍历好的path加到结果中。
            res.push_back(path);
            return;
        }
        for (int i = 0; i < nums.size(); i++) {
            if (visited[i] == 0) {
                path.push_back(nums[i]);
                visited[i] = 1;
                //dfs上下分别为反逻辑,代表回溯
                dfs(nums, start + 1, path, visited, res);
                //dfs上下分别为反逻辑,代表回溯
                visited[i] = 0;
                path.pop_back();
            }
        }
    }
};

总结:深搜回溯上下为反逻辑;提前定义好回溯函数参数含义(start)。

3. Leetcode 513 找树左下角的值(搜索)

题目链接
深搜:可以用从上到下,从左到右(前序)的方式遍历二叉树,维护最大深度的最左边节点值。

/**
 * 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 max_depth = -1, val = 0;
    int findBottomLeftValue(TreeNode* root) {
        if (root == nullptr) return val;
        dfs(root, 0);
        return val;
    }
    void dfs(TreeNode *node, int depth) {
    	//node为当前遍历的节点,depth为节点深度。
        if (node->left == nullptr && node->right == nullptr) {
            if (depth > max_depth) {
            	//因为是从左到右遍历的,所以最深节点出现的第一个值一定是该深度的最左边的节点
                val = node->val;
                max_depth = depth;
            }
        }
        if (node->left != nullptr) dfs(node->left, depth + 1);
        if (node->right != nullptr) dfs(node->right, depth + 1);

    }
};

总结:二叉树的前序,中序,后续遍历经典深搜代码,增加深度变量。

广搜:将二叉树从右往左层级遍历,最后一个遍历的结果即为答案。

/**
 * 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 findBottomLeftValue(TreeNode* root) {
        if (root == nullptr) return val;
        // dfs(root, 0);
        queue<TreeNode*> que;
        TreeNode *node;
        que.push(root);
        while (!que.empty()) {
            node = que.front();
            que.pop();
            if (node->right) que.push(node->right);
            if (node->left) que.push(node->left);
        }
        return node->val;
    }
};

总结:深搜和广搜的复习

4. Leetcode 135: 分发糖果(动态规划)

题目链接
题目解析:对于某一个学生,向左看有x1个连续递减的学生,向右看有x2个连续递减的学生,则该同学应该被分到的糖果数量为max(x1, x2).
举例:排名为【1,2,2, 3,4,1】
对于4这个学生而言,向左看连续递减的序列为【2,3,4 】,长度为3;
向右看连续递减的序列为【4,1】长度为2;
所以4这个同学被分到3个糖果是最合适的。
因此题目转化为对于数组中的每个元素,求出向左的连续递减子序列和向右的连续递减子序列,最后求两个子序列长度的最大值。将每个元素都做这样的运算,求出所有的最大值之和,即为答案。
而求连续递减子序列的长度,可以用动态规划来求解。

class Solution {
public:
    int candy(vector<int>& ratings) {
        int n = ratings.size();
        vector<int> dp_left(n, 1); 
        //dp_left[i]代表位置i的学生向左看最长连续递减子序列的长度
        vector<int> dp_right(n, 1);
        //dp_right[i]代表位置i的学生向右看最长连续递减子序列的长度
        int ans = 0;
        for (int i = 1; i < n; i++) {
            if (ratings[i] > ratings[i - 1]) {
                dp_left[i] = dp_left[i - 1] + 1;
            }
        }
        for (int i = n - 2; i >= 0; i--) {
            if (ratings[i] > ratings[i + 1]) {
                dp_right[i] = dp_right[i + 1] + 1;
            }
        }
        for (int i = 0; i < n; i++) {
            ans += max(dp_left[i], dp_right[i]);
        }
        return ans;
    }
};

总结:将实际问题转化为动态规划建模问题。

5. Leetcode 43: 字符串相乘(大整数)

题目链接
题目解析:字符串类型大整数运算问题,用两个整数数组分别存储两个字符串,整数的第一位存储字符串长度,第二位以后倒序存储字符串。例如:
s1=“123”, s2 = “45”, 则用arr1和arr2分别存储s1和s2:
arr1 = [3, 3,2,1], arr2 = [2, 4,5];
然后参考下图小学学过的乘法运算计算方式,将不同位依次相乘,然后相加。如第一位和第一位,追加到结果的第一位,第二位和第二位,追加到结果的第三位。
即arr1的第 i 位和arr2的第 j 位相乘,追加到结果的 i + j - 1 中。
结果数组也用一个整数数组表示,第一位仍然代表长度,往后的位代表相乘相加的直接结果。
如上例中结果数组最终为 ans = [4, 15, 22, 13, 4].
最后再对结果数组做进位操作得出最终的字符串。
在这里插入图片描述
代码演示:

class Solution {
public:
    string multiply(string num1, string num2) {
        if (num1 == "0" || num2 == "0") {
            return "0";
        }
        int n1 = num1.size();
        int n2 = num2.size();
        vector<int> arr1(n1 + 1);
        vector<int> arr2(n2 + 1);
        vector<int> ans(n1 + n2 + 1);
        arr1[0] = n1; arr2[0] = n2;  
        ans[0] = n1 + n2 - 1;
        //n1位和n2位的两个数相乘,结果最少为n1+n2-1位,最多为n1+n2位,10*10, 99*99举例即可发现
        
        //按照特定格式用arr1和arr2分别存储num1和num2。
        for (int i = n1 - 1; i >= 0; i--) {
            arr1[n1 - i] = (num1[i] - '0');
        }
        for (int i = n2 - 1; i >= 0; i--) {
            arr2[n2 - i] = (num2[i] - '0');
        }
        //开始运算乘法
        for (int i = 1; i <= n1; i++) {
            for (int j = 1; j <= n2; j++) {
                ans[i + j - 1] += (arr1[i] * arr2[j]); //相乘追加到结果中
                if ((i + j - 1) > ans[0]) {
                    ans[0]++;
                }
            }
        }
        //对结果数组做进位运算得出最终的字符串。
        string res;
        for (int i = 1; i <= ans[0]; i++) {
            if (ans[i] > 9) {
                ans[i + 1] += (ans[i] / 10);
                ans[i] %= 10;
                if (i + 1 > ans[0]) { //触发了进位的操作
                    ans[0]++;
                }
            }
            res += ('0' + ans[i]);
        }
        //res的个位在前,所以需反转输出。
        return reverse(res);
    }
    string reverse(string s) {
    	//反转函数
        int n = s.size();
        for (int i = 0; i < n / 2; i++) {
            swap(s[i], s[n - 1 - i]);
        }
        return s;
    }
};

总结:字符串整数运算,1.倒序存储整数数组; 2. 进行相乘(相加)操作;3. 处理进位;4. 反转输出。

6. Leetcode 365: 水壶问题(经典bfs)

题目链接
题目解析:用temp来代表两个水壶水的总和,初始时temp为0, 每次temp可以做如下变化:
temp - jug1Capacity,
temp + jug1Capacity,
temp - jug2Capacity,
temp + jug2Capacity
只要每次变化后的结果在0~jug1Capacity+jug2Capacity范围内,便是一次合理的变化。
所以用队列+去重的哈希集实现的广搜即可解决问题。

class Solution {
public:
    bool canMeasureWater(int jug1Capacity, int jug2Capacity, int targetCapacity) {
        if (jug1Capacity + jug2Capacity < targetCapacity) return false;
        queue<int> que;
        unordered_set<int> st;
        que.push(0);
        st.insert(0);
        while (!que.empty()) {
            int temp = que.front();
            que.pop();
            if (temp + jug1Capacity <= jug1Capacity + jug2Capacity && st.find(temp + jug1Capacity) == st.end()) {
                st.insert(temp + jug1Capacity);
                que.push(temp + jug1Capacity);
            }
            if (temp + jug2Capacity <= jug1Capacity + jug2Capacity && st.find(temp + jug2Capacity) == st.end()) {
                st.insert(temp + jug2Capacity);
                que.push(temp + jug2Capacity);
            }
            if (temp - jug1Capacity >= 0 && st.find(temp - jug1Capacity) == st.end()) {
                st.insert(temp - jug1Capacity);
                que.push(temp - jug1Capacity);
            }
            if (temp - jug2Capacity >= 0 && st.find(temp - jug2Capacity) == st.end()) {
                st.insert(temp - jug2Capacity);
                que.push(temp - jug2Capacity);
            }
            if (st.find(targetCapacity) != st.end()) {
                return true;
            }
        }
        return false;
    }
};

总结:将实际问题转化为状态转化问题,进一步利用广搜解决。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值