LeetCode 第 194 场周赛


这次周赛比以往难很多.

数组异或操作

给你两个整数,n 和 start 。

数组 nums 定义为:nums[i] = start + 2*i(下标从 0 开始)且 n == nums.length 。

请返回 nums 中所有元素按位异或(XOR)后得到的结果。

思路和代码

直接暴力

class Solution:
    def xorOperation(self, n: int, start: int) -> int:
        nums = [start + 2 * i for i in range(n)];
        ret = 0;
        for val in nums:
            ret ^= val
        return ret

保证文件名唯一

给你一个长度为 n 的字符串数组 names 。你将会在文件系统中创建 n 个文件夹:在第 i 分钟,新建名为 names[i] 的文件夹。

由于两个文件 不能 共享相同的文件名,因此如果新建文件夹使用的文件名已经被占用,系统会以 (k) 的形式为新文件夹的文件名添加后缀,其中 k 是能保证文件名唯一的 最小正整数 。

返回长度为 n 的字符串数组,其中 ans[i] 是创建第 i 个文件夹时系统分配给该文件夹的实际名称。

示例:

输入:names = ["kaido","kaido(1)","kaido","kaido(1)"]
输出:["kaido","kaido(1)","kaido(2)","kaido(1)(1)"]
解释:注意,如果含后缀文件名被占用,那么系统也会按规则在名称后添加新的后缀 (k) 。

思路及代码

用一个字典保存文件下一次生成的索引, 举个例子

['a', 'a', 'a', 'a']

那么生成的字典为

d = {'a': 4, 'a(1)':1, 'a(2)': 1, 'a(3)': 1}

例如, 如果后续加入文件a, 先从字典里面查, d['a'] = 4, 那么我们尝试使用a(4), 发现成立, 这时候字典更新为

d = {'a': 5, 'a(1)':1, 'a(2)': 1, 'a(3)': 1, 'a(4)': 1}

如果下一次加入文件a(1) 那么我们知道a(1)下一次对应生成的索引为1,那么尝试生成a(1)(1), 也是成立的.

考虑另外一种情况, 例如现在字典为

d = {'a':5, 'a(1)':1, 'a(1)(1)':1}

下一次加入a(1), 首先下一次生成的索引应该为a(1)(1), 此时发现被占用, 所以对索引增加操作a(1)(2) 此时成立, 此时字典需要更新a(1)a(1)(2), 更新为, 通过一个循环可以处理

d = {'a':5, 'a(1)': 1 + 2, 'a(1)(1)': 1, 'a(1)(2)': 1}

代码如下: python

class Solution:
    def getFolderNames(self, names: List[str]) -> List[str]:
        visited = collections.defaultdict(int)
        ret = []
        for name in names:            
            if name not in visited:
                ret.append(name)
                visited[name] += 1
            else:
                n = visited[name]
                cnt = 0
                while name + '(' + str(n + cnt)+ ')' in visited:
                    cnt += 1;
                visited[name] += cnt + 1;
                name += '(' + str(n + cnt)+ ')'
                ret.append(name)
                visited[name] += 1
        return ret

CPP:

class Solution {
public:
    vector<string> getFolderNames(vector<string>& names) {
        unordered_map<string, int> counter;
        vector<string> ret;
        for(auto name: names){
            if (counter.count(name)){
                int &n = counter[name];
                int cnt = 0;
                while (counter.count(name + "(" + to_string(n + cnt) + ")")) ++cnt;
                ret.push_back(name + "(" + to_string(n + cnt) + ")");
                ++counter[name + "(" + to_string(n + cnt) + ")"];
                n += cnt + 1;
            }
            else{
                ret.push_back(name);
                ++counter[name];
            }
        }
        return ret;
    }
};

避免洪水泛滥

你的国家有无数个湖泊,所有湖泊一开始都是空的。当第 n 个湖泊下雨的时候,如果第 n 个湖泊是空的,那么它就会装满水,否则这个湖泊会发生洪水。你的目标是避免任意一个湖泊发生洪水。

给你一个整数数组 rains ,其中:

  • rains[i] > 0 表示第 i 天时,第 rains[i] 个湖泊会下雨。

  • rains[i] == 0 表示第 i 天没有湖泊会下雨,你可以选择 一个 湖泊并 抽干 这个湖泊的水。
    请返回一个数组 ans ,满足:

  • ans.length == rains.length

  • 如果 rains[i] > 0 ,那么ans[i] == -1

  • 如果 rains[i] == 0ans[i] 是你第 i 天选择抽干的湖泊。

