本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问。

大家好,我是小彭。

这周比较忙,上周末的双周赛题解现在才更新,虽迟但到哈。上周末这场是 LeetCode 第 101 场双周赛,整体有点难度,第 3 题似乎比第 4 题还难一些。


周赛大纲

2605. 从两个数字数组里生成最小数字(Easy)

  • 题解一:散列表 $O(n + m)$ 空间
  • 题解二:位运算 $O(1)$ 空间

2606. 找到最大开销的子字符串(Medium)

  • 动态规划 O(n)

2607. 使子数组元素和相等(Medium)

  • 题解 1:拼接数组 + 中位数贪心 · 错误
  • 题解 2:数组分组 + 中位数贪心 $O(nlgn)$
  • 题解 3:裴蜀定理 + 中位数贪心 $O(nlgn)$
  • 题解 4:裴蜀定理 + 中位数贪心 + 快速选择 $O(n)$

2608. 图中的最短环(Hard)

  • 题解 1:枚举边 + Dijkstra 最短路 + 最小堆 $O(m + m^2·lgn)$
  • 题解 2:枚举边 + BFS $O(m + m^2)$

2605. 从两个数字数组里生成最小数字(Easy)

题目地址

https://leetcode.cn/problems/form-smallest-number-from-two-digit-arrays/description/

题目描述

给你两个只包含 1 到 9 之间数字的数组 nums1 和 nums2 ,每个数组中的元素 互不相同 ,请你返回 最小 的数字,两个数组都 至少 包含这个数字的某个数位。

题解一(散列表)

简单模拟题,需要对 API 比较熟悉才能写出精炼的代码。

思路:优先选择两个数组交集的最小值,否则取两个数组的最小值再拼接。

class Solution {
fun minNumber(nums1: IntArray, nums2: IntArray): Int {
val set1 = nums1.toHashSet()
val set2 = nums2.toHashSet()
// 优先选择交集
val set = set1.intersect(set2)
if (!set.isEmpty()) return Collections.min(set)
// 选择最小值
val min1 = Collections.min(set1)
val min2 = Collections.min(set2)
// 拼接
return Math.min(10 * min1 + min2, 10 * min2 + min1)
}
}

复杂度分析:

  • 时间复杂度:$O(n + m)$ 其中 $n$ 是 $nums1$ 数组的长度,$m$ 是 $nums2$ 数组的长度;
  • 空间复杂度:$O(n + m)$ 散列表空间

题解二(位运算)

使用二进制位标记代替散列表

class Solution {
fun minNumber(nums1: IntArray, nums2: IntArray): Int {
var flag1 = 0
var flag2 = 0
for (num in nums1) {
flag1 = flag1 or (1 shl num)
}
for (num in nums2) {
flag2 = flag2 or (1 shl num)
}
// numberOfTrailingZeros:最低位连续 0 的个数
// 交集
val flag = flag1 and flag2
if (flag > 0) return Integer.numberOfTrailingZeros(flag)
// 最小值
val min1 = Integer.numberOfTrailingZeros(flag1)
val min2 = Integer.numberOfTrailingZeros(flag2)
// 拼接
return Math.min(10 * min1 + min2, 10 * min2 + min1)
}
}

复杂度分析:

  • 时间复杂度:$O(n + m)$ 其中 $n$ 是 $nums1$ 数组的长度,$m$ 是 $nums2$ 数组的长度;
  • 空间复杂度:$O(1)$ 散列表空间

2606. 找到最大开销的子字符串(Medium)

题目地址

https://leetcode.cn/problems/find-the-substring-with-maximum-cost/

题目描述

给你一个字符串 s ,一个字符 互不相同 的字符串 chars 和一个长度与 chars 相同的整数数组 vals 。

子字符串的开销 是一个子字符串中所有字符对应价值之和。空字符串的开销是 0 。

字符的价值 定义如下:

  • 如果字符不在字符串 chars 中,那么它的价值是它在字母表中的位置(下标从 1 开始)。

    • 比方说,'a' 的价值为 1 ,'b' 的价值为 2 ,以此类推,'z' 的价值为 26 。
  • 否则,如果这个字符在 chars 中的位置为 i ,那么它的价值就是 vals[i] 。

