LeetCode 周赛(2023/07/08)渐入佳境

leetcode,渐入佳境 · 浏览次数 : 28

小编点评

## Code Explanation The code implements a solution for a LeetCode problem related to counting the number of black blocks in a given grid. **Data Structures:** * `set` stores all possible positions of black blocks. * `memo` stores the minimum beautiful substring length for each position in the grid. * `dp` stores the minimum beautiful substring length for each position in the grid. **Algorithm:** 1. **Initialization:** * Set `memo` to `-1` for all positions in the grid. * Set `dp` to `INF` for all positions in the grid. * Set `dp[n]` to `0` for all `n` in the grid. 2. **Iterative DP:** * For each position in the grid, considering only positions where the block can be placed, and ignoring the case when the position is a zero. * For each possible value of the block at that position, calculate the minimum number of black blocks that can be placed around that position to make the string beautiful. * Update `dp` for all positions with the minimum number of black blocks. * Store the `dp` values in `memo`. 3. **Final Result:** * Return the value stored in `dp[0]` if it is not `-1`. Otherwise, return `-1`. **Time Complexity:** * Time complexity is O(`n^2`) due to the iterative DP and the need to check all possible positions in the grid. * Space complexity is O(`n`) as we need to store the `memo` and `dp` arrays. **Key Points:** * The code uses a set to efficiently check which positions have black blocks. * It uses memoization to avoid computing the same subproblems. * It uses dynamic programming to find the minimum number of black blocks for each position. * The final result is returned if the `dp[0]` value is not `-1`. Otherwise, it returns `-1`.

正文

本文已收录到 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 为黑格格子数

往期回顾

与LeetCode 周赛(2023/07/08)渐入佳境相似的内容:

LeetCode 周赛(2023/07/08)渐入佳境

> **本文已收录到 [AndroidFamily](https://github.com/pengxurui/AndroidFamily),技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。** - 往期回顾:[LeetCode 单周赛第 351 场 · 一

LeetCode 周赛 352(2023/07/02)一场关于子数组的专题周赛

> **本文已收录到 [AndroidFamily](https://github.com/pengxurui/AndroidFamily),技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。** - 往期回顾:[LeetCode 单周赛第 350 场 · 滑

LeetCode 周赛 353(2023/07/09)看似没考 LIS 最长递增子序列,好像又考了

> **本文已收录到 [AndroidFamily](https://github.com/pengxurui/AndroidFamily),技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。** - 往期回顾:[LeetCode 单周赛第 352 场 · 一

LeetCode 周赛 344(2023/05/07)手写递归函数的固定套路

本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问。 大家好,我是小彭。 今天下午有力扣杯战队赛,不知道官方是不是故意调低早上周赛难度给选手们练练手。 往期周赛回顾:LeetCode 单周赛第 343 场 · 结合「下一个排列」的贪心构造问题 周赛概览 T1.

LeetCode 周赛 342(2023/04/23)容斥原理、计数排序、滑动窗口、子数组 GCB

本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问。 大家好,我是小彭。 前天刚举办 2023 年力扣杯个人 SOLO 赛,昨天周赛就出了一场 Easy - Easy - Medium - Medium 的水场,不得不说 LeetCode 是懂礼数的 😁。 接

LeetCode 周赛 343(2023/04/30)结合「下一个排列」的贪心构造问题

本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问。 大家好,我是小彭。 今天是五一假期的第二天,打周赛的人数比前一天的双周赛多了,难道大家都只玩一天吗?这场周赛是 LeetCode 第 343 场单周赛,如果不考虑第一题摆烂的翻译,整体题目质量还是很不错哒。

LeetCode 周赛 345(2023/05/14)体验一题多解的算法之美

本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问。 往期回顾:LeetCode 双周赛第 104 场 · 流水的动态规划,铁打的结构化思考 周赛概览 T1. 找出转圈游戏输家(Easy) 标签:模拟、计数 T2. 相邻值的按位异或(Medium) 标签:模拟、

LeetCode 周赛 346(2023/05/21)仅 68 人 AK 的最短路问题

> **本文已收录到 [AndroidFamily](https://github.com/pengxurui/AndroidFamily),技术和职场问题,请关注公众号 [彭旭锐] 提问。** - [LeetCode 单周赛第 345 场 · 体验一题多解的算法之美](https://mp.wei

LeetCode 周赛 347(2023/05/28)二维空间上的 LIS 最长递增子序列问题

> **本文已收录到 [AndroidFamily](https://github.com/pengxurui/AndroidFamily),技术和职场问题,请关注公众号 [彭旭锐] 提问。** - 往期回顾:[LeetCode 单周赛第 346 场 · 仅 68 人 AK 的最短路问题](http

LeetCode 周赛 348(2023/06/05)数位 DP 模板学会了吗

> **本文已收录到 [AndroidFamily](https://github.com/pengxurui/AndroidFamily),技术和职场问题,请关注公众号 [彭旭锐] 加入知识星球提问!** - 往期回顾:[LeetCode 单周赛第 347 场 · 二维空间上的 LIS 最长递增子