D357周赛复盘:模拟双端队列反转⭐⭐+贪心

2810.故障键盘

你的笔记本键盘存在故障,每当你在上面输入字符 'i' 时,它会反转你所写的字符串。而输入其他字符则可以正常工作。

给你一个下标从 0 开始的字符串 s ,请你用故障键盘依次输入每个字符。

返回最终笔记本屏幕上输出的字符串。

示例 1:

输入:s = "string"
输出:"rtsng"
解释:
输入第 1 个字符后,屏幕上的文本是:"s" 。
输入第 2 个字符后,屏幕上的文本是:"st" 。
输入第 3 个字符后,屏幕上的文本是:"str" 。
因为第 4 个字符是 'i' ,屏幕上的文本被反转,变成 "rts" 。
输入第 5 个字符后,屏幕上的文本是:"rtsn" 。
输入第 6 个字符后,屏幕上的文本是: "rtsng" 。
因此,返回 "rtsng" 。

示例 2:

输入:s = "poiinter"
输出:"ponter"
解释:
输入第 1 个字符后,屏幕上的文本是:"p" 。
输入第 2 个字符后,屏幕上的文本是:"po" 。
因为第 3 个字符是 'i' ,屏幕上的文本被反转,变成 "op" 。
因为第 4 个字符是 'i' ,屏幕上的文本被反转,变成 "po" 。
输入第 5 个字符后,屏幕上的文本是:"pon" 。
输入第 6 个字符后,屏幕上的文本是:"pont" 。
输入第 7 个字符后,屏幕上的文本是:"ponte" 。
输入第 8 个字符后,屏幕上的文本是:"ponter" 。
因此,返回 "ponter" 。

提示:

  • 1 <= s.length <= 100
  • s 由小写英文字母组成
  • s[0] != 'i'

1.直接用reverse解决

反转字符串可以直接用reverse来解决,如下:

class Solution {
public:
    string finalString(string s) {
        string result=" ";
        for(char c:s){
            if(c=='i'){
                reverse(result.begin(),result.end());
            }
            else{
                result.push_back(c);
            }
        }
        return result;
    }
};

2.双端队列

遇到字符i时,可以通过栈来实现字符串反转的效果。这是因为栈是后进先出(LIFO)的数据结构,它可以帮助我们将之前输入的字符逆序输出。

但是,用栈的话,无法处理字符串本身没有i这种情况,因此应该使用双端队列。

双端队列思路:

  1. 初始化一个双端队列 deque<char> q:双端队列允许我们在队列的头部和尾部都进行添加和删除操作。

  2. 使用一个布尔变量 right 来确定插入的方向:当 righttrue 时,我们在队列的尾部插入字符;当 rightfalse 时,我们在队列的头部插入字符。

  3. 遍历输入的字符串 s 中的每个字符

    • 如果字符是 ‘i’:则将 right 的值取反,即改变插入方向。因此,如果之前是在尾部插入,现在就在头部插入,反之亦然。
    • 如果字符不是 ‘i’ 且 righttrue:则在双端队列的尾部插入该字符。
    • 如果字符不是 ‘i’ 且 rightfalse:则在双端队列的头部插入该字符。
  4. 将双端队列 q 转化为字符串 result:这里,利用了字符串的迭代器构造函数,将 q 的所有字符转化为一个字符串。

  5. 检查最后的插入方向:如果最后的插入方向是在头部(即 rightfalse),说明队列中的字符序列是相反的,因此我们需要将 result 反转。

  6. 返回结果字符串 result

    关键思想是,利用双端队列模拟字符串的插入过程,每当遇到字符 ‘i’ 时,改变插入的方向。这种方法的效率很高,因为在双端队列的头部和尾部插入字符的时间复杂度都是 O(1)