请你返回字符串 s 的所有子字符串中的最大开销。

题解(动态规划)

简单动态规划问题。

先根据题意维护 a-z 每个字母的开销,再求 53. 最长子数组和 问题。

定义 dp[i] 表示以 [i] 为结尾的最大子数组和,则有

  • 与 $a[0, i - 1]$ 拼接:$dp[i] = dp[i - 1] + vals[i]$
  • 不与 $a[i - 1]$ 拼接(单独作为子数组):$dp[i] = vals[i]$
class Solution {
fun maximumCostSubstring(s: String, chars: String, vals: IntArray): Int {
// 初值
val fullVals = IntArray(26) { it + 1 }
// 更新
for ((i, c) in chars.withIndex()) {
fullVals[c - 'a'] = vals[i]
}
// 动态规划
val n = s.length
var max = 0
val dp = IntArray(n + 1)
for (i in 1..n) {
val curValue = fullVals[s[i - 1] - 'a']
dp[i] = Math.max(curValue, dp[i - 1] + curValue)
max = Math.max(max, dp[i])
}
return max
}
}

滚动数组优化:

class Solution {
fun maximumCostSubstring(s: String, chars: String, vals: IntArray): Int {
// 初值
val fullVals = IntArray(26) { it + 1 }
// 更新
for ((i, c) in chars.withIndex()) {
fullVals[c - 'a'] = vals[i]
}
// 动态规划
val n = s.length
var max = 0
var pre = 0
for (i in 1..n) {
val curValue = fullVals[s[i - 1] - 'a']
pre = Math.max(curValue, pre + curValue)
max = Math.max(max, pre)
}
return max
}
}

另一种理解,视为 vals[i] 总与前序子数组拼接,而前序子数组的权值不低于 0:

  • $dp[i] = Math.max(dp[i - 1], 0) + vals[i]$
class Solution {
fun maximumCostSubstring(s: String, chars: String, vals: IntArray): Int {
// 初值
val fullVals = IntArray(26) { it + 1}
// 更新
for ((i, c) in chars.withIndex()) {
fullVals[c - 'a'] = vals[i]
}
// 动态规划
val n = s.length
var max = 0
var pre = 0
for (i in 1..n) {
pre = Math.max(pre, 0) + fullVals[s[i - 1] - 'a']
max = Math.max(max, pre)
}
return max
}
}

2607. 使子数组元素和相等(Medium)

题目地址

https://leetcode.cn/problems/make-k-subarray-sums-equal/

题目描述

给你一个下标从 0 开始的整数数组 arr 和一个整数 k 。数组 arr 是一个循环数组。换句话说,数组中的最后一个元素的下一个元素是数组中的第一个元素,数组中第一个元素的前一个元素是数组中的最后一个元素。

你可以执行下述运算任意次:

  • 选中 arr 中任意一个元素,并使其值加上 1 或减去 1 。

执行运算使每个长度为 k 的 子数组 的元素总和都相等,返回所需要的最少运算次数。

子数组 是数组的一个连续部分。

问题分析

分析 1: 先不考虑循环数组的前提,分析数据约束 “对于满足每个长度为 k 的子数组的和相等”,那么

$a[i]+a[i+1] +…+a[i+k-1] == a[i+1]+a[i+2]+…+a[i+k-1]+a[i+k]$

等式两边化简得:

$a[i]=a[i+k]$

也就是说,数组上每间隔 k 的元素要相等。因此我们需要将每间隔 k 的元素分为一组,再将组内元素调整为相等值;

分析 2: 如何将组内元素调整为相等值呢?可以证明选择中位数的贪心做法是最优的。

分析 3: 考虑循环数组的前提,对于 i + k ≥ len(arr) 的情况,需要对数组下标取模来模拟循环

题解一(拼接数组 + 中位数贪心 · 错误)

循环数组有拼接一倍数组的模拟做法,我们模拟出 2*n 长度的数组,在访问每个位置时,将所有同组的数组分为一组,再排序取中位数。

