LeetCode周赛284,图论压轴,难度给力

869 阅读5分钟

大家好,我是梁唐。

我们来回顾一下上周末的LeetCode周赛,首发于公众号:Coder梁

这次的周赛是理想汽车赞助的,大奖只是给了理想汽车的周边,和之前豪气公司送iWatch相比,不免有些小气……

这次比赛的题目难度不低,比赛的时候给我整破防了好几次,甚至第四题也是赛后才做出来的……

从题目质量上来说还是很高的,非常考验思维,很值得一做。

好了,废话不多说,我们来看题吧。

找出数组中的所有K近邻下标

难度:☆

给你一个下标从 0 开始的整数数组 nums 和两个整数 key 和 k 。K 近邻下标 是 nums 中的一个下标 i ,并满足至少存在一个下标 j 使得 |i - j| <= k 且 nums[j] == key 。

以列表形式返回按 递增顺序 排序的所有 K 近邻下标。

题解

题目读起来有些拗口,其实意思是说找到所有的下标i,使得它们的k近邻当中存在等于key。

由于数据范围不大,只有一千,所以我们既可以先枚举每一个下标,去判断它是否存在k近邻等于key,也可以反过来枚举所有等于key的位置,找到这些所有位置的k近邻。

两种做法都是可以的,从代码上来看,第一种更简单一些。

class Solution {
public:
    vector<intfindKDistantIndices(vector<int>& nums, int key, int k) {
        int n = nums.size();
        vector<int> res;
        for (int i = 0; i < n; i++) {
            for (int j = i-k; j <= i+k; j++) {
                if (j < 0 || j >= n) continue;
                if (nums[j] == key) {
                    res.push_back(i);
                    break;
                }
            }
        }
        return res;
    }
};

统计可以提取的工件

难度:☆☆

存在一个 n x n 大小、下标从 0 开始的网格,网格中埋着一些工件。给你一个整数 n 和一个下标从 0 开始的二维整数数组 artifactsartifacts 描述了矩形工件的位置,其中 artifacts[i] = [r1i, c1i, r2i, c2i] 表示第 i 个工件在子网格中的填埋情况:

  • (r1i, c1i) 是第 i 个工件 左上 单元格的坐标,且
  • (r2i, c2i) 是第 i 个工件 右下 单元格的坐标。

你将会挖掘网格中的一些单元格,并清除其中的填埋物。如果单元格中埋着工件的一部分,那么该工件这一部分将会裸露出来。如果一个工件的所有部分都都裸露出来,你就可以提取该工件。

给你一个下标从 0 开始的二维整数数组 dig ,其中 dig[i] = [ri, ci] 表示你将会挖掘单元格 (ri, ci) ,返回你可以提取的工件数目。

生成的测试用例满足:

  • 不存在重叠的两个工件。
  • 每个工件最多只覆盖 4 个单元格。
  • dig 中的元素互不相同。

题解

提示当中给了非常关键的信息,即每个工件最多只覆盖4个单元格且工件之间不会重叠。这题有没有这个提示完全是两种难度。

有这个提示有什么用呢?由于最多只有1e5个工件,即使每个工件都面积4,那么整体的各自数量级也依然是1e5。那么我们就可以维护所有的格子是否被发掘,再维护一下每个格子还没被发掘的面积。当一个工件的未被发掘的面积为0时,即说明该格子可以提取。

只要能注意到最后的提示,不难想到思路,代码会稍稍复杂一些。

class Solution {
public:
    int digArtifacts(int n, vector<vector<int>>& art, vector<vector<int>>& dig) {
        // 映射格子和工件的序号
        map<pair<intint>, int> mp;
        // 存储每个工件未被发掘的面积
        map<intint> area;
        
        for (int i = 0; i < art.size(); i++) {
            auto & a = art[i];
            auto x1 = a[0], y1 = a[1], x2 = a[2], y2 = a[3];
            
            int squ = (x2-x1+1) * (y2-y1+1);
            area[i] = squ;
            
            // 将工件i对应的每个格子映射到i
            for (int _i = x1; _i <= x2; _i++) {
                for (int _j = y1; _j <= y2; _j++) {
                    auto pnt = make_pair(_i, _j);
                    mp[pnt] = i;
                }
            }
        }
        
        int ret = 0;
        for (auto& vt: dig) {
            int x = vt[0], y = vt[1];
            auto p = make_pair(x, y);
            if (mp.count(p) == 0continue;
            // 工具mp[p]未被挖掘的面积-1,当面积为0时即可提取
            area[mp[p]]--;
            if (area[mp[p]] == 0) ret++;
        }
        return ret;
    }
};

K次操作后最大化顶端元素

难度:☆☆☆

给你一个下标从 0 开始的整数数组 nums ,它表示一个 ,其中 nums[0] 是栈顶的元素。

每一次操作中,你可以执行以下操作 之一