class Solution {
public:
    string finalString(string s) {
        //双端队列
        deque<char>q;
        bool right = true;//设定一个量用来修改添加方向
        for(char c:s){
            if(c=='i'){
                right=!right;//方向返过来,而且i本身是不管的,所以可以直接else if
            }
            else if(right){
                q.push_back(c);
            }
            else {
                q.push_front(c);
            }
        }
        string result(q.begin(),q.end());//可以直接把双端队列放在字符串里

        if(!right){//有偶数个i的时候,实际上最后的result是不变的,直接看right是不是true就可以
            reverse(result.begin(),result.end());
        }
        return result;

    }
};

2811.判断能否拆分数组(比较巧妙的贪心)

给你一个长度为 n 的数组 nums 和一个整数 m 。请你判断能否执行一系列操作,将数组拆分成 n非空 数组。

在每一步操作中,你可以选择一个 长度至少为 2 的现有数组(之前步骤的结果) 并将其拆分成 2 个子数组,而得到的 每个 子数组,至少 需要满足以下条件之一:

  • 子数组的长度为 1 ,或者
  • 子数组元素之和 大于或等于 m

如果你可以将给定数组拆分成 n 个满足要求的数组,返回 true ;否则,返回 false

**注意:**子数组是数组中的一个连续非空元素序列。

示例 1:

输入:nums = [2, 2, 1], m = 4
输出:true
解释:
第 1 步,将数组 nums 拆分成 [2, 2] 和 [1] 。
第 2 步,将数组 [2, 2] 拆分成 [2] 和 [2] 。
因此,答案为 true 。

示例 2:

输入:nums = [2, 1, 3], m = 5 
输出:false
解释:
存在两种不同的拆分方法:
第 1 种,将数组 nums 拆分成 [2, 1] 和 [3] 。
第 2 种,将数组 nums 拆分成 [2] 和 [1, 3] 。
然而,这两种方法都不满足题意。因此,答案为 false 。

示例 3:

输入:nums = [2, 3, 3, 2, 3], m = 6
输出:true
解释:
第 1 步,将数组 nums 拆分成 [2, 3, 3, 2] 和 [3] 。
第 2 步,将数组 [2, 3, 3, 2] 拆分成 [2, 3, 3] 和 [2] 。
第 3 步,将数组 [2, 3, 3] 拆分成 [2] 和 [3, 3] 。
第 4 步,将数组 [3, 3] 拆分成 [3] 和 [3] 。
因此,答案为 true 。 

提示:

  • 1 <= n == nums.length <= 100
  • 1 <= nums[i] <= 100
  • 1 <= m <= 200

思路

这道题用的是贪心的思想,只要有两个连续的数字加起来>=m,就可以把除了这俩之外的都拆出去。

也就是说,只要有一组连续数字相加>=m,那么一定可以满足条件2。条件1是一定可以满足的,需要满足的是有两个连续的数字相加>=m。这样最后才能拆出来两个1。

完整版

class Solution {
public:
    bool canSplitArray(vector<int>& nums, int m) {
        if(nums.size()==1||nums.size()==2){
            return true;
        }
        for(int i=0;i<nums.size()-1;i++){
            if(nums[i]+nums[i+1]>=m){
                return true;
            }
        }
        return false;
    }
};

这是一种很巧妙的贪心,主要是这道题两个条件满足一个即可,而且数组全是正整数,有两个相加>=m,最后结果一定>=m。

2812.找出最安全路径

给你一个下标从 0 开始、大小为 n x n 的二维矩阵 grid ,其中 (r, c) 表示:

  • 如果 grid[r][c] = 1 ,则表示一个存在小偷的单元格
  • 如果 grid[r][c] = 0 ,则表示一个空单元格

你最开始位于单元格 (0, 0) 。在一步移动中,你可以移动到矩阵中的任一相邻单元格,包括存在小偷的单元格。

矩阵中路径的 安全系数 定义为:从路径中任一单元格到矩阵中任一小偷所在单元格的 最小 曼哈顿距离。