不过,这个思路在这道题里是不对的,因为同一个分组有可能循环多轮才会遇到。即使不考虑错误,在这道题的数据范围上也会内存溢出。

错误测试用例:$arr = [1, 5, 8, 10], k = 3$

class Solution {
fun makeSubKSumEqual(arr: IntArray, k: Int): Long {
val n = arr.size
var ret = 0L
// 延长一倍数组
val visited = BooleanArray(2 * n)
for (i in 0 until 2 * n) {
if (visited[i]) continue
// 分组
val bucket = ArrayList<Int>()
for (j in i until 2 * n step k) {
bucket.add(arr[j % n])
visited[j] = true
}
// 排序
Collections.sort(bucket)
// println(bucket.joinToString())
// 中位数贪心
val midVal = bucket[bucket.size / 2]
for (element in bucket) {
ret += Math.abs(element - midVal)
}
}
return ret / 2 // 扩充了一倍数组,所以操作数也翻倍了
}
}

题解二(数组分组 + 中位数贪心)

既然不能使用数组,那么可以在内存循环中一直循环取同分组为止,直到出现回环后退出:

class Solution {
fun makeSubKSumEqual(arr: IntArray, k: Int): Long {
val n = arr.size
var ret = 0L
val visited = BooleanArray(n)
for (i in 0 until n) {
if (visited[i]) continue
// 分组
val bucket = ArrayList<Int>()
var j = i
while (!visited[j]) {
bucket.add(arr[j % n])
visited[j] = true
j = (j + k) % n
}
// 排序
Collections.sort(bucket)
// 中位数贪心
val midVal = bucket[bucket.size / 2]
for (element in bucket) {
ret += Math.abs(element - midVal)
}
}
return ret
}
}

复杂度分析:

  • 时间复杂度:$O(nlgn)$ 其中 $n$ 为 $arr$ 数组长度,每个元素最多访问一次,且排序一次,所以整体时间是 $O(nlgn)$;
  • 空间复杂度:$O(n + lgn)$ 标记数组空间 + 排序递归栈空间。

题解三(裴蜀定理 + 中位数贪心)

根据前文分析,我们需要保证最终数组是以 $k$ 为循环周期的,而循环数组本身又是以 $n$ 为循环周期的。根据 裴蜀定理 ,如果一个数组存在周期 $k$ 和周期 $n$,那么必然存在周期 $gcb(k, n)$,而 $gcb(k, n)$ 必然小于 $n$,我们就将问题变成非循环数组问题。

  • 裴蜀定理:设 a,b 是不全为零的整数,则存在整数 x , y,使得 ax + by = gcb(a,b)
class Solution {
fun makeSubKSumEqual(arr: IntArray, k: Int): Long {
val n = arr.size
// 最大公约数
val m = gcb(n, k)
var ret = 0L
// 最多只有 m 组
for (i in 0 until m) {
// 分组
val bucket = ArrayList<Int>()
for (j in i until n step m) {
bucket.add(arr[j])
}
// 排序
Collections.sort(bucket)
val midVal = bucket[bucket.size / 2]
for (element in bucket) {
ret += Math.abs(element - midVal)
}
} return ret
} private fun gcb(a: Int, b: Int): Int {
if (b == 0) return a
return gcb(b, a % b)
}
}

复杂度分析:

  • 时间复杂度:$O(nlgn)$ 其中 $n$ 为 $arr$ 数组长度,每个元素最多访问一次,且排序一次,所以整体时间是 $O(nlgn)$;
  • 空间复杂度:$O(n + lgn)$ 分组空间 + 排序递归栈空间,分组空间最大为 $n$;

题解四(裴蜀定理 + 中位数贪心 + 快速选择)

排序是为了寻找中位数,没必要对整个分组排序,可以优化为快速选择,时间复杂度优化到 $O(n)$,Nice!

