# Leetcode 第242场周赛题解

# Problem A - 哪种连续子字符串更长 (opens new window)

# 方法一:暴力

记录当前的0串长度和最长的0串长度,以及当前的1串长度和最长的1串长度,遍历并维护即可。

  • 时间复杂度O(S)\mathcal{O}(|S|)
  • 空间复杂度O(1)\mathcal{O}(1)
参考代码(C++)
class Solution {
public:
    bool checkZeroOnes(string s) {
        int om = 0, zm = 0;
        int o = 0, z = 0;
        for (char c : s) {
            if (c == '0') {
                z++;
                o = 0;
            } else {
                o++;
                z = 0;
            }
            om = max(om, o);
            zm = max(zm, z);
        }
        return om > zm;
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# Problem B - 准时到达的列车最小时速 (opens new window)

# 方法一:二分

非常明显的二分答案。需要注意的是,最后一趟车需要单独判断,因为这里不需要等到整点。

  • 时间复杂度O(NlogD)\mathcal{O}(N\log D)
  • 空间复杂度O(1)\mathcal{O}(1)

为了防止浮点数有关的精度问题,下面的代码中预先将hour乘以100变为整数。但这一操作是否是必要的,我暂时不能肯定。

参考代码(C++)
using ll = long long;

template<typename ... Args>
std::string string_format( const std::string& format, Args ... args )
{
    int size_s = std::snprintf( nullptr, 0, format.c_str(), args ... ) + 1;
    if( size_s <= 0 ){ throw std::runtime_error( "Error during formatting." ); }
    auto size = static_cast<size_t>( size_s );
    auto buf = std::make_unique<char[]>( size );
    std::snprintf( buf.get(), size, format.c_str(), args ... );
    return std::string( buf.get(), buf.get() + size - 1 );
}

class Solution {
public:
    int minSpeedOnTime(vector<int>& dist, double hour) {
        int n = dist.size();
        int lo = 1, hi = 1e7;
        string s = string_format("%.2f", hour);
        int m = s.size();
        s = s.substr(0, m - 3) + s.substr(m - 2, 2);
        ll limit = stoll(s);
        
        while (lo <= hi) {
            int mid = (lo + hi) >> 1;
            ll tot = 0;
            for (int i = 0; i < n - 1; ++i) {
                tot += (dist[i] - 1) / mid + 1;
            }
            tot *= 100;
            tot += (dist[n - 1] * 100 - 1) / mid + 1;
            if (tot <= limit)
                hi = mid - 1;
            else
                lo = mid + 1;
        }
        if (lo > 1e7)
            return -1;
        return lo;
    }
};
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

# Problem C - 跳跃游戏 VII (opens new window)

# 方法一:离散化+树状数组+双指针

我们可以首先找出所有为0的位置;如果某一位置可到达,则将[i+minJump,i+maxJump][i+minJump,i+maxJump]范围内为0的位置都设为可到达。

这里,我们可以用树状数组来实现区间操作,并用双指针维护对应的区间。

  • 时间复杂度O(S+MlogM)\mathcal{O}(|S|+M\log M),其中MM为字符串中0的个数。
  • 空间复杂度O(M)\mathcal{O}(M)
参考代码(C++)
inline int lowbit(int x) {
    return x & (-x);
}

class Solution {
public:
    bool canReach(string s, int minJump, int maxJump) {        
        int n = s.size();
        if (s[n - 1] == '1')
            return false;
        
        vector<int> zero;
        for (int i = 0; i < n; ++i)
            if (s[i] == '0')
                zero.emplace_back(i);
        int m = zero.size();
        
        vector<int> a(m + 1);
        auto query = [&](int pos) {
            int ans = 0;
            for (; pos; pos -= lowbit(pos))
                ans += a[pos];
            return ans;
        };
        auto update = [&](int pos, int val) {
            for (; pos <= m; pos += lowbit(pos))
                a[pos] += val;
        };

        update(1, 1);
        update(2, -1);
        int l = 0, r = 0;
        for (int i = 1; i < m; ++i) {
            if (query(i) > 0) {
                while (l < m && zero[l] < zero[i - 1] + minJump)
                    l++;
                while (r < m && zero[r] <= zero[i - 1] + maxJump)
                    r++;
                update(l + 1, 1), update(r + 1, -1);
            }
        }
        
        return query(m) > 0;
    }
};
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

# 方法二:平衡二叉搜索树

另一种思路是用有序集合(如C++的set)维护所有为0的位置。注意,这里我们需要把已经能够到达的位置从集合中删除,这样就能够保证遍历的时间复杂度为O(MlogM)\mathcal{O}(M\log M)

  • 时间复杂度O(S+MlogM)\mathcal{O}(|S|+M\log M),其中MM为字符串中0的个数。
  • 空间复杂度O(S+M)\mathcal{O}(|S|+M)
参考代码(C++)
class Solution {
public:
    bool canReach(string s, int minJump, int maxJump) {        
        int n = s.size();
        if (s[n - 1] == '1')
            return false;
        
        vector<int> zero;
        for (int i = 0; i < n; ++i)
            if (s[i] == '0')
                zero.emplace_back(i);
        int m = zero.size();
        vector<bool> can(n);
        can[0] = true;
        set<int> st(zero.begin(), zero.end());
        for (int i = 0; i + 1 < m; ++i) {
            if (can[zero[i]]) {
                vector<int> destinations;
                for (auto it = st.lower_bound(zero[i] + minJump); it != st.upper_bound(zero[i] + maxJump); ++it) {
                    destinations.emplace_back(*it);
                }
                for (int destination : destinations) {
                    can[destination] = true;
                    st.erase(destination);
                }
            }
        }
        
        return can[n - 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
26
27
28
29
30
31

# 方法三:双端队列

逆向思考,想要到达一个位置,就必须能够到达[imaxJump,iminJump][i-maxJump,i-minJump]范围内的某个位置。显然,我们可以用一个双端队列来维护这样的位置,从而实现线性的时间复杂度。

  • 时间复杂度O(S+M)\mathcal{O}(|S|+M),其中MM为字符串中0的个数。
  • 空间复杂度O(S+M)\mathcal{O}(|S|+M)
参考代码(C++)
class Solution {
public:
    bool canReach(string s, int minJump, int maxJump) {        
        int n = s.size();
        if (s[n - 1] == '1')
            return false;
        
        vector<int> zero;
        for (int i = 0; i < n; ++i)
            if (s[i] == '0')
                zero.emplace_back(i);
        int m = zero.size();
        vector<bool> can(n);
        can[0] = true;
        deque<int> dq;
        int ptr = 0;
        for (int i = 1; i < m; ++i) {
            while (ptr < i && zero[ptr] + minJump <= zero[i]) {
                if (can[zero[ptr]])
                    dq.emplace_back(zero[ptr]);
                ptr++;
            }
            while (!dq.empty() && dq.front() + maxJump < zero[i])
                dq.pop_front();
            can[zero[i]] = !dq.empty();
        }
        
        return can[n - 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
26
27
28
29
30

# Problem D - 石子游戏 VIII (opens new window)

# 方法一:动态规划

观察可知,无论如何操作,数组的前缀和是恒定的。这里,我们定义dp[i]dp[i]为当次操作取至原数组第ii个数时的最大分值,可得到转移方程:

dp[i]=j=1istones[i]maxj=i+1ndp[j]dp[i]=\sum_{j=1}^istones[i]-\max_{j=i+1}^ndp[j]

其中j=1istones[i]\sum_{j=1}^istones[i]是本次操作的得分,maxj=i+1ndp[j]\max_{j=i+1}^ndp[j]则是对手后续操作的最大净得分。

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

class Solution {
public:
    int stoneGameVIII(vector<int>& stones) {
        int n = stones.size();
        vector<ll> s(n + 1);
        for (int i = 1; i <= n; ++i)
            s[i] = s[i - 1] + stones[i - 1];
        vector<ll> dp(n + 1);
        ll ans = dp[n] = s[n];
        for (int i = n - 1; i > 1; --i) {
            dp[i] = s[i] - ans;
            ans = max(ans, dp[i]);
        }
        return ans;
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18