返回所有通向单元格 (n - 1, n - 1) 的路径中的 最大安全系数

单元格 (r, c) 的某个 相邻 单元格,是指在矩阵中存在的 (r, c + 1)(r, c - 1)(r + 1, c)(r - 1, c) 之一。

两个单元格 (a, b)(x, y) 之间的 曼哈顿距离 等于 | a - x | + | b - y | ,其中 |val| 表示 val 的绝对值。

示例 1:

2810.故障键盘

你的笔记本键盘存在故障,每当你在上面输入字符 'i' 时,它会反转你所写的字符串。而输入其他字符则可以正常工作。

给你一个下标从 0 开始的字符串 s ,请你用故障键盘依次输入每个字符。

返回最终笔记本屏幕上输出的字符串。

示例 1:

输入:s = "string"
输出:"rtsng"
解释:
输入第 1 个字符后,屏幕上的文本是:"s" 。
输入第 2 个字符后,屏幕上的文本是:"st" 。
输入第 3 个字符后,屏幕上的文本是:"str" 。
因为第 4 个字符是 'i' ,屏幕上的文本被反转,变成 "rts" 。
输入第 5 个字符后,屏幕上的文本是:"rtsn" 。
输入第 6 个字符后,屏幕上的文本是: "rtsng" 。
因此,返回 "rtsng" 。

示例 2:

输入:s = "poiinter"
输出:"ponter"
解释:
输入第 1 个字符后,屏幕上的文本是:"p" 。
输入第 2 个字符后,屏幕上的文本是:"po" 。
因为第 3 个字符是 'i' ,屏幕上的文本被反转,变成 "op" 。
因为第 4 个字符是 'i' ,屏幕上的文本被反转,变成 "po" 。
输入第 5 个字符后,屏幕上的文本是:"pon" 。
输入第 6 个字符后,屏幕上的文本是:"pont" 。
输入第 7 个字符后,屏幕上的文本是:"ponte" 。
输入第 8 个字符后,屏幕上的文本是:"ponter" 。
因此,返回 "ponter" 。

提示:

  • 1 <= s.length <= 100
  • s 由小写英文字母组成
  • s[0] != 'i'

1.直接用reverse解决

反转字符串可以直接用reverse来解决,如下:

class Solution {
public:
    string finalString(string s) {
        string result=" ";
        for(char c:s){
            if(c=='i'){
                reverse(result.begin(),result.end());
            }
            else{
                result.push_back(c);
            }
        }
        return result;
    }
};

2.双端队列

遇到字符i时,可以通过栈来实现字符串反转的效果。这是因为栈是后进先出(LIFO)的数据结构,它可以帮助我们将之前输入的字符逆序输出。

但是,用栈的话,无法处理字符串本身没有i这种情况,因此应该使用双端队列。

双端队列思路:

  1. 初始化一个双端队列 deque<char> q:双端队列允许我们在队列的头部和尾部都进行添加和删除操作。

  2. 使用一个布尔变量 right 来确定插入的方向:当 righttrue 时,我们在队列的尾部插入字符;当 rightfalse 时,我们在队列的头部插入字符。

  3. 遍历输入的字符串 s 中的每个字符

    • 如果字符是 ‘i’:则将 right 的值取反,即改变插入方向。因此,如果之前是在尾部插入,现在就在头部插入,反之亦然。
    • 如果字符不是 ‘i’ 且 righttrue:则在双端队列的尾部插入该字符。
    • 如果字符不是 ‘i’ 且 rightfalse:则在双端队列的头部插入该字符。
  4. 将双端队列 q 转化为字符串 result:这里,利用了字符串的迭代器构造函数,将 q 的所有字符转化为一个字符串。

  5. 检查最后的插入方向:如果最后的插入方向是在头部(即 rightfalse),说明队列中的字符序列是相反的,因此我们需要将 result 反转。

  6. 返回结果字符串 result

    关键思想是,利用双端队列模拟字符串的插入过程,每当遇到字符 ‘i’ 时,改变插入的方向。这种方法的效率很高,因为在双端队列的头部和尾部插入字符的时间复杂度都是 O(1)

