LeetCode 力扣周赛 262

102 阅读5分钟

[周赛传送门]leetcode-cn.com/contest/wee…

和冠军的差距太太太太大啦,我过第二题的时候,冠军已经早早做完了。

5894. 至少在两个数组中出现的值

思路:暴力查找

时间复杂度O(nv)O(n*v)vv 是取值范围的长度,nn 是三个数组的元素数量。

由于各数组中元素的取值范围为 [1,100][1,100],因此可枚举 i[1,100]i\in[1,100],然后检查 ii 是否在至少两个数组中出现即可。

class Solution {
public:
    vector<int> twoOutOfThree(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3) {
        vector<int> anw;
        for (int i = 1; i <= 100; i++) {
            int cnt = 0;
            cnt += (std::find(nums1.begin(), nums1.end(), i) != nums1.end());
            cnt += (std::find(nums2.begin(), nums2.end(), i) != nums2.end());
            cnt += (std::find(nums3.begin(), nums3.end(), i) != nums3.end());
            if (cnt >= 2) {
                anw.push_back(i);
            }
        }
        return anw;
    }
};

5895. 获取单值网格的最小操作数

思路:前缀和,等差数列

时间复杂度O(nm+v)O(n*m+v)vv 是取值范围的长度。

不难想到,当且仅当「 nmn*m 个数字在去重后能组成公差为 x 的等差序列」,才可以通过若干次操作获得单值网格。

首先检查能否组成上述序列。这个不难,可以 O(nm)O(n*m) 的实现。设有一个一维数组 cntcntcnticnt_i 表示数字 iigridgrid 中出现次数。

统计完 cntcnt 后,只需检查 cntcnt 中所有不为零元素的下标能否组成公差为 xx 的等差序列即可。

接下来,问题变为了「给出长度为 kk 的等差序列 seqseq,将其中数字变为单值的最小次数」。

不难想到,将 seqseq 中数字都变为 seqiseq_i 的操作次数为 costicost_i:

costi=j=0k1(seqiseqj)xcntseqjcost_i = \sum_{j=0}^{k-1}\frac{(seq_i-seq_j)}{x}*cnt_{seq_j}

通过预处理前缀和以及后缀和,可以 O(n)O(n) 的计算出所有的 costicost_i。显然,其中最小的 costicost_i 即为答案。预处理过程详见注释。

class Solution {
public:
    int minOperations(vector<vector<int>>& grid, int x) {
        // 比取值上限大一,用于定义数组。
        const int MAXN = 10001;
        
        // cnt[i] 表示数字 i 在 grid 中出现的次数
        int cnt[MAXN] = {0};
        // num 表示在 grid 一共出现了多少种数字
        int num = 0;
        
        // 记录一下出现的最小值
        int minNum = INT_MAX;
        // 记录一下出现的最大值
        int maxNum = INT_MIN;
        
        // 更新 cnt 以及 num
        for (const auto &row : grid) {
            for (const auto &c : row) {
                if(cnt[c] == 0) {
                    // 如果 cnt[c] 为 0,说明出现了一种新数字。
                    num++;
                }
                cnt[c]++;
                minNum = min(minNum, c);
                maxNum = max(maxNum, c);
            }
        }
        
        // 检查所有出现的数字在去重后能否组成等差序列。
        // 从 minNum 开始检查 minNum,minNum+x,minNum+2x,...,minNum+(num-1)*x 是否都出现过。
        for (int j = minNum; j < MAXN; j += x) {
            if (cnt[j] > 0) {
                num--;
            }   
        }
        // 不是关于 x 的等差序列,直接返回 -1 吧
        if (num != 0) {
            return -1;
        }

        // suf[i] 表示把不小于 i 的数字都变为 i 的花销。
        // sufCnt[i] 表示不小于 i 的数字一共出现了多少次。
        int suf[MAXN] = {0};
        int sufCnt[MAXN] = {0};
        for (int i = maxNum; i >= minNum; i -= x) {
            sufCnt[i] = cnt[i];
            if (i+x < MAXN) {
                sufCnt[i] += sufCnt[i+x];
                suf[i] = suf[i+x] + sufCnt[i+x];
            }
        }
        
        int anw = INT_MAX;
        // pre[i] 表示把不大于 i 的数字都变为 i 的花销。
        // preCnt[i] 表示不大于 i 的数字一共出现了多少次。
        int pre[MAXN] = {0};
        int preCnt[MAXN] = {0};
        for (int i = minNum; i <= maxNum; i += x) {
            preCnt[i] = cnt[i];
            if (i >= x) {
                preCnt[i] += preCnt[i-x];
                pre[i] = pre[i-x] + preCnt[i-x];
            }
            if (cnt[i] > 0) {
                anw = min(anw, pre[i] + suf[i]);
            }
        }
        return anw;
    }
};

5896. 股票价格波动

思路:multimap

