LeetCode周赛295,赛后看了大佬的代码,受益匪浅

1,043 阅读6分钟

大家好,日拱一卒,我是梁唐。

今天是周一,我们照惯例来聊聊昨天的LeetCode周赛。

这一场比赛的赞助商是博乐科技,前1000名都可以获得内推机会。和之前50名、100名获得内推的场次比起来有诚意多了。

这一场比赛很有意思,由于第三题难度太大,以至于一直到比赛结束也只有不到150人通过,甚至要小于第四题。并且第三题的解题思路也超出了以往出题的套路,让很多人都措手不及。

因为这样的反套路,使得很多人10分钟切出了两题,一直卡在第三题,甚至卡到比赛结束。毕竟比赛的结果除了取决于实力强弱之外,和临场发挥、心态以及战术、策略都相关。做一做这样的题目对于拓展视野,掌握更多的算法和思路很有帮助,因此非常推荐大家赛后练习。

好了,废话就说这么多,我们来看题吧。

重排字符形成目标字符串

给你两个下标从 0 开始的字符串 s 和 target 。你可以从 s 取出一些字符并将其重排,得到若干新的字符串。

从 s 中取出字符并重新排列,返回可以形成 target 的 最大 副本数。

题解

题目的范围很小,基本上怎么玩都可以。

我们就照着题意模拟即可,分别统计出starget当中每个字母出现的次数。枚举所有字符,计算出s

相同字符能够组成的字符串的数量。根据木桶理论,所有字符当中数量最少的即为答案。

class Solution {ub
public:
    int rearrangeCharacters(string s, string target) {
        map<char, int> mp, used;
        int ret = 0;
        for (auto c : s) {
            mp[c]++;
        }
        
        for (auto c : target) {
            used[c] ++;
        }
        
        ret = 0x3f3f3f3f;
        
        for (auto p = used.begin(); p != used.end(); p++) {
            char c = p->first;
            int v = p->second;
            if (mp.count(c) == 0) return 0;
            ret = min(mp[c] / v, ret);
        }
        return ret;
    }
};

价格减免

句子 是由若干个单词组成的字符串,单词之间用单个空格分隔,其中每个单词可以包含数字、小写字母、和美元符号 '$' 。如果单词的形式为美元符号后跟着一个非负实数,那么这个单词就表示一个价格。

  • 例如 "100""100"、"23" 和 "6.75"表示价格,而"100""6.75" 表示价格,而 "100"、"" 和 "2$3" 不是。

**注意:**本题输入中的价格均为整数。

给你一个字符串 sentence 和一个整数 discount 。对于每个表示价格的单词,都在价格的基础上减免 discount% ,并 更新 该单词到句子中。所有更新后的价格应该表示为一个 恰好保留小数点后两位 的数字。

返回表示修改后句子的字符串。

题解

这题的题面也没什么难度,非常直观,就是要遵照题目的意思对字符串进行操作。

但实现起来还是有些麻烦的,特别是如果你使用C++的话,连split函数都没有。赛后看了一些大佬的代码,发现了代替split的一些trick。比如可以使用stringstream来获得类似split的效果:

string sentence;
stringstream ss(sentence);
string s;

while (ss >> s) {
    // do something
}

但在比赛的时候,我并不知道这个,所以果断选择了Python。

实际上也的确如此,在字符串和list的处理上,Python提供的库函数比C++更多。再比如,在本题当中,我们需要将字符串转成数字来进行折扣的计算。我们可以直接使用Python中的强制转换来完成,对于非法的字符串,强制转换会报错,因此需要加上try catch。

不过本题当中明确说了价格一定是整数,所以也可以使用Python中的isnumeric函数。

class Solution:
    def discountPrices(self, sentence: str, discount: int) -> str:
        words = sentence.split(' ')
        
        dis = 1.0 - discount / 100
        
        wds = []
        for w in words:
            if w[0] == '$':
                try:
                    v = float(w[1:])
                    w = '${:.2f}'.format(v * dis)
                except:
                    pass
            wds.append(w)
        
        return ' '.join(wds)

使数组按非递减顺序排列

给你一个下标从 0 开始的整数数组 nums 。在一步操作中,移除所有满足 nums[i - 1] > nums[i] 的 nums[i] ,其中 0 < i < nums.length 。

重复执行步骤,直到 nums 变为 非递减 数组,返回所需执行的操作数。

题解

数据的范围是1e5,我们使用暴力的方法求解是一定会超时的。

我一开始觉得像是用单调栈,但推导了半天也没有想出解法来。赛后看了大佬们的题解才找到思路,不得不说是太巧妙了。

题目当中隐藏了一个关键的条件,当我们第一轮删除之后,下一次可能发生删除的位置一定是第一次有过删除的位置。比如[5, 3, 4],第一次删除之后变成了[5, 4],可以继续触发第二次删除。也就是说因为前一次删除导致了元素相邻的情况发生了变化,才有可能构成下一次删除,之前没有删除的位置之后也不会再删除了。

所以我们可以记录下每次发生删除的位置,每一轮当中只需要判断这些位置的元素是否会触发下一次删除即可。但还有一个问题是数组删除元素的复杂度太高是img,所以这里可以使用链表。还有一个细节是删除的时候是可以连续删除的,比如[5, 4, 3],只需要删除一次就可以变成[5]。