class Solution {
fun makeSubKSumEqual(arr: IntArray, k: Int): Long {
val n = arr.size
// 最大公约数
val m = gcb(n, k)
var ret = 0L
// 最多只有 m 组
for (i in 0 until m) {
// 分组
val bucket = ArrayList<Int>()
for (j in i until n step m) {
bucket.add(arr[j])
}
// 快速选择
quickSelect(bucket)
val midVal = bucket[bucket.size / 2]
for (element in bucket) {
ret += Math.abs(element - midVal)
}
}
return ret
} // 快速选择中位数
private fun quickSelect(bucket: ArrayList<Int>) {
val mid = bucket.size / 2
var left = 0
var right = bucket.size - 1
while (true) {
val pivot = partition(bucket, left, right)
if (mid == pivot) {
break
} else if (pivot < mid) {
left = pivot + 1
} else {
right = pivot - 1
}
}
} // return:分区
private fun partition(bucket: ArrayList<Int>, left: Int, right: Int): Int {
var p = left
for (i in left until right) {
if (bucket[i] < bucket[right]) {
bucket.swap(p++, i)
}
}
bucket.swap(p, right)
return p
} private fun <T> ArrayList<T>.swap(first: Int, second: Int) {
val temp = this[first]
this[first] = this[second]
this[second] = temp
} // 迭代写法
private fun gcb(a: Int, b: Int): Int {
var x = a
var y = b
while (y != 0) {
val temp = x % y
x = y
y = temp
}
return x
}
}

复杂度分析:

  • 时间复杂度:$O(n)$ 其中 $n$ 为 $arr$ 数组长度,每个元素最多访问一次;
  • 空间复杂度:$O(n)$ 分组空间,分组空间最大为 $n$;

相似题目:


2608. 图中的最短环(Hard)

题目地址

https://leetcode.cn/problems/shortest-cycle-in-a-graph/

题目描述

现有一个含 n 个顶点的 双向 图,每个顶点按从 0 到 n - 1 标记。图中的边由二维整数数组 edges 表示,其中 edges[i] = [ui, vi] 表示顶点 ui 和 vi 之间存在一条边。每对顶点最多通过一条边连接,并且不存在与自身相连的顶点。

返回图中 最短 环的长度。如果不存在环,则返回 -1 。

 是指以同一节点开始和结束,并且路径中的每条边仅使用一次。

题解一(枚举边 + Dijkstra 最短路 + 最小堆)

这道题是 最小环 模板题:给出一个图,问图中边权和最小的环是多大,图的最小环也称围长。

暴力解法:对于每条边 $(u, v)$,求不经过 $(u,v)$ 边从 $u$ 到 $v$ 的最短路 $len$,那么包含 $(u,v)$ 的最短环就是 $len + 1$。枚举所有边,则所有答案的最小值就是图的最小环。

class Solution {

    private val INF = Integer.MAX_VALUE

    fun findShortestCycle(n: Int, edges: Array<IntArray>): Int {
// 建图
val graph = Array(n) { ArrayList<Int>() }.apply {
for (edge in edges) {
this[edge[0]].add(edge[1])
this[edge[1]].add(edge[0])
}
}
// 枚举边
var ret = INF
for (edge in edges) {
ret = Math.min(ret, dijkstra(graph, edge[0], edge[1]))
}
return if (INF == ret) -1 else ret
} private fun dijkstra(graph: Array<ArrayList<Int>>, u: Int, v: Int): Int {
// 最短路长度
val dis = IntArray(graph.size) { INF }.apply {
this[u] = 0
}
// 最小堆
val heap = PriorityQueue<Int>() { e1, e2 ->
dis[e1] - dis[e2]
}.apply {
this.offer(u)
}
// BFS
outer@ while (!heap.isEmpty()) {
// 使用 O(lgn) 找出已选集中最短路长度最小的节点
val x = heap.poll()
// 松弛相邻点
for (y in graph[x]) {
// 忽略 (u, v) 边
if (x == u && y == v) continue
if (dis[x] + 1 /* 边权为 1 */ < dis[y]) {
dis[y] = dis[x] + 1
heap.offer(y)
}
// 找到 u -> v 的最短路
if (y == v) break@outer
}
}
return if(INF == dis[v]) INF else dis[v] + 1
}
}