如果有多种可行解,请返回它们中的任意一个 。如果没办法阻止洪水,请返回一个 空的数组 。

请注意,如果你选择抽干一个装满水的湖泊,它会变成一个空的湖泊。但如果你选择抽干一个空的湖泊,那么将无事发生(详情请看示例 4)。
示例:

输入:rains = [69,0,0,0,69]
输出:[-1,69,1,1,-1]
解释:任何形如 [-1,69,x,y,-1], [-1,x,69,y,-1] 或者 [-1,x,y,69,-1] 都是可行的解,其中 1 <= x,y <= 10^9

思路及代码

一个最朴素的思路是回溯, 即当面某天下雨时, 随机抽一个满谁的湖泊, 看最后能否成立 ,超时.

如果同一个湖泊在第i天和第j被填满, 我们应该在ij 之间选择不下雨的一天, 来抽干该湖泊的水, 选择的策略是如果其中很多天不下雨, 那么选择在第i天之后且最接近第i天的不下雨的那天

那么思路可以是, 遍历rains, 维护一个不下雨天的索引, 显然这个索引是递增的, 同时记录被填满的湖泊和对应天数, 当遍历到某一天出现下雨到同一个湖泊这种情况, 则找出上一次这个湖泊被填满那天i 用二分搜索 O ( log ⁡ n ) O(\log n) O(logn), 搜索iupper_bound(大于 i 的最小索引), 将次索引的结果填入对应胡泊序号, 同时在维护的索引数据删除这个位置的元素 O ( n ) O(n) O(n)(对于 python). (Cpp可以用 set 保证查找和删除都是 log ⁡ n \log n logn)

代码如下:

class Solution:
    def avoidFlood(self, rains: List[int]) -> List[int]:
        import bisect
        ids = []
        v = {}
        ret = [0] * len(rains)
        for idx, val in enumerate(rains):
            if val:
                ret[idx] = -1
                if val in v:
                    i = bisect.bisect_right(ids, v[val])
                    if i == len(ids):
                        return []
                    else:
                        ret[ids[i]] = val
                        ids.pop(i)
                v[val] = idx
            else:
                ids.append(idx)
        for idx, val in enumerate(ret):
            if val == 0:
                ret[idx] = 1
        return ret

进一步优化: 使用优先队列, 首先逆序遍历rains, 使用数组记录当前下雨湖泊下一次下雨的天数.

顺序遍历rains, 使用优先队列记录[下一个下雨天数, 对应胡泊], 当某天不下雨的时候, 开始出队. Cpp 代码如下:

class Solution {
public:
    vector<int> avoidFlood(vector<int>& rains) {
        unordered_map<int, int> pos;
        int n = rains.size();
        vector<int> nxt(n, n);
        for(int i = n - 1; i >=0; --i){
            if(rains[i] == 0 || pos.count(rains[i]) == 0){
                pos[rains[i]] = i;
            }
            else{
                nxt[i] = pos[rains[i]];
                pos[rains[i]] = i;
            } 
        }
        vector<int> ret;
        priority_queue<pair<int, int> > que;
        unordered_set<int> visited;
        for (int i = 0; i < n; ++i){
            if (rains[i] == 0){
                if(que.empty()) ret.push_back(1);
                else{
                    auto p = que.top();
                    que.pop();
                    ret.push_back(p.second);
                    visited.erase(p.second);
                }
            }
            else{
                if (visited.count(rains[i])) return {};
                if(nxt[i] != n){
                    que.push(make_pair(-nxt[i], rains[i]));
                    visited.insert(rains[i]);
                }
                ret.push_back(-1);
            }
        }
        return ret;
    }
};

找到最小生成树里的关键边和伪关键边

给你一个 n 个点的带权无向连通图,节点编号为 0n-1 ,同时还有一个数组 edges ,其中 edges[i] = [fromi, toi, weighti] 表示在 fromitoi 节点之间有一条带权无向边。最小生成树 (MST) 是给定图中边的一个子集,它连接了所有节点且没有环,而且这些边的权值和最小。

