本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。
双周赛 108 概览
T1. 最长交替子序列(Easy)
- 标签:模拟、同向双指针
T2. 重新放置石块(Medium)
- 标签:模拟、散列表
T3. 将字符串分割为最少的美丽子字符串(Medium)
- 标签:记忆化递归、动态规划
T4. 黑格子的数目(Medium)
- 标签:枚举、贡献
T1. 最长交替子序列(Easy)
https://leetcode.cn/problems/longest-alternating-subarray/
题解一(模拟)
这道题与上周周赛 T1 还是比较相似的。
使用两层循环,枚举从每个元素 nums[i] 为起点开始的最长交替子序列长度。
class Solution {
fun alternatingSubarray(nums: IntArray): Int {
var ret = -1
for (i in 0 until nums.size) {
var target = 1
for (j in i + 1 until nums.size) {
if (nums[j] - nums[j - 1] != target) break
ret = Math.max(ret, j - i + 1)
target *= -1
}
}
return ret
}
}
复杂度分析:
- 时间复杂度:$O(n^2)$ 其中 n 为 nums 数组的长度;
- 空间复杂度:仅使用常量级别空间。
题解二(同向双指针)
这个解法基于 KMP 思想。
在题解一中,我们会重复计算同一段交替子序列的,我们可以使用一次遍历,再交替子序列终止时避免重复回退到该子序列内部。需要注意的是,由于不同的交替子序列可能存在 1 位重叠,所以要把 i 指针指向 j 指针,而不是指向 j 指针的下一位,才能保证没有缺失。例如 [3,4,3,4,5,4,5] 数组,第一组交替子数组为 [3,4,3,4] 和第二组交替子数组为 [4,5,4,5] 这两组有重叠部分。
class Solution {
fun alternatingSubarray(nums: IntArray): Int {
val n = nums.size
var ret = -1
var i = 0
while (i < n - 1) {
// 寻找起点
while (i < n - 1 && nums[i + 1] - nums[i] != 1) {
i++
}
var target = 1
var j = i
while (j < n - 1 && nums[j + 1] - nums[j] == target) {
ret = Math.max(ret, ++j - i + 1)
target *= -1
}
i = j
}
return ret
}
}
复杂度分析:
- 时间复杂度:$O(n)$ 线性遍历
- 空间复杂度:$O(1)$ 仅使用常量级别空间。
T2. 重新放置石块(Medium)
https://leetcode.cn/problems/relocate-marbles/
题解(模拟 + 散列表)
在每部操作中,我们会将位置 moveFrom[i] 上所有的石头移动到 moveTo[i] 上,「所有」的含义意味着石头的数量是无关紧要的,我们可以使用散列表维护剩余的石头,最后对剩余石头排序。
class Solution {
fun relocateMarbles(nums: IntArray, moveFrom: IntArray, moveTo: IntArray): List<Int> {
if (moveFrom.size != moveTo.size) return Collections.emptyList()
val set = nums.toHashSet()
for (i in moveFrom.indices) {
set.remove(moveFrom[i])
set.add(moveTo[i])
}
return set.toMutableList().sorted()
}
}
复杂度分析:
- 时间复杂度:$O(nlgn)$ 瓶颈在排序上;
- 空间复杂度:$O(n)$ 散列表空间。
T3. 将字符串分割为最少的美丽子字符串(Medium)
https://leetcode.cn/problems/partition-string-into-minimum-beautiful-substrings/
题解一(记忆化递归)
比较直观的子集问题,我们枚举所有分割点(可以构造 5 的幂)的位置并记录最短结果。由于题目的数据范围比较小,我们可以预处理出数据范围内所有 5 的幂。
- 定义 backTrack(i) 表示从 [i] 为起点的最少美丽字符串个数,枚举以 [i] 为起点的所有可行方案,从中得出最优解。
class Solution {
companion object {
// 预处理
private val U = 15
private val INF = Integer.MAX_VALUE
private val set = HashSet<Int>()
init {
var x = 1
while (x.toString(2).length <= U) {
set.add(x)
x *= 5
}
}
}
fun minimumBeautifulSubstrings(s: String): Int {
return backTrack(s, HashMap<Int,Int>(), 0)
}
private fun backTrack(s: String, memo: MutableMap<Int, Int>, i: Int): Int {
// 终止条件
if (i == s.length) return 0
// 剪枝(不允许前导零)
if (s[i] == '0') return -1
// 读备忘录
if (memo.contains(i)) return memo[i]!!
// 枚举
var x = 0
var ret = INF
for (j in i until s.length) {
x = x.shl(1) + (s[j] - '0')
if (set.contains(x)) {
// 递归
val childRet = backTrack(s, memo, j + 1)
if (-1 != childRet) ret = Math.min(ret, childRet)
}
}
val finalRet = if (INF == ret) -1 else ret + 1
memo[i] = finalRet
return finalRet
}
}
复杂度分析:
- 时间复杂度:$O(n^2)$ 一共 n 个分割点,每个分割点有「选和不选」两种方案,看起来总共有 $2^n$ 种子状态,其实并没有。我们的 backTrack(i) 的定义是以 [i] 为起点可以构造的最少美丽字符串数,因此总共只有 n 种状态,而每种状态需要检查 $O(n)$ 种子状态,因此整体时间复杂度是 $O(n^2)$;
- 空间复杂度:$O(n)$ 备忘录空间。
题解二(动态规划)
可以把记忆化递归翻译为动态规划的版本:
class Solution {
companion object {
// 预处理
private val U = 15
private val INF = Integer.MAX_VALUE
private val set = HashSet<Int>()
init {
var x = 1
while (x.toString(2).length <= U) {
set.add(x)
x *= 5
}
}
}
fun minimumBeautifulSubstrings(s: String): Int {
val INF = 0x3F3F3F3F // 便于判断
val n = s.length
val dp = IntArray(n + 1) { INF }
dp[n] = 0
// 倒序遍历(先求小问题)
for (i in n - 1 downTo 0) {
// 不允许前导零
if (s[i] == '0') continue
// 枚举
var x = 0
for (j in i until n) {
x = x.shl(1) + (s[j] - '0')
if (set.contains(x)) dp[i] = Math.min(dp[i], dp[j + 1] + 1)
}
}
return if (dp[0] != INF) dp[0] else -1
}
}
复杂度分析:
- 时间复杂度:$O(n^2)$ 同上;
- 空间复杂度:$O(n)$ DP 数组空间。
T4. 黑格子的数目(Medium)
https://leetcode.cn/problems/number-of-black-blocks/
题解(枚举黑格 + 贡献度)
直接枚举所有块的时间复杂度是 O(nm) 会超时,我们发现真正影响结果的是黑格格子,但是暴力枚举块的方法会枚举到那些完全是白色的块。
因此,我们将枚举维度从所有块调整到黑色格子附近的块,对于每一个黑色格子 [x, y] 最多仅会对 4 个块产生影响(贡献)。所以我们的算法是:枚举所有黑色格子,并记录黑色格子可以产生贡献的块,最后统计出所有可以被影响到的块以及的贡献度,这可以用散列表来记录。
剩下一个问题是怎么表示一个唯一的块,我们可以规定块中 4 个点中的其中一个点作为块的代表元(以右下角的点为例),然后将该点的行和列压缩到一个 Long 变量中来唯一标识不同的块。
class Solution {
fun countBlackBlocks(m: Int, n: Int, coordinates: Array<IntArray>): LongArray {
val U = 100000
val map = HashMap<Long, Int>()
// 以右下角为代表元的块
val blocks = arrayOf(intArrayOf(0,0), intArrayOf(0, 1), intArrayOf(1,1), intArrayOf(1,0))
for (e in coordinates) {
// 枚举 4 个块
for (block in blocks) {
val x = e[0] + block[0]
val y = e[1] + block[1]
// 检查块有效性
if (x >= 1 && x < m && y >= 1 && y < n) {
// 记录贡献度
val key = 1L * x * U + y
map[key] = map.getOrDefault(key, 0) + 1
}
}
}
val ret = LongArray(5)
for ((_, cnt) in map) {
ret[cnt] ++
}
ret[0] = 1L * (n - 1) * (m - 1) - map.size
return ret
}
}
复杂度分析:
- 时间复杂度:$O(m)$ 其中 m 为黑格格子数
- 空间复杂度:$O(m)$ 其中 m 为黑格格子数