  • 如果栈非空,那么 删除 栈顶端的元素。
  • 如果存在 1 个或者多个被删除的元素,你可以从它们中选择任何一个,添加 回栈顶,这个元素成为新的栈顶元素。

同时给你一个整数 k ,它表示你总共需要执行操作的次数。

请你返回 恰好 执行 k 次操作以后,栈顶元素的 最大值 。如果执行完 k 次操作以后,栈一定为空,请你返回 -1

题解

比赛的时候看错题意,给我整破防了,错了很多次才终于通过。进一步说明了,比赛的时候读题和冷静很重要。

我们观察一下数据范围会发现本题的难点,就是数组长度和k都很大,尤其是k,范围是1e9,显然我们模拟或者是暴力的方法是行不通的。正面强攻不行,我们只能从反面入手了,既然模拟每一步的操作的代价太大,我们能不能反向构造,枚举一下每个元素成为答案的可能性?

我们维护一下每个元素能否成为最后栈顶的值,然后从这些所有可能的情况当中选出最大的,自然就是答案了。

对于第i个元素来说,如果它要成为最后出现在栈顶的元素,那么需要满足什么条件呢?

首先需要看i之前的元素全部出栈,那么这需要i-1步操作。执行这些操作之后,如果还有剩余,那么会出现几种情况。如果剩余的步骤数是偶数,那么很简单了,我们只需要重复执行插入删除的操作,最后就可以保证i一定出现在栈顶。

如果是奇数呢?

如果剩余的步数是奇数,又需要再分情况,如果剩余的步数是1,显然无论如何也不可能让i成为答案了。如果步数大于1呢?假设剩余步数是3,我们可以先插入一个元素,再同时删除它和i,最后再插入i,或者可以删除i和i+1,再插入回i,都可以保证i出现在栈顶。但这有一个条件,就是至少存在一个已经删除的元素或者是i之后至少还存在一个元素。相当于限制i的范围,必须在(1, n-1)之间。

代码如下:

class Solution {
public:
    int maximumTop(vector<int>& nums, int k) {
        int n = nums.size();
        int ret = -1;
        int maxi = -1;
        
        for (int i = 0; i < n; i++) {
            if (k == 0) {
                ret = max(ret, nums[i]);
                break;
            }
            int u = nums[i];
            ret = max(ret, maxi);
            
            // 如果剩余步骤数是偶数,那么u可以成为答案
            if (k % 2 == 0) ret = max(ret, u);
            else {
                // 否则必须要k > 1,并且i在(1, n-1)中间
                if (k > 1 && (i > 0 || i < n-1)) ret = max(ret, u);
            }
            maxi = max(maxi, u);
            k--;
        }
        return ret;
    }
};

进一步思考和归纳, 我们又可以有新的发现。

当k > n时,一定可以取到nums中的最大值。如果k等于n呢?无论我们如何操作也不可能让最后一个值成为答案,因为删除掉之前n-1个元素刚好消耗掉n-1步操作,所以答案是max(nums[:n-1])

如果k小于n呢?我们可以删除掉前k个元素,让k+1个元素处于栈顶,也可以从前k-1个元素当中选一个最大的出来作为答案。

与此同时,我们再考虑一些极端情况,例如n=1,k=0等情况,整个代码会变得更加简单,这里为了方便展示,所以使用了Python。

class Solution:
    def maximumTop(self, nums: List[int], k: int) -> int:
        
        n = len(nums)
        
        if n == 1:
            return -1 if k % 2 == 1 else nums[0]

        if k <= 1:
            return nums[k]

        if k > n:
            return max(nums)

        if k == n:
            return max(nums[0: n - 1])