这里面的思路推导总体来说不算复杂,但由于日常的比赛当中少见链表的问题,因此很难往这个角度思考。也正因此才这么多人都做不出来。

class Solution {
public:
    int totalSteps(vector<int>& nums) {
        list<int> lst(nums.begin(), nums.end());
        
        vector<list<int>::iterator> to_delete;
        
        for (auto p = lst.begin(); next(p) != lst.end(); p++) {
            if (*p > *(next(p)))    to_delete.push_back(next(p));
        }
        
        int ret = 0;
        
        while (!to_delete.empty()) {
            ret++;
            vector<list<int>::iterator> buff;
            
            for (auto p: to_delete) {
                auto pre = prev(p);
                lst.erase(p);
                // 判断能否连续删除
                if (buff.empty() || buff.back() != pre) {
                    buff.push_back(pre);
                }
            }
            to_delete.clear();
            for (auto p: buff) {
                if (next(p) != lst.end() && *p > *(next(p))) to_delete.push_back(next(p));
            }
        }
        return ret;
    }
};

到达角落需要移除障碍物的最小数目

给你一个下标从 0 开始的二维整数数组 grid ,数组大小为 m x n 。每个单元格都是两个值之一:

  • 0 表示一个 单元格,
  • 1 表示一个可以移除的 障碍物

你可以向上、下、左、右移动,从一个空单元格移动到另一个空单元格。

现在你需要从左上角 (0, 0) 移动到右下角 (m - 1, n - 1) ,返回需要移除的障碍物的 最小 数目。

img

题解

这题看着其实不算难,如果对算法熟悉的话,很容易发现这是标准的宽度优先搜索的问题。

但是很遗憾,直接用裸的bfs的代码去做是会超时的。因为太多的路重复走了太多次。接着可以想到分层递进的策略,我们首先找到代价为0就可以到达的点。接着从这些点出发,再找出花费一点代价能够达到的所有点,这些点就是代价为1的点。接着重复以上过程,直到到达终点。

由于我们是分层递进的,可以保证每个点只会遍历一次。这样实现的话我们需要维护两个队列,一个用于存储当前代价的点,一个用于存储当前代价+1的点。当当前代价的所有点遍历完了之后,遍历代价+1的点,直到遇见终点为止。

typedef pair<int, int> pii;

class Solution {
public:
    int minimumObstacles(vector<vector<int>>& grid) {
        int n = grid.size();
        int m = grid[0].size();
        vector<vector<int>> dis(n, vector<int>(m, INT_MAX));
        
        queue<pii> cur, nxt;
        cur.emplace(0, 0);
        dis[0][0] = 0;
        
        int fx[4][2] = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};
        
        int cost = 0;
        while (true) {
            if (cur.empty()) {
                cur = nxt;
                nxt = queue<pii>();
                cost++;
            }
            auto [x, y] = cur.front();
            cur.pop();
            if (x == n-1 && y == m-1) break;
            
            for (int i = 0; i < 4; i++) {
                int xx = x + fx[i][0];
                int yy = y + fx[i][1];
                if (xx < 0 || xx >= n || yy < 0 || yy >= m) {
                    continue;
                }
                if (grid[xx][yy]) {
                    if (dis[xx][yy] == INT_MAX) {
                        nxt.emplace(xx, yy);
                        dis[xx][yy] = dis[x][y] + 1;
                    }
                }else {
                    if (dis[xx][yy] == INT_MAX) {
                        dis[xx][yy] = dis[x][y];
                        cur.emplace(xx, yy);
                    }
                }
            }
        }
        
        return cost;
    }
};

赛后看了大佬的代码之后,发现同样的思路还有更好的实现方式,就是使用deque——双端队列。在插入的时候区别对待,将当前代价的插入队首,而当前代价+1的点插入队尾。这样只需要一个队列就可以实现按照距离区分遍历顺序的效果,不得不说实在是太优雅了。

大家可以对比一下代码细节感受一下其中的差异。

typedef pair<int, int> pii;

class Solution {
public:
    int minimumObstacles(vector<vector<int>>& grid) {
        int n = grid.size();
        int m = grid[0].size();
        vector<vector<int>> dis(n, vector<int>(m, INT_MAX));
        
        deque<pii> que;
        que.emplace_front(0, 0);
        dis[0][0] = 0;
        
        int fx[4][2] = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};
        
        while (!que.empty()) {
            auto [x, y] = que.front();
            que.pop_front();
            
            for (int i = 0; i < 4; i++) {
                int xx = x + fx[i][0];
                int yy = y + fx[i][1];
                if (xx < 0 || xx >= n || yy < 0 || yy >= m || dis[xx][yy] < INT_MAX) {
                    continue;
                }
                dis[xx][yy] = dis[x][y] + grid[xx][yy];
                if (!grid[xx][yy]) {
                    que.emplace_front(xx, yy);
                }else {
                    que.emplace_back(xx, yy);
                }
            }
        }
        
        return dis[n-1][m-1];
    }
};

每次赛后看看大佬的是一个很好的习惯,大佬的代码当中可以学到很多小技巧以及trick。尤其是有一些巨佬是不屑写题解的,大家以后周赛结束都可以试试,经常能有惊喜。

好了,关于这次周赛就聊到这里,感谢大家的阅读。