时间复杂度:查询为 O(1)O(1),更新为 O(lgn)O(\lg n)

首先使用 mapmap 记录时间戳和价格的映射关系。这样对于 current 查询,map.rbegin() 即为答案。时间复杂度为 O(1)O(1)

使用 multimapmultimap 保存有效的价格,对于 maximum 查询,multimap.rbegin() 即为答案,对于 minimum 查询,minimum.begin() 即为答案。时间复杂度均为 O(1)O(1)

每次 update 操作,仅需对这两个容器进行常数次的增删改。时间复杂度为 O(lgn)O(\lg n)

class StockPrice {
public:
    map<int, int> t2p; // 保存时间和价格的映射
    multiset<int> price; // 保存已知的有效价格
    
    StockPrice() {
    }
    
    void update(int t, int p) {
        auto it = t2p.find(t);
        
        // 没有找到,说明是个新的时间戳,直接加入到 t2p 和 price
        if (it == t2p.end()) {
            t2p[t] = p;
            price.insert(p);
            return ;
        }
        // 是个修改操作,先删除错误的价格
        auto pit = price.find(it->second);
        price.erase(pit);
        // 插入更新后的价格
        price.insert(p);
        // 更新 t2p
        it->second = p;
    }
    
    int current() {
        return t2p.rbegin()->second;
    }
    
    int maximum() {
        return *price.rbegin();
    }
    
    int minimum() {
        return *price.begin();
    }
};

5897. 将数组分成两个数组并最小化数组和的差

思路:位运算,三分

时间复杂度O(2nn)O(2^n*n)

不难想到,先选 nn 个数字构成数组一,那么剩下的 nn 的数字就是数组二了。

接下来,将数组一的选取过程分成两步:

  • 在前 nn 个数字中选取 ii 个。
  • 在后 nn 个数字中选取 nin-i 个。

两步各有 2n2^n 种选取方案,组合起来就是22n2^{2n}。尝试用三分优化一下组合过程。

假设在前一半中选取了 ii 个数字,其和为 pp,则后一半中有 2ni2^{n-i} 种待选方案,假设我们选取了第 k 种方案,其和为 sks_k。则两数组的差值可表示为

abs(sum2(p+sk))abs(sum-2*(p+s_k))

不难发现,随着 sks_k 从小到大,上述式子的值可能会先变小后增大,即上式是一个凹函数。因此可用三分找出关于 pp 的最优的 sks_k

class Solution {
public:
    void work(const vector<int> &part, unordered_map<int, vector<int>> &c) {
        int n = part.size();
        // 位运算处理 2^n 种方案
        for (int i = 0, m = (1<<n); i < m; i++) {
            // 记录第 i 种方案的累加和
            int sum = 0;
            // 记录第 i 种方案的选取的数字个数
            int cnt = 0;
            for (int j = 0, bit = 1; j < n; j++, bit <<= 1) {
                if (i&bit) {
                    sum += part[j];
                    cnt++;
                }
            }
            // 记录第 i 种方案
            c[cnt].push_back(sum);
        }
        // 排个序,方便后续的三分操作
        for (auto &p : c) {
            sort(p.second.begin(), p.second.end());
        }
    }
    // 三分
    int find(int v, int sum, const std::vector<int> &arr) {
        int l = 0, r = arr.size()-1;
        int anw = INT_MAX;
        while(l <= r) {
            int mid = (l+r)>>1;
            int mmid = (mid+r)>>1;
            int c = abs(2*(v+arr[mid]) - sum);
            int cc = abs(2*(v+arr[mmid]) - sum);
            if (c <= cc) {
                r = mmid-1;
            } else {
                l = mid+1;
            }
            anw = min(anw, min(c, cc));
        }
        return anw;
    }
    int minimumDifference(vector<int>& nums) {
        int n = nums.size()/2;
        
        // 计算 n 个数字之和
        int sum = 0;
        for_each(nums.begin(), nums.end(), [&sum](int v) {sum += v;});
        
        // pre[i] 记录在前 n 个数字中选取 i 个数字的C(n,i)种方案
        // suf[i] 记录在后 n 个数字中选取 i 个数字的C(n,i)种方案 
        unordered_map<int, vector<int>> pre, suf;
        // 处理 pre
        work(std::vector<int>(nums.begin(), nums.begin()+n), pre);
        // 处理 suf
        work(std::vector<int>(nums.begin()+n, nums.end()), suf);
        
        // anw 用于存储答案
        int anw = INT_MAX;
        // 「数组一」在前 n 个数字中的选取了 i 个数字。
        for (int i = 0; i <= n; i++) {
            // 枚举 C(n,i) 种方案
            for (auto p : pre[i]) {
                // 「数组一」必然在后 n 个数字中选取 n-i 个数字,利用三分查找最优解。
                anw = min(anw, find(p, sum, suf[n-i]));
            }
        }
        return anw;
    }
};