文章目录
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;
}
};
总结:将实际问题转化为状态转化问题,进一步利用广搜解决。