复杂度分析:

  • 时间复杂度:$O(m + m^2·lgn)$ 其中 $n$ 为顶点个数,$m$ 为边个数,每条边跑 Dijkstra 最短路每轮迭代以 $O(lgn)$ 取出已选集中最短路长度最小的节点,每次 Dijkstra 的时间是 $O(m·lgn)$;
  • 空间复杂度:$O(m + n)$ 图空间 + 最小堆空间,使用邻接表可以降低空间到 $O(m + n)$。

题解二(枚举边 + BFS)

由于这道题的边权是 1,所以不需要使用高级的图论算法也能做。

为什么呢,因为每个边权的长度是 1,所以已经访问过的节点是不会存在更短路径的。所以我们不需要使用堆,直接使用队列,最先进入队列中的节点一定是最短路长度最短的节点。

class Solution {

    private val INF = Integer.MAX_VALUE

    fun findShortestCycle(n: Int, edges: Array<IntArray>): Int {
// 建图
val graph = Array(n) { ArrayList<Int>() }.apply {
for (edge in edges) {
this[edge[0]].add(edge[1])
this[edge[1]].add(edge[0])
}
}
// 枚举边
var ret = INF
for (edge in edges) {
ret = Math.min(ret, bfs(graph, edge[0], edge[1]))
}
return if (INF == ret) -1 else ret
} private fun bfs(graph: Array<ArrayList<Int>>, u: Int, v: Int): Int {
// 最短路长度
val dis = IntArray(graph.size) { INF }.apply {
this[u] = 0
}
// 最小堆
val queue = LinkedList<Int>().apply {
this.offer(u)
}
// BFS
outer@ while (!queue.isEmpty()) {
// 取队头
val x = queue.poll()
// 松弛相邻点
for (y in graph[x]) {
// 忽略 (u, v) 边
if (x == u && y == v) continue
// 已经访问过的节点不会存在更短路
if (INF != dis[y]) continue
dis[y] = dis[x] + 1
queue.offer(y)
// 找到 u -> v 的最短路
if (y == v) break@outer
}
}
return if (INF == dis[v]) INF else dis[v] + 1
}
}

复杂度分析:

  • 时间复杂度:$O(m + m^2)$ 在每轮 BFS 中,每条边最多访问 2 次,因此每轮 BFS 的时间复杂度是 $O(m)$;
  • 空间复杂度:$O(m + n)$。

