本周周赛出的挺好的,每道题都需要在理解的基础上稍微转个弯。无奈 T3,T4 赛中没做出来,一闪而过的思路没有坚持,总想着一步到位:该了您内。
7 分,1100+ 名
题目
T1. 圆形赛道上经过次数最多的扇区
题目
给你一个整数
n
和一个整数数组rounds
。有一条圆形赛道由n
个扇区组成,扇区编号从1
到n
。现将在这条赛道上举办一场马拉松比赛,该马拉松全程由m
个阶段组成。其中,第i
个阶段将会从扇区rounds[i - 1]
开始,到扇区rounds[i]
结束。举例来说,第1
阶段从rounds[0]
开始,到rounds[1]
结束。请你以数组形式返回经过次数最多的扇区,按扇区编号 升序 排列。
注意,赛道按扇区编号升序逆时针形成一个圆(请参见第一个示例)。
示例 1
输入: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]]
参考代码
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
个最小的
- 给 Bob 那个倒霉蛋留
- 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
,该数组表示一个从1
到n
的数字排列。有一个长度为n
的二进制字符串,该字符串上的所有位最初都设置为0
。在从
1
到n
的每个步骤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))
,查找key
为log(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
题解思路
- 题意:切分数组,统计少的那一半,递归累计
- 怎么切,都试试:对分割点
p
的for
迭代尝试 - 怎么算切的好:看看继续往下切的最优能得多少,即:依赖于更细粒度的切分
- 动态规划,适合 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)
,每个memo
都O(1)
- 空间复杂度:
O(n^2)
,memo
的花费
赛后复盘
- T1 理解起来挺费劲,中学知识「数形结合」没白学,理解之后就好多了。暴力解法
O(mn)
也是可以过的 - T2 比 T1 简单,可以作为简单题,理清贪心思想就好写了
- T3 比赛时没写出来,想的并查集,结果没并出个所以然,一闪而过的「反推」也仅仅是闪过了,白费了类似的还写过多解法的题解,可惜了
- T4 明显确定是动态规划,也是一开始想到 dfs + memo,结果嘚瑟了,非得要试试迭代,也没迭出来,赛后 dfs 十分钟提交也然并卵
总体是出的不错的一周,值得复盘与复习。
吃透一题未必就吃透一类,多多举一反三,现在的题量可以逐渐开始同类型题一起做了,专项的刻意练习,加深对算法思想、写法的体会。
后续的比赛有时间都会发上来,专项的总结也考虑发出来做个回顾。