思路要稳步推进,跨步太大会扯着

LeetCode 第 203 场周赛

Posted by lyle on August 23, 2020

本周周赛出的挺好的,每道题都需要在理解的基础上稍微转个弯。无奈 T3,T4 赛中没做出来,一闪而过的思路没有坚持,总想着一步到位:该了您内。

7 分,1100+ 名

题目

  1. 圆形赛道上经过次数最多的扇区
  2. 你可以获得的最大硬币数目
  3. 查找大小为 M 的最新分组
  4. 石子游戏 V
  5. 赛后复盘

T1. 圆形赛道上经过次数最多的扇区

题目

给你一个整数 n 和一个整数数组 rounds 。有一条圆形赛道由 n 个扇区组成,扇区编号从 1n 。现将在这条赛道上举办一场马拉松比赛,该马拉松全程由 m 个阶段组成。其中,第 i 个阶段将会从扇区 rounds[i - 1] 开始,到扇区 rounds[i] 结束。举例来说,第 1 阶段从 rounds[0] 开始,到 rounds[1] 结束。

请你以数组形式返回经过次数最多的扇区,按扇区编号 升序 排列。

注意,赛道按扇区编号升序逆时针形成一个圆(请参见第一个示例)。

示例 1

t1 eg1

输入:n = 4, rounds = [1,3,1,2]
输出:[1,2]
解释:本场马拉松比赛从扇区 1 开始。经过各个扇区的次序如下所示:
1 --> 2 --> 3(阶段 1 结束)
  --> 4 --> 1(阶段 2 结束)
  --> 2(阶段 3 结束,即本场马拉松结束)
其中,扇区 1 和 2 都经过了两次,它们是经过次数最多的两个扇区。
扇区 3 和 4 都只经过了一次。

示例 2

输入:n = 2, rounds = [2,1,2,1,2,1,2,1,2]
输出:[2]

示例 3

输入:n = 7, rounds = [1,3,5,7]
输出:[1,2,3,4,5,6,7]

提示

  • 2 <= n <= 100
  • 1 <= m <= 100
  • rounds.length == m + 1
  • 1 <= rounds[i] <= n
  • rounds[i] != rounds[i + 1],其中 0 <= i < m

题解思路

  • 题意:rounds(个人习惯,改为 arr)为接力棒的人所在位置
    • arr[0] 开始跑,到 ax = arr[n - 1] 结束,求经过最多的区域
  • 跑过的扇区,形如 y = tan(x) 的曲线
    • arr[0] 即与 Y 轴交点 (0, arr[0]),开始向上跑
    • 最终会跑到与 x = n - 1 纵线的交点 (n - 1, arr[n - 1]) 结束
    • 中间所有从底部 1 到顶部 n 的上坡路次数都相同
    • 只需关注头尾两段斜坡
    • 头斜坡 [arr[0], n],尾斜坡 [1, arr[n - 1]]

t1 eg1

参考代码

public List<Integer> mostVisited(int n, int[] arr) {
    int a0 = arr[0], ax = arr[arr.length - 1]; // 头、尾端点
    if (a0 == ax) return Collections.singletonList(a0); // a0 恰为重叠处
    
    List<Integer> res = new ArrayList<>();
    if (a0 > ax) { // 无重叠,两段都算
        for (int v = 1; v <= ax; v++) res.add(v);
        for (int v = a0; v <= n; v++) res.add(v);
        return res;
    }
    
    // a0 < ax,有重叠,只算重叠部分
    for (int v = a0; v <= ax; v++) res.add(v);
    return res;
}

复杂度分析

  • 时间复杂度:O(n)n 为 参数 n,而非 arr.length
  • 空间复杂度:O(1),不计返回值

T2. 你可以获得的最大硬币数目

题目

有 3n 堆数目不一的硬币,你和你的朋友们打算按以下方式分硬币:

  • 每一轮中,你将会选出 任意 3 堆硬币(不一定连续)。
  • Alice 将会取走硬币数量最多的那一堆。
  • 你将会取走硬币数量第二多的那一堆。
  • Bob 将会取走最后一堆。
  • 重复这个过程,直到没有更多硬币。

给你一个整数数组 piles ,其中 piles[i] 是第 i 堆中硬币的数目。

返回你可以获得的最大硬币数目。

示例 1:

输入:piles = [2,4,1,2,7,8]
输出:9
解释:选出 (2, 7, 8) ,Alice 取走 8 枚硬币的那堆,你取走 7 枚硬币的那堆,Bob 取走最后一堆。
选出 (1, 2, 4) , Alice 取走 4 枚硬币的那堆,你取走 2 枚硬币的那堆,Bob 取走最后一堆。
你可以获得的最大硬币数目:7 + 2 = 9.
考虑另外一种情况,如果选出的是 (1, 2, 8) 和 (2, 4, 7) ,你就只能得到 2 + 4 = 6 枚硬币,这不是最优解。

示例 2:

输入:piles = [2,4,5]
输出:4

示例 3:

输入:piles = [9,8,7,6,5,1,2,3,4]
输出:18

提示:

  • 3 <= piles.length <= 10^5
  • piles.length % 3 == 0
  • 1 <= piles[i] <= 10^4

题解思路

  • 每次都选最大的两个 + 最小的一个
    • 给 Bob 那个倒霉蛋留 n 个最小的
  • Alice 选每次的最大,则我们取第二大最优(千年老二?
  • 答案为 从大到小,间隔取:贪心思想

参考代码

public int maxCoins(int[] arr) {
    Arrays.sort(arr);
    int sum = 0, n = arr.length / 3;
    for (int i = arr.length - 2, cnt = 0; cnt < n; i -= 2, cnt++)
        sum += arr[i];
    return sum;
}

复杂度分析

  • 时间复杂度:O(nlog(n))
  • 空间复杂度:O(1)

T3. 查找大小为 M 的最新分组

题目

给你一个数组 arr ,该数组表示一个从 1n 的数字排列。有一个长度为 n 的二进制字符串,该字符串上的所有位最初都设置为 0

在从 1n 的每个步骤 i 中(假设二进制字符串和 arr 都是从 1 开始索引的情况下),二进制字符串上位于位置 arr[i] 的位将会设为 1

给你一个整数 m ,请你找出二进制字符串上存在长度为 m 的一组 1 的最后步骤。一组 1 是一个连续的、由 1 组成的子串,且左右两边不再有可以延伸的 1

返回存在长度 恰好m一组 1 的最后步骤。如果不存在这样的步骤,请返回 -1

示例 1:

输入:arr = [3,5,1,2,4], m = 1
输出:4
解释:
步骤 1:"00100",由 1 构成的组:["1"]
步骤 2:"00101",由 1 构成的组:["1", "1"]
步骤 3:"10101",由 1 构成的组:["1", "1", "1"]
步骤 4:"11101",由 1 构成的组:["111", "1"]
步骤 5:"11111",由 1 构成的组:["11111"]
存在长度为 1 的一组 1 的最后步骤是步骤 4 。

示例 2:

输入:arr = [3,1,5,4,2], m = 2
输出:-1
解释:
步骤 1:"00100",由 1 构成的组:["1"]
步骤 2:"10100",由 1 构成的组:["1", "1"]
步骤 3:"10101",由 1 构成的组:["1", "1", "1"]
步骤 4:"10111",由 1 构成的组:["1", "111"]
步骤 5:"11111",由 1 构成的组:["11111"]
不管是哪一步骤都无法形成长度为 2 的一组 1 。

示例 3:

输入:arr = [1], m = 1
输出:1

示例 4:

输入:arr = [2,1], m = 2
输出:2

提示:

  • n == arr.length
  • 1 <= n <= 10^5
  • 1 <= arr[i] <= n
  • arr 中的所有整数 互不相同
  • 1 <= m <= arr.length

题解思路

  • 起始全 0,最终全 1
  • 求「最后一步」,倒序反推:从全 1 到 全 0
  • 每次 arr[i] 理解为「因一个 0,将原来连续的多个 1 分隔开了
  • 注意:分割点 0 刚好在连续 1 的头或尾
  • 补充:不用 TreeMap 也可用其他方式(如二分),从 keys 中找到比指定值 p 小的 key

参考代码

public int findLatestStep(int[] arr, int m) {
    int n = arr.length;
    if (m == n) return n;
    
    TreeMap<Integer, Integer> map = new TreeMap<>(); // (from, to)
    map.put(1, n); // [1, n] 整段全 1
    
    for (int i = n - 1; i >= 0; i--) {
        int p = arr[i]; // 分割点

        // 找比 p 小的第一个 key
        int from = map.floorKey(p); // 一定存在
        int to = map.get(from);

        if (p > from) { // 包括 p == to
            if (p - from == m) return i;
            map.put(from, p - 1); // [from, p - 1]
        }

        if (p < to) { // 包括 p == from
            if (to - p == m) return i;
            map.put(p + 1, to); // [p + 1, to]
        }
    }
    return -1;
}

复杂度分析

  • 时间复杂度:O(nlog(n)),查找 keylog(n)
  • 空间复杂度:O(n)

T4. 石子游戏 V

题目

几块石子 排成一行 ,每块石子都有一个关联值,关联值为整数,由数组 stoneValue 给出。

游戏中的每一轮:

  • Alice 会将这行石子分成两个 非空行(即,左侧行和右侧行);
  • Bob 负责计算每一行的值,即此行中所有石子的值的总和。
  • Bob 会丢弃值最大的行,Alice 的得分为剩下那行的值(每轮累加)。

如果两行的值相等,Bob 让 Alice 决定丢弃哪一行。下一轮从剩下的那一行开始。

剩下一块石子 时,游戏结束。Alice 的分数最初为 0

返回 Alice 能够获得的最大分数

示例 1:

输入:stoneValue = [6,2,3,4,5,5]
输出:18
解释:在第一轮中,Alice 将行划分为 [6,2,3],[4,5,5] 。左行的值是 11 ,右行的值是 14 。Bob 丢弃了右行,Alice 的分数现在是 11 。
在第二轮中,Alice 将行分成 [6],[2,3] 。这一次 Bob 扔掉了左行,Alice 的分数变成了 16(11 + 5)。
最后一轮 Alice 只能将行分成 [2],[3] 。Bob 扔掉右行,Alice 的分数现在是 18(16 + 2)。游戏结束,因为这行只剩下一块石头了。

示例 2:

输入:stoneValue = [7,7,7,7,7,7,7]
输出:28

示例 3:

输入:stoneValue = [4]
输出:0

提示:

  • 1 <= stoneValue.length <= 500
  • 1 <= stoneValue[i] <= 10^6

题解思路

  • 题意:切分数组,统计少的那一半,递归累计
  • 怎么切,都试试:对分割点 pfor 迭代尝试
  • 怎么算切的好:看看继续往下切的最优能得多少,即:依赖于更细粒度的切分
  • 动态规划,适合 Bottom-Up
    • 实现可选:深度优先 + 记忆化
  • 技巧:前缀和 pre 空间换时间,方便计算某段数组和

参考代码

public int stoneGameV(int[] arr) {
    int n = arr.length;
    if (n == 1) return 0;
    if (n == 2) return Math.min(arr[0], arr[1]);

    // 前缀和 presum
    int[] pre = new int[n + 1];
    for (int i = 0; i < n; i++)
        pre[i + 1] = pre[i] + arr[i];
        
    return dfs(pre, 0, n - 1, new int[n][n]);
}

private int dfs(int[] pre, int l, int r, int[][] memo) {
    if (l >= r) return 0;
    if (memo[l][r] > 0) return memo[l][r];
    
    int max = 0;
    
    // [l, p, r]
    for (int p = l; p < r; p++) {
        int lp = pre[p + 1] - pre[l]; // [l, p]
        int pr = pre[r + 1] - pre[p + 1]; // [p + 1, r]
        if (lp > pr) { // 左段大
            max = Math.max(max, pr + dfs(pre, p + 1, r, memo));
        } else if (lp < pr) { // 右段大
            max = Math.max(max, lp + dfs(pre, l, p, memo));
        } else { // 一样大
            int dfs1 = dfs(pre, l, p, memo);
            int dfs2 = dfs(pre, p + 1, r, memo);
            max = Math.max(max, lp + Math.max(dfs1, dfs2));
        }
    }
    
    return memo[l][r] = max;
}

复杂度分析

  • 时间复杂度:O(n^2),每个 memoO(1)
  • 空间复杂度:O(n^2)memo 的花费

赛后复盘

  • T1 理解起来挺费劲,中学知识「数形结合」没白学,理解之后就好多了。暴力解法 O(mn) 也是可以过的
  • T2 比 T1 简单,可以作为简单题,理清贪心思想就好写了
  • T3 比赛时没写出来,想的并查集,结果没并出个所以然,一闪而过的「反推」也仅仅是闪过了,白费了类似的还写过多解法的题解,可惜了
  • T4 明显确定是动态规划,也是一开始想到 dfs + memo,结果嘚瑟了,非得要试试迭代,也没迭出来,赛后 dfs 十分钟提交也然并卵

总体是出的不错的一周,值得复盘与复习。

吃透一题未必就吃透一类,多多举一反三,现在的题量可以逐渐开始同类型题一起做了,专项的刻意练习,加深对算法思想、写法的体会。

后续的比赛有时间都会发上来,专项的总结也考虑发出来做个回顾。