LeetCode 双周赛 101,DP/中心位贪心/裴蜀定理/Dijkstra/最小环的更多相关文章

  1. [每日一题2020.06.16] leetcode双周赛T3 5423 找两个和为目标值且不重叠的子数组 DP, 前缀和

    题目链接 给你一个整数数组 arr 和一个整数值 target . 请你在 arr 中找 两个互不重叠的子数组 且它们的和都等于 target .可能会有多种方案,请你返回满足要求的两个子数组长度和的 ...

  2. LeetCode双周赛#36

    1604. 警告一小时内使用相同员工卡大于等于三次的人 题目链接 题意 给定两个字符串数组keyName和keyTime,分别表示名字为keytime[i]的人,在某一天内使用员工卡的时间(格式为24 ...

  3. LeetCode双周赛#35

    1589. 所有排列中的最大和 #差分 #贪心 题目链接 题意 给定整数数组nums,以及查询数组requests,其中requests[i] = [starti, endi] .第i个查询求 num ...

  4. LeetCode双周赛#34

    5492. 分割字符串的方案数 #组合公式 #乘法原理 #区间分割 题目链接 题意 给定01二进制串\(s\),可将\(s\)分割为三个非空 字符串\(s_1,s_2,s_3\),即(\(s_1+s_ ...

  5. LeetCode双周赛#33 题解

    5480. 可以到达所有点的最少点数目 #贪心 题目链接 题意 给定有向无环图,编号从0到n-1,一个边集数组edges(表示从某个顶点到另一顶点的有向边),现要找到最小的顶点集合,使得从这些点出发, ...

  6. leetcode 双周赛9 进击的骑士

    一个坐标可以从 -infinity 延伸到 +infinity 的 无限大的 棋盘上,你的 骑士 驻扎在坐标为 [0, 0] 的方格里. 骑士的走法和中国象棋中的马相似,走 “日” 字:即先向左(或右 ...

  7. leetcode 双周赛9 找出所有行中最小公共元素

    给你一个矩阵 mat,其中每一行的元素都已经按 递增 顺序排好了.请你帮忙找出在所有这些行中 最小的公共元素. 如果矩阵中没有这样的公共元素,就请返回 -1. 示例: 输入:mat = [[,,,,] ...

  8. Leetcode 双周赛#32 题解

    1540 K次操作转变字符串 #计数 题目链接 题意 给定两字符串\(s\)和\(t\),要求你在\(k\)次操作以内将字符串\(s\)转变为\(t\),其中第\(i\)次操作时,可选择如下操作: 选 ...

  9. 牛客练习赛52 | C | [烹饪] (DP,裴蜀定理,gcd)

    牛客练习赛52 C 烹饪 链接:https://ac.nowcoder.com/acm/contest/1084/C来源:牛客网 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 327 ...

  10. 【BZOJ4069】[Apio2015]巴厘岛的雕塑 按位贪心+DP

    [BZOJ4069][Apio2015]巴厘岛的雕塑 Description 印尼巴厘岛的公路上有许多的雕塑,我们来关注它的一条主干道. 在这条主干道上一共有 N 座雕塑,为方便起见,我们把这些雕塑从 ...

随机推荐

  1. 网易面经-hashmap是否能存null(debug源码)

    面试时一定要坚持自己的认知,不要让面试官两三下pua了. 结果是可以的 null作为key时被放在了tab下标为0的位置,只能有一个null null作为value时不受限制 虽然说value为nul ...

  2. CentOS7 64位 部署AVA项目:jar包方式

    步骤:1.挂载磁盘2.安装jdk1.83.安装mysql5.74.导入数据库5.防火墙端口放行5.运行jar文件 1.挂载磁盘https://www.cnblogs.com/xiang96/p/102 ...

  3. BOM的概述及方法

    BOM的概述: bom 称为浏览器对象模型(bowser object model),也就意味他可以获取浏览器上的所有内容以及相关的操作.BOM缺乏规范的,存在共有对象来解决这个问题,但是共有对象也存 ...

  4. kafka 学习

    https://kafka.apache.org/quickstart C:\W_O_R_K\kafka_2.12-2.2.0\kafka_2.12-2.2.0\bin\windows\zookeep ...

  5. format UTF-8 BOM by AX

    #File CommaTextIo commaTextIo; FileIOPermission permission; CustTable custTable; str fileName = @&qu ...

  6. 复制文本到粘贴板 (vue3)(兼容ios)

    // 点击复制到剪贴板 const copyToClipboard = (content)=> { if (window.clipboardData) { window.clipboardDat ...

  7. 9. 实现包括前端后台的预约洗狗功能 - 使用Power Automate发送预约邮件 - 使用Power Automate发送带选择按钮(option)的邮件

    ​ 除了发送普通的电子邮件外,我们还可以选择发送带选项的电子邮件来得到客户的反馈,下面我们就一起来创建带有选择功能的电子邮件吧. 1. 打开我们的Power Portal,在左侧导航栏选择流,点击左上 ...

  8. vulnhub靶场之MOMENTUM: 1

    准备: 攻击机:虚拟机kali.本机win10. 靶机:Momentum: 1,下载地址:https://download.vulnhub.com/momentum/Momentum.ova,下载后直 ...

  9. 141. Linked List Cycle (Easy)

    ps:能力有限,若有错误及纰漏欢迎指正.交流 Linked List Cycle (Easy) https://leetcode.cn/problems/linked-list-cycle/descr ...

  10. docker 部署 postgres

    1. 打开dockerhub查找postgres版本 地址 https://registry.hub.docker.com/_/postgres/tags 2.复制需要的版本 docker pull ...