请你找到给定图中最小生成树的所有关键边和伪关键边。如果从图中删去某条边,会导致最小生成树的权值和增加,那么我们就说它是一条关键边。伪关键边则是可能会出现在某些最小生成树中但不会出现在所有最小生成树中的边。

请注意,你可以分别以任意顺序返回关键边的下标和伪关键边的下标。

思路及代码

参考零神题解, 由于数据量不大, 反复使用 Kruskal 算法, 关于 MST 和 Kruskal 算法, 参考我的这篇博客

其中, 注意关键边和伪关键边的含义:
关键边: 所有 MST 共享的边
伪关键边: 任意 MST 的有一条边, 且这条边不是关键边.

流程如下:

  1. 按照权值对边进行排序
  2. 调用 Kruskal 计算 MST 的值, 即为v
  3. 任意去掉一条边, 调用 Kruskal 计算 MST 的 value, 如果v != value, 说明这条边为关键边, 记录下所有关键边
  4. 将关键边先连接起来, 选定一条剩余边, 将其强制加入 MST 中, 然后使用 Kruskal 生成 MST , 如果value == v 则这条剩余边为伪关键边.

代码如下

class DSU{
    public:
    vector<int> parent;
    DSU(DSU &dsu){
        int n = dsu.parent.size();
        parent.resize(n);
        for(int i = 0; i < n; ++i) parent[i] = dsu.parent[i];
    }
    DSU(int n){
        parent.resize(n);
        for(int i = 0; i < n; ++i) parent[i] = i;
    }

    int find(int x){
        if (parent[x] != x)
            parent[x] = find(parent[x]);
        return parent[x];
    }

    void union_(int x, int y){
        int rx = find(x), ry = find(y);
        if (rx != ry){
            parent[rx] = ry;
        }
    }

    bool isCircle(int x, int y){
        return find(x) == find(y);
    }

    void clear(){
        for(int i = 0; i < parent.size(); ++i) parent[i] = i;
    }
};

bool cmp(vector<int> &a, vector<int> &b){return a[2] < b[2];}

class Solution {
public:
    vector<vector<int>> findCriticalAndPseudoCriticalEdges(int n, vector<vector<int>>& edges) {
        int v = 0;
        for(int i = 0; i < edges.size(); ++i){
            edges[i].push_back(i);
        }
        DSU dsu(n);
        sort(edges.begin(), edges.end(), cmp);
        int cnt = 0;
        for(auto &edge: edges){
            if(!dsu.isCircle(edge[0], edge[1])){
                ++cnt;
                v += edge[2];
                dsu.union_(edge[0], edge[1]);
            }
            if(cnt == n - 1) break;
        }

        vector<int > ret1;
        unordered_set<int> st;
        for (int cur = 0; cur < edges.size(); ++ cur){
            dsu.clear();
            int value = 0;
            int ccnt = 0;
            for(int i = 0; i < edges.size(); ++i){
                auto &edge = edges[i];
                if(i == cur) continue;
                if(!dsu.isCircle(edge[0], edge[1])){
                    ++ccnt;
                    value += edge[2];
                    dsu.union_(edge[0], edge[1]);
                }
                if(ccnt == n - 1) break;
            }
            if (value != v) {ret1.push_back(edges[cur][3]); st.insert(cur);}
        }
        vector<int> ret2;
        int rest = n - 1 - st.size();
        dsu.clear();
        int st_v = 0;
        for(auto &i: st) {dsu.union_(edges[i][0], edges[i][1]);st_v += edges[i][2];}
        for(int cur = 0; cur < edges.size(); ++cur){
            if(st.count(cur)) continue;
            DSU tmp(dsu);
            int value = st_v;
            int _rest = rest;
            if(tmp.isCircle(edges[cur][0], edges[cur][1])) continue;
            else{
                value += edges[cur][2];
                --_rest;
                tmp.union_(edges[cur][0], edges[cur][1]);
            }
            for(int i = 0; i < edges.size(); ++i){
                if(i == cur || st.count(i)) continue;
                if(!tmp.isCircle(edges[i][0], edges[i][1])){
                    --_rest;
                    value += edges[i][2];
                    tmp.union_(edges[i][0], edges[i][1]);
                }
                if(_rest == 0) break;
            }
            if (_rest == 0 && value == v) ret2.push_back(edges[cur][3]);
        }
        return {ret1, ret2};

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值