# Leetcode 第41场双周赛题解

# Problem A - 统计一致字符串的数目 (opens new window)

直接模拟即可。

  • 时间复杂度O(A+Wi)\mathcal{O}(|A|+\sum|W_i|)
  • 空间复杂度O(C)\mathcal{O}(C)CC为字母表大小,本题中为26。
参考代码(C++)
class Solution {
public:
    int countConsistentStrings(string allowed, vector<string>& words) {
        int ans = 0;
        vector<bool> good(26);
        for (char c : allowed)
            good[c - 'a'] = true;
        for (string &word : words) {
            bool ok = true;
            for (char c : word)
                if (!good[c - 'a']) {
                    ok = false;
                    break;
                }
            if (ok)
                ans++;
        }
        return ans;
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# Problem B - 有序数组中差绝对值之和 (opens new window)

抓住原数组有序这一条件。首先计算出所有数的和,然后从左向右依次处理,过程中维护左边数的总和和右边数的总和,则可以在O(1)\mathcal{O}(1)时间内计算出任一位置数与其他所有数的差的绝对值之和。

  • 时间复杂度O(N)\mathcal{O}(N)
  • 空间复杂度O(N)\mathcal{O}(N)
参考代码(C++)
class Solution {
public:
    vector<int> getSumAbsoluteDifferences(vector<int>& nums) {
        int n = nums.size();
        vector<int> ans(n);
        int right = 0;
        for (int num : nums)
            right += num;
        int left = 0;
        for (int i = 0; i < n; ++i) {
            right -= nums[i];
            if (i > 0)
                ans[i] += nums[i] * i - left;
            if (i < n - 1)
                ans[i] += right - nums[i] * (n - 1 - i);
            left += nums[i];
        }
        return ans;
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# Problem C - 石子游戏 VI (opens new window)

我们发现,一枚石子的价值实际上等于a[i]+b[i]a[i]+b[i]

  • 对Alice来说,拿走这枚石子,自己得了a[i]a[i]分,同时Bob少了b[i]b[i]分,因此等于得了a[i]+b[i]a[i]+b[i]
  • 对Bob来说,同理。

因此,我们将石子按照a[i]+b[i]a[i]+b[i]降序排列,两个人会按照这个顺序依次取石子。在这一情况下,计算两个人的总得分进行比较即可。

  • 时间复杂度O(NlogN)\mathcal{O}(N\log N)
  • 空间复杂度O(N)\mathcal{O}(N)
参考代码(C++)
class Solution {
public:
    int stoneGameVI(vector<int>& aliceValues, vector<int>& bobValues) {
        int n = aliceValues.size();
        vector<int> order(n);
        for (int i = 0; i < n; ++i)
            order[i] = i;
        sort(order.begin(), order.end(), [&](int i, int j){
            return aliceValues[i] + bobValues[i] > aliceValues[j] + bobValues[j]; 
        });
        int alice = 0, bob = 0;
        for (int i = 0; i < n; ++i) {
            int id = order[i];
            if (i % 2 == 0)
                alice += aliceValues[id];
            else
                bob += bobValues[id];
        }
        if (alice > bob)
            return 1;
        if (alice == bob)
            return 0;
        return -1;
    }
};
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 D - 从仓库到码头运输箱子 (opens new window)

# 方法一:线段树

  • 前缀和:为了计算一段区间内箱子的总重量,以判断是否超出限制,我们预计算重量的前缀和。
  • 动态规划:我们使用dp[i]dp[i]表示以第ii个箱子为区间结束时的最小成本。
  • 线段树:我们在线段树中维护最后一个开放区间起点在[l,r][l,r]区间内时的最小成本。
  • 双指针:我们用双指针来维护当前的开放区间。

首先说明一下“开放区间”的概念:当我们在考虑第ii个箱子的时候,我们实际上有两种选择。一是以它作为区间的起点;二是将它加入到现有的以i1i-1为右端点,并且能够继续加入箱子的区间中,这样的区间就称为“开放区间”。显然,由于数量和重量的限制,最左侧的开放区间会首先失效(不能再加入箱子),因此可以使用双指针的方法。

代码的主体部分很简单,分为以下步骤:

  1. 维护左指针:如果当前左指针到右指针(第ii个箱子)这段区间不合法,则不断将左指针右移。
  2. 计算以当前箱子为结束的最小成本,也即dp[i]dp[i]
    • 首先,我们可以从ii开始,到ii结束,此时的成本为dp[i1]+2dp[i-1]+2
    • 如果当前左指针在ii的左侧,也即存在“开放区间”,我们查询线段树上的[l,i1][l,i-1]区间的最小值。
    • dp[i]dp[i]取为二者中较小的那一个值。
  3. 更新线段树。
    • 加入以第ii个箱子为开始的最小成本,对ii单点赋值为dp[i1]+2dp[i-1]+2。注意区分线段树和动态规划数组的含义!线段树存放的是以ii开始的成本,而数组中存放的是以ii结束的成本。
    • 考虑第i+1i+1个箱子,如果其与第ii个箱子的编号不同,则需要对线段树的[l,i][l,i]区间整体加上1(因为需要进行一次码头间的移动),也即进行一次区间修改。

最后的答案就是dp[n]dp[n]

代码量主要在线段树部分,我们需要实现两种修改操作:区间修改和单点赋值。这里不具体解释实现过程,感兴趣的可以查阅线段树相关教程。

  • 时间复杂度O(NlogN)\mathcal{O}(N\log N)
  • 空间复杂度O(N)\mathcal{O}(N)
参考代码(C++)
#define lson (idx << 1)
#define rson (idx << 1 | 1)
#define MAXN 100005

typedef long long ll;
const int INF = 0x3f3f3f3f;

struct Node {
    int l, r, v, lazy;
} s[MAXN << 2];

void calc(int idx) { s[idx].v = min(s[lson].v, s[rson].v); }

void build(int idx, int l, int r) {
    s[idx].l = l, s[idx].r = r, s[idx].v = INF, s[idx].lazy = 0;
    if (l == r)
        return;
    int mid = (l + r) >> 1;
    build(lson, l, mid);
    build(rson, mid + 1, r);
}

void pushdown(int idx) {
    if (s[idx].lazy)
        for (int i = lson; i <= rson; ++i) {
            s[i].v += s[idx].lazy;
            s[i].lazy += s[idx].lazy;
        }
    s[idx].lazy = 0;
}

void setp(int idx, int pos, int x) {
    if (s[idx].l == pos && s[idx].r == pos) {
        s[idx].v = x;
        return;
    }
    pushdown(idx);
    int mid = (s[idx].l + s[idx].r) >> 1;
    if (pos <= mid)
        setp(lson, pos, x);
    else
        setp(rson, pos, x);
    calc(idx);
}

void update(int idx, int l, int r, int x) {
    if (s[idx].l >= l && s[idx].r <= r) {
        s[idx].v += x;
        s[idx].lazy += x;
        return;
    }
    pushdown(idx);
    int mid = (s[idx].l + s[idx].r) >> 1;
    if (mid >= l)
        update(lson, l, r, x);
    if (mid + 1 <= r)
        update(rson, l, r, x);
    calc(idx);
}

int query(int idx, int l, int r) {
    if (s[idx].l >= l && s[idx].r <= r)
        return s[idx].v;
    pushdown(idx);
    int mid = (s[idx].l + s[idx].r) >> 1;
    int ans = INF;
    if (mid >= l)
        ans = min(ans, query(lson, l, r));
    if (mid + 1 <= r)
        ans = min(ans, query(rson, l, r));
    return ans;
}

class Solution {
public:
    int boxDelivering(vector<vector<int>> &boxes, int portsCount, int maxBoxes, int maxWeight) {
        int n = boxes.size();
        if (n == 1)
            return 2;
        vector<ll> w(n + 1);
        for (int i = 1; i <= n; ++i)
            w[i] = w[i - 1] + boxes[i - 1][1];
        build(1, 1, n);
        
        vector<int> dp(n + 1);
        auto valid = [&](int l, int r) {
            return r - l + 1 <= maxBoxes && w[r] - w[l - 1] <= maxWeight;
        };
        int l = 1;
        for (int i = 1; i <= n; ++i) {
            while (l < i && !valid(l, i))
                l++;
            int lo = dp[i - 1] + 2;
            if (l < i)
                lo = min(lo, query(1, l, i - 1));
            dp[i] = lo;
            setp(1, i, dp[i - 1] + 2);
            if (i < n && boxes[i][0] != boxes[i - 1][0])
                update(1, l, i, 1);
        }
        return dp[n];
    }
};
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

# 方法二:优先队列

我们注意到,在方法一中,实际上是在用线段树维护当前所有合法的“开放区间”的最小成本。我们是否可以直接用优先队列(堆)来对这一最小成本进行维护呢?

答案是肯定的。方法一中,我们采用区间修改的办法来维护所有“开放区间”的成本。显然,我们无法接受对所有元素进行修改。但我们可以逆向思考,修改新加入的元素,而非修改原有的元素。

这里,我们用变量diffdiff维护全局截止到目前为止相邻箱子属于不同码头的次数,则在将以第ii个箱子为起点的“开放区间”加入堆中时,我们加入dp[i1]+2diffdp[i-1]+2-diff;而在考虑“开放区间”时,我们则使用堆的最小值加上diffdiff来得到实际的总成本。

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

class Solution {
public:
    int boxDelivering(vector<vector<int>> &boxes, int portsCount, int maxBoxes, int maxWeight) {
        int n = boxes.size();
        if (n == 1)
            return 2;
        vector<ll> w(n + 1);
        for (int i = 1; i <= n; ++i)
            w[i] = w[i - 1] + boxes[i - 1][1];
        
        vector<int> dp(n + 1);
        auto valid = [&](int l, int r) {
            return r - l + 1 <= maxBoxes && w[r] - w[l - 1] <= maxWeight;
        };
        priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq;
        int l = 1, diff = 0;
        for (int i = 1; i <= n; ++i) {
            while (l < i && !valid(l, i))
                l++;
            int lo = dp[i - 1] + 2;
            while (!pq.empty() && pq.top().second < l)
                pq.pop();
            if (!pq.empty())
                lo = min(lo, pq.top().first + diff);
            dp[i] = lo;
            pq.emplace(dp[i - 1] + 2 - diff, i);
            if (i < n && boxes[i][0] != boxes[i - 1][0])
                diff++;
        }
        return dp[n];
    }
};
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
33
34

# 方法三 单调队列

在方法二的基础上,我们可以进一步用单调队列替换优先队列来维护最小成本,从而将时间复杂度降低到O(N)\mathcal{O}(N)

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

class Solution {
public:
    int boxDelivering(vector<vector<int>> &boxes, int portsCount, int maxBoxes, int maxWeight) {
        int n = boxes.size();
        if (n == 1)
            return 2;
        vector<ll> w(n + 1);
        for (int i = 1; i <= n; ++i)
            w[i] = w[i - 1] + boxes[i - 1][1];
        
        vector<int> dp(n + 1);
        auto valid = [&](int l, int r) {
            return r - l + 1 <= maxBoxes && w[r] - w[l - 1] <= maxWeight;
        };
        deque<pair<int, int>> dq;
        int l = 1, diff = 0;
        for (int i = 1; i <= n; ++i) {
            while (l < i && !valid(l, i))
                l++;
            int lo = dp[i - 1] + 2;
            while (!dq.empty() && dq.front().second < l)
                dq.pop_front();
            if (!dq.empty())
                lo = min(lo, dq.front().first + diff);
            dp[i] = lo;
            int cost = dp[i - 1] + 2 - diff;
            while (!dq.empty() && dq.back().first >= cost)
                dq.pop_back();
            dq.emplace_back(cost, i);
            if (i < n && boxes[i][0] != boxes[i - 1][0])
                diff++;
        }
        return dp[n];
    }
};
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
33
34
35
36
37