LeetCode周赛308,AK之后我怎么觉得还是第一题最难呢……

186 阅读5分钟

大家好,我是梁唐。

今天是周一,照惯例,我们来一起看一下昨天的LeetCode周赛。昨天是LeetCode第308场,由地平线赞助。

这次的赛题比上周水了很多,评论区里说自己第一次ak的就有好几个。所以对于新手来说,这是一个很好的练习的机会。

闲言少叙,我们来看题吧。

和有限的最长子序列

给你一个长度为 n 的整数数组 nums ,和一个长度为 m 的整数数组 queries

返回一个长度为 m 的数组 answer ,其中 answer[i]nums 中 元素之和小于等于 queries[i]子序列最大 长度 。

子序列 是由一个数组删除某些元素(也可以不删除)但不改变剩余元素顺序得到的一个数组。

题解

这题作为第一题估计很多人看到会蒙,一下子没有思路。

实际上这题用到的一个技巧在之前的题目当中出现过,就是关于子序列和的问题。虽然子序列中的元素是固定的,但是子序列的元素选择是不受限的。当我们确定了子序列中元素个数之后,和最小的子序列就是确定的,即最小的元素之和。

在本题当中我们要小于某个和的子序列长度最大,也就是说要找到同等长度和最小的子序列。同等长度和最小的子序列当然是排序之后从小到大的元素组合。所以我们只需要将nums数组排序,长度为1的就是最小的元素,长度为2的序列之和就是最小的两个元素之和,以此类推。

这样,我们只用一次排序之后就可以得到1到n之间所有长度的最小序列和。由于nums中的所有元素都大于0,所以对应的各个长度的序列和也是递增的。这样我们就可以使用二分法来快速查找了。

class Solution {
public:
    vector<int> answerQueries(vector<int>& nums, vector<int>& queries) {
        int n = nums.size();
        int m = queries.size();
        
        vector<long long> sums(n+2, 0x3f3f3f3f3f3f3f);
        sums[0] = 0;
        
        sort(nums.begin(), nums.end());
        
      	// 求出任意长度的序列和
        for (int i = 0; i < n; i++) {
            sums[i+1] = sums[i] + nums[i];    
        }
        
        vector<int> ret;
        
        for (int i = 0; i < m; i++) {
          	// 二分法查找大于等于queries[i]的下标
            int cur = lower_bound(sums.begin(), sums.begin() + n + 1, queries[i]) - sums.begin();
          	// 如果序列和大于queries[i],那么长度-1
            ret.push_back(cur-(sums[cur] > queries[i]));
        }
        return ret;
    }
};

从字符串中移除星号

给你一个包含若干星号 * 的字符串 s

在一步操作中,你可以:

  • 选中 s 中的一个星号。
  • 移除星号 左侧 最近的那个 非星号 字符,并移除该星号自身。

返回移除 所有 星号之后的字符串**。**

注意:

  • 生成的输入保证总是可以执行题面中描述的操作。
  • 可以证明结果字符串是唯一的。

题解

模拟题,我们可以把字符串当做是栈来操作。当遇到*时弹出栈顶字符。

class Solution {
public:
    string removeStars(string s) {
        string ret = "";
        
        for (auto c : s) {
            if (c == '*') {
                ret.pop_back();
            }else ret.push_back(c);
        }
        return ret;
    }
};

收集垃圾的最少总时间

给你一个下标从 0 开始的字符串数组 garbage ,其中 garbage[i] 表示第 i 个房子的垃圾集合。garbage[i] 只包含字符 'M''P''G' ,但可能包含多个相同字符,每个字符分别表示一单位的金属、纸和玻璃。垃圾车收拾 单位的任何一种垃圾都需要花费 1 分钟。

同时给你一个下标从 0 开始的整数数组 travel ,其中 travel[i] 是垃圾车从房子 i 行驶到房子 i + 1 需要的分钟数。

城市里总共有三辆垃圾车,分别收拾三种垃圾。每辆垃圾车都从房子 0 出发,按顺序 到达每一栋房子。但它们 不是必须 到达所有的房子。

任何时刻只有 一辆 垃圾车处在使用状态。当一辆垃圾车在行驶或者收拾垃圾的时候,另外两辆车 不能 做任何事情。

请你返回收拾完所有垃圾需要花费的 最少 总分钟数。

题解

同样是模拟题,要处理移动的时间,也要考虑处理垃圾的时间,叠加起来显得有些麻烦。

实际上我们冷静分析会发现,不论是什么品种的垃圾车,移动同样距离的耗时是不变的。所以我们完全可以把垃圾处理和垃圾车移动分开考虑,首先求出垃圾车需要移动的最远距离。再对每一堆垃圾堆进行单独处理即可,因为所有垃圾的处理时间也一样,都是1个单位时间。

因为数据量级不大,基本怎么操作都可以,不用担心超时。

