LeetCode 力扣双周赛61

72 阅读3分钟

周赛传送门

周六加班了,十一点多才下班,没来及参加。。

2006. 差的绝对值为 K 的数对数目

思路: 暴力枚举

时间复杂度O(n2)O(n^2)

枚举所有的数对(i,j)(i,j),判断其差值是否符合要求。

class Solution {
public:
    int countKDifference(vector<int>& nums, int k) {
        int anw = 0;
        for (int i = 0; i < nums.size(); i++) {
            for (int j = i+1; j < nums.size(); j++) {
                if (abs(nums[i] - nums[j]) == k) {
                    anw ++;
                }
            }
        }
        return anw;
    }
};

2007. 从双倍数组中还原原数组

思路:双指针

时间复杂度O(nlgn)O(n\lg n)

根据定义,双倍数组必然满足下列条件:

  • 有偶数个元素。
  • vv 为其中最小的元素,则 vv 必在 originaloriginal 中。
  • 删除 vv2v2v 后,仍然是一个双倍数组。

因此,可先将 changedchanged 升序排序,然后不断的删除其中的最小值 vv 以及 2v2v,直到 changedchanged 为空。

如果 changedchanged 变为空,则说明是双倍数组,则删除过程中的 vv 组成了 originaloriginal。反之,则说明 changedchanged 不是双倍数组。

class Solution {
public:
    vector<int> findOriginalArray(vector<int>& changed) {
        // 先判断就
        if (changed.size()&1) {
            return vector<int>();
        }
        // 排序
        sort(changed.begin(), changed.end());
        vector<int> anw; // 用于存放 original
        anw.reserve(changed.size()/2);

        // 双指针:i 用于寻找删除过程中的最小值 v; j 用于寻找 2v
        // changed[i] = -1,则说明该位置的元素被删除了,反之则尚未删除。
        for (int i = 0, j = 0; i < changed.size(); i++) {    
            if (changed[i] != -1) {
                // v 是递增的,2v 也肯定是递增的,且 2v >= v。所以 j > i。
                j = max(i+1, j);
                while (j < changed.size() && changed[j] < changed[i]*2) {
                    j++;
                }
                // 没找到 2v,不是双倍数组
                if (j == changed.size() || changed[j] != changed[i]*2) {
                    return vector<int>();
                }
                // 保存一下答案。
                anw.push_back(changed[i]);
                // 标记删除
                changed[j] = -1;
            }
        }
        return anw;
    }
};

2008. 出租车的最大盈利

方法一

思路:枚举rides,区间查询

时间复杂度O(n(lgn+lgm))O(n(\lg n+\lg m)), nn 为乘客数量,mm 为地点数量。

设有一维数组 dpdpdpidp_i 表示从位置 11 到达 ii 处的最大盈利。

首先将 ridesrides 按照终点升序排列。然后从前向后依次遍历每个 rideride,则有:

dpride.end=max1jride.startdpj+ride.earndp_{ride.end} = \max_{1\le j\le ride.start}{dp_j} + ride.earn

则最终答案为:

max1jmdpi\max_{1\le j \le m} dp_i

其中 maxmax 可借助线段树,RMQ 等实现。下面给出一个基于线段树的实现。

class Solution {
public:
    int64_t st[100001*4] = {0}; // 存储线段树的数组

    // 尝试将位置 goal 更新为 val
    void update(int root, int l, int r, int goal, int64_t val) {
        // st[root] 记录了 区间 [l,r] 中的最大值
        // l == r 表示到达叶子节点,且递归过程保证,l == r == goal。
        if (l == r) {
            // 可能重复更新,保留最大值
            st[root] = max(st[root], val);
            return;
        } 
        // 判读一下 goal 在左子树对应的区间[l,mid]中,还是右子树对应的区间 [mid+1, r]中。更新对应子树即可。
        int mid = (l+r)>>1;
        (goal <= mid) ? update(root<<1, l, mid, goal, val) : update(root<<1|1, mid+1, r, goal, val);

        // [l,mid] 或者 [mid+1,r] 可能发生了更新,那就更新下 [l,r] 吧。
        st[root] = max(st[root<<1], st[root<<1|1]);
    }