class Solution {
public:
    string finalString(string s) {
        //双端队列
        deque<char>q;
        bool right = true;//设定一个量用来修改添加方向
        for(char c:s){
            if(c=='i'){
                right=!right;//方向返过来,而且i本身是不管的,所以可以直接else if
            }
            else if(right){
                q.push_back(c);
            }
            else {
                q.push_front(c);
            }
        }
        string result(q.begin(),q.end());//可以直接把双端队列放在字符串里

        if(!right){//有偶数个i的时候,实际上最后的result是不变的,直接看right是不是true就可以
            reverse(result.begin(),result.end());
        }
        return result;

    }
};

2811.判断能否拆分数组(比较巧妙的贪心)

给你一个长度为 n 的数组 nums 和一个整数 m 。请你判断能否执行一系列操作,将数组拆分成 n非空 数组。

在每一步操作中,你可以选择一个 长度至少为 2 的现有数组(之前步骤的结果) 并将其拆分成 2 个子数组,而得到的 每个 子数组,至少 需要满足以下条件之一:

  • 子数组的长度为 1 ,或者
  • 子数组元素之和 大于或等于 m

如果你可以将给定数组拆分成 n 个满足要求的数组,返回 true ;否则,返回 false

**注意:**子数组是数组中的一个连续非空元素序列。

示例 1:

输入:nums = [2, 2, 1], m = 4
输出:true
解释:
第 1 步,将数组 nums 拆分成 [2, 2] 和 [1] 。
第 2 步,将数组 [2, 2] 拆分成 [2] 和 [2] 。
因此,答案为 true 。

示例 2:

输入:nums = [2, 1, 3], m = 5 
输出:false
解释:
存在两种不同的拆分方法:
第 1 种,将数组 nums 拆分成 [2, 1] 和 [3] 。
第 2 种,将数组 nums 拆分成 [2] 和 [1, 3] 。
然而,这两种方法都不满足题意。因此,答案为 false 。

示例 3:

输入:nums = [2, 3, 3, 2, 3], m = 6
输出:true
解释:
第 1 步,将数组 nums 拆分成 [2, 3, 3, 2] 和 [3] 。
第 2 步,将数组 [2, 3, 3, 2] 拆分成 [2, 3, 3] 和 [2] 。
第 3 步,将数组 [2, 3, 3] 拆分成 [2] 和 [3, 3] 。
第 4 步,将数组 [3, 3] 拆分成 [3] 和 [3] 。
因此,答案为 true 。 

提示:

  • 1 <= n == nums.length <= 100
  • 1 <= nums[i] <= 100
  • 1 <= m <= 200

思路

这道题用的是贪心的思想,只要有两个连续的数字加起来>=m,就可以把除了这俩之外的都拆出去。

也就是说,只要有一组连续数字相加>=m,那么一定可以满足条件2。条件1是一定可以满足的,需要满足的是有两个连续的数字相加>=m。这样最后才能拆出来两个1。

完整版

class Solution {
public:
    bool canSplitArray(vector<int>& nums, int m) {
        if(nums.size()==1||nums.size()==2){
            return true;
        }
        for(int i=0;i<nums.size()-1;i++){
            if(nums[i]+nums[i+1]>=m){
                return true;
            }
        }
        return false;
    }
};

这是一种很巧妙的贪心,主要是这道题两个条件满足一个即可,而且数组全是正整数,有两个相加>=m,最后结果一定>=m。

2812.找出最安全路径

给你一个下标从 0 开始、大小为 n x n 的二维矩阵 grid ,其中 (r, c) 表示:

  • 如果 grid[r][c] = 1 ,则表示一个存在小偷的单元格
  • 如果 grid[r][c] = 0 ,则表示一个空单元格