class Solution {
public:
    int garbageCollection(vector<string>& gbg, vector<int>& tra) {
        int ret = 0;
        int n = gbg.size();
        
        vector<int> tot(n+1, 0);
        
      	// 前缀和维护移动到坐标i的耗时
        for (int i = 0; i < n-1; i++) {
            tot[i+1] = tot[i] + tra[i];
        }
        
        int last_g = 0, last_p = 0, last_m = 0;
        for (int i = 0; i < n; i++) {
            auto &gbr = gbg[i];
          	// 维护每种垃圾车需要移动的最远距离
            for (auto c: gbr) {
                if (c == 'G') last_g = i;
                if (c == 'M') last_m = i;
                if (c == 'P') last_p = i;
            }
          	// 直接加上当前这堆垃圾的数量
            ret += gbr.length();
        }
        
        ret += (tot[last_g] + tot[last_m] + tot[last_p]);
        return ret;
    }
};

给定条件下构造矩阵

给你一个 整数 k ,同时给你:

  • 一个大小为 n 的二维整数数组 rowConditions ,其中 rowConditions[i] = [abovei, belowi]
  • 一个大小为 m 的二维整数数组 colConditions ,其中 colConditions[i] = [lefti, righti]

两个数组里的整数都是 1k 之间的数字。

你需要构造一个 k x k 的矩阵,1k 每个数字需要 恰好出现一次 。剩余的数字都是 0

矩阵还需要满足以下条件:

  • 对于所有 0n - 1 之间的下标 i ,数字 abovei 所在的 必须在数字 belowi 所在行的上面。
  • 对于所有 0m - 1 之间的下标 i ,数字 lefti 所在的 必须在数字 righti 所在列的左边。

返回满足上述要求的 任意 矩阵。如果不存在答案,返回一个空的矩阵。

题解

拓扑排序的裸题,我们要在行和列两个方面保证满足限制条件。所以我们要在行和列两个维度做两次拓扑排序,由于我们只需要返回一个合法结果,所以并不用穷举所有的情况。

拓扑排序算法是用来解决依赖叠加问题的算法,当有若干个任务互相依赖时,我们需要找到一个执行顺序,使得我们可以保证所有任务都能被完成。如果若干个任务之间存在互相之间的循环依赖,拓扑排序算法也可以判断出这种情况。

具体的原理也比较简单,通过有向图来判断。如果a任务依赖b任务,可以抽象成从b连出一条指向a的边。同时,我们维护每个点的入度。可以知道,入度为0的点即没有任何依赖的点,即当前可以完成的任务。我们将它完成之后,将以它为初始点的边删除,并且将指向点的入度减一。如果指向的点入度减一之后为0,说明该点也没有了依赖。

我们可以使用一个队列来维护所有当前可以执行的任务,队列弹出的顺序即为一个合法的拓扑排序的序列。如果队列为空时依然存在一些边没有被删除, 则说明图中存在环,即互相循环依赖的任务。对应本题当中,如果有环,说明无解。

class Solution {
public:
    vector<vector<int>> buildMatrix(int k, vector<vector<int>>& rowC, vector<vector<int>>& colC) {
      
      	// 拓扑排序
        auto top = [&](vector<vector<int>> &data) -> vector<int> {
            int n = data.size();
            
            map<int, int> ind;
          
          	// 使用邻接表建图
            vector<vector<int>> graph(k+1, vector<int>());
            
            for (auto &vt : data) {
                int u = vt[0], v = vt[1];
                ind[v]++;
                graph[u].push_back(v);
            }
            
            vector<int> ret;
            
            queue<int> que;
          	// 将入度为0的点放入队列
            for (int i = 1; i <= k; i++) {
                if (ind.count(i) == 0) {
                    que.push(i);
                }
            }
            
            while (!que.empty()) {
                int u = que.front();
                ret.push_back(u);
                que.pop();
              	// 删除从u连出的所有边,并且将目标点v的入度-1
                for (auto v: graph[u]) {
                    ind[v]--;
                  	// 如果v入度为0,说明v也没有了依赖,加入队列
                    if (ind[v] == 0) {
                        que.push(v);
                    }
                  	// 边的数量-1
                    n--;
                }
            }
            
          	// 如果还存在边没有被删除,说明有环,无解
            if (n > 0) ret.clear();
            return ret;
        };
        
        auto ret1 = top(rowC);
        auto ret2 = top(colC);
        if (ret1.size() == 0 || ret2.size() == 0) return vector<vector<int>>();
        
        vector<vector<int>> ret(k, vector<int>(k, 0));
        
      	// 组装答案
        map<int, int> row_map, col_map;
        for (int i = 0; i < k; i++) {
            row_map[ret1[i]] = i;
            col_map[ret2[i]] = i;
        }
        
        for (int i = 1; i <= k; i++) {
            int r = row_map[i], c = col_map[i];
            ret[r][c] = i;
        }
        return ret;
    }
};

因为昨天比赛的时候我在高铁上,所以没能实时参与,我是赛后进行的模拟赛。这场的题都是一次通过,比我预想的还要简单一些。最后一题的拓扑排序可能相对比较冷门,但它在日常工作场景当中的出现频率却不低,很多地方都能用得上。所以当做一个学习和练手的机会也不错。