# Leetcode 第222场周赛题解

本场比赛的实况录像:Bilibili (opens new window) | YouTube (opens new window)

# Problem A - 卡车上的最大单元数 (opens new window)

因为箱子可以一个一个装,所以显然应该优先选择单元数最大的箱子,因此对箱子类型按照单元数目降序排序,然后贪心选取即可。

  • 时间复杂度O(NlogN)\mathcal{O}(N\log N)
  • 空间复杂度O(1)\mathcal{O}(1)
参考代码(C++)
class Solution {
public:
    int maximumUnits(vector<vector<int>>& boxTypes, int truckSize) {
        sort(boxTypes.begin(), boxTypes.end(), [](vector<int> &a, vector<int> &b){
            return a[1] > b[1]; 
        });
        int ans = 0;
        for (auto &v : boxTypes) {
            int can = min(v[0], truckSize);
            ans += can * v[1];
            truckSize -= can;
        }
        return ans;
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# Problem B - 大餐计数 (opens new window)

用哈希表统计每种美味程度的餐品的数目,然后对于每一种美味程度,枚举22的幂次即可。注意最后得到的结果要除以22再取模,因为每一个组合我们计算了两次。

  • 时间复杂度O(NlogMAXN)\mathcal{O}(N\log MAXN),本题中MAXN=221MAXN=2^{21}(可以由两个2202^{20}$组合得到)。
  • 空间复杂度O(N)\mathcal{O}(N)
参考代码(C++)
typedef long long ll;
const ll MOD = 1e9 + 7;

class Solution {
public:
    int countPairs(vector<int>& deliciousness) {
        ll ans = 0;
        unordered_map<int, int> cnt;
        for (int i : deliciousness)
            cnt[i]++;
        for (auto [i, f] : cnt) {
            for (int k = 0; k <= 21; ++k) {
                int mask = 1 << k;
                int comp = mask - i;
                if (comp < 0)
                    continue;
                if (comp != i && cnt.count(comp))
                    ans += 1LL * f * cnt[comp];
                if (comp == i && f >= 2)
                    ans += 1LL * f * (f - 1);
            }
        }
        return ans / 2 % MOD;
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# Problem C - 将数组分成三个子数组的方案数 (opens new window)

# 方法一:双指针+二分

  • 双指针保证中间区间的和不小于左边区间。
  • 二分查找能够使得中间区间的和不大于右边区间的最右位置。

当然,为了快速计算各区间的和,计算前缀和也是一个必不可少的准备工作。

  • 时间复杂度O(NlogN)\mathcal{O}(N\log N)
  • 空间复杂度O(N)\mathcal{O}(N)
参考代码(C++)
typedef long long ll;
const ll MOD = 1e9 + 7;

class Solution {
public:
    int waysToSplit(vector<int>& nums) {
        int n = nums.size();
        vector<int> s(n + 1);
        for (int i = 1; i <= n; ++i)
            s[i] = s[i - 1] + nums[i - 1];
        ll ans = 0;
        int m = 2;
        for (int l = 1; l <= n - 2; ++l) {
            int L = s[l];
            m = max(m, l + 1);
            while (m < n && s[m] - L < L)
                m++;
            if (m == n)
                break;
            int lo = m, hi = n - 1;
            while (lo <= hi) {
                int mid = (lo + hi) >> 1;
                if (s[n] - s[mid] < s[mid] - L)
                    hi = mid - 1;
                else
                    lo = mid + 1;
            }
            ans += hi - m + 1;
        }
        return ans % MOD;
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 方法二:三指针

进一步观察可以发现,在方法一中,我们二分查找的右边界实际上是单调递增的,所以我们可以再增加一个指针,以达到线性的时间复杂度。

  • 时间复杂度O(N)\mathcal{O}(N)
  • 空间复杂度O(N)\mathcal{O}(N)
参考代码(C++)
typedef long long ll;
const ll MOD = 1e9 + 7;

class Solution {
public:
    int waysToSplit(vector<int>& nums) {
        int n = nums.size();
        vector<int> s(n + 1);
        for (int i = 1; i <= n; ++i)
            s[i] = s[i - 1] + nums[i - 1];
        ll ans = 0;
        int l = 2, r = 2;
        for (int m = 2; m <= n - 1; ++m) {
            l = max(l, m), r = max(r, m);
            while (r + 1 < n && s[n] - s[r + 1] >= s[r + 1] - s[m - 1])
                r++;
            while (l < n && s[l] - s[m - 1] < s[m - 1])
                l++;
            if (r < n && l <= r && s[l] - s[m - 1] >= s[m - 1] && s[n] - s[r] >= s[r] - s[m - 1])
                ans += r - l + 1;
        }
        return ans % MOD;
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# Problem D - 得到子序列的最少操作次数 (opens new window)

本题中,一个初看不起眼的条件成为了解题的最关键要素,也即“targettarget不包含任何重复元素”。

不难看出,本题是一个最长公共子序列问题,但是两个数组的长度达到了10510^5,而通常的最长公共子序列的动态规划解法的时间复杂度为O(NM)\mathcal{O}(NM),显然会超时。

不妨考虑一下我们是如何进行动态规划的。当且仅当a[i]=b[j]a[i]=b[j]时,我们可以得到dp[i][j]=dp[i1][j1]+1dp[i][j]=dp[i-1][j-1]+1,而其余情况下公共子序列的长度是不会增长的。这提示我们进一步关注两个数组之间的相等关系。

因为targettarget不包含任何重复元素,所以我们可以对所有元素从左到右进行编号。之后,我们将arrarr中的元素替换为对应的编号(如果在targettarget中存在相同元素的话)或1-1(如果不存在这一元素)。

这时,我们不难发现,如果要使用arrarr中的某个元素,我们在其之前只能使用编号比它小的元素。

注意

如果targettarget中存在相同元素,我们还需要考虑arrarr中的这一元素应当作为第几个该元素,而对于前面每一个元素也都需要考虑这一点,这一方法的复杂度将会极大地劣化。

这样,最长公共子序列就变成了最长上升子序列,而我们知道后者是有O(NlogN)\mathcal{O}(N\log N)的解法的,于是本题得以解决。

  • 时间复杂度O(Mlogmin(N,M))\mathcal{O}(M\log\min(N,M)),这里min(N,M)\min(N,M)是因为我们得到的公共子序列长度不会超过min(N,M)\min(N,M),于是我们进行二分搜索的时间复杂度为O(logmin(N,M))\mathcal{O}(\log\min(N,M))
  • 空间复杂度O(N+M)\mathcal{O}(N+M)
参考代码(C++)
class Solution {
public:
    int minOperations(vector<int>& target, vector<int>& arr) {
        unordered_map<int, int> dict;
        for (int i = 0; i < target.size(); ++i)
            dict[target[i]] = i;
        vector<int> rank(arr.size());
        for (int i = 0; i < arr.size(); ++i)
            if (dict.count(arr[i]))
                rank[i] = dict[arr[i]];
            else
                rank[i] = -1;
        vector<int> LIS;
        for (int i : rank) {
            if (i == -1)
                continue;
            auto it = lower_bound(LIS.begin(), LIS.end(), i);
            if (it == LIS.end())
                LIS.emplace_back(i);
            else
                *it = i;
        }
        return target.size() - LIS.size();
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25