你最开始位于单元格 (0, 0) 。在一步移动中,你可以移动到矩阵中的任一相邻单元格,包括存在小偷的单元格。

矩阵中路径的 安全系数 定义为:从路径中任一单元格到矩阵中任一小偷所在单元格的 最小 曼哈顿距离。

返回所有通向单元格 (n - 1, n - 1) 的路径中的 最大安全系数

单元格 (r, c) 的某个 相邻 单元格,是指在矩阵中存在的 (r, c + 1)(r, c - 1)(r + 1, c)(r - 1, c) 之一。

两个单元格 (a, b)(x, y) 之间的 曼哈顿距离 等于 | a - x | + | b - y | ,其中 |val| 表示 val 的绝对值。

示例 1:
在这里插入图片描述

输入:grid = [[1,0,0],[0,0,0],[0,0,1]]
输出:0
解释:从 (0, 0) 到 (n - 1, n - 1) 的每条路径都经过存在小偷的单元格 (0, 0) 和 (n - 1, n - 1) 。

示例 2:

在这里插入图片描述

输入:grid = [[0,0,1],[0,0,0],[0,0,0]]
输出:2
解释:
上图所示路径的安全系数为 2:
- 该路径上距离小偷所在单元格(0,2)最近的单元格是(0,0)。它们之间的曼哈顿距离为 | 0 - 0 | + | 0 - 2 | = 2 。
可以证明,不存在安全系数更高的其他路径。

示例 3:

在这里插入图片描述

输入:grid = [[0,0,0,1],[0,0,0,0],[0,0,0,0],[1,0,0,0]]
输出:2
解释:
上图所示路径的安全系数为 2:
- 该路径上距离小偷所在单元格(0,3)最近的单元格是(1,2)。它们之间的曼哈顿距离为 | 0 - 1 | + | 3 - 2 | = 2 。
- 该路径上距离小偷所在单元格(3,0)最近的单元格是(3,2)。它们之间的曼哈顿距离为 | 3 - 3 | + | 0 - 2 | = 2 。
可以证明,不存在安全系数更高的其他路径。

提示:

  • 1 <= grid.length == n <= 400
  • grid[i].length == n
  • grid[i][j]01
  • grid 至少存在一个小偷

这道题是bfs,笔试考的比较多。先记录一下做法。

class Solution {
public:
    int maximumSafenessFactor(vector<vector<int>>& grid) {
        int n = grid.size();
        vector<vector<int>> dist(n, vector<int>(n, INT_MAX));
        vector<vector<int>> safe(n, vector<int>(n, 0));
        queue<pair<int, int>> q;

        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == 1) {
                    dist[i][j] = 0;
                    q.push({i, j});
                }
            }
        }

        vector<vector<int>> dirs = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
        while (!q.empty()) {
            int r = q.front().first, c = q.front().second;
            q.pop();
            for (auto &dir : dirs) {
                int nr = r + dir[0], nc = c + dir[1];
                if (nr >= 0 && nr < n && nc >= 0 && nc < n && dist[r][c] + 1 < dist[nr][nc]) {
                    dist[nr][nc] = dist[r][c] + 1;
                    q.push({nr, nc});
                }
            }
        }

        safe[n-1][n-1] = dist[n-1][n-1];
        q.push({n-1, n-1});
        while (!q.empty()) {
            int r = q.front().first, c = q.front().second;
            q.pop();
            for (auto &dir : dirs) {
                int nr = r + dir[0], nc = c + dir[1];
                if (nr >= 0 && nr < n && nc >= 0 && nc < n) {
                    int newSafe = min(safe[r][c], dist[nr][nc]);
                    if (newSafe > safe[nr][nc]) {
                        safe[nr][nc] = newSafe;
                        q.push({nr, nc});
                    }
                }
            }
        }

        return safe[0][0];
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值