    // 查询 [l,r] 中的最大值。
    int64_t query(int root, int l, int r, int goal) {
        if (r == goal) {
            return st[root];
        }
        int mid = (l+r)>>1;
        if (goal <= mid) {
            return query(root<<1, l, mid, goal);
        }
        return max(query(root<<1, l, mid, mid), query(root<<1|1, mid+1, r, goal));
    }
    long long maxTaxiEarnings(int n, vector<vector<int>>& rides) {
        // 线段树的三个变量:左边界,有边界,根节点编号
        const int L = 1, R = 100000, root = 1;

        // 将 rides 按照 end 升序排序
        sort(rides.begin(), rides.end(), [](const auto &l, const auto &r) {
            return l[0] < r[0];
        });

        // 遍历 rides
        for(const auto &r : rides) {
            // 找出 [1, r.start] 中的最大收益
            int64_t val = r[1]-r[0]+r[2] + query(root, L, R, r[0]);
            // 更新一下 r.end 处的最大收益
            update(root, L, R, r[1], val);
        }
        // 查询最大收益
        return query(root, L, R, R);
    }
};

方法二

思路:枚举位置

时间复杂度O(n+m)O(n+m)

设有一维数组 dpdpdpidp_i 表示从位置 11 到达 ii 处的最大盈利。

如果没有以位置 ii 为终点的乘客,则 dpi=dpi1dp_i = dp_{i-1}

如果有一个或多个乘客,则有两种策略:

  • 不接:dpi=dpi1dp_i = dp_{i-1}
  • 接一个最赚的:dpi=max(dpride.start+ride.earn)dp_i = \max(dp_{ride.start} + ride.earn)

则最终答案为 dpmdp_{m}

借助哈希表,可通过 O(n)O(n) 的预处理,知道每个位置处有哪些乘客。然后,只需从前向后枚举位置,枚举过程中按照伤处策略计算即可。

class Solution {
public:
    long long maxTaxiEarnings(int n, vector<vector<int>>& rides) {
        unordered_map<int, vector<int>> pos;
        for (int i = 0; i < rides.size(); i++) {
            pos[rides[i][1]].emplace_back(i);
        }
        const int m = 1e5;
        int64_t dp[m+1] = {0};
        for (int i = 1; i <= m; i++) {
            dp[i] = max(dp[i], dp[i-1]);
            if (pos.count(i) > 0) {
                for (auto p : pos[i]) {
                    int start = rides[p][0];
                    int earn = rides[p][1] - rides[p][0] + rides[p][2];
                    dp[i] = max(dp[start]+earn, dp[i]);
                }
            }
        }
        return dp[m];
    }
};

2009. 使数组连续的最少操作数

思路:排序,去重,枚举右边界

时间复杂度O(nlgn)O(n\lg n)

将输入的 nums 去重并升序排序,那么我们得到了一个严格递增的数组。

假设我们将 numsinums_i 作为连续数组的最大值,则易得连续数组的最小值为 numsin+1nums_i\mathrel{-}n\mathrel{+}1nn 输入数字的个数。

因为 numsnums 严格递增,不难统计出 numsnums 中,值位于 [numsin+1,numsi][nums_i\mathrel{-}n\mathrel{+}1, nums_i] 内的元素数量,记为 xix_i。则以 numsinums_i 作为连续数组的最大值的操作数为 nxin-x_i。最终答案即为: min0i<n(nxi)\min_{0\le i \lt n} (n-x_i)

为了证明解法的正确性,只需证明「一定存在一种最优的连续数组是以某个 numsinums_i 为最大元素的」。

连续数组的最大元素无非有两种来源:

  • 情形一:原本就存在于 numsnums 中。
  • 情形二:numsnums 中没有,是通过某次操作得来的。

对于情形一,无需多言。

对于情形二,可将最大元素替换为最小值减一,此时仍然是连续数组且操作次数没变,所以可得到一种同样优的且符合情形一的最优解。

考虑到去重排序后的 numsnums 是递增的,可以通过双指针计算出所有的 xix_i

class Solution {
public:
    int minOperations(vector<int>& nums) {
        int n = nums.size();
        sort(nums.begin(), nums.end());
        auto eit = unique(nums.begin(), nums.end());
        nums.erase(eit, nums.end());
        int anw = n;
        for (int l = 0, r = 0; r < nums.size(); r++) {
            while(nums[l] < nums[r] - n + 1) {
                l++;
            }
            anw = min(anw, n-(r-l+1));
        }

        return anw;
    }
};