LeetCode Go · 贪心(11) ​
来源与许可
题目版权归 LeetCode;题解来自 Doocs/leetcode,按 CC BY-SA 4.0 共享。补充 Go 代码来自灵茶山艾府、halfrost,保留 MIT 许可及逐题署名。本站提取 Go、调整分类及折叠排版;标注“本站整理”的 Go 解法依据题意与参考思路补写,按 CC BY-SA 4.0 共享。收录日期:2026-10-31;主题源版本:91ef8c241a5e。
本卷 30 题 · 点击题目展开,先独立思考,再查看解法。
3107. 使数组中位数等于 K 的最少操作数 ​
题目、示例与约束
给你一个整数数组 nums 和一个 非负 整数 k 。一次操作中,你可以选择任一元素 加 1 或者减 1 。
请你返回将 nums 中位数 变为 k 所需要的 最少 操作次数。
一个数组的中位数指的是数组按非递减顺序排序后最中间的元素。如果数组长度为偶数,我们选择中间两个数的较大值为中位数。
示例 1:
输入:nums = [2,5,6,8,5], k = 4
输出:2
解释:我们将 nums[1] 和 nums[4] 减 1 得到 [2, 4, 6, 8, 4] 。现在数组的中位数等于 k 。
示例 2:
输入:nums = [2,5,6,8,5], k = 7
输出:3
解释:我们将 nums[1] 增加 1 两次,并且将 nums[2] 增加 1 一次,得到 [2, 7, 7, 8, 5] 。
示例 3:
输入:nums = [1,2,3,4,5,6], k = 4
输出:0
解释:数组中位数已经等于 k 了。
提示:
1 <= nums.length <= 2 * 1051 <= nums[i] <= 1091 <= k <= 109
方法一:贪心 + 排序 · Go 实现
思考
中位数由有序后的中间位置决定。若不排序而枚举哪些元素增减,组合数过大; 允许 排序。
将中间元改为 是必要的。若它大于 ,其右侧本已不小于它,只需把左侧仍大于 的值压到 ;若它不大于 ,则只需把右侧仍小于 的值抬到 。
为此先排序并定位中位下标 ,把 计入答案,再按上述方向单侧扫描补齐。每侧遇已满足 的元素即可停止。
我们首先对数组 进行排序,然后找到中位数的位置 ,那么初始时我们需要的操作次数就是 。
接下来,我们分情况讨论:
- 如果 ,那么 右侧的元素都大于等于 ,我们只需要将 左侧的元素中,大于 的元素减小到 即可。
- 如果 ,那么 左侧的元素都小于等于 ,我们只需要将 右侧的元素中,小于 的元素增大到 即可。
时间复杂度 ,空间复杂度 。其中 是数组 的长度。
func minOperationsToMakeMedianK(nums []int, k int) (ans int64) {
sort.Ints(nums)
n := len(nums)
m := n >> 1
ans = int64(abs(nums[m] - k))
if nums[m] > k {
for i := m - 1; i >= 0 && nums[i] > k; i-- {
ans += int64(nums[i] - k)
}
} else {
for i := m + 1; i < n && nums[i] < k; i++ {
ans += int64(k - nums[i])
}
}
return
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
3111. 覆盖所有点的最少矩形数目 ​
题目、示例与约束
给你一个二维整数数组 point ,其中 points[i] = [xi, yi] 表示二维平面内的一个点。同时给你一个整数 w 。你需要用矩形 覆盖所有 点。
每个矩形的左下角在某个点 (x1, 0) 处,且右上角在某个点 (x2, y2) 处,其中 x1 <= x2 且 y2 >= 0 ,同时对于每个矩形都 必须 满足 x2 - x1 <= w 。
如果一个点在矩形内或者在边上,我们说这个点被矩形覆盖了。
请你在确保每个点都 至少 被一个矩形覆盖的前提下,最少 需要多少个矩形。
注意:一个点可以被多个矩形覆盖。
示例 1:

输入:points = [[2,1],[1,0],[1,4],[1,8],[3,5],[4,6]], w = 1
输出:2
解释:
上图展示了一种可行的矩形放置方案:
- 一个矩形的左下角在
(1, 0),右上角在(2, 8)。 - 一个矩形的左下角在
(3, 0),右上角在(4, 8)。
示例 2:

输入:points = [[0,0],[1,1],[2,2],[3,3],[4,4],[5,5],[6,6]], w = 2
输出:3
解释:
上图展示了一种可行的矩形放置方案:
- 一个矩形的左下角在
(0, 0),右上角在(2, 2)。 - 一个矩形的左下角在
(3, 0),右上角在(5, 5)。 - 一个矩形的左下角在
(6, 0),右上角在(6, 6)。
示例 3:

输入:points = [[2,3],[1,2]], w = 0
输出:2
解释:
上图展示了一种可行的矩形放置方案:
- 一个矩形的左下角在
(1, 0),右上角在(1, 2)。 - 一个矩形的左下角在
(2, 0),右上角在(2, 3)。
提示:
1 <= points.length <= 105points[i].length == 20 <= xi == points[i][0] <= 1090 <= yi == points[i][1] <= 1090 <= w <= 109- 所有点坐标
(xi, yi)互不相同。
方法一:贪心 + 排序 · Go 实现
思考
矩形高度不受限,覆盖只由横坐标与宽度 决定。若枚举每个点所属矩形的划分,状态随点数指数增长。
最优策略是按 排序后尽量把当前矩形向右铺满:一旦某点落在已覆盖范围外,必须新开一个以该点为左端、长度为 的矩形。
为此排序后维护当前矩形右边界 ,遇 则答案加一并令 。贪心覆盖次数即为最少矩形数。
根据题目描述,我们不需要考虑矩形的高度,只需要考虑矩形的宽度。
我们可以将所有的点按照横坐标进行排序,用一个变量 记录当前矩形所能覆盖的最右边的横坐标,初始时 。
接下来我们遍历所有的点,如果当前点的横坐标 大于 ,说明已有的矩形无法覆盖当前点,我们就需要增加一个矩形,答案加一,然后我们更新 。
遍历完成后,我们就得到了最少需要的矩形数目。
时间复杂度 ,空间复杂度 。其中 是点的数量。
func minRectanglesToCoverPoints(points [][]int, w int) (ans int) {
sort.Slice(points, func(i, j int) bool { return points[i][0] < points[j][0] })
x1 := -1
for _, p := range points {
if x := p[0]; x > x1 {
ans++
x1 = x + w
}
}
return
}
3119. 最大数量的可修复坑洼 🔒 ​
题目、示例与约束
给定一个字符串 road,只包含字符 "x" 和 ".",其中每个 "x" 代表一个坑洼,每个 "." 代表一个平滑的道路,以及一个整数 budget。
在一次修复操作中,您可以以 n + 1 的价格修复 n 个连续坑洼。
返回可以修复的坑洼的 最大 数量,以便所有修复的价格总和 不会超过 给定的预算 budget。
示例 1:
输入: road = "..", budget = 5
输出: 0
解释:
没有坑洼需要修复。
示例 2:
输入: road = "..xxxxx", budget = 4
输出: 3
解释:
我们修复了前三个坑洼(它们是连续的)。任务所需的预算为 3 + 1 = 4。
示例 3:
输入: road = "x.x.xxx...x", budget = 14
输出: 6
解释:
我们可以修复所有的坑洼。总花费为 (1 + 1) + (1 + 1) + (3 + 1) + (1 + 1) = 10 在我们的预算 14 之内。
提示:
1 <= road.length <= 1051 <= budget <= 105 + 1road只包含字符'.'和'x'。
方法一:计数 + 贪心 · Go 实现
思考
连续 个坑的修补代价为 ,预算有限。若把每段坑独立枚举修多长,段数多时组合爆炸。
同样预算下优先修更长的段能固定更多坑:长度为 的段修完后,余下未修部分等价于若干长度为 的段。
为此先统计各长度的段数 ,从大到小取 计入答案,剩余段并入 。预算耗尽即停。
我们首先统计出每个连续的坑洼的数量,记录在数组 中,即 表示有 个长度为 的连续坑洼。
由于我们要尽可能多地修补坑洼,而对于长度为 的连续坑洼,我们需要花费 的代价,应该优先修补长度较长的坑洼,这样可以使得代价最小。
因此,我们从最长的坑洼开始修补,对于长度为 的坑洼,我们最多可以修补的个数为 ,我们将修补的个数乘以长度 加到答案中,然后更新剩余的预算。对于长度为 的其余 个坑洼,我们将它们合并到长度为 的坑洼中。继续这个过程,直到遍历完所有的坑洼。
时间复杂度 ,空间复杂度 。其中 为字符串 的长度。
func maxPotholes(road string, budget int) (ans int) {
road += "."
n := len(road)
cnt := make([]int, n)
k := 0
for _, c := range road {
if c == 'x' {
k++
} else if k > 0 {
cnt[k]++
k = 0
}
}
for k = n - 1; k > 0 && budget > 0; k-- {
t := min(budget/(k+1), cnt[k])
ans += t * k
budget -= t * (k + 1)
cnt[k-1] += cnt[k] - t
}
return
}
3125. 使得按位与结果为 0 的最大数字 🔒 ​
题目、示例与约束
n,返回 最大的 整数 x 使得 x <= n,并且所有在范围 [x, n] 内的数字的按位 AND 为 0。
示例 1:
输入:n = 7
输出:3
解释:
[6, 7] 的按位 AND 为 6。
[5, 6, 7] 的按位 AND 为 4。
[4, 5, 6, 7] 的按位 AND 为 4。
[3, 4, 5, 6, 7] 的按位 AND 为 0。
示例 2:
输入:n = 9
输出:7
解释:
[7, 8, 9] 的按位 AND 为 0。
示例 3:
输入:n = 17
输出:15
解释:
[15, 16, 17] 的按位 AND 为 0。
提示:
1 <= n <= 1015
方法一:位运算 · Go 实现
思考
要找不超过 的最大 使 的按位与为 。从 向下试会在最坏情况下线性扫描。
与 互为进位相邻,其与为 意味着 的二进制是一段低位全 。不超过 的最大此类数,恰是 最高位以下的掩码。
因此取 的最高位置位,返回 。该数与其后继的与必为 ,且已是上界。
我们可以找到 的二进制表示中最高位的 ,那么最大的 一定小于 且该位为 ,其他低位均为 ,即 。这是因为 一定成立。
时间复杂度 ,空间复杂度 。
func maxNumber(n int64) int64 {
return int64(1<<(bits.Len64(uint64(n))-1)) - 1
}
3139. 使数组中所有元素相等的最小开销 ​
题目、示例与约束
给你一个整数数组 nums 和两个整数 cost1 和 cost2 。你可以执行以下 任一 操作 任意 次:
- 从
nums中选择下标i并且将nums[i]增加1,开销为cost1。 - 选择
nums中两个 不同 下标i和j,并且将nums[i]和nums[j]都 增加1,开销为cost2。
你的目标是使数组中所有元素都 相等 ,请你返回需要的 最小开销 之和。
由于答案可能会很大,请你将它对 109 + 7 取余 后返回。
示例 1:
输入:nums = [4,1], cost1 = 5, cost2 = 2
输出:15
解释:
执行以下操作可以使数组中所有元素相等:
- 将
nums[1]增加 1 ,开销为 5 ,nums变为[4,2]。 - 将
nums[1]增加 1 ,开销为 5 ,nums变为[4,3]。 - 将
nums[1]增加 1 ,开销为 5 ,nums变为[4,4]。
总开销为 15 。
示例 2:
输入:nums = [2,3,3,3,5], cost1 = 2, cost2 = 1
输出:6
解释:
执行以下操作可以使数组中所有元素相等:
- 将
nums[0]和nums[1]同时增加 1 ,开销为 1 ,nums变为[3,4,3,3,5]。 - 将
nums[0]和nums[2]同时增加 1 ,开销为 1 ,nums变为[4,4,4,3,5]。 - 将
nums[0]和nums[3]同时增加 1 ,开销为 1 ,nums变为[5,4,4,4,5]。 - 将
nums[1]和nums[2]同时增加 1 ,开销为 1 ,nums变为[5,5,5,4,5]。 - 将
nums[3]增加 1 ,开销为 2 ,nums变为[5,5,5,5,5]。
总开销为 6 。
示例 3:
输入:nums = [3,5,3], cost1 = 1, cost2 = 3
输出:4
解释:
执行以下操作可以使数组中所有元素相等:
- 将
nums[0]增加 1 ,开销为 1 ,nums变为[4,5,3]。 - 将
nums[0]增加 1 ,开销为 1 ,nums变为[5,5,3]。 - 将
nums[2]增加 1 ,开销为 1 ,nums变为[5,5,4]。 - 将
nums[2]增加 1 ,开销为 1 ,nums变为[5,5,5]。
总开销为 4 。
提示:
1 <= nums.length <= 1051 <= nums[i] <= 1061 <= cost1 <= 1061 <= cost2 <= 106
方法一:Go 实现(灵茶山艾府) · Go 实现
灵茶山艾府 · 源码 · MIT 许可 · 原题解
前言
为方便描述,将 和 简记为 和 。
设 。
本题看上去把所有数都变成 即可,但请看 这个例子:
- 全部变成 :用操作二把数字 和 都加一,现在数组为 ;然后用操作一把数字 加二,现在数组为 。总开销为 。
- 全部变成 :只用操作二,把数字 和 加一两次,现在数组为 ;然后把数字 和 加一,现在数组为 ;然后把数字 和 加一,现在数组为 。总开销为 。
这意味着,在 很大 很小的情况下(比如 ),变成一个比 更大的数,可能得到一个更小的总开销。
假设都变成 ,那么当 取多少时,可以得到最小的总开销?
要枚举 吗?假如 ,具体要如何操作呢?知道怎么操作就知道怎么算总开销。
情况一
如果 ,那么只能用操作一,最小总开销为
下面的讨论中,。
情况二
如果 ,用一次操作二不如用两次操作一,所以只需用操作一,把每个数都变成 的总开销是最小的。
总共需要执行
次操作一。
设
那么最小总开销为
情况三
如果 ,用操作二更划算,所以应当尽量多地使用操作二。
假设都变成 ,那么所有数都需要在 的基础上额外增加 ,总共要增加
具体要如何操作呢?
为方便大家理解,想象有 个盒子,第 个盒子装有 个小球,这是 需要加一的次数。
操作一相当于从一个非空盒子中取出一个球,操作二相当于从两个不同的非空盒子中各取一个小球。
我们需要计算最多能执行多少次操作二。
设总共有 个小球,其中装有小球数最多的盒子,装了 个小球。
结论:最多执行 次操作二。
根据结论,计算都变成 的总开销 :
- 如果 ,那么先执行 次操作二,然后执行 次操作一,总开销为
- 如果 ,那么先执行 次操作二,然后执行 次操作一,总开销为
两种情况可以合并为
枚举 ,取 的最小值,即为最小总开销。
至多枚举到哪?
最后还剩下一个问题: 至多要枚举到哪个数为止?
注意到, 增加 , 只增大 ,而 增大 。所以 时我们会先满足 ,然后再满足 。而当 时,总开销只和 有关, 越大 越大,至多再枚举一次就无需继续枚举了。至多再枚举一次是因为如果 是奇数,额外有 的开销。多枚举一次可以让 从奇数变成偶数,可能得到更小的总开销。
由 可得
移项得
解得
所以 至多枚举到
加一可以保证我们一定可以枚举到 是偶数的情况。
注意当 时,上式 ,所以枚举到 的做法是正确的。
优化

如上图所示:
- 对应 ,可以视作一条斜率或正或负(还可能为零)的一次函数。
- 对应 ,根据 的奇偶性,可以视作两条斜率为正的一次函数。
如果函数交点横坐标 , 只需枚举 和 ;否则 应当枚举交点横坐标附近的数(右图),以及 (左图)。
设 ,最小总开销为
请看 视频讲解 第四题,欢迎点赞关注!
复杂度分析
- 时间复杂度:,其中 为 的长度。瓶颈在计算 上,如果已知这些数据,则时间复杂度为 。
- 空间复杂度:。
专题训练
见下面贪心题单的「§1.8 相邻不同」。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府
import "slices"
// https://space.bilibili.com/206214
func minCostToEqualizeArray(nums []int, c1, c2 int) int {
const mod = 1_000_000_007
n := len(nums)
m := slices.Min(nums)
M := slices.Max(nums)
base := n * M
for _, x := range nums {
base -= x
}
if n <= 2 || c1*2 <= c2 {
return base * c1 % mod
}
f := func(x int) int {
s := base + (x-M)*n
d := x - m
return max(s/2*c2+s%2*c1, (s-d)*c2+(d*2-s)*c1)
}
i := (n*M - m*2 - base + n - 3) / (n - 2)
if i <= M {
return min(f(M), f(M+1)) % mod
}
return min(f(M), f(i-1), f(i), f(i+1)) % mod
}
3170. 删除星号以后字典序最小的字符串 ​
题目、示例与约束
给你一个字符串 s 。它可能包含任意数量的 '*' 字符。你的任务是删除所有的 '*' 字符。
当字符串还存在至少一个 '*' 字符时,你可以执行以下操作:
- 删除最左边的
'*'字符,同时删除该星号字符左边一个字典序 最小 的字符。如果有多个字典序最小的字符,你可以删除它们中的任意一个。
请你返回删除所有 '*' 字符以后,剩余字符连接而成的 字典序最小 的字符串。
示例 1:
输入:s = "aaba*"
输出:"aab"
解释:
删除 '*' 号和它左边的其中一个 'a' 字符。如果我们选择删除 s[3] ,s 字典序最小。
示例 2:
输入:s = "abc"
输出:"abc"
解释:
字符串中没有 '*' 字符。
提示:
1 <= s.length <= 105s只含有小写英文字母和'*'字符。- 输入保证操作可以删除所有的
'*'字符。
方法一:按字符记录下标 · Go 实现
思考
每个
*删除自身以及其左侧字典序最小且最靠右的字母。优先队列可以做,但要同时记下落点以便最后重建。按字母分桶存下标,遇到星号时从
a扫到z弹出该桶最后一个下标,即最小字母的最右出现。用 标记删除,最后按原序拼接未删字符。每颗星最多看 个桶。
我们定义一个数组 ,用于记录每个字符的下标列表,定义一个长度为 的布尔数组 ,用于记录每个字符是否需要删除。
遍历字符串 :
如果当前字符是星号,我们就需要删除它,因此我们将 标记为已删除。同时,我们需要删除此时字典序最小且下标最大的字符。我们从小到大遍历 个小写字母,如果 不为空,我们就删除 中的最后一个下标,并将 中对应的下标置为已删除。
如果当前字符不是星号,我们就将当前字符的下标加入 中。
最后,我们遍历 ,将未删除的字符拼接起来即可。
时间复杂度 ,空间复杂度 。其中 为字符串 的长度,而 为字符集大小,本题中 。
func clearStars(s string) string {
g := make([][]int, 26)
n := len(s)
rem := make([]bool, n)
for i, c := range s {
if c == '*' {
rem[i] = true
for j := 0; j < 26; j++ {
if len(g[j]) > 0 {
rem[g[j][len(g[j])-1]] = true
g[j] = g[j][:len(g[j])-1]
break
}
}
} else {
g[c-'a'] = append(g[c-'a'], i)
}
}
ans := []byte{}
for i := range s {
if !rem[i] {
ans = append(ans, s[i])
}
}
return string(ans)
}
3189. 得到一个和平棋盘的最少步骤 🔒 ​
题目、示例与约束
给定一个长度为 n 的二维数组 rooks,其中 rooks[i] = [xi, yi] 表示 n x n 棋盘上一个车的位置。你的任务是每次在垂直或水平方向上移动 1 格 车(到一个相邻的格子)使得棋盘变得 和平。
如果每行每列都 只有 一个车,那么这块棋盘就是和平的。
返回获得和平棋盘所需的 最少 步数。
注意 任何时刻 两个车都不能在同一个格子。
示例 1:
输入:rooks = [[0,0],[1,0],[1,1]]
输出:3
解释:

示例 2:
输入:rooks = [[0,0],[0,1],[0,2],[0,3]]
输出:6
解释:

提示:
1 <= n == rooks.length <= 5000 <= xi, yi <= n - 1- 输入保证没有两个车在相同的格子。
方法一:贪心 · Go 实现
思考
车只能四向平移,目标是每行每列恰一车。行与列的冲突互相独立,二维匹配没有必要。
把车按行坐标排序后,第 小的行应落到行 ,列同理。交换任意两车的目标只会不减总步数。
两次排序分别累加 与 。曼哈顿步数在行列上可加。
我们可以将所有的车按照横坐标排序,然后将车按顺序分配给每一行,计算每个车到目标位置的距离之和。然后将所有的车按照纵坐标排序,按照同样的方法计算每个车到目标位置的距离之和。最后将两个距离之和相加即为答案。
时间复杂度 ,空间复杂度 。其中 为车的数量。
func minMoves(rooks [][]int) (ans int) {
sort.Slice(rooks, func(i, j int) bool { return rooks[i][0] < rooks[j][0] })
for i, row := range rooks {
ans += int(math.Abs(float64(row[0] - i)))
}
sort.Slice(rooks, func(i, j int) bool { return rooks[i][1] < rooks[j][1] })
for j, col := range rooks {
ans += int(math.Abs(float64(col[1] - j)))
}
return
}
3207. 与敌人战斗后的最大分数 ​
题目、示例与约束
给你一个下标从 0 开始的整数数组 enemyEnergies ,它表示一个下标从 0 开始的敌人能量数组。
同时给你一个整数 currentEnergy ,它表示你一开始拥有的能量值总量。
你一开始的分数为 0 ,且一开始所有的敌人都未标记。
你可以通过以下操作 之一 任意次(也可以 0 次)来得分:
- 选择一个 未标记 且满足
currentEnergy >= enemyEnergies[i]的敌人i。在这个操作中:- 你会获得
1分。 - 你的能量值减少
enemyEnergies[i],也就是说currentEnergy = currentEnergy - enemyEnergies[i]。
- 你会获得
- 如果你目前 至少 有
1分,你可以选择一个 未标记 的敌人i。在这个操作中:- 你的能量值增加
enemyEnergies[i],也就是说currentEnergy = currentEnergy + enemyEnergies[i]。 - 敌人
i被标记 。
- 你的能量值增加
请你返回通过以上操作,最多 可以获得多少分。
示例 1:
输入:enemyEnergies = [3,2,2], currentEnergy = 2
输出:3
解释:
通过以下操作可以得到最大得分 3 分:
- 对敌人 1 使用第一种操作:
points增加 1 ,currentEnergy减少 2 。所以points = 1且currentEnergy = 0。 - 对敌人 0 使用第二种操作:
currentEnergy增加 3 ,敌人 0 被标记。所以points = 1,currentEnergy = 3,被标记的敌人包括[0]。 - 对敌人 2 使用第一种操作:
points增加 1 ,currentEnergy减少 2 。所以points = 2且currentEnergy = 1,被标记的敌人包括[0]。 - 对敌人 2 使用第二种操作:
currentEnergy增加 2 ,敌人 2 被标记。所以points = 2,currentEnergy = 3且被标记的敌人包括[0, 2]。 - 对敌人 1 使用第一种操作:
points增加 1 ,currentEnergy减少 2 。所以points = 3,currentEnergy = 1,被标记的敌人包括[0, 2]。
示例 2:
输入:enemyEnergies = [2], currentEnergy = 10
输出:5
解释:
通过对敌人 0 进行第一种操作 5 次,得到最大得分。
提示:
1 <= enemyEnergies.length <= 1051 <= enemyEnergies[i] <= 1090 <= currentEnergy <= 109
方法一:贪心 + 排序 · Go 实现
思考
每次操作为「用当前能量整除某未标记敌人得分」或「消耗 分换取其全部能量」。 且能量可达 ,按回合逐步模拟次数过多。
得分操作应始终对准能量最小的敌人,能量补充则应取最大者。因此先排序,若当前能量已不足最小值则答案为 ;否则从大到小依次把最大敌人的能量并入,并在并入前用最小值尽量多地得分。排序后一遍扫描即可。
根据题目描述,我们每次需要通过具有最小能量值的敌人来获得分数,通过具有最大能量值的敌人来增加能量值并进行标记。
因此,我们可以对敌人的能量值进行排序,然后从能量值最大的敌人开始,每次都选择能量值最小的敌人来获得分数,并消耗能量值。然后,我们将能量值最大的敌人的能量值加到当前能量值上,并标记该敌人。重复上述操作,直到所有敌人都被标记。
时间复杂度 ,空间复杂度 。其中 是敌人的数量。
func maximumPoints(enemyEnergies []int, currentEnergy int) (ans int64) {
sort.Ints(enemyEnergies)
if currentEnergy < enemyEnergies[0] {
return 0
}
for i := len(enemyEnergies) - 1; i >= 0; i-- {
ans += int64(currentEnergy / enemyEnergies[0])
currentEnergy %= enemyEnergies[0]
currentEnergy += enemyEnergies[i]
}
return
}
3216. 交换后字典序最小的字符串 ​
题目、示例与约束
给你一个仅由数字组成的字符串 s,在最多交换一次 相邻 且具有相同 奇偶性 的数字后,返回可以得到的字典序最小的字符串。
如果两个数字都是奇数或都是偶数,则它们具有相同的奇偶性。例如,5 和 9、2 和 4 奇偶性相同,而 6 和 9 奇偶性不同。
示例 1:
输入: s = "45320"
输出: "43520"
解释:
s[1] == '5' 和 s[2] == '3' 都具有相同的奇偶性,交换它们可以得到字典序最小的字符串。
示例 2:
输入: s = "001"
输出: "001"
解释:
无需进行交换,因为 s 已经是字典序最小的。
提示:
2 <= s.length <= 100s仅由数字组成。
方法一:贪心 + 模拟 · Go 实现
思考
至多交换一次相邻且同奇偶的数字。 ,枚举所有合法交换再取最小串可以接受,但字典序最优交换必在最左侧。
从左到右找第一对「同奇偶且左大于右」的相邻位并交换,其后不必再看:更靠右的交换无法改动已经更优的前缀。若无此对则原串已最小。
我们可以从左到右遍历字符串 ,对于每一对相邻的数字,如果它们具有相同的奇偶性且前一个数字大于后一个数字,那么我们就交换这两个数字,使得字符串 的字典序变小,然后返回交换后的字符串。
遍历结束后,如果没有找到可以交换的数字对,说明字符串 已经是字典序最小的,直接返回即可。
时间复杂度 ,空间复杂度 。其中 为字符串 的长度。
func getSmallestString(s string) string {
cs := []byte(s)
n := len(cs)
for i := 1; i < n; i++ {
a, b := cs[i-1], cs[i]
if a > b && a%2 == b%2 {
cs[i-1], cs[i] = b, a
return string(cs)
}
}
return s
}
3219. 切蛋糕的最小总开销 II ​
题目、示例与约束
有一个 m x n 大小的矩形蛋糕,需要切成 1 x 1 的小块。
给你整数 m ,n 和两个数组:
horizontalCut的大小为m - 1,其中horizontalCut[i]表示沿着水平线i切蛋糕的开销。verticalCut的大小为n - 1,其中verticalCut[j]表示沿着垂直线j切蛋糕的开销。
一次操作中,你可以选择任意不是 1 x 1 大小的矩形蛋糕并执行以下操作之一:
- 沿着水平线
i切开蛋糕,开销为horizontalCut[i]。 - 沿着垂直线
j切开蛋糕,开销为verticalCut[j]。
每次操作后,这块蛋糕都被切成两个独立的小蛋糕。
每次操作的开销都为最开始对应切割线的开销,并且不会改变。
请你返回将蛋糕全部切成 1 x 1 的蛋糕块的 最小 总开销。
示例 1:
输入:m = 3, n = 2, horizontalCut = [1,3], verticalCut = [5]
输出:13
解释:

- 沿着垂直线 0 切开蛋糕,开销为 5 。
- 沿着水平线 0 切开
3 x 1的蛋糕块,开销为 1 。 - 沿着水平线 0 切开
3 x 1的蛋糕块,开销为 1 。 - 沿着水平线 1 切开
2 x 1的蛋糕块,开销为 3 。 - 沿着水平线 1 切开
2 x 1的蛋糕块,开销为 3 。
总开销为 5 + 1 + 1 + 3 + 3 = 13 。
示例 2:
输入:m = 2, n = 2, horizontalCut = [7], verticalCut = [4]
输出:15
解释:
- 沿着水平线 0 切开蛋糕,开销为 7 。
- 沿着垂直线 0 切开
1 x 2的蛋糕块,开销为 4 。 - 沿着垂直线 0 切开
1 x 2的蛋糕块,开销为 4 。
总开销为 7 + 4 + 4 = 15 。
提示:
1 <= m, n <= 105horizontalCut.length == m - 1verticalCut.length == n - 11 <= horizontalCut[i], verticalCut[i] <= 103
方法一:贪心 + 双指针 · Go 实现
思考
题意与切蛋糕 I 相同,但 ,指数或平方 DP 不再可行,必须沿用「大费用先切」的线性合并。
仍将横竖费用降序,双指针每次切当前更大的一边并累加 或 倍费用。排序 ,之后线性,正好匹配本题规模。
对于一个位置,越早切,所需要切的次数越少,因此,显然是开销越大的位置越早切。
所以,我们可以对数组 和 按照从大到小的顺序排序,然后使用两个指针 和 分别指向 和 的开销,每次选择开销较大的位置进行切割,同时更新对应的行数和列数。
每次在水平方向上切割时,如果此前列数为 ,那么此次的开销为 ,然后行数 加一;同理,每次在垂直方向上切割时,如果此前行数为 ,那么此次的开销为 ,然后列数 加一。
最后,当 和 都到达末尾时,返回总开销即可。
时间复杂度 ,空间复杂度 。其中 和 分别为 和 的长度。
func minimumCost(m int, n int, horizontalCut []int, verticalCut []int) (ans int64) {
sort.Sort(sort.Reverse(sort.IntSlice(horizontalCut)))
sort.Sort(sort.Reverse(sort.IntSlice(verticalCut)))
i, j := 0, 0
h, v := 1, 1
for i < m-1 || j < n-1 {
if j == n-1 || (i < m-1 && horizontalCut[i] > verticalCut[j]) {
ans += int64(horizontalCut[i] * v)
h++
i++
} else {
ans += int64(verticalCut[j] * h)
v++
j++
}
}
return
}
3221. 最大数组跳跃得分 II 🔒 ​
题目、示例与约束
给定一个数组 nums,你必须从索引 0 开始跳跃,直到到达数组的最后一个元素,使得获取 最大 分数。
每一次 跳跃 中,你可以从下标 i 跳到一个 j > i 的下标,并且可以得到 (j - i) * nums[j] 的分数。
返回你能够取得的最大分数。
示例 1:
输入:nums = [1,5,8]
输出:16
解释:
有两种可能的方法可以到达最后一个元素:
0 -> 1 -> 2得分为(1 - 0) * 5 + (2 - 1) * 8 = 13。0 -> 2得分为(2 - 0) * 8 = 16。
示例 2:
输入:nums = [4,5,2,8,9,1,3]
输出:42
解释:
我们可以按 0 -> 4 -> 6 进行跳跃,得分为 (4 - 0) * 9 + (6 - 4) * 3 = 42。
提示:
2 <= nums.length <= 1051 <= nums[i] <= 105
方法一:单调栈 · Go 实现
思考
跳跃得分规则与「数组跳跃得分 I」相同,但本题规模更大,平方 DP 无法使用。最优仍是每次跳到后方下一个不小于当前值的位置。
单调递减栈扫出这条下标链,再从 沿栈中下标累加 。线性时间,无需记忆化。
我们观察发现,对于当前位置 ,我们应该跳到下一个值最大的位置 ,这样才能获得最大的分数。
因此,我们遍历数组 ,维护一个从栈底到栈顶单调递减的栈 。对于当前遍历到的位置 ,如果栈顶元素对应的值小于等于 ,我们就不断地弹出栈顶元素,直到栈为空或者栈顶元素对应的值大于 ,然后将 入栈。
然后,我们初始化答案 和当前位置 ,遍历栈中的元素,每次取出栈顶元素 ,更新答案 ,然后更新 。
最后返回答案 。
时间复杂度 ,空间复杂度 。其中 是数组的长度。
func maxScore(nums []int) (ans int64) {
stk := []int{}
for i, x := range nums {
for len(stk) > 0 && nums[stk[len(stk)-1]] <= x {
stk = stk[:len(stk)-1]
}
stk = append(stk, i)
}
i := 0
for _, j := range stk {
ans += int64((j - i) * nums[j])
i = j
}
return
}
3228. 将 1 移动到末尾的最大操作次数 ​
题目、示例与约束
给你一个 二进制字符串 s。
你可以对这个字符串执行 任意次 下述操作:
- 选择字符串中的任一下标
i(i + 1 < s.length),该下标满足s[i] == '1'且s[i + 1] == '0'。 - 将字符
s[i]向 右移 直到它到达字符串的末端或另一个'1'。例如,对于s = "010010",如果我们选择i = 1,结果字符串将会是s = "000110"。
返回你能执行的 最大 操作次数。
示例 1:
输入: s = "1001101"
输出: 4
解释:
可以执行以下操作:
- 选择下标
i = 0。结果字符串为s = "0011101"。 - 选择下标
i = 4。结果字符串为s = "0011011"。 - 选择下标
i = 3。结果字符串为s = "0010111"。 - 选择下标
i = 2。结果字符串为s = "0001111"。
示例 2:
输入: s = "00111"
输出: 0
提示:
1 <= s.length <= 105s[i]为'0'或'1'。
方法一:贪心 · Go 实现
思考
每次把某个 变成 ,即把一个 右移一格。 ,模拟每一步移位次数可能达平方。一次操作对应「某个 越过其后的一个 」,且同一段 可以反复越过后续每个 段。
从左统计已出现的 的个数 ,每当遇到由 转入 的边界,这 个 都可以再向右移动一次,答案加上 。一遍线性。
我们用一个变量 记录答案,用一个变量 记录当前的 的个数。
然后我们遍历字符串 ,如果当前字符是 ,则 加一,否则如果存在前一个字符,且前一个字符是 ,那么前面的 个 可以往后移动,答案加上 。
最后返回答案即可。
时间复杂度 ,其中 为字符串 的长度。空间复杂度 。
func maxOperations(s string) (ans int) {
cnt := 0
for i, c := range s {
if c == '1' {
cnt++
} else if i > 0 && s[i-1] == '1' {
ans += cnt
}
}
return
}
3273. 对 Bob 造成的最少伤害 ​
题目、示例与约束
给你一个整数 power 和两个整数数组 damage 和 health ,两个数组的长度都为 n 。
Bob 有 n 个敌人,如果第 i 个敌人还活着(也就是健康值 health[i] > 0 的时候),每秒钟会对 Bob 造成 damage[i] 点 伤害。
每一秒中,在敌人对 Bob 造成伤害 之后 ,Bob 会选择 一个 还活着的敌人进行攻击,该敌人的健康值减少 power 。
请你返回 Bob 将 所有 n 个敌人都消灭之前,最少 会受到多少伤害。
示例 1:
输入:power = 4, damage = [1,2,3,4], health = [4,5,6,8]
输出:39
解释:
- 最开始 2 秒内都攻击敌人 3 ,然后敌人 3 会被消灭,这段时间内对 Bob 的总伤害是
10 + 10 = 20点。 - 接下来 2 秒内都攻击敌人 2 ,然后敌人 2 会被消灭,这段时间内对 Bob 的总伤害是
6 + 6 = 12点。 - 接下来 1 秒内都攻击敌人 0 ,然后敌人 0 会被消灭,这段时间内对 Bob 的总伤害是
3点。 - 接下来 2 秒内都攻击敌人 1 ,然后敌人 1 会被消灭,这段时间内对 Bob 的总伤害是
2 + 2 = 4点。
示例 2:
输入:power = 1, damage = [1,1,1,1], health = [1,2,3,4]
输出:20
解释:
- 最开始 1 秒内都攻击敌人 0 ,然后敌人 0 会被消灭,这段时间对 Bob 的总伤害是
4点。 - 接下来 2 秒内都攻击敌人 1 ,然后敌人 1 会被消灭,这段时间对 Bob 的总伤害是
3 + 3 = 6点。 - 接下来 3 秒内都攻击敌人 2 ,然后敌人 2 会被消灭,这段时间对 Bob 的总伤害是
2 + 2 + 2 = 6点。 - 接下来 4 秒内都攻击敌人 3 ,然后敌人 3 会被消灭,这段时间对 Bob 的总伤害是
1 + 1 + 1 + 1 = 4点。
示例 3:
输入:power = 8, damage = [40], health = [59]
输出:320
提示:
1 <= power <= 1041 <= n == damage.length == health.length <= 1051 <= damage[i], health[i] <= 104
方法一:Go 实现(灵茶山艾府) · Go 实现
灵茶山艾府 · 源码 · MIT 许可 · 原题解
首先,一直攻击同一个敌人,相比来回攻击多个敌人(雨露均沾)更好,因为这样我们被敌人攻击的次数更少。
从特殊到一般,想一想,如果只有两个敌人 A 和 B,我们应该先攻击谁?
消灭 A 需要攻击的次数为
证明见 上取整下取整转换公式的证明。
同理可得消灭 B 需要的攻击次数,记作 。
如果先消灭 A,再消灭 B,那么受到的伤害总和为
如果先消灭 B,再消灭 A,那么受到的伤害总和为
如果先消灭 A,再消灭 B 更好,那么有
化简得
也就是优先消灭 更小的敌人。结合你玩过的某些游戏,应当优先消灭那个又脆皮伤害又高的敌人。
推广到更多的敌人,可以按照上式对 和 排序,理由如下。
先假定按照输入的顺序消灭敌人。如果发现相邻两个敌人不满足上面的不等式,就交换这两个敌人的位置,这可以让受到的总伤害变小。
不断交换敌人,直到所有相邻敌人都满足上面的不等式。
本质上来说,这个不断交换相邻敌人的过程,和冒泡排序是一样的。那么按照不等式对数组排序即可。
排序后,按照顺序消灭敌人。用一个变量 维护从一开始到击败当前敌人,所经过的秒数。把 加入答案。
具体请看 视频讲解 第四题,欢迎点赞关注~
答疑
问:本题可以用 比大小吗?除法的结果需要用浮点数表示,精度误差是否会影响结果?
答:本题 的值域范围是可以的,但如果范围是 就可能会有问题,见 2280. 表示一个折线图的最少线段数,可以用浮点数写试试。
复杂度分析
- 时间复杂度:,其中 是 的长度。瓶颈在排序上。
- 时间复杂度:。
专题训练
见下面贪心题单中的「§1.7 交换论证法」。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府
import "slices"
// https://space.bilibili.com/206214
func minDamage(power int, damage, health []int) (ans int64) {
type pair struct{ k, d int }
a := make([]pair, len(health))
for i, h := range health {
a[i] = pair{(h-1)/power + 1, damage[i]}
}
slices.SortFunc(a, func(p, q pair) int { return p.k*q.d - q.k*p.d })
s := 0
for _, p := range a {
s += p.k
ans += int64(s) * int64(p.d)
}
return
}
3281. 范围内整数的最大得分 ​
题目、示例与约束
给你一个整数数组 start 和一个整数 d,代表 n 个区间 [start[i], start[i] + d]。
你需要选择 n 个整数,其中第 i 个整数必须属于第 i 个区间。所选整数的 得分 定义为所选整数两两之间的 最小 绝对差。
返回所选整数的 最大可能得分 。
示例 1:
输入: start = [6,0,3], d = 2
输出: 4
解释:
可以选择整数 8, 0 和 4 获得最大可能得分,得分为 min(|8 - 0|, |8 - 4|, |0 - 4|),等于 4。
示例 2:
输入: start = [2,6,13,13], d = 5
输出: 5
解释:
可以选择整数 2, 7, 13 和 18 获得最大可能得分,得分为 min(|2 - 7|, |2 - 13|, |2 - 18|, |7 - 13|, |7 - 18|, |13 - 18|),等于 5。
提示:
2 <= start.length <= 1050 <= start[i] <= 1090 <= d <= 109
方法一:排序 + 二分查找 · Go 实现
思考
每个区间 选一个整数,最大化相邻所选值之差的最小值。 ,枚举选值不可行。排序后该最小差具有单调性:能达到 则也能达到更小值。
二分 ,从左贪心:在区间内选不小于 的最小可行点,选不出则失败。左闭右开二分最大可行 。
我们不妨先对 数组进行排序,然后我们考虑从左到右选择整数,那么得分就等于我们选择的相邻两个整数的差值的最小值。
如果一个差值 满足条件,那么对于任意 ,也一定满足条件。因此我们可以使用二分查找的方法来找到最大的满足条件的差值。
我们定义二分查找的左边界 ,右边界 ,然后我们每次取中间值 ,然后判断是否满足条件。
我们定义一个函数 来判断是否满足条件,具体实现如下:
- 我们定义一个变量 ,表示上一个选择的整数。
- 我们遍历 数组,如果 ,那么说明我们无法选择整数 ,返回 。否则,我们更新 。
- 如果遍历完整个 数组都满足条件,那么返回 。
时间复杂度 ,其中 和 分别是 数组的长度和最大值。空间复杂度 。
func maxPossibleScore(start []int, d int) int {
check := func(mi int) bool {
last := math.MinInt64
for _, st := range start {
if last+mi > st+d {
return false
}
last = max(st, last+mi)
}
return true
}
sort.Ints(start)
l, r := 0, start[len(start)-1]+d-start[0]
for l < r {
mid := (l + r + 1) >> 1
if check(mid) {
l = mid
} else {
r = mid - 1
}
}
return l
}
3282. 到达数组末尾的最大得分 ​
题目、示例与约束
给你一个长度为 n 的整数数组 nums 。
你的目标是从下标 0 出发,到达下标 n - 1 处。每次你只能移动到 更大 的下标处。
从下标 i 跳到下标 j 的得分为 (j - i) * nums[i] 。
请你返回你到达最后一个下标处能得到的 最大总得分 。
示例 1:
输入:nums = [1,3,1,5]
输出:7
解释:
一开始跳到下标 1 处,然后跳到最后一个下标处。总得分为 1 * 1 + 2 * 3 = 7 。
示例 2:
输入:nums = [4,3,1,3,2]
输出:16
解释:
直接跳到最后一个下标处。总得分为 4 * 4 = 16 。
提示:
1 <= nums.length <= 1051 <= nums[i] <= 105
方法一:贪心 · Go 实现
思考
从 跳到 得 ,可理解为中间每走一格都加上 。 ,若某次跳到更小的 再出发,中间那些格用 计价更优,故不应跳到更小值。
维护从左到当前的最大值 ,对除终点外的每一格累加 。等价于总是在「历史最大」上停留到必须离开。线性。
假设我们从下标 ,跳到下标 ,那么得分为 。这相当于我们走了 步,每一步都得到了 的得分。然后我们从 继续跳到下一个下标 ,得分为 ,以此类推。如果 ,那么我们就不应该从 跳到 ,因为这样得到的得分一定比从 直接跳到 得到的得分要少。因此,我们每一次应该跳到下一个比当前下标对应的值更大的下标。
我们可以维护一个变量 ,表示当前为止,我们遇到的最大的 的值。然后我们从左到右遍历数组,直到倒数第二个元素,每次更新 ,并且累加得分。
遍历结束后,我们得到的就是最大的总得分。
时间复杂度 ,其中 为数组 的长度。空间复杂度 。
func findMaximumScore(nums []int) (ans int64) {
mx := 0
for _, x := range nums[:len(nums)-1] {
mx = max(mx, x)
ans += int64(mx)
}
return
}
3296. 移山所需的最少秒数 ​
题目、示例与约束
给你一个整数 mountainHeight 表示山的高度。
同时给你一个整数数组 workerTimes,表示工人们的工作时间(单位:秒)。
工人们需要 同时 进行工作以 降低 山的高度。对于工人 i :
- 山的高度降低 1,需要
workerTimes[i]秒。 - 山的高度降低 2,需要
workerTimes[i] * 2秒。 - ...
- 山的高度降低
x,需要workerTimes[i] * x秒。
工人 i 所花费的总时间是所有 x 单位所需时间的总和。由于所有工人同时操作,所需的总时间是任何工人花费的 最大 时间。
返回一个整数,表示工人们使山的高度降低到 0 所需的 最少 秒数。
示例 1:
输入: mountainHeight = 4, workerTimes = [2,1,1]
输出: 3
解释:
将山的高度降低到 0 的一种方式是:
- 工人 0 将高度降低 1,花费
workerTimes[0] = 2秒。 - 工人 1 将高度降低 2,花费
workerTimes[1] + workerTimes[1] * 2 = 3秒。 - 工人 2 将高度降低 1,花费
workerTimes[2] = 1秒。
因为工人同时工作,所需的最少时间为 max(2, 3, 1) = 3 秒。
示例 2:
输入: mountainHeight = 10, workerTimes = [3,2,2,4]
输出: 12
解释:
- 工人 0 将高度降低 2,花费
workerTimes[0] + workerTimes[0] * 2 = 9秒。 - 工人 1 将高度降低 3,花费
workerTimes[1] + workerTimes[1] * 2 + workerTimes[1] * 3 = 12秒。 - 工人 2 将高度降低 3,花费
workerTimes[2] + workerTimes[2] * 2 + workerTimes[2] * 3 = 12秒。 - 工人 3 将高度降低 2,花费
workerTimes[3] + workerTimes[3] * 2 = 12秒。
所需的最少时间为 max(9, 12, 12, 12) = 12 秒。
示例 3:
输入: mountainHeight = 5, workerTimes = [1]
输出: 15
解释:
这个示例中只有一个工人,所以答案是 workerTimes[0] + workerTimes[0] * 2 + workerTimes[0] * 3 + workerTimes[0] * 4 + workerTimes[0] * 5 = 15 秒。
提示:
1 <= mountainHeight <= 1051 <= workerTimes.length <= 1041 <= workerTimes[i] <= 106
方法一:二分查找 · Go 实现
思考
工人 降低 层耗时 ,可并行,求使总降低量达到 的最短时间。 、工人 ,枚举时间不可行。时间越大完成量越大,可二分。
对每个工人解不等式得到其在 内最多降低的层数并求和。二分上界取足够大的常数(如 ),
bisect_left找最小可行 。
我们注意到,如果所有的工人能在 秒内将山的高度降低到 ,那么对于任意 ,工人们也能在 秒内将山的高度降低到 。因此,我们可以使用二分查找的方法找到最小的 ,使得工人们能在 秒内将山的高度降低到 。
我们定义一个函数 ,表示工人们能否在 秒内将山的高度降低到 。具体地,我们遍历每一个工人,对于当前工人 ,假设他在 秒内降低了 的高度,那么我们可以得到一个不等式:
解不等式得到:
我们可以将所有工人的 相加,得到总共降低的高度 ,如果 ,那么说明工人们能在 秒内将山的高度降低到 。
接下来,我们确定二分查找的左边界 ,由于最少有一个工作,且每个工人的工作时间不超过 ,要想使山的高度降低到 ,最少需要 秒,因此我们可以确定二分查找的右边界 。然后我们不断地将区间 二分,直到 ,此时 即为答案。
时间复杂度 ,其中 为工人的数量,而 为二分查找的右边界,本题中 。空间复杂度 。
func minNumberOfSeconds(mountainHeight int, workerTimes []int) int64 {
return int64(sort.Search(1e16, func(t int) bool {
var h int64
for _, wt := range workerTimes {
h += int64(math.Sqrt(float64(t)*2.0/float64(wt)+0.25) - 0.5)
}
return h >= int64(mountainHeight)
}))
}
3301. 高度互不相同的最大塔高和 ​
题目、示例与约束
给你一个数组 maximumHeight ,其中 maximumHeight[i] 表示第 i 座塔可以达到的 最大 高度。
你的任务是给每一座塔分别设置一个高度,使得:
- 第
i座塔的高度是一个正整数,且不超过maximumHeight[i]。 - 所有塔的高度互不相同。
请你返回设置完所有塔的高度后,可以达到的 最大 总高度。如果没有合法的设置,返回 -1 。
示例 1:
输入:maximumHeight = [2,3,4,3]
输出:10
解释:
我们可以将塔的高度设置为:[1, 2, 4, 3] 。
示例 2:
输入:maximumHeight = [15,10]
输出:25
解释:
我们可以将塔的高度设置为:[15, 10] 。
示例 3:
输入:maximumHeight = [2,2,1]
输出:-1
解释:
无法设置塔的高度为正整数且高度互不相同。
提示:
1 <= maximumHeight.length <= 1051 <= maximumHeight[i] <= 109
方法一:排序 + 贪心 · Go 实现
思考
各塔高度须互不相同且不超过各自上界。 ,枚举分配方案不可行。若从矮塔开始占位,高塔可能失去本可使用的较大高度。
总和最大要求优先把更大的整数分给上界更大的塔。将 降序处理后,用 记录上一座已分配高度。
当前塔取 :既不超过自身上界,又与已分配高度严格递减。若得到非正值,则无法构造合法方案。
我们可以将塔的最大高度按照从大到小排序,然后从最大高度开始逐个分配高度,用一个变量 记录当前分配的最大高度。
如果当前高度 大于 ,则将 更新为 。此时如果 小于等于 ,说明无法分配高度,直接返回 。否则,我们将 加到答案中,并更新 为 。
最后返回答案即可。
时间复杂度 ,空间复杂度 。其中 是数组 的长度。
func maximumTotalSum(maximumHeight []int) int64 {
slices.SortFunc(maximumHeight, func(a, b int) int { return b - a })
ans := int64(0)
mx := 1 << 30
for _, x := range maximumHeight {
x = min(x, mx-1)
if x <= 0 {
return -1
}
ans += int64(x)
mx = x
}
return ans
}
3326. 使数组非递减的最少除法操作次数 ​
题目、示例与约束
给你一个整数数组 nums 。
一个正整数 x 的任何一个 严格小于 x 的 正 因子都被称为 x 的 真因数 。比方说 2 是 4 的 真因数,但 6 不是 6 的 真因数。
你可以对 nums 的任何数字做任意次 操作 ,一次 操作 中,你可以选择 nums 中的任意一个元素,将它除以它的 最大真因数 。
你的目标是将数组变为 非递减 的,请你返回达成这一目标需要的 最少操作 次数。
如果 无法 将数组变成非递减的,请你返回 -1 。
示例 1:
输入:nums = [25,7]
输出:1
解释:
通过一次操作,25 除以 5 ,nums 变为 [5, 7] 。
示例 2:
输入:nums = [7,7,6]
输出:-1
示例 3:
输入:nums = [1,1,1,1]
输出:0
提示:
1 <= nums.length <= 1051 <= nums[i] <= 106
方法一:预处理 + 贪心 · Go 实现
思考
每次把 除以一个真因数。质数无法再变小;合数除以最大真因数后变成最小质因数,此后不能再除。 、 ,需预处理最小质因数。
从左往右改会破坏尚未确定的右侧约束。从右往左看,右边已经固定为最终值,左边若更大则只能立刻变成 。
若变成最小质因数后仍大于右侧,则无法非递减。每个位置至多操作一次,故从右往左扫描并计数即可。
根据题目描述,
如果整数 是质数,那么它的最大真因数是 ,那么 ,即 不能再被除了;
如果整数 不是质数,我们假设 的最大真因数为 ,那么 一定是质数,因此,我们寻找最小质数 ,使得 ,使得 变成 ,此时无法再被除了。
因此,我们可以预处理出 到 的每个整数的最小质因数,然后从右往左遍历数组,如果当前元素大于下一个元素,我们将当前元素变为它的最小质因数,如果当前元素变为它的最小质因数后,仍然大于下一个元素,说明无法将数组变成非递减的,返回 。否则,操作次数加一。继续遍历,直到遍历完整个数组。
预处理的时间复杂度为 ,其中 ,遍历数组的时间复杂度为 ,其中 为数组的长度。空间复杂度为 。
const mx int = 1e6
var lpf = [mx + 1]int{}
func init() {
for i := 2; i <= mx; i++ {
if lpf[i] == 0 {
for j := i; j <= mx; j += i {
if lpf[j] == 0 {
lpf[j] = i
}
}
}
}
}
func minOperations(nums []int) (ans int) {
for i := len(nums) - 2; i >= 0; i-- {
if nums[i] > nums[i+1] {
nums[i] = lpf[nums[i]]
if nums[i] > nums[i+1] {
return -1
}
ans++
}
}
return
}
3357. 最小化相邻元素的最大差值 ​
题目、示例与约束
给你一个整数数组 nums 。nums 中的一些值 缺失 了,缺失的元素标记为 -1 。
你需要选择 一个正 整数数对 (x, y) ,并将 nums 中每一个 缺失 元素用 x 或者 y 替换。
你的任务是替换 nums 中的所有缺失元素,最小化 替换后数组中相邻元素 绝对差值 的 最大值 。
请你返回上述要求下的 最小值 。
示例 1:
输入:nums = [1,2,-1,10,8]
输出:4
解释:
选择数对 (6, 7) ,nums 变为 [1, 2, 6, 10, 8] 。
相邻元素的绝对差值分别为:
|1 - 2| == 1|2 - 6| == 4|6 - 10| == 4|10 - 8| == 2
示例 2:
输入:nums = [-1,-1,-1]
输出:0
解释:
选择数对 (4, 4) ,nums 变为 [4, 4, 4] 。
示例 3:
输入:nums = [-1,10,-1,8]
输出:1
解释:
选择数对 (11, 9) ,nums 变为 [11, 10, 9, 8] 。
提示:
2 <= nums.length <= 105nums[i]要么是 -1 ,要么是范围[1, 109]中的一个整数。
方法一:Go 实现(灵茶山艾府) · Go 实现
灵茶山艾府 · 源码 · MIT 许可 · 原题解
从特殊到一般。
没有连续 -1 的情况
下文用下划线 表示 (空位)。
先来看这个例子:
如果只能填一种数字,应该填什么?
考虑到填入的数字要和 计算绝对差,为了让最大绝对差最小,可以都填 或者 ,最大绝对差为 。填其他数字只会让最大绝对差更大。
如果能填两种数字呢?
考虑把空位分成两组: 和 。
- 靠近最小的空位 ,可以都填 ,最大绝对差为 。
- 靠近最大的空位 ,可以都填 或者 ,最大绝对差也为 。
如果改成(更加一般的情况)
同样地:
- 左边两个空位所填数字,绝对差和 有关,可以都填 ,最大绝对差为 。
- 右边两个空位所填数字,绝对差和 有关,可以都填 或者 ,最大绝对差也为 。
一般地,在没有连续 的情况下,我们可以:
- 先找到和空位相邻的最小数字 ,最大数字 。
- 对于任意空位,设其左右两侧数字为 和 (),如果 离 比 离 更近,即 ,那么绝对差和 有关,取二者中间值,绝对差为 ;否则,绝对差和 有关,取二者中间值,绝对差为 。
两种情况可以整合为
用上式更新答案的最大值。
有连续 -1 的情况
如果连续空位都填 或者都填 ,那么做法同上。例如数组中有一段 ,上面定义的 和 就分别是 和 。按照上面给出的公式计算最大绝对差。
但是,还可以在连续空位中填入两个不同的数 和 ,这可能得到更小的绝对差。
一旦我们填了两个不同的数,就不能再填其他数字了。
设我们填入的两个数分别为 和 ()。
由于 都和空位相邻,这两个数必须和 计算绝对差。
谁和 计算绝对差?谁和 计算绝对差?
由于 ,为了让答案尽量小, 和 计算绝对差 , 和 计算绝对差 。
我们的目标是让 尽量小。
例如 ,那么令 ,得 ,所以 。
一般地,取 和 之间的三等分点,可以得到
也可以这样理解,在不存在孤立 的情况下,上式相当于答案的上界。
总结:
- 连续空位都填 或者都填 ,最大绝对差为 。
- 连续空位填 以及 ,最大绝对差为 。
二者取最小值,得
用上式更新答案的最大值。
复杂度分析
- 时间复杂度:,其中 是 的长度。
- 空间复杂度:。
思考题
如果可以把 改成三个数 中的一个呢?可以改成 个数中的一个呢?
更多相似题目,见下面贪心题单中的「区间贪心」。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
- 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 【本题相关】贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、二叉树与一般树(前后指针/快慢指针/DFS/BFS/直径/LCA)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
import "math"
// https://space.bilibili.com/206214
func minDifference(nums []int) (ans int) {
// 和空位相邻的最小数字 minL 和最大数字 maxR
minL, maxR := math.MaxInt, 0
for i, v := range nums {
if v != -1 && (i > 0 && nums[i-1] == -1 || i < len(nums)-1 && nums[i+1] == -1) {
minL = min(minL, v)
maxR = max(maxR, v)
}
}
preI := -1
for i, v := range nums {
if v == -1 {
continue
}
if preI >= 0 {
if i-preI == 1 {
ans = max(ans, abs(v-nums[preI]))
} else {
l, r := min(nums[preI], v), max(nums[preI], v)
d := (min(r-minL, maxR-l) + 1) / 2
if i-preI > 2 {
d = min(d, (maxR-minL+2)/3) // d 不能超过上界
}
ans = max(ans, d)
}
}
preI = i
}
return
}
func abs(x int) int { if x < 0 { return -x }; return x }
3362. 零数组变换 III ​
题目、示例与约束
给你一个长度为 n 的整数数组 nums 和一个二维数组 queries ,其中 queries[i] = [li, ri] 。
每一个 queries[i] 表示对于 nums 的以下操作:
- 将
nums中下标在范围[li, ri]之间的每一个元素 最多 减少 1 。 - 坐标范围内每一个元素减少的值相互 独立 。
零数组 指的是一个数组里所有元素都等于 0 。
请你返回 最多 可以从 queries 中删除多少个元素,使得 queries 中剩下的元素仍然能将 nums 变为一个 零数组 。如果无法将 nums 变为一个 零数组 ,返回 -1 。
示例 1:
输入:nums = [2,0,2], queries = [[0,2],[0,2],[1,1]]
输出:1
解释:
删除 queries[2] 后,nums 仍然可以变为零数组。
- 对于
queries[0],将nums[0]和nums[2]减少 1 ,将nums[1]减少 0 。 - 对于
queries[1],将nums[0]和nums[2]减少 1 ,将nums[1]减少 0 。
示例 2:
输入:nums = [1,1,1,1], queries = [[1,3],[0,2],[1,3],[1,2]]
输出:2
解释:
可以删除 queries[2] 和 queries[3] 。
示例 3:
输入:nums = [1,2,3,4], queries = [[0,3]]
输出:-1
解释:
nums 无法通过 queries 变成零数组。
提示:
1 <= nums.length <= 1050 <= nums[i] <= 1051 <= queries.length <= 105queries[i].length == 20 <= li <= ri < nums.length
方法一:贪心 + 差分数组 + 优先队列 · Go 实现
思考
与前两题不同,询问可任选子集,目标是尽量少用(即多剩)。每位 需要至少 次覆盖,否则无解。
按左端排序后扫描 :把已开始的区间右端丢进最大堆。仅当当前覆盖不足时,才取出仍覆盖 的最晚结束区间。
最晚结束的区间对后续更有用,因此贪心保留它们到不得不取之时。差分数组在 处回退覆盖。堆中剩余即为未使用的询问数。
我们希望尽可能多地「移除」区间查询,但又要保证对每个位置 来说,被选中、覆盖到它的查询次数 至少达到原数组值 ,这样才能把该位置的值“减”到 0 或以下。如果对于某个位置 无法满足 ,就说明再多选任何查询都不可能让该位置归零,返回 。
为了做到这一点,我们按查询区间的左端点从小到大遍历,并维护:
- 差分数组
d:用于记录当前已应用的查询在何处分界——当我们在区间 上“应用”一次查询时,立刻在差分数组位置d[l] += 1,并在d[r+1] -= 1,这样在遍历到下标 时累加前缀和就能知道有多少次查询覆盖了 。 - 最大堆
pq:存放当前「候选」区间查询的右端点(取负数以便 Python 最小堆模拟最大堆)。为什么选「最晚结束」的区间?因为它能覆盖更远的位置,我们的贪心策略是:在每个 处,只在必要时才摘取堆顶最长的区间来增加一次覆盖,这样能为后续位置保留更多可用的区间。
具体步骤如下:
-
先对
queries按左端点l升序排序; -
初始化差分数组
d长度为n+1(用来处理r+1处的减操作),以及当前覆盖次数s=0、堆指针j=0; -
从 遍历到 :
-
先把
d[i]累加到s,即时更新已有的覆盖次数; -
将所有左端点 的查询 压入最大堆
pq(存-r),并推进j; -
当当前覆盖次数
s小于所需值nums[i],且堆不空且堆顶区间仍能覆盖 (即 )时:- 弹出堆顶(最长的区间),等价于“应用”这次查询;
- 令
s += 1并在d[r+1] -= 1(使得在跨过r后覆盖次数自动回退);
-
重复上述步骤,直到
s>=nums[i]或无法再选区间; -
若此时
s<nums[i],说明无法将位置 归零,直接返回 。
-
-
全部遍历完毕后,剩在堆里的区间都是「未被弹出」的,也就是真正被保留(即未被用来完成“归零”任务)的查询。堆大小即为答案。
时间复杂度 ,空间复杂度 ,其中 是数组的长度,而 是查询的个数。
func maxRemoval(nums []int, queries [][]int) int {
sort.Slice(queries, func(i, j int) bool {
return queries[i][0] < queries[j][0]
})
var h hp
heap.Init(&h)
n := len(nums)
d := make([]int, n+1)
s, j := 0, 0
for i := 0; i < n; i++ {
s += d[i]
for j < len(queries) && queries[j][0] <= i {
heap.Push(&h, queries[j][1])
j++
}
for s < nums[i] && h.Len() > 0 && h.IntSlice[0] >= i {
s++
end := heap.Pop(&h).(int)
if end+1 < len(d) {
d[end+1]--
}
}
if s < nums[i] {
return -1
}
}
return h.Len()
}
type hp struct{ sort.IntSlice }
func (h hp) Less(i, j int) bool { return h.IntSlice[i] > h.IntSlice[j] }
func (h *hp) Push(v any) { h.IntSlice = append(h.IntSlice, v.(int)) }
func (h *hp) Pop() any {
a := h.IntSlice
v := a[len(a)-1]
h.IntSlice = a[:len(a)-1]
return v
}
3397. 执行操作后不同元素的最大数量 ​
题目、示例与约束
给你一个整数数组 nums 和一个整数 k。
你可以对数组中的每个元素 最多 执行 一次 以下操作:
- 将一个在范围
[-k, k]内的整数加到该元素上。
返回执行这些操作后,nums 中可能拥有的不同元素的 最大 数量。
示例 1:
输入: nums = [1,2,2,3,3,4], k = 2
输出: 6
解释:
对前四个元素执行操作,nums 变为 [-1, 0, 1, 2, 3, 4],可以获得 6 个不同的元素。
示例 2:
输入: nums = [4,4,4,4], k = 1
输出: 3
解释:
对 nums[0] 加 -1,以及对 nums[1] 加 1,nums 变为 [3, 5, 4, 4],可以获得 3 个不同的元素。
提示:
1 <= nums.length <= 1051 <= nums[i] <= 1090 <= k <= 109
方法一:贪心 + 排序 · Go 实现
思考
每个 可变成 内任一整数,求最多能得到多少个不同值。 ,排序后从左往右贪心占最小还能用的整数。
已用最大值记为 。当前 取 ,若仍大于 则占用并计数。
先占较小值给后面更大的区间留下空位,因此必须先排序。
我们不妨对数组 排序,然后从左到右考虑每个元素 。
对于第一个元素,我们可以贪心地将其变为 ,这样可以使得 尽可能小,给后续的元素留下更多的空间。我们用变量 当前使用到的元素的最大值,初始化为负无穷大。
对于后续的元素 ,我们可以贪心地将其变为 。这里的 表示我们尽可能地将 变得更小,但不能小于 ,如果存在该值,且小于 ,我们就可以将 变为该值,不重复元素数加一,然后我们更新 为该值。
遍历结束,我们就得到了不重复元素的最大数量。
时间复杂度 ,空间复杂度 。其中 为数组 的长度。
func maxDistinctElements(nums []int, k int) (ans int) {
sort.Ints(nums)
pre := math.MinInt32
for _, x := range nums {
cur := min(x+k, max(x-k, pre+1))
if cur > pre {
ans++
pre = cur
}
}
return
}
3402. 使每一列严格递增的最少操作次数 ​
题目、示例与约束
给你一个由 非负 整数组成的 m x n 矩阵 grid。
在一次操作中,你可以将任意元素 grid[i][j] 的值增加 1。
返回使 grid 的所有列 严格递增 所需的 最少 操作次数。
示例 1:
输入: grid = [[3,2],[1,3],[3,4],[0,1]]
输出: 15
解释:
- 为了让第
0列严格递增,可以对grid[1][0]执行 3 次操作,对grid[2][0]执行 2 次操作,对grid[3][0]执行 6 次操作。 - 为了让第
1列严格递增,可以对grid[3][1]执行 4 次操作。

示例 2:
输入: grid = [[3,2,1],[2,1,0],[1,2,3]]
输出: 12
解释:
- 为了让第
0列严格递增,可以对grid[1][0]执行 2 次操作,对grid[2][0]执行 4 次操作。 - 为了让第
1列严格递增,可以对grid[1][1]执行 2 次操作,对grid[2][1]执行 2 次操作。 - 为了让第
2列严格递增,可以对grid[1][2]执行 2 次操作。

提示:
m == grid.lengthn == grid[i].length1 <= m, n <= 500 <= grid[i][j] < 2500
方法一:逐列计算 · Go 实现
思考
操作只能把格子增大,列与列互不影响。若对一列枚举每个位置的最终取值,搜索空间过大,而列长与行数的乘积并不大,一遍扫描即可。
瓶颈在于「严格递增」会把后面格子的下界一层层抬高:当前位置一旦被迫抬升,后续位置的下界也随之增加。
因此自上而下维护前一格的最终值 。若当前值已大于 ,则沿用;否则必须增至 ,并把差额计入答案。这样每列独立、贪心地确定了最少增量。
我们可以逐列遍历矩阵,对于每一列,我们可以计算出使其严格递增所需的最少操作次数。具体地,对于每一列,我们可以维护一个变量 ,表示当前列中前一个元素的值。然后,我们从上到下遍历当前列,对于当前元素 ,如果 ,则说明当前元素已经大于前一个元素,我们只需要更新 ;否则,我们需要将当前元素增加到 ,并将增加的次数累加到答案中。
时间复杂度 ,其中 和 分别是矩阵 的行数和列数。空间复杂度 。
func minimumOperations(grid [][]int) (ans int) {
m, n := len(grid), len(grid[0])
for j := 0; j < n; j++ {
pre := -1
for i := 0; i < m; i++ {
cur := grid[i][j]
if pre < cur {
pre = cur
} else {
pre++
ans += pre - cur
}
}
}
return
}
3413. 收集连续 K 个袋子可以获得的最多硬币数量 ​
题目、示例与约束
在一条数轴上有无限多个袋子,每个坐标对应一个袋子。其中一些袋子里装有硬币。
给你一个二维数组 coins,其中 coins[i] = [li, ri, ci] 表示从坐标 li 到 ri 的每个袋子中都有 ci 枚硬币。
数组 coins 中的区间互不重叠。
另给你一个整数 k。
返回通过收集连续 k 个袋子可以获得的 最多 硬币数量。
示例 1:
输入: coins = [[8,10,1],[1,3,2],[5,6,4]], k = 4
输出: 10
解释:
选择坐标为 [3, 4, 5, 6] 的袋子可以获得最多硬币:2 + 0 + 4 + 4 = 10。
示例 2:
输入: coins = [[1,10,3]], k = 2
输出: 6
解释:
选择坐标为 [1, 2] 的袋子可以获得最多硬币:3 + 3 = 6。
提示:
1 <= coins.length <= 1051 <= k <= 109coins[i] == [li, ri, ci]1 <= li <= ri <= 1091 <= ci <= 1000- 给定的区间互不重叠。
方法一:Go 实现(灵茶山艾府) · Go 实现
灵茶山艾府 · 源码 · MIT 许可 · 原题解
本题是 2271. 毯子覆盖的最多白色砖块数 的带权版本,请先完成那题。
回顾一下,在 2271 我的题解 中,做法是把毯子的右端点和瓷砖右端点对齐,然后跑一遍滑动窗口。
对于本题来说,如果出现两个相邻区间,左边区间 大,右边区间 小的情况,那么和右端点对齐就不是最优的,和左端点对齐反而是最优的。
所以在 2271 题的基础上,额外跑一遍和左端点对齐的滑动窗口即可。
代码实现时,把 反转,每个区间 改为 ,就可以复用和右端点对齐的代码了。
具体请看 视频讲解,欢迎点赞关注~
复杂度分析
- 时间复杂度:,其中 是 的长度。瓶颈在排序上。
- 空间复杂度:。忽略排序的栈开销。
变形题
如果题目没有保证区间互不重叠呢?
解答:
可以转换成互不重叠的区间。
例如 ,这两个区间叠加后,等同于 这三个互不重叠区间,这样就可以用上面的算法解决了。
如何计算叠加后的区间有哪些?用差分数组(哈希表),具体可以看数据结构题单。
专题训练
见下面滑动窗口题单的「§2.1 越短越合法/求最长/最大」。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府
import "slices"
// https://space.bilibili.com/206214
// 2271. 毯子覆盖的最多白色砖块数
func maximumWhiteTiles(tiles [][]int, carpetLen int) (ans int) {
cover, left := 0, 0
for _, tile := range tiles {
tl, tr, c := tile[0], tile[1], tile[2]
cover += (tr - tl + 1) * c
carpetLeft := tr - carpetLen + 1
for tiles[left][1] < carpetLeft {
cover -= (tiles[left][1] - tiles[left][0] + 1) * tiles[left][2]
left++
}
uncover := max((carpetLeft-tiles[left][0])*tiles[left][2], 0)
ans = max(ans, cover-uncover)
}
return
}
func maximumCoins(coins [][]int, k int) int64 {
slices.SortFunc(coins, func(a, b []int) int { return a[0] - b[0] })
ans := maximumWhiteTiles(coins, k)
slices.Reverse(coins)
for _, t := range coins {
t[0], t[1] = -t[1], -t[0]
}
return int64(max(ans, maximumWhiteTiles(coins, k)))
}
3424. 将数组变相同的最小代价 ​
题目、示例与约束
给你两个长度都为 n 的整数数组 arr 和 brr 以及一个整数 k 。你可以对 arr 执行以下操作任意次:
- 将
arr分割成若干个 连续的 子数组,并将这些子数组按任意顺序重新排列。这个操作的代价为k。 -
选择
arr中的任意一个元素,将它增加或者减少一个正整数x。这个操作的代价为x。
请你返回将 arr 变为 brr 的 最小 总代价。
子数组 是一个数组中一段连续 非空 的元素序列。
示例 1:
输入:arr = [-7,9,5], brr = [7,-2,-5], k = 2
输出:13
解释:
- 将
arr分割成两个连续子数组:[-7]和[9, 5]然后将它们重新排列成[9, 5, -7],代价为 2 。 - 将
arr[0]减小 2 ,数组变为[7, 5, -7],操作代价为 2 。 - 将
arr[1]减小 7 ,数组变为[7, -2, -7],操作代价为 7 。 - 将
arr[2]增加 2 ,数组变为[7, -2, -5],操作代价为 2 。
将两个数组变相等的总代价为 2 + 2 + 7 + 2 = 13 。
示例 2:
输入:arr = [2,1], brr = [2,1], k = 0
输出:0
解释:
由于数组已经相等,不需要进行任何操作,所以总代价为 0 。
提示:
1 <= arr.length == brr.length <= 1050 <= k <= 2 * 1010-105 <= arr[i] <= 105-105 <= brr[i] <= 105
方法一:贪心 + 排序 · Go 实现
思考
对应位置差的绝对值之和是不重排时的代价。重排需要先付 ,之后可以把 的元素任意对应到 。
,不能搜索排列。把两个数组排序后按位配对,正是使 最小的对法。
因此算两种方案:不分割的 ,以及排序后配对再加上 的 ,取较小者。部分重排不会优于「完全不排」或「付 后全排」。
如果不允许对数组进行分割,那么我们可以直接计算两个数组的绝对差值之和,作为总代价 。如果允许对数组进行分割,那么我们可以将数组 分割成 个长度为 1 的子数组,然后以任意顺序重新排列,然后与数组 进行比较,计算绝对差值之和,作为总代价 ,要使得 最小,我们可以将两个数组都排序,然后计算绝对差值之和。最终的结果为 。
时间复杂度 ,空间复杂度 。其中 为数组 的长度。
func minCost(arr []int, brr []int, k int64) int64 {
calc := func(a, b []int) (ans int64) {
for i := range a {
ans += int64(abs(a[i] - b[i]))
}
return
}
c1 := calc(arr, brr)
sort.Ints(arr)
sort.Ints(brr)
c2 := calc(arr, brr) + k
return min(c1, c2)
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
3439. 重新安排会议得到最多空余时间 I ​
题目、示例与约束
给你一个整数 eventTime 表示一个活动的总时长,这个活动开始于 t = 0 ,结束于 t = eventTime 。
同时给你两个长度为 n 的整数数组 startTime 和 endTime 。它们表示这次活动中 n 个时间 没有重叠 的会议,其中第 i 个会议的时间为 [startTime[i], endTime[i]] 。
你可以重新安排 至多 k 个会议,安排的规则是将会议时间平移,且保持原来的 会议时长 ,你的目的是移动会议后 最大化 相邻两个会议之间的 最长 连续空余时间。
移动前后所有会议之间的 相对 顺序需要保持不变,而且会议时间也需要保持互不重叠。
请你返回重新安排会议以后,可以得到的 最大 空余时间。
注意,会议 不能 安排到整个活动的时间以外。
示例 1:
输入:eventTime = 5, k = 1, startTime = [1,3], endTime = [2,5]
输出:2
解释:

将 [1, 2] 的会议安排到 [2, 3] ,得到空余时间 [0, 2] 。
示例 2:
输入:eventTime = 10, k = 1, startTime = [0,2,9], endTime = [1,4,10]
输出:6
解释:

将 [2, 4] 的会议安排到 [1, 3] ,得到空余时间 [3, 9] 。
示例 3:
输入:eventTime = 5, k = 2, startTime = [0,1,2,3,4], endTime = [1,2,3,4,5]
输出:0
解释:
活动中的所有时间都被会议安排满了。
提示:
1 <= eventTime <= 109n == startTime.length == endTime.length2 <= n <= 1051 <= k <= n0 <= startTime[i] < endTime[i] <= eventTimeendTime[i] <= startTime[i + 1]其中i在范围[0, n - 2]之间。
方法一:滑动窗口 · Go 实现
思考
会议已按时间排好且互不重叠。重排至多 个会议,等价于把它们挤到一边,从而合并两侧的空闲段。
,不能枚举被移动的子集。相邻空闲段有 个,移动 个会议最多把连续 段空闲拼在一起。
先算出各空闲长度放入 ,再对长度为 的窗口求和取最大。窗口和即一次合法重排能得到的最长空闲。
题目相当于把相邻的空闲时间段合并成一个更长的空闲时间段。一共有 个空闲时间段,分别是:
- 第一个空闲时间段是从活动开始到第一个会议开始的时间段;
- 中间的 个空闲时间段是相邻两个会议之间的时间段;
- 最后一个空闲时间段是最后一个会议结束到活动结束的时间段。
题目最多可以重新安排 个会议,等价于最多可以合并 个空闲时间段。我们需要找到这 个空闲时间段的最大长度。
我们可以将这些空闲时间段的长度存储在一个数组中 中。然后,我们一个长度为 的滑动窗口,遍历这个数组,计算每个窗口的和,找到最大的和,即为所求的最大空闲时间。
时间复杂度 ,空间复杂度 。其中 是会议的数量。
func maxFreeTime(eventTime int, k int, startTime []int, endTime []int) int {
n := len(endTime)
nums := make([]int, n+1)
nums[0] = startTime[0]
for i := 1; i < n; i++ {
nums[i] = startTime[i] - endTime[i-1]
}
nums[n] = eventTime - endTime[n-1]
ans, s := 0, 0
for i := 0; i <= n; i++ {
s += nums[i]
if i >= k {
ans = max(ans, s)
s -= nums[i-k]
}
}
return ans
}
方法二:滑动窗口(空间优化) · Go 实现
思考
方法一已经在线性时间内得到正确窗口和,但额外数组保存了全部空闲长度。
第 段空闲由端点公式直接算出,不必预先存下。
用 在滑动窗口中即时取值,空间降为 ,时间和结论与方法一相同。
在方法一中,我们使用了一个数组来存储空闲时间段的长度。实际上,我们不需要存储整个数组,可以用一个函数 来表示第 个空闲时间段的长度。这样可以节省空间。
时间复杂度 ,其中 是会议的数量。空间复杂度 。
func maxFreeTime(eventTime int, k int, startTime []int, endTime []int) int {
n := len(endTime)
f := func(i int) int {
if i == 0 {
return startTime[0]
}
if i == n {
return eventTime - endTime[n-1]
}
return startTime[i] - endTime[i-1]
}
ans, s := 0, 0
for i := 0; i <= n; i++ {
s += f(i)
if i >= k {
ans = max(ans, s)
s -= f(i - k)
}
}
return ans
}
3440. 重新安排会议得到最多空余时间 II ​
题目、示例与约束
给你一个整数 eventTime 表示一个活动的总时长,这个活动开始于 t = 0 ,结束于 t = eventTime 。
同时给你两个长度为 n 的整数数组 startTime 和 endTime 。它们表示这次活动中 n 个时间 没有重叠 的会议,其中第 i 个会议的时间为 [startTime[i], endTime[i]] 。
你可以重新安排 至多 一个会议,安排的规则是将会议时间平移,且保持原来的 会议时长 ,你的目的是移动会议后 最大化 最长 连续空余时间。
请你返回重新安排会议以后,可以得到的 最大 空余时间。
注意,会议 不能 安排到整个活动的时间以外,且会议之间需要保持互不重叠。
注意:重新安排会议以后,会议之间的顺序可以发生改变。
示例 1:
输入:eventTime = 5, startTime = [1,3], endTime = [2,5]
输出:2
解释:

将 [1, 2] 的会议安排到 [2, 3] ,得到空余时间 [0, 2] 。
示例 2:
输入:eventTime = 10, startTime = [0,7,9], endTime = [1,8,10]
输出:7
解释:

将 [0, 1] 的会议安排到 [8, 9] ,得到空余时间 [0, 7] 。
示例 3:
输入:eventTime = 10, startTime = [0,3,7,9], endTime = [1,4,8,10]
输出:6
解释:

将 [3, 4] 的会议安排到 [8, 9] ,得到空余时间 [1, 7] 。
示例 4:
输入:eventTime = 5, startTime = [0,1,2,3,4], endTime = [1,2,3,4,5]
输出:0
解释:
活动中的所有时间都被会议安排满了。
提示:
1 <= eventTime <= 109n == startTime.length == endTime.length2 <= n <= 1050 <= startTime[i] < endTime[i] <= eventTimeendTime[i] <= startTime[i + 1]其中i在范围[0, n - 2]之间。
方法一:贪心 · Go 实现
思考
与 I 不同,这里只能移动一个会议,且必须放进某段已有空闲(不能与其它会议重叠)。 ,需线性处理。
若会议 只在原地左右平移,空闲为 。若它能整段搬进左侧或右侧的某段空闲,则原来占据的 整段空出。
预处理 、 为左右两侧的最大空闲。枚举被搬走的会议 ,能搬则答案取 ,否则取 。
根据题目描述,对于会议 ,我们记它左侧非空闲位置为 ,右侧非空闲位置为 ,记会议 的时长为 ,则:
那么它可以向左移动,也可以向右移动,此时空闲时间为:
如果左侧存在最大的空闲时间 ,满足 ,则可以将会议 向左移动到该位置,得到新的空闲时间:
同理,如果右侧存在最大的空闲时间 ,满足 ,则可以将会议 向右移动到该位置,得到新的空闲时间:
因此,我们首先预处理两个数组 和 ,其中 表示 范围内的最大空闲时间, 表示 范围内的最大空闲时间。然后遍历每个会议 ,计算它移动后的最大空闲时间,取最大值即可。
时间复杂度 ,空间复杂度 。其中 为会议的数量。
func maxFreeTime(eventTime int, startTime []int, endTime []int) int {
n := len(startTime)
pre := make([]int, n)
suf := make([]int, n)
pre[0] = startTime[0]
suf[n-1] = eventTime - endTime[n-1]
for i := 1; i < n; i++ {
pre[i] = max(pre[i-1], startTime[i]-endTime[i-1])
}
for i := n - 2; i >= 0; i-- {
suf[i] = max(suf[i+1], startTime[i+1]-endTime[i])
}
ans := 0
for i := 0; i < n; i++ {
l := 0
if i > 0 {
l = endTime[i-1]
}
r := eventTime
if i < n-1 {
r = startTime[i+1]
}
w := endTime[i] - startTime[i]
ans = max(ans, r-l-w)
if i > 0 && pre[i-1] >= w {
ans = max(ans, r-l)
} else if i+1 < n && suf[i+1] >= w {
ans = max(ans, r-l)
}
}
return ans
}
3449. 最大化游戏分数的最小值 ​
题目、示例与约束
给你一个长度为 n 的数组 points 和一个整数 m 。同时有另外一个长度为 n 的数组 gameScore ,其中 gameScore[i] 表示第 i 个游戏得到的分数。一开始对于所有的 i 都有 gameScore[i] == 0 。
你开始于下标 -1 处,该下标在数组以外(在下标 0 前面一个位置)。你可以执行 至多 m 次操作,每一次操作中,你可以执行以下两个操作之一:
- 将下标增加 1 ,同时将
points[i]添加到gameScore[i]。 - 将下标减少 1 ,同时将
points[i]添加到gameScore[i]。
注意,在第一次移动以后,下标必须始终保持在数组范围以内。
请你返回 至多 m 次操作以后,gameScore 里面最小值 最大 为多少。
示例 1:
输入:points = [2,4], m = 3
输出:4
解释:
一开始,下标 i = -1 且 gameScore = [0, 0].
| 移动 | 下标 | gameScore |
|---|---|---|
增加 i |
0 | [2, 0] |
增加 i |
1 | [2, 4] |
减少 i |
0 | [4, 4] |
gameScore 中的最小值为 4 ,这是所有方案中可以得到的最大值,所以返回 4 。
示例 2:
输入:points = [1,2,3], m = 5
输出:2
解释:
一开始,下标 i = -1 且 gameScore = [0, 0, 0] 。
| 移动 | 下标 | gameScore |
|---|---|---|
增加 i |
0 | [1, 0, 0] |
增加 i |
1 | [1, 2, 0] |
减少 i |
0 | [2, 2, 0] |
增加 i |
1 | [2, 4, 0] |
增加 i |
2 | [2, 4, 3] |
gameScore 中的最小值为 2 ,这是所有方案中可以得到的最大值,所以返回 2 。
提示:
2 <= n == points.length <= 5 * 1041 <= points[i] <= 1061 <= m <= 109
方法一:Go 实现(灵茶山艾府) · Go 实现
灵茶山艾府 · 源码 · MIT 许可 · 原题解
假设 中的每个数都至少为 ,那么 越大,操作次数就越多,有单调性,可以二分答案。关于二分的原理,请看视频 二分查找 红蓝染色法【基础算法精讲 04】。
在计算前,还需要说明一个转换关系:我们可以把任何一种或长或短的、来来回回的移动方式,转换成若干组「左右横跳」,也就是先在 和 之间左右横跳,然后在 和 之间左右横跳,在 和 之间左右横跳……直到最终位置为 或者 。如下图:

从第一个数开始计算。设 ,至少要增加 次。
第一次操作需要从 走到 ,后面的 次增加可以在 和 之间左右横跳。
所以一共需要
次操作。
注意这会导致下一个数已经操作了 次。
如此循环,直到最后一个数。如果循环中发现操作次数已经超过 ,退出循环。
注意,如果最后一个数还需要操作的次数 ,那么是不需要继续操作的,退出循环。
细节
1)
下面代码采用开区间二分,这仅仅是二分的一种写法,使用闭区间或者半闭半开区间都是可以的。
- 开区间左端点初始值:。无需操作,一定可以满足要求。
- 开区间右端点初始值:。假设第一个数是最小值,那么它可以通过左右横跳操作 次。结果 之后一定无法满足要求。
2)
关于上取整的计算,当 和 均为正整数时,我们有
证明见 上取整下取整转换公式的证明。
本题视频讲解,欢迎点赞关注~
复杂度分析
- 时间复杂度:,其中 是 的长度,。
- 空间复杂度:。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 【本题相关】二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
- 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 【本题相关】贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
import (
"slices"
"sort"
)
// https://space.bilibili.com/206214
func maxScore(points []int, m int) int64 {
u1 := (m + 1) / 2 * slices.Min(points)
u2 := m / len(points) * slices.Max(points)
ans := sort.Search(min(u1, u2), func(low int) bool {
// 二分最小的不满足要求的 low+1,即可得到最大的满足要求的 low
low++
left := m
pre := 0
for i, p := range points {
k := (low-1)/p + 1 - pre // 还需要操作的次数
if i == len(points)-1 && k <= 0 { // 最后一个数已经满足要求
break
}
k = max(k, 1) // 至少要走 1 步
left -= k*2 - 1 // 左右横跳
if left < 0 {
return true
}
pre = k - 1 // 右边那个数已经操作 k-1 次
}
return false
})
return int64(ans)
}
3457. 吃披萨 ​
题目、示例与约束
给你一个长度为 n 的整数数组 pizzas,其中 pizzas[i] 表示第 i 个披萨的重量。每天你会吃 恰好 4 个披萨。由于你的新陈代谢能力惊人,当你吃重量为 W、X、Y 和 Z 的披萨(其中 W <= X <= Y <= Z)时,你只会增加 1 个披萨的重量!体重增加规则如下:
- 在 奇数天(按 1 开始计数)你会增加
Z的重量。 - 在 偶数天,你会增加
Y的重量。
请你设计吃掉 所有 披萨的最优方案,并计算你可以增加的 最大 总重量。
注意:保证 n 是 4 的倍数,并且每个披萨只吃一次。
示例 1:
输入: pizzas = [1,2,3,4,5,6,7,8]
输出: 14
解释:
- 第 1 天,你吃掉下标为
[1, 2, 4, 7] = [2, 3, 5, 8]的披萨。你增加的重量为 8。 - 第 2 天,你吃掉下标为
[0, 3, 5, 6] = [1, 4, 6, 7]的披萨。你增加的重量为 6。
吃掉所有披萨后,你增加的总重量为 8 + 6 = 14。
示例 2:
输入: pizzas = [2,1,1,1,1,1,1,1]
输出: 3
解释:
- 第 1 天,你吃掉下标为
[4, 5, 6, 0] = [1, 1, 1, 2]的披萨。你增加的重量为 2。 - 第 2 天,你吃掉下标为
[1, 2, 3, 7] = [1, 1, 1, 1]的披萨。你增加的重量为 1。
吃掉所有披萨后,你增加的总重量为 2 + 1 = 3。
提示:
4 <= n == pizzas.length <= 2 * 1051 <= pizzas[i] <= 105n是 4 的倍数。
方法一:贪心 + 排序 · Go 实现
思考
每天吃四块:奇数天拿最大值,偶数天拿次大值。 为 的倍数且可达 ,不能搜索分组。
奇数天应独占最大的若干块;偶数天必须「浪费」一块更大的来取出次大。排序后这两种贡献的位置是固定的。
设奇数天 。答案先加上最大的 块,再从剩余里每隔一块取一个作为偶数天的次大值。
根据题目描述,我们每天可以吃 个披萨。在奇数天,我们可以得到这 个披萨中的最大值,而在偶数天,我们可以得到这 个披萨中的第二大值。
因此,我们可以将披萨按重量从小到大排序,一共能吃 天,那么一共有 天是奇数天,一共有 天是偶数天。
考虑奇数天,我们可以选择最大的 个披萨,以及最小的 个披萨,增加的重量为 。
考虑偶数天,我们在剩余的披萨中,每次贪心地选择最大的两个披萨,以及最小的两个披萨,增加的重量为 。
时间复杂度 ,空间复杂度 。其中 是数组 的长度。
func maxWeight(pizzas []int) (ans int64) {
n := len(pizzas)
days := n / 4
sort.Ints(pizzas)
odd := (days + 1) / 2
even := days - odd
for i := n - odd; i < n; i++ {
ans += int64(pizzas[i])
}
for i := n - odd - 2; even > 0; even-- {
ans += int64(pizzas[i])
i -= 2
}
return
}
3462. 提取至多 K 个元素的最大总和 ​
题目、示例与约束
给你一个大小为 n x m 的二维矩阵 grid ,以及一个长度为 n 的整数数组 limits ,和一个整数 k 。你的目标是从矩阵 grid 中提取出 至多 k 个元素,并计算这些元素的最大总和,提取时需满足以下限制:
-
从
grid的第i行提取的元素数量不超过limits[i]。
返回最大总和。
示例 1:
输入:grid = [[1,2],[3,4]], limits = [1,2], k = 2
输出:7
解释:
- 从第 2 行提取至多 2 个元素,取出 4 和 3 。
- 至多提取 2 个元素时的最大总和
4 + 3 = 7。
示例 2:
输入:grid = [[5,3,7],[8,2,6]], limits = [2,2], k = 3
输出:21
解释:
- 从第 1 行提取至多 2 个元素,取出 7 。
- 从第 2 行提取至多 2 个元素,取出 8 和 6 。
- 至多提取 3 个元素时的最大总和
7 + 8 + 6 = 21。
提示:
n == grid.length == limits.lengthm == grid[i].length1 <= n, m <= 5000 <= grid[i][j] <= 1050 <= limits[i] <= m0 <= k <= min(n * m, sum(limits))
方法一:贪心 + 优先队列(小根堆) · Go 实现
思考
每行至多取 个,总共至多 个,求和最大。应优先取全局最大的合法格子。
先在行内排序,只保留该行最大的 个,再在这些候选里选 个最大。
小根堆维持当前选中的 个数:行内从大到小入堆,堆超过 就弹出最小值。堆内之和即答案。
我们可以用一个优先队列(小根堆) 来维护最大的 个元素。
遍历每一行,对每一行的元素进行排序,然后取出每一行最大的 个元素,将这些元素加入 中。如果 的大小超过了 ,就将堆顶元素弹出。
最后,将 中的元素相加即可。
时间复杂度 ,空间复杂度 。其中 和 分别为矩阵 的行数和列数。
type MinHeap []int
func (h MinHeap) Len() int { return len(h) }
func (h MinHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h MinHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *MinHeap) Push(x interface{}) {
*h = append(*h, x.(int))
}
func (h *MinHeap) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
func maxSum(grid [][]int, limits []int, k int) int64 {
pq := &MinHeap{}
heap.Init(pq)
n := len(grid)
for i := 0; i < n; i++ {
nums := make([]int, len(grid[i]))
copy(nums, grid[i])
limit := limits[i]
sort.Ints(nums)
for j := 0; j < limit; j++ {
heap.Push(pq, nums[len(nums)-j-1])
if pq.Len() > k {
heap.Pop(pq)
}
}
}
var ans int64 = 0
for pq.Len() > 0 {
ans += int64(heap.Pop(pq).(int))
}
return ans
}
3474. 字典序最小的生成字符串 ​
题目、示例与约束
给你两个字符串,str1 和 str2,其长度分别为 n 和 m 。
如果一个长度为 n + m - 1 的字符串 word 的每个下标 0 <= i <= n - 1 都满足以下条件,则称其由 str1 和 str2 生成:
- 如果
str1[i] == 'T',则长度为m的 子字符串(从下标i开始)与str2相等,即word[i..(i + m - 1)] == str2。 - 如果
str1[i] == 'F',则长度为m的 子字符串(从下标i开始)与str2不相等,即word[i..(i + m - 1)] != str2。
返回可以由 str1 和 str2 生成 的 字典序最小 的字符串。如果不存在满足条件的字符串,返回空字符串 ""。
如果字符串 a 在第一个不同字符的位置上比字符串 b 的对应字符在字母表中更靠前,则称字符串 a 的 字典序 小于 字符串 b。
如果前 min(a.length, b.length) 个字符都相同,则较短的字符串字典序更小。
子字符串 是字符串中的一个连续、非空 的字符序列。
示例 1:
输入: str1 = "TFTF", str2 = "ab"
输出: "ababa"
解释:
下表展示了字符串 "ababa" 的生成过程:
| 下标 | T/F | 长度为 m 的子字符串 |
|---|---|---|
| 0 | 'T' |
"ab" |
| 1 | 'F' |
"ba" |
| 2 | 'T' |
"ab" |
| 3 | 'F' |
"ba" |
字符串 "ababa" 和 "ababb" 都可以由 str1 和 str2 生成。
返回 "ababa",因为它的字典序更小。
示例 2:
输入: str1 = "TFTF", str2 = "abc"
输出: ""
解释:
无法生成满足条件的字符串。
示例 3:
输入: str1 = "F", str2 = "d"
输出: "a"
提示:
1 <= n == str1.length <= 1041 <= m == str2.length <= 500str1仅由'T'或'F'组成。str2仅由小写英文字母组成。
方法一:贪心 · Go 实现
思考
强制 等于 ; 则禁止相等。答案长 ,希望字典序最小,故先全部填 。
T 约束可能互相冲突,必须先落盘并标记 。F 约束若此时仍等于 ,只能改一个未固定位置。
从右往左把未固定格改成 ,尽量不动更靠左的 。找不到可改位置则无解。
不妨记字符串 为 ,字符串 为 。
我们可以用一个长度为 的字符串 来存储生成的字符串,初始时将 的每个字符都设置为 。我们还需要一个长度为 的布尔数组 来记录 中哪些位置已经被固定了。
首先,我们遍历字符串 ,对于每个下标 ,如果 是 'T',则我们需要将 中从下标 开始的长度为 的子字符串设置为 。在设置过程中,如果发现某个位置已经被固定了,并且与 中对应位置的字符不相同,那么说明无法生成满足条件的字符串,我们直接返回空字符串。
接下来,我们再次遍历字符串 ,对于每个下标 ,如果 是 'F',则我们需要检查 中从下标 开始的长度为 的子字符串是否与 相同。如果相同,那么我们需要在这个子字符串中找到一个位置,将其字符修改为 'b'(因为 'b' 在字典序上比 'a' 大),以保证这个子字符串不等于 。如果无法找到这样的一个位置,那么说明无法生成满足条件的字符串,我们直接返回空字符串。
最后,我们将 中的字符连接成一个字符串并返回。
时间复杂度 ,空间复杂度 。
func generateString(s string, t string) string {
n, m := len(s), len(t)
ans := make([]byte, n+m-1)
fixed := make([]bool, n+m-1)
for i := range ans {
ans[i] = 'a'
}
for i, b := range s {
if b != 'T' {
continue
}
for j, c := range t {
k := i + j
if fixed[k] && ans[k] != byte(c) {
return ""
}
ans[k] = byte(c)
fixed[k] = true
}
}
for i, b := range s {
if b != 'F' {
continue
}
same := true
for j := 0; j < m; j++ {
if ans[i+j] != t[j] {
same = false
break
}
}
if !same {
continue
}
ok := false
for j := i + m - 1; j >= i; j-- {
if !fixed[j] {
ans[j] = 'b'
ok = true
break
}
}
if !ok {
return ""
}
}
return string(ans)
}
继续阅读
- 实践案例
- 实践案例
- 实践案例