        # 1 < k < n
        return max(max(nums[0: k - 1]), nums[k])

得到要求路径的最小带权子图

难度:☆☆☆☆☆

给你一个整数 n ,它表示一个 带权有向 图的节点数,节点编号为 0n - 1

同时给你一个二维整数数组 edges ,其中 edges[i] = [fromi, toi, weighti] ,表示从 fromitoi 有一条边权为 weighti有向 边。

最后,给你三个 互不相同 的整数 src1src2dest ,表示图中三个不同的点。

请你从图中选出一个 边权和最小 的子图,使得从 src1src2 出发,在这个子图中,都 可以 到达 dest 。如果这样的子图不存在,请返回 -1

子图 中的点和边都应该属于原图的一部分。子图的边权和定义为它所包含的所有边的权值之和。

题解

显然,这是一道图论题,这在LeetCode当中比较少见,虽然在图论当中并不算难,对于很少做图论问题的同学来说绝对算得上是顶级难度了。

这题的难点在于我们需要找到两个起点出发构成的子图,分别找两个子图而且还要求最优,显然非常麻烦,几乎不可能完成。那么到这里,我们又可以得出结论,这又是一题需要反向构造的问题,正面强攻是不行的。

那怎么反向构造呢?我比赛的时候的思路是将图反向,这样我们从两个起点s1和s2找dest的过程就变成了从dest出发寻找s1和s2。从dest出发分别找s1和s2的路径就简单多了,但这仍然达不成同时构成的子图最优的前提。所以当时我就卡住了,没能想到解法。

赛后看了一些大佬的题解, 发现了症结,其实还需要再往深想一步,我们可以枚举节点x,同时计算s1,s2和dest到x的最短路,这三条路径之和,即是答案。

在计算从dest出发的最短路时需要将图反向,因为本质上是从x出发到dest,只不过dest是确定的,因此我们计算从dest出发更为容易。

在图论的最短路算法当中,我们可以快速求解出从某一个顶点s出发到其他所有点的最短路。这当中的算法其实有很多,比如著名的Dijkstra和spfa,我个人更喜欢使用spfa,因为它实现更加简单,几乎近似于宽搜。

所以我们只需要同时建立正向图和反向图,再分别使用三次spfa,计算从src1、src2和dest点出发到其它点的最短路,枚举一下x,找到最优解即可。

代码如下:

typedef pair<intint> pii;

class Solution {
public:
    vector<vector<pii>> g, rg;

    // 使用邻接表存储图g[u].push_back({v, w}),表示u点出发有一条指向v的边,长度为w
    void add_edge(vector<vector<pii>>& g, int u, int v, int w) {
        g[u].push_back({v, w});
    }

    void spfa(vector<vector<pii>> &g, vector<long long>& dis, int s) {
        // spfa最短路算法
        dis[s] = 0;
        queue<int> que;
        que.push(s);
        // 维护点是否在队列中,如果已经在队列,则避免重复添加
        vector<intst(dis.size(), 0);
  st[s] = 1;

        while (!que.empty()) {
            int u = que.front();
            que.pop();
            st[u] = 0;
            for (auto& x: g[u]) {
                int v = x.first, w = x.second;
                if (dis[v] > dis[u] + (long long)w) {
                    dis[v] = dis[u] + (long long)w;
                    if (st[v] == 0) {
                        que.push(v);
                        st[v] = 1;
                    }
                }
            }
        }
    }

    long long minimumWeight(int n, vector<vector<int>>& edges, int src1, int src2, int dest) {
        g.resize(n); rg.resize(n);
        for (auto & e: edges) {
            int u = e[0], v = e[1], w = e[2];
            add_edge(g, u, v, w);
            add_edge(rg, v, u, w);
        }

        vector<long longd1(n, 1e11)d2(n, 1e11)d3(n, 1e11);
        // 同时计算src1,src2,dest为源点的最短路,注意dest使用反向图
        spfa(g, d1, src1);
        spfa(g, d2, src2);
        spfa(rg, d3, dest);

        long long res = 0x3f3f3f3f3f3f3f3f;
        // 枚举中间点
        for (int i = 0; i < n; i++) {
            res = min(res, d1[i] + d2[i] + d3[i]);
        }
        return res >= 1e11 ? -1: res;
    }
};

这题的难度不小,除了思维很巧妙比较难想到之外,对于编码的要求也不低,想要在比赛的时候完整地实现最短路算法对于非竞赛选手来说显然是比较困难的。但这题的质量很不错,我个人认为甚至不下于专业竞赛的问题。

虽然很遗憾,这次周赛的表现不佳,但这次的题目质量还算是不错,很值得大家一试。

好了,关于这次的问题就先聊到这里,感谢大家的阅读。