🔥博客主页 : A_SHOWY
🎥 系列专栏 :力扣刷题总结录 数据结构 云计算 数字图像处理 力扣每日一题_
【2.1】LCP 24.数字游戏
LCP 24. 数字游戏https://leetcode.cn/problems/5TxKeK/
这个题目可以变换一下就是将一个递增的需求经过nums【i】-i变换变成一个求相等的需求,找中位数,用到了大小堆栈(优先队列),收获颇多。
-
首先是对于大小堆栈的定义,对于大堆栈可以默认定义为 如下,小顶堆需要声明一下
cpppriority_queue<int> L;//大顶堆 priority_queue<int,vector<int>,greater<int>> R;//小顶堆 //第一个int是堆中数据类型,第二个数组是定义底层容器类型,第三个是比较函数大顶堆可以默认,也可以写上less<int>,小顶堆是greater
大顶堆是堆顶是最大元素,后边push的一直维护一个递减,小顶堆相反
-
这个操作是一个动态平衡的一个思想,每加入一个元素,优先放到左边,进行动态调整,始终保持左边最多比右边多一个,堆会自动排好序。计算结果的时候,考虑先找出这一段的中位数,左右分开计算。
-
在计算这个ans【i】的时候,要注意这个整除溢出问题,所以给mid一个long long
cpp
class Solution {
public:
vector<int> numsGame(vector<int>& nums) {
priority_queue<int> L;//大顶堆
priority_queue<int,vector<int>,greater<int>> R;//小顶堆
for(int i = 0; i < nums.size(); i++){
nums[i] = nums[i] - i;
}
long sumL = 0;
long sumR = 0;
int mod = 1e9 +7;
vector<int> ans(nums.size(),0);
for(int i = 0; i < nums.size(); i++){
if(L.empty() || nums[i] <= L.top()) {
L.push(nums[i]);
sumL += nums[i];
//平衡
if(L.size() > R.size() + 1){
R.push(L.top());
sumL -= L.top();
sumR += R.top();
L.pop();
}
}
else{
R.push(nums[i]);
sumR += nums[i];
if(L.size() < R.size()){
L.push(R.top());
sumR -= R.top();
sumL += L.top();
R.pop();
}
}
long long mid = L.top();
ans[i] = (((i + 2) / 2) * mid - sumL + sumR - ((i + 1) / 2) * mid) % mod ;
}
return ans;
}
};
【2.2】1686.石子游戏Ⅵ
1686. 石子游戏 VIhttps://leetcode.cn/problems/stone-game-vi/
对贪心的理解还是不够深入,贪心+排序即可解决这个问题,当你选择取一个石子的时候,你不仅是取得了你的分数,也让这个分数不能被你的对手获取,所以每次取的时候,你应该取使得aliceValues[i] + bobVlaues[i]最大的石子。
- 贪心的话,不仅要让自己取得的数比较大,还不能让别人取到大的你应该取使得aliceValues[i] + bobVlaues[i]最大的石子。
- 整体的代码思路为创建一个数组,这个数组是一维的,但是存储的是一个键值对,因为一会儿要排序,但是要存储它原始数组对应的位置,用这个新数组存储两个和,先排序再翻转就得到了一个递减数组,然后每两个一对做一个循环两个人依次取比较即可。
cpp
class Solution {
public:
int stoneGameVI(vector<int>& aliceValues, vector<int>& bobValues) {
int n = aliceValues.size();
vector<pair<int,int>> stones;
for(int i = 0 ; i < n ; i++){
stones.push_back({aliceValues[i] + bobValues[i],i});
}
sort(stones.begin(),stones.end());
reverse(stones.begin(),stones.end());
int a = 0,b = 0;
for(int i = 0; i < n; i += 2){
a += aliceValues[stones[i].second];
if(i + 1 < n) b += bobValues[stones[i + 1].second];
}
return a == b ? 0 : (a > b ? 1 : -1);
}
};
【2.3】1690.石子游戏
1690. 石子游戏 VIIhttps://leetcode.cn/problems/stone-game-vii/
这个题目是一个动态规划问题,相对来说还是比较复杂,可能也是我对动态规划整体不够熟悉,值得整理的一个题目,思路清晰很重要。
- 对于这个题目的解题首先要理解明确几个问题 ,找出dp关系
-
首先明确------谁是先手谁的得分就最大.
-
对于 dp[i][j] 定义为区间 [i,j] 我们要的结果,在区间 [i, j],dp[i][j] = 先手的总分 - 后手的总分。
-
如果 dp[i][j]这个区间当前是鲍勃操作,那么鲍勃的得分一定最大。选择去掉 stones[i] 后当前的分数为 sum(stones[i + 1], stones[j]).那么区间 [i + 1, j]鲍勃的得分是多少呢?不用管它dp[i + 1][j] 一定为对手爱丽丝作为先手得到的结果,因为谁先手谁的得分最大,则 dp[i + 1][j] = 爱丽丝得分 - 鲍勃的得分。
sum(stones[i + 1], stones[j]) - dp[i + 1][j]
= 鲍勃当前操作得分 - (爱丽丝的总分 - 鲍勃的总分)
= 鲍勃当前操作得分 + 鲍勃的总分 - 爱丽丝的总分
= 鲍勃新的总分 - 爱丽丝的总分 > 0(谁先手谁最大)。
如果去掉 stones[j] 则原理同上.如果当前 dp[i][j] 是爱丽丝,则将上面的叙述中爱丽丝和鲍勃名字互换。
-
对于爱丽丝我们很好理解为什么要最大化dp[i][j] = max(sum(stones[i + 1], stones[j]) - dp[i + 1][j], sum(stones[i], stones[j - 1]) - dp[i][j - 1]);那么鲍勃为什么也要最大化 dp[i][j] 呢,因为爱丽丝先手,鲍勃必输,题目给出了。所以只有当鲍勃操作时 dp[i][j] 最大,才能让爱丽丝操作时得到的结果最小,满足鲍勃的野心
爱丽丝当前操作得分 - (鲍勃的总分 - 爱丽丝的总分)(鲍勃操作时的最大化差值)
cpp//假设现在Bob先手 //现在要得到的结果dp[i][j] = Bob当前操作的得分 - (Alice的总得分 - Bob的总得分)= sum(stones[i + 1], stones[j]) - dp[i + 1][j] class Solution { public: int stoneGameVII(vector<int>& stones) { vector<int> sums(stones.size() + 1,0); vector<vector<int>> dp(stones.size(),vector<int>(stones.size(),0)); // 前缀和 for(int i = 0; i < stones.size(); i++){ sums[i + 1] = sums[i] + stones[i]; } // dp关系 for(int i = stones.size() - 2; i >= 0; --i){ for(int j = i + 1; j < stones.size(); ++j){ dp[i][j] = max(sums[j + 1] - sums[i + 1] - dp[i + 1][j],sums[j] - sums[i] - dp[i][j -1]); } } return dp.front().back(); } };
- 对于代码的核心其实是前缀和 + dp关系。定义两个数组,一个记录sum(n+1),一个记录dp关系,做dp关系的循环时候,要从内部往外部递归,不要错了顺序。最后.front.back是【0】【size - 1】的意思。
【2.4】292.Nim游戏
292. Nim 游戏https://leetcode.cn/problems/nim-game/
一个小小的思维题,当有1、2、3个时候,你可以直接拿走直接赢了,但是当有4个时候,你拿几个,对面都能直接赢,当有5,6,7个时候。你可以先拿走一些剩下四个,这样对面拿啥你都行,但是当第8个的时候,你拿几个,对面都能给你剩下四个,依次类推,当有4,8,12等的时候4的倍数的时候,你都不能赢。
cpp
class Solution {
public:
bool canWinNim(int n) {
return n % 4 != 0;
}
};
【2.5】1696.跳跃游戏Ⅵ
1696. 跳跃游戏 VIhttps://leetcode.cn/problems/jump-game-vi/
这道题目,第一思路就是递推,但是很遗憾2000分的题直接超时,需要用到单调队列去处理,好久不用了,有点生疏了。
方法一:递归(超时)
cpp
dp[i] = *max_element(dp.begin() +max(i - k,0),dp.begin() + i) + nums[i];
看一下从i往前k个数中的最大值加上nums【i】
cpp
//递推
class Solution {
public:
int maxResult(vector<int>& nums, int k) {
int n = nums.size();
vector<int> dp(n);
dp[0] = nums[0];
for(int i = 1; i < n; i++){
dp[i] = *max_element(dp.begin() +max(i - k,0),dp.begin() + i) + nums[i];
}
return dp[n - 1];
}
};
方法二: 单调队列
仔细观察题目,和我们之前做的一个题目特别的像,滑动窗口最大值,我们用到就是单调双端队列!,这个其实一个意思。入的时候,先看前面有没有比他小的,如果有,pop掉再入,保证整体是一个单调递减,开头判断是否需要出。然后转移关系是 f[i] = f[q.front()] + nums[i];
cpp
class Solution {
public:
int maxResult(vector<int>& nums, int k) {
int n = nums.size();
vector<int> f(n);
f[0] = nums[0];
deque<int> q = {0};
for(int i = 1; i < n; i++){
//数量超了,出
if(q.front() < i - k){
q.pop_front();
}
//转移
f[i] = f[q.front()] + nums[i];
//入,注意这里是while
while(!q.empty() && f [i] >= f[q.back()]){
q.pop_back();
}
q.push_back(i);
}
return f[n - 1];
}
};
回顾一下滑动窗口最大值那个题目
239.滑动窗口最大值
239. 滑动窗口最大值https://leetcode.cn/problems/sliding-window-maximum/
cpp
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
deque<int> q;
vector<int>res (nums.size() - k + 1);
// 形成窗口前
for(int i = 0; i < k; i++){
while(!q.empty() && q.back() < nums[i]){
q.pop_back();
}
q.push_back(nums[i]);
}
res[0] = q.front();
// 形成窗口后
for(int i = k; i < n; i++){
if(q.front() == nums[i - k]){
q.pop_front();
}
while(!q.empty() && q.back() < nums[i]){
q.pop_back();
}
q.push_back(nums[i]);
res[i - k + 1] = q.front();
}
return res;
}
};
【2.6】LCP 30魔塔游戏
LCP 30. 魔塔游戏https://leetcode.cn/problems/p0NxJO/
魔塔游戏,就是让这个血量在最后前不能小于0,是一个贪心加优先队列的一个思路,用一个小顶堆去维护一个顶端最小的一个数,每当这个hp小于0的时候,就贪心地把一个最小的扔到最后,但是不要忘了设置一个delay,最后要加上验算是否总和大于0.
需要注意以下数据量,大概10的15次方,hp和dalay需要声明long long
cpp
class Solution {
public:
int magicTower(vector<int>& nums) {
// 小顶堆
priority_queue<int,vector<int>,greater<int>> q;
int ans = 0;
long long hp = 1,delay = 0;
int sum = 0;
for(int num : nums){
if(num < 0) {
q.push(num);
}
hp += num;
if(hp <= 0){
ans ++;
delay += q.top();
hp -= q.top();
q.pop();
}
}
// 最后要把这个delay加上,如果小于0,铁不行
hp += delay;
return hp > 0 ? ans : -1;
}
};
补充:LCP 24数字游戏
LCP 24. 数字游戏https://leetcode.cn/problems/5TxKeK/
同样是用优先队列,大顶推和小顶堆都用上了。整体思路就是,对顶堆维护动态的中位数,两个相对的顶堆,大顶堆的left就是中位数。
注意,顶堆在push和pop后,会自动插入相应位置形成单调。再次回顾顶堆的定义。大顶堆可以直接默认,小顶堆需要声明完全
cpp
priority_queue<int> q1;//大顶堆
priority_queue<int,vector<int>,greater<int>>q2;//小顶堆
cpp
class Solution {
public:
vector<int> numsGame(vector<int>& nums) {
int n = nums.size();
priority_queue<int> q1;//大顶堆
priority_queue<int,vector<int>,greater<int>>q2;//小顶堆
for(int i = 0; i < n; i++){
nums[i] = nums[i] - i;
}
vector<int> res(n,0);
long long mod = 1e9 + 7;
long long sum1 = 0;
long long sum2 = 0;
//对顶堆维护动态的中位数
for(int i = 0 ;i < n; i++){
if(q1.empty() || nums[i] <= q1.top()){
q1.push(nums[i]);
sum1 += nums[i];
//维护平衡
if(q1.size() > q2.size() + 1){
q2.push(q1.top());
sum1 -= q1.top();
sum2 += q1.top();
q1.pop();
}
}
else{
q2.push(nums[i]);
sum2 += nums[i];
if(q2.size() > q1.size()){
q1.push(q2.top());
sum2 -= q2.top();
sum1 += q2.top();
q2.pop();
}
}
long long mid = q1.top();
res[i] = (((i + 2) / 2) * mid - sum1 + sum2 - mid * ((i + 1) / 2)) % mod;
}
return res;
}
};
【2.8】993.二叉树的堂兄弟结点
993. 二叉树的堂兄弟节点https://leetcode.cn/problems/cousins-in-binary-tree/
关于找堂兄弟,可以用深度优先和广度优先,虽然简单,但是确实冗长,我更熟悉用DFS解决。
方法一:DFS(深度优先遍历)
核心代码在于dfs函数内部逻辑。
cpp
/**
* 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:
//x 数据
int x;
int x_depth;
TreeNode *x_parent;
bool x_found = false;
//y 数据
int y;
int y_depth;
TreeNode *y_parent;
bool y_found = false;
void dfs(TreeNode *node, int depth,TreeNode *parent){
if(!node){return;}
if(node -> val == x){
x_depth = depth;
x_parent = parent;
bool x_found = true;
}
else if(node -> val == y){
y_depth = depth;
y_parent = parent;
bool y_found = true;
}
//两个都找到了,就return
if(x_found && y_found){
return;
}
dfs(node -> left,depth + 1,node);
if(x_found && y_found){
return;
}
dfs(node -> right,depth + 1, node);
}
bool isCousins(TreeNode* root, int x, int y) {
this -> x = x;
this -> y = y;
dfs(root,0,nullptr);
return(x_depth == y_depth && x_parent != y_parent);
}
};
【2.09】236.二叉树的最近公共祖先
236. 二叉树的最近公共祖先https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/
整体思路还是递归,树的知识感觉大同小异,直接dfs ,分类四种情况
- 分三步考虑,第一终止条件,当root等于p或者q的时候,返回root,说明找到了,当为空的时候,返回null,说明找穿了
- 第二步递推工作,就是递归完左边,递归右边
- 第三步返回值,分为四种情况,第一种,left,right都是空,说明root的左右子树都不包含,返回空,如果left,right、同时不为空,说明在root的两侧,root就是最近子节点,返回root,如果left为空,说明p或者q在root的右侧直接返回right,right为空同理。
cpp
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root == p || root == q || root == nullptr) return root;
TreeNode* left = lowestCommonAncestor(root -> left,p,q);
TreeNode* right = lowestCommonAncestor(root -> right,p,q);
if(left == NULL && right == NULL) return NULL;
if(left == NULL) return right;
if(right == NULL) return left;
return root;
}
};
【2.10】94.二叉树的中序遍历
【2.11】144.二叉树的前序遍历
【2.12】145.二叉树的后序遍历
94. 二叉树的中序遍历https://leetcode.cn/problems/binary-tree-inorder-traversal/
144. 二叉树的前序遍历https://leetcode.cn/problems/binary-tree-preorder-traversal/
145. 二叉树的后序遍历https://leetcode.cn/problems/binary-tree-postorder-traversal/
这三个题目过年三天官方福利,三个简单题目思路完全一样,前序后序和中序遍历,搞清楚概念用dfs解决即可。
前序遍历:按照访问根节点------左子树------右子树
中序遍历:按照访问左子树------根节点------右子树
后序遍历:按照访问左子树------右子树------根节点
这个序是按照根节点位置来的
cpp
//94
class Solution {
public:
void inorder(TreeNode* root,vector<int>& res)
{
if(!root){
return;
}
inorder(root -> left,res);
res.push_back(root -> val);
inorder(root ->right,res);
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
inorder(root,res);
return res;
}
};
cpp
//144
class Solution {
public:
void qianxu(TreeNode* root,vector<int> &res){
if(!root) {return;}
res.push_back(root -> val);
qianxu(root -> left,res);
qianxu(root -> right,res);
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
qianxu(root,res);
return res;
}
};
cpp
//145
class Solution {
public:
void houXu(TreeNode* root,vector<int> &res){
if(!root){
return;
}
houXu(root -> left,res);
houXu(root-> right,res);
res.push_back(root -> val);
}
vector<int> postorderTraversal(TreeNode* root) {
vector<int>res;
houXu(root,res);
return res;
}
};
【2.13】987.二叉树的垂序遍历
987. 二叉树的垂序遍历https://leetcode.cn/problems/vertical-order-traversal-of-a-binary-tree/
整体思路运用dfs输出每个结点的行列和值的对应关系,用哈希表存储,再对哈希表进行遍历输出。
- 值得注意的是,在哈希表遍历输入的时候,一定要用emplace_back(),这样可以一个键存多个值对,如果用push_back则不行,在后续遍历哈希表的时候,无法将键一样的存在一个数组里。
- 整体思路,dfs经典递归,然后先对每一个键的值对遍历,排序后存到每一个数组中,还有就是ranges::sort排序更为方便直接()内加容器。
cpp
class Solution {
map<int, vector<pair<int, int>>> groups;
public:
void dfs(TreeNode *node, int row,int col){
if(!node){return;}
groups[col].emplace_back(row,node -> val);
dfs(node -> left,row + 1,col - 1);
dfs(node -> right,row + 1, col + 1);
}
vector<vector<int>> verticalTraversal(TreeNode* root) {
dfs(root,0,0);
vector<vector<int>> ans;
for(auto &[_,g] : groups){
ranges :: sort(g);
vector<int> vals;
for(auto &[_,val] : g){
vals.push_back(val);
}
ans.push_back(vals);
}
return ans;
}
};
【2.14】102.二叉树的层序遍历
102. 二叉树的层序遍历https://leetcode.cn/problems/binary-tree-level-order-traversal/
对于这种层序遍历就不得不用BFS了。层序遍历和最短路径是BFS的主要就业空间。
- 值得注意的几个点,第一个是 在二维向量中添加一维向量之前最好是先创建一个一维向量。
- 在BFS中,常用队列数据结构,因为其先进先出的特性,可以pop出以后,对结点的左右子树进行遍历实现广度优先遍历。
cpp
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
if(!root) {
return res;
}
queue<TreeNode*> q;
q.push(root);
while(!q.empty()){
int currentSize = q.size();
res.push_back(vector<int>());//在二维向量中添加一维向量之前最好是先创建一个一维向量
for(int i = 1; i <= currentSize;++i){
auto node = q.front();q.pop();
res.back().push_back(node -> val);
if(node -> left) q.push(node -> left);
if(node -> right) q.push(node -> right);
}
}
return res;
}
};
【2.15】102.二叉树的层序遍历Ⅱ
107. 二叉树的层序遍历 IIhttps://leetcode.cn/problems/binary-tree-level-order-traversal-ii/
和上一题基本一样,就是加一个reverse的函数,BFS以后数组再swap一下反向输出就行
cpp
/**
* 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:
void reverseArray(vector<vector<int>> &arr,int size){
int start = 0;
int end = size -1;
while(start <= end){
// auto temp = arr[start];
// arr[start] = arr[end];
// arr[end] = temp;
// start ++;
// end --;
swap(arr[start],arr[end]);
start ++;
end --;
}
}
vector<vector<int>> levelOrderBottom(TreeNode* root) {
vector<vector<int>> res;
if(!root) {
return res;
}
queue<TreeNode*> q;
q.push(root);
while(!q.empty()){
int currentSize = q.size();
res.push_back(vector<int>());//在二维向量中添加一维向量之前最好是先创建一个一维向量
for(int i = 1; i <= currentSize;++i){
auto node = q.front();q.pop();
res.back().push_back(node -> val);
if(node -> left) q.push(node -> left);
if(node -> right) q.push(node -> right);
}
}
reverseArray(res,res.size());
return res;
}
};