LeetCode Go · 贪心(8) ​
来源与许可
题目版权归 LeetCode;题解来自 Doocs/leetcode,按 CC BY-SA 4.0 共享。补充 Go 代码来自灵茶山艾府、halfrost,保留 MIT 许可及逐题署名。本站提取 Go、调整分类及折叠排版;标注“本站整理”的 Go 解法依据题意与参考思路补写,按 CC BY-SA 4.0 共享。收录日期:2026-10-31;主题源版本:91ef8c241a5e。
本卷 30 题 · 点击题目展开,先独立思考,再查看解法。
2358. 分组的最大数量 ​
题目、示例与约束
给你一个正整数数组 grades ,表示大学中一些学生的成绩。你打算将 所有 学生分为一些 有序 的非空分组,其中分组间的顺序满足以下全部条件:
- 第
i个分组中的学生总成绩 小于 第(i + 1)个分组中的学生总成绩,对所有组均成立(除了最后一组)。 - 第
i个分组中的学生总数 小于 第(i + 1)个分组中的学生总数,对所有组均成立(除了最后一组)。
返回可以形成的 最大 组数。
示例 1:
输入:grades = [10,6,12,7,3,5] 输出:3 解释:下面是形成 3 个分组的一种可行方法: - 第 1 个分组的学生成绩为 grades = [12] ,总成绩:12 ,学生数:1 - 第 2 个分组的学生成绩为 grades = [6,7] ,总成绩:6 + 7 = 13 ,学生数:2 - 第 3 个分组的学生成绩为 grades = [10,3,5] ,总成绩:10 + 3 + 5 = 18 ,学生数:3 可以证明无法形成超过 3 个分组。
示例 2:
输入:grades = [8,8] 输出:1 解释:只能形成 1 个分组,因为如果要形成 2 个分组的话,会导致每个分组中的学生数目相等。
提示:
1 <= grades.length <= 1051 <= grades[i] <= 105
方法一:贪心 + 二分查找 · Go 实现
思考
组的人数须严格递增,分数和也须严格递增。 ,不能搜索分组。将成绩排序后依次分配 人,分数和自动递增。
人数约束成为 。对 二分(实现上对 相对 做 )得到最大可行组数。
我们观察题目中的条件,第 组的学生人数要小于第 组的学生人数,且第 组的学生总成绩要小于第 组的学生总成绩,我们只需要将学生按照成绩从小到大排序,然后每一组依次分配 , , ..., 个学生即可。如果最后一组的学生人数不足 个,那么我们可以将这些学生分配到前面的最后一组中。
因此,我们要找到最大的 ,使得 ,其中 为学生的总人数。我们可以使用二分查找来求解。
我们定义二分查找的左边界为 ,右边界为 ,每一次二分查找的中点为 ,如果 ,则说明 太大,我们需要将右边界缩小至 ,否则我们需要将左边界增大至 。
最后,我们将 作为答案返回即可。
时间复杂度 ,空间复杂度 。其中 为学生的总人数。
func maximumGroups(grades []int) int {
n := len(grades)
return sort.Search(n, func(k int) bool {
k++
return k*k+k > n*2
})
}
2366. 将数组排序的最少替换次数 ​
题目、示例与约束
给你一个下标从 0 开始的整数数组 nums 。每次操作中,你可以将数组中任何一个元素替换为 任意两个 和为该元素的数字。
- 比方说,
nums = [5,6,7]。一次操作中,我们可以将nums[1]替换成2和4,将nums转变成[5,2,4,7]。
请你执行上述操作,将数组变成元素按 非递减 顺序排列的数组,并返回所需的最少操作次数。
示例 1:
输入:nums = [3,9,3] 输出:2 解释:以下是将数组变成非递减顺序的步骤: - [3,9,3] ,将9 变成 3 和 6 ,得到数组 [3,3,6,3] - [3,3,6,3] ,将 6 变成 3 和 3 ,得到数组 [3,3,3,3,3] 总共需要 2 步将数组变成非递减有序,所以我们返回 2 。
示例 2:
输入:nums = [1,2,3,4,5] 输出:0 解释:数组已经是非递减顺序,所以我们返回 0 。
提示:
1 <= nums.length <= 1051 <= nums[i] <= 109
方法一:贪心 · Go 实现
思考
把一个数拆成若干正整数之和,目标是最终非递减,且拆分次数最少。 ,须从右往左确定上界。
右侧已确定的最大值 不应再被拆小。若 则更新 ;否则拆成 段,次数加 ,并把新的上界改成 ,以便左侧仍尽量大。
我们观察发现,要使得数组 变成非递减有序,也即单调递增,那么数组后面的元素应该尽可能大,所以,将数组 的最后一个元素 替换成多个更小的数是没有必要的。
也即是说,我们可以从后往前遍历数组 ,并且维护当前的最大值 ,初始时 。
- 若当前遍历到的元素 ,此时不需要将 进行替换,我们直接更新 即可。
- 否则,我们需要将 替换成多个和为 的数,这些数的最大值为 ,总共替换成 个数,所以需要进行 次操作,累加到答案中。这 个数中,最小的数为 ,因此,我们更新 。
遍历结束,返回总的操作次数即可。
时间复杂度 ,其中 为数组 的长度。空间复杂度 。
func minimumReplacement(nums []int) (ans int64) {
n := len(nums)
mx := nums[n-1]
for i := n - 2; i >= 0; i-- {
if nums[i] <= mx {
mx = nums[i]
continue
}
k := (nums[i] + mx - 1) / mx
ans += int64(k - 1)
mx = nums[i] / k
}
return
}
2383. 赢得比赛需要的最少训练时长 ​
题目、示例与约束
你正在参加一场比赛,给你两个 正 整数 initialEnergy 和 initialExperience 分别表示你的初始精力和初始经验。
另给你两个下标从 0 开始的整数数组 energy 和 experience,长度均为 n 。
你将会 依次 对上 n 个对手。第 i 个对手的精力和经验分别用 energy[i] 和 experience[i] 表示。当你对上对手时,需要在经验和精力上都 严格 超过对手才能击败他们,然后在可能的情况下继续对上下一个对手。
击败第 i 个对手会使你的经验 增加 experience[i],但会将你的精力 减少 energy[i] 。
在开始比赛前,你可以训练几个小时。每训练一个小时,你可以选择将增加经验增加 1 或者 将精力增加 1 。
返回击败全部 n 个对手需要训练的 最少 小时数目。
示例 1:
输入:initialEnergy = 5, initialExperience = 3, energy = [1,4,3,2], experience = [2,6,3,1] 输出:8 解释:在 6 小时训练后,你可以将精力提高到 11 ,并且再训练 2 个小时将经验提高到 5 。 按以下顺序与对手比赛: - 你的精力与经验都超过第 0 个对手,所以获胜。 精力变为:11 - 1 = 10 ,经验变为:5 + 2 = 7 。 - 你的精力与经验都超过第 1 个对手,所以获胜。 精力变为:10 - 4 = 6 ,经验变为:7 + 6 = 13 。 - 你的精力与经验都超过第 2 个对手,所以获胜。 精力变为:6 - 3 = 3 ,经验变为:13 + 3 = 16 。 - 你的精力与经验都超过第 3 个对手,所以获胜。 精力变为:3 - 2 = 1 ,经验变为:16 + 1 = 17 。 在比赛前进行了 8 小时训练,所以返回 8 。 可以证明不存在更小的答案。
示例 2:
输入:initialEnergy = 2, initialExperience = 4, energy = [1], experience = [3] 输出:0 解释:你不需要额外的精力和经验就可以赢得比赛,所以返回 0 。
提示:
n == energy.length == experience.length1 <= n <= 1001 <= initialEnergy, initialExperience, energy[i], experience[i] <= 100
方法一:贪心 + 模拟 · Go 实现
思考
必须按顺序击败对手,开战时精力与经验都要严格更大。 ,按序模拟并在不足时立刻补齐。
精力不够则训练到对手精力加一,经验同理;战后精力减少、经验增加。各场不足量之和即最少训练时数。
我们不妨记当前的精力为 ,经验为 。
接下来,我们遍历每个对手,对于第 个对手,记其精力为 ,经验为 。
- 如果 ,那么我们需要训练 小时,将精力提升到 ;
- 如果 ,那么我们需要训练 小时,将经验提升到 ;
- 然后我们将精力减去 ,经验加上 。
最后返回答案即可。
时间复杂度 ,其中 为对手的数量。空间复杂度 。
func minNumberOfHours(x int, y int, energy []int, experience []int) (ans int) {
for i, dx := range energy {
dy := experience[i]
if x <= dx {
ans += dx + 1 - x
x = dx + 1
}
if y <= dy {
ans += dy + 1 - y
y = dy + 1
}
x -= dx
y += dy
}
return
}
2384. 最大回文数字 ​
题目、示例与约束
给你一个仅由数字(0 - 9)组成的字符串 num 。
请你找出能够使用 num 中数字形成的 最大回文 整数,并以字符串形式返回。该整数不含 前导零 。
注意:
- 你 无需 使用
num中的所有数字,但你必须使用 至少 一个数字。 - 数字可以重新排序。
示例 1:
输入:num = "444947137" 输出:"7449447" 解释: 从 "444947137" 中选用数字 "4449477",可以形成回文整数 "7449447" 。 可以证明 "7449447" 是能够形成的最大回文整数。
示例 2:
输入:num = "00009" 输出:"9" 解释: 可以证明 "9" 能够形成的最大回文整数。 注意返回的整数不应含前导零。
提示:
1 <= num.length <= 105num由数字(0 - 9)组成
方法一:统计 + 贪心 · Go 实现
思考
用给定数字重排成最大回文,可丢弃部分数字。 ,应按数位贪心而非搜索排列。
统计频次后,最大的奇数次数字做中心并留偶数次。再从 到 把一半贴到两侧。去掉两端的 ;若空则答案为 。
用 数组记录每个数字出现的次数。
回文数字需要满足回文串中最多有一个数字出现了奇数次,因此我们先找最大的、出现了奇数次的数字 (也可能不存在),作为回文数字的中间数字,对应的 减 。
接下来我们从回文数字中间,按数字从小到大的顺序,往左右两边扩散(也可以枚举回文串的右半部分,然后将右半部分反转,得到左半部分)。这样我们可以得到一个“可能”含有前导零的回文串。
我们去除回文串的前导零,若回文串为空,则返回“0”。否则返回该回文串。
时间复杂度 ,其中 为 的长度。
func largestPalindromic(num string) string {
cnt := make([]int, 10)
for _, c := range num {
cnt[c-'0']++
}
ans := ""
for i := 9; i >= 0; i-- {
if cnt[i]%2 == 1 {
ans = strconv.Itoa(i)
cnt[i]--
break
}
}
for i := 0; i < 10; i++ {
if cnt[i] > 0 {
cnt[i] >>= 1
s := strings.Repeat(strconv.Itoa(i), cnt[i])
ans = s + ans + s
}
}
ans = strings.Trim(ans, "0")
if ans == "" {
return "0"
}
return ans
}
2389. 和有限的最长子序列 ​
题目、示例与约束
给你一个长度为 n 的整数数组 nums ,和一个长度为 m 的整数数组 queries 。
返回一个长度为 m 的数组 answer ,其中 answer[i] 是 nums 中 元素之和小于等于 queries[i] 的 子序列 的 最大 长度 。
子序列 是由一个数组删除某些元素(也可以不删除)但不改变剩余元素顺序得到的一个数组。
示例 1:
输入:nums = [4,5,2,1], queries = [3,10,21] 输出:[2,3,4] 解释:queries 对应的 answer 如下: - 子序列 [2,1] 的和小于或等于 3 。可以证明满足题目要求的子序列的最大长度是 2 ,所以 answer[0] = 2 。 - 子序列 [4,5,1] 的和小于或等于 10 。可以证明满足题目要求的子序列的最大长度是 3 ,所以 answer[1] = 3 。 - 子序列 [4,5,2,1] 的和小于或等于 21 。可以证明满足题目要求的子序列的最大长度是 4 ,所以 answer[2] = 4 。
示例 2:
输入:nums = [2,3,4,5], queries = [1] 输出:[0] 解释:空子序列是唯一一个满足元素和小于或等于 1 的子序列,所以 answer[0] = 0 。
提示:
n == nums.lengthm == queries.length1 <= n, m <= 10001 <= nums[i], queries[i] <= 106
方法一:排序 + 前缀和 + 二分查找 · Go 实现
思考
子序列和不超过查询值且尽量长,应取最小的若干元素。 ,排序后前缀和即可。
升序后前缀 是选 个最小元的和。对每个查询二分第一个超过上限的位置,下标即最大长度。
根据题目描述,对于每个 ,我们需要找到一个子序列,使得该子序列的元素和不超过 ,且该子序列的长度最大化。显然,我们应该选择尽可能小的元素,这样才能使得子序列的长度最大化。
因此,我们可以先将数组 进行升序排序,然后对于每个 ,我们可以使用二分查找,找到最小的下标 ,使得 。此时 就是满足条件的子序列的元素和,且该子序列的长度为 。因此,我们可以将 加入答案数组中。
时间复杂度 ,空间复杂度 或 。其中 和 分别是数组 和 的长度。
func answerQueries(nums []int, queries []int) (ans []int) {
sort.Ints(nums)
for i := 1; i < len(nums); i++ {
nums[i] += nums[i-1]
}
for _, q := range queries {
ans = append(ans, sort.SearchInts(nums, q+1))
}
return
}
方法二:排序 + 离线查询 + 双指针 · Go 实现
思考
方法一对每个查询独立二分。查询离线按上限排序后,双指针只向前加元素,总时间在排序之外变为线性。
与方法一类似,我们可以先对数组 进行升序排列。
接下来,我们定义一个长度与 相同的下标数组 ,其中 ,然后我们对数组 按照 中的元素值进行升序排序。这样,我们就可以按照 中的元素值从小到大的顺序进行处理。
我们使用一个变量 记录当前已经选择的元素的和,使用一个变量 记录当前已经选择的元素的个数。初始时 。
我们遍历下标数组 ,对于其中的每个下标 ,我们循环地将数组 中的元素加入到当前的子序列中,直到 ,此时 即为满足条件的子序列的长度,我们将 的值设为 ,然后继续处理下一个下标。
遍历完下标数组 后,我们即可得到答案数组 ,其中 即为满足 的子序列的长度。
时间复杂度 ,空间复杂度 。其中 和 分别是数组 和 的长度。
func answerQueries(nums []int, queries []int) (ans []int) {
sort.Ints(nums)
m := len(queries)
idx := make([]int, m)
for i := range idx {
idx[i] = i
}
sort.Slice(idx, func(i, j int) bool { return queries[idx[i]] < queries[idx[j]] })
ans = make([]int, m)
s, j := 0, 0
for _, i := range idx {
for j < len(nums) && s+nums[j] <= queries[i] {
s += nums[j]
j++
}
ans[i] = j
}
return
}
2405. 子字符串的最优划分 ​
题目、示例与约束
给你一个字符串 s ,请你将该字符串划分成一个或多个 子字符串 ,并满足每个子字符串中的字符都是 唯一 的。也就是说,在单个子字符串中,字母的出现次数都不超过 一次 。
满足题目要求的情况下,返回 最少 需要划分多少个子字符串。
注意,划分后,原字符串中的每个字符都应该恰好属于一个子字符串。
示例 1:
输入:s = "abacaba"
输出:4
解释:
两种可行的划分方法分别是 ("a","ba","cab","a") 和 ("ab","a","ca","ba") 。
可以证明最少需要划分 4 个子字符串。
示例 2:
输入:s = "ssssss"
输出:6
解释:
只存在一种可行的划分方法 ("s","s","s","s","s","s") 。
提示:
1 <= s.length <= 105s仅由小写英文字母组成
方法一:贪心 · Go 实现
思考
划分方案随段数指数增长, 无法枚举。题目要求每段字符互异且份数最少,等价于每段尽量长:当前段再纳入某字符会重复时才切开。
小写字母仅 个,用一个整数 的各位记录本段已出现字母。遇冲突则份数加一并清空 ,再纳入当前字符。一遍扫描即可。
根据题意,每个子字符串应该尽可能长,且包含的字符唯一,因此,我们只需要贪心地进行划分即可。
我们定义一个二进制整数 来记录当前子字符串中出现的字符,其中 的第 位为 表示第 个字母已经出现过,为 表示未出现过。另外,我们还需要一个变量 来记录划分的子字符串个数,初始时 。
遍历字符串 中的每个字符,对于每个字符 ,我们将其转换为 到 之间的整数 ,然后判断 的第 位是否为 ,如果为 ,说明当前字符 与当前子字符串中的字符有重复,此时 需要加 ,并将 置为 ;否则,将 的第 位置为 。然后,我们将 更新为 与 的按位或结果。
最后,返回 即可。
时间复杂度 ,其中 为字符串 的长度。空间复杂度 。
func partitionString(s string) int {
ans, mask := 1, 0
for _, c := range s {
x := int(c - 'a')
if mask>>x&1 == 1 {
ans++
mask = 0
}
mask |= 1 << x
}
return ans
}
2406. 将区间分为最少组数 ​
题目、示例与约束
给你一个二维整数数组 intervals ,其中 intervals[i] = [lefti, righti] 表示 闭 区间 [lefti, righti] 。
你需要将 intervals 划分为一个或者多个区间 组 ,每个区间 只 属于一个组,且同一个组中任意两个区间 不相交 。
请你返回 最少 需要划分成多少个组。
如果两个区间覆盖的范围有重叠(即至少有一个公共数字),那么我们称这两个区间是 相交 的。比方说区间 [1, 5] 和 [5, 8] 相交。
示例 1:
输入:intervals = [[5,10],[6,8],[1,5],[2,3],[1,10]] 输出:3 解释:我们可以将区间划分为如下的区间组: - 第 1 组:[1, 5] ,[6, 8] 。 - 第 2 组:[2, 3] ,[5, 10] 。 - 第 3 组:[1, 10] 。 可以证明无法将区间划分为少于 3 个组。
示例 2:
输入:intervals = [[1,3],[5,6],[8,10],[11,13]] 输出:1 解释:所有区间互不相交,所以我们可以把它们全部放在一个组内。
提示:
1 <= intervals.length <= 105intervals[i].length == 21 <= lefti <= righti <= 106
方法一:贪心 + 优先队列(小根堆) · Go 实现
思考
,若对每个区间扫描已有组,最坏平方级。一组内区间不能相交,故组数等于同一时刻重叠数的最大值。按左端排序后,一个区间能加入某组当且仅当该组当前最右端点小于其左端。
用小根堆维护各组右端:能接到堆顶则弹出再压入新右端,否则新开一组。堆的大小即为最少组数。
我们先将区间按左端点排序,用小根堆维护每组的最右端点(堆顶是所有组的最右端点的最小值)。
接下来,我们遍历每个区间:
- 若当前区间左端点大于堆顶元素,说明当前区间可以加入到堆顶元素所在的组中,我们直接弹出堆顶元素,然后将当前区间的右端点放入堆中;
- 否则,说明当前没有组能容纳当前区间,那么我们就新建一个组,将当前区间的右端点放入堆中。
时间复杂度 ,空间复杂度 。其中 是数组 intervals 的长度。
func minGroups(intervals [][]int) int {
sort.Slice(intervals, func(i, j int) bool { return intervals[i][0] < intervals[j][0] })
q := hp{}
for _, e := range intervals {
if q.Len() > 0 && q.IntSlice[0] < e[0] {
heap.Pop(&q)
}
heap.Push(&q, e[1])
}
return q.Len()
}
type hp struct{ sort.IntSlice }
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
}
2410. 运动员和训练师的最大匹配数 ​
题目、示例与约束
给你一个下标从 0 开始的整数数组 players ,其中 players[i] 表示第 i 名运动员的 能力 值,同时给你一个下标从 0 开始的整数数组 trainers ,其中 trainers[j] 表示第 j 名训练师的 训练能力值 。
如果第 i 名运动员的能力值 小于等于 第 j 名训练师的能力值,那么第 i 名运动员可以 匹配 第 j 名训练师。除此以外,每名运动员至多可以匹配一位训练师,每位训练师最多可以匹配一位运动员。
请你返回满足上述要求 players 和 trainers 的 最大 匹配数。
示例 1:
输入:players = [4,7,9], trainers = [8,2,5,8] 输出:2 解释: 得到两个匹配的一种方案是: - players[0] 与 trainers[0] 匹配,因为 4 <= 8 。 - players[1] 与 trainers[3] 匹配,因为 7 <= 8 。 可以证明 2 是可以形成的最大匹配数。
示例 2:
输入:players = [1,1,1], trainers = [10] 输出:1 解释: 训练师可以匹配所有 3 个运动员 每个运动员至多只能匹配一个训练师,所以最大答案是 1 。
提示:
1 <= players.length, trainers.length <= 1051 <= players[i], trainers[j] <= 109
方法一:贪心 + 双指针 · Go 实现
思考
,两两尝试配对会超时。训练师若能匹配更弱的运动员,也就能匹配更强的运动员,所以应把刚好够用的训练师分给当前最弱的运动员。
两边按能力升序排列后,从弱到强处理运动员,训练师指针只向前走:能力不足的跳过,遇到第一个够用的就配上一对。总时间由排序决定。
把运动员和训练师都按能力从小到大排序。指针 和 分别指向当前考虑到的运动员和训练师。
从最弱的运动员开始匹配。训练师能力低于这名运动员时,将 右移,直到能力足够,或训练师已经用完。训练师用完后,后面的运动员只会更强,匹配结束,答案就是已经配上的人数 。否则当前这一对可以匹配,两个指针各前进一步。所有运动员都匹配成功时,返回运动员人数。
时间复杂度 ,空间复杂度 。其中 和 分别为运动员和训练师的数量。
func matchPlayersAndTrainers(players []int, trainers []int) int {
sort.Ints(players)
sort.Ints(trainers)
m, n := len(players), len(trainers)
for i, j := 0, 0; i < m; i, j = i+1, j+1 {
for j < n && trainers[j] < players[i] {
j++
}
if j == n {
return i
}
}
return m
}
方法二:贪心 + 双指针(遍历训练师) · Go 实现
思考
方法一在处理每名运动员时,还要用内层循环跳过能力不足的训练师,训练师用完则提前返回。改成遍历训练师之后,这两步都可以省去。
两边按能力升序排列,从弱到强查看训练师,指针停在最弱的未匹配运动员上。训练师能力不足时,剩余运动员只会更强,直接跳过。能力足够就完成匹配,指针前进一步。指针最终的位置就是匹配数,复杂度与方法一相同。
同样先把运动员和训练师按能力从小到大排序。指针 指向尚未匹配的最弱运动员,初始为 。
从弱到强查看每名训练师。若仍有运动员未匹配,且该运动员的能力不超过这名训练师,就配成一对,并将 加一。否则这名训练师无法匹配剩下的任何运动员,直接跳过。遍历结束后, 就是最大匹配数。
时间复杂度 ,空间复杂度 。其中 和 分别为运动员和训练师的数量。
func matchPlayersAndTrainers(players []int, trainers []int) int {
sort.Ints(players)
sort.Ints(trainers)
j, m := 0, len(players)
for _, t := range trainers {
if j < m && players[j] <= t {
j++
}
}
return j
}
2412. 完成所有交易的初始最少钱数 ​
题目、示例与约束
给你一个下标从 0 开始的二维整数数组 transactions,其中transactions[i] = [costi, cashbacki] 。
数组描述了若干笔交易。其中每笔交易必须以 某种顺序 恰好完成一次。在任意一个时刻,你有一定数目的钱 money ,为了完成交易 i ,money >= costi 这个条件必须为真。执行交易后,你的钱数 money 变成 money - costi + cashbacki 。
请你返回 任意一种 交易顺序下,你都能完成所有交易的最少钱数 money 是多少。
示例 1:
输入:transactions = [[2,1],[5,0],[4,2]] 输出:10 解释: 刚开始 money = 10 ,交易可以以任意顺序进行。 可以证明如果 money < 10 ,那么某些交易无法进行。
示例 2:
输入:transactions = [[3,0],[0,3]] 输出:3 解释: - 如果交易执行的顺序是 [[3,0],[0,3]] ,完成所有交易需要的最少钱数是 3 。 - 如果交易执行的顺序是 [[0,3],[3,0]] ,完成所有交易需要的最少钱数是 0 。 所以,刚开始钱数为 3 ,任意顺序下交易都可以全部完成。
提示:
1 <= transactions.length <= 105transactions[i].length == 20 <= costi, cashbacki <= 109
方法一:贪心 · Go 实现
思考
,枚举交易顺序不可行。亏钱交易的净亏损必须发生,总和 固定;钱最少的时刻出现在某笔交易扣款之后、回款之前。
把每一笔轮流当作「现金最低点所在的那笔」:若该笔亏钱,此前已计入的亏损含 ,再加上本笔支出 ,即需 ;若该笔赚钱,此前仍需备齐全部亏损 再支付 。取所有情况的最大值。
我们先累计所有负收益,记为 。然后枚举每个交易 作为最后一个交易,如果 ,说明当前的交易是亏钱的,而这个交易在此前我们累计负收益的时候,已经被计算,因此取 更新答案;否则,取 更新答案。
时间复杂度 ,其中 为交易数。空间复杂度 。
func minimumMoney(transactions [][]int) int64 {
s, ans := 0, 0
for _, e := range transactions {
s += max(0, e[0]-e[1])
}
for _, e := range transactions {
if e[0] > e[1] {
ans = max(ans, s+e[1])
} else {
ans = max(ans, s+e[0])
}
}
return int64(ans)
}
2422. 使用合并操作将数组转换为回文序列 🔒 ​
题目、示例与约束
给定一个由 正整数 组成的数组 nums。
可以对阵列执行如下操作,次数不限:
- 选择任意两个 相邻 的元素并用它们的 和 替换 它们。
- 例如,如果
nums = [1,2,3,1],则可以应用一个操作使其变为[1,5,1]。
- 例如,如果
返回将数组转换为 回文序列 所需的 最小 操作数。
示例 1:
输入: nums = [4,3,2,1,2,3,1] 输出: 2 解释: 我们可以通过以下 2 个操作将数组转换为回文: - 在数组的第 4 和第 5 个元素上应用该操作,nums 将等于 [4,3,2,3,3,1]. - 在数组的第 5 和第 6 个元素上应用该操作,nums 将等于 [4,3,2,3,4]. 数组 [4,3,2,3,4] 是一个回文序列。 可以证明,2 是所需的最小操作数。
示例 2:
输入: nums = [1,2,3,4] 输出: 3 解释: 我们在任意位置进行 3 次运算,最后得到数组 [10],它是一个回文序列。
提示:
1 <= nums.length <= 1051 <= nums[i] <= 106
方法一:贪心 + 双指针 · Go 实现
思考
合并只能把相邻数相加,目标是回文,操作次数应尽量少。两端最终必须相等,因此应从外向内配对:当前左右和谁更小,就在较小一侧继续合并。
双指针维护左右累计和 、 :较小一侧并入下一个元素并计数,相等则同时内收。每个元素至多被并入一次,线性完成。
定义两个指针 和 ,分别指向数组的首尾,用变量 和 分别表示首尾两个元素的值,变量 表示操作次数。
如果 ,我们将指针 向右移动一位,即 ,然后将 加上指针 指向的元素的值,即 ,同时将操作次数加一,即 。
如果 ,我们将指针 向左移动一位,即 ,然后将 加上指针 指向的元素的值,即 ,同时将操作次数加一,即 。
否则,说明 ,此时我们将指针 向右移动一位,即 ,将指针 向左移动一位,即 ,并且更新 和 的值,即 以及 。
循环上述过程,直至指针 ,返回操作次数 即可。
时间复杂度 ,其中 为数组的长度。空间复杂度 。
func minimumOperations(nums []int) int {
i, j := 0, len(nums)-1
a, b := nums[i], nums[j]
ans := 0
for i < j {
if a < b {
i++
a += nums[i]
ans++
} else if b < a {
j--
b += nums[j]
ans++
} else {
i, j = i+1, j-1
a, b = nums[i], nums[j]
}
}
return ans
}
2429. 最小异或 ​
题目、示例与约束
给你两个正整数 num1 和 num2 ,找出满足下述条件的正整数 x :
x的置位数和num2相同,且x XOR num1的值 最小
注意 XOR 是按位异或运算。
返回整数 x 。题目保证,对于生成的测试用例, x 是 唯一确定 的。
整数的 置位数 是其二进制表示中 1 的数目。
示例 1:
输入:num1 = 3, num2 = 5
输出:3
解释:
num1 和 num2 的二进制表示分别是 0011 和 0101 。
整数 3 的置位数与 num2 相同,且 3 XOR 3 = 0 是最小的。
示例 2:
输入:num1 = 1, num2 = 12
输出:3
解释:
num1 和 num2 的二进制表示分别是 0001 和 1100 。
整数 3 的置位数与 num2 相同,且 3 XOR 1 = 2 是最小的。
提示:
1 <= num1, num2 <= 109
方法一:贪心 + 位运算 · Go 实现
思考
的置位数必须等于 ,并与 的异或尽量小,即尽量让 在高位上对齐 的 。置位数至多 ,可按位贪心。
先从高到低占用 已有的 ;若名额仍剩,再从低到高填 为 的位,以免抬高异或结果。
根据题目描述,我们先求出 的置位数 ,然后从高位到低位枚举 的每一位,如果该位为 ,则将 的对应位设为 ,并将 减 ,直到 为 。如果此时 仍不为 ,则从低位开始将 的每一位为 的位置设为 ,并将 减 ,直到 为 。
时间复杂度 ,其中 为 和 的最大值。空间复杂度 。
func minimizeXor(num1 int, num2 int) int {
cnt := bits.OnesCount(uint(num2))
x := 0
for i := 30; i >= 0 && cnt > 0; i-- {
if num1>>i&1 == 1 {
x |= 1 << i
cnt--
}
}
for i := 0; cnt > 0; i++ {
if num1>>i&1 == 0 {
x |= 1 << i
cnt--
}
}
return x
}
2434. 使用机器人打印字典序最小的字符串 ​
题目、示例与约束
给你一个字符串 s 和一个机器人,机器人当前有一个空字符串 t 。执行以下操作之一,直到 s 和 t 都变成空字符串:
- 删除字符串
s的 第一个 字符,并将该字符给机器人。机器人把这个字符添加到t的尾部。 - 删除字符串
t的 最后一个 字符,并将该字符给机器人。机器人将该字符写到纸上。
请你返回纸上能写出的字典序最小的字符串。
示例 1:
输入:s = "zza" 输出:"azz" 解释:用 p 表示写出来的字符串。 一开始,p="" ,s="zza" ,t="" 。 执行第一个操作三次,得到 p="" ,s="" ,t="zza" 。 执行第二个操作三次,得到 p="azz" ,s="" ,t="" 。
示例 2:
输入:s = "bac" 输出:"abc" 解释:用 p 表示写出来的字符串。 执行第一个操作两次,得到 p="" ,s="c" ,t="ba" 。 执行第二个操作两次,得到 p="ab" ,s="c" ,t="" 。 执行第一个操作,得到 p="ab" ,s="" ,t="c" 。 执行第二个操作,得到 p="abc" ,s="" ,t="" 。
示例 3:
输入:s = "bdda" 输出:"addb" 解释:用 p 表示写出来的字符串。 一开始,p="" ,s="bdda" ,t="" 。 执行第一个操作四次,得到 p="" ,s="" ,t="bdda" 。 执行第二个操作四次,得到 p="addb" ,s="" ,t="" 。
提示:
1 <= s.length <= 105s只包含小写英文字母。
方法一:贪心 + 栈 · Go 实现
思考
,用栈模拟纸带:字符必须先入栈再弹出到答案。字典序最小意味着能弹出时,若栈顶不大于剩余未读的最小字符,就应立刻弹出。
先计数剩余字符,维护尚未出现的最小字母 。依次入栈,并在栈顶 时持续出栈。
题目可以转化为,给定一个字符串序列,在借助一个辅助栈的情况下,将其转化为字典序最小的字符串序列。
我们可以用数组 维护字符串 中每个字符的出现次数,用栈 作为题目中的辅助栈,用变量 维护还未遍历到的字符串中最小的字符。
遍历字符串 ,对于每个字符 ,我们先将字符 在数组 中的出现次数减一,更新 。然后将字符 入栈,此时如果栈顶元素小于等于 ,则循环将栈顶元素出栈,并将出栈的字符加入答案。
遍历结束,返回答案即可。
时间复杂度 ,空间复杂度 。其中 为字符串 的长度,而 为字符集大小,本题中 。
func robotWithString(s string) string {
cnt := make([]int, 26)
for _, c := range s {
cnt[c-'a']++
}
mi := byte('a')
stk := []byte{}
ans := []byte{}
for i := range s {
cnt[s[i]-'a']--
for mi < 'z' && cnt[mi-'a'] == 0 {
mi++
}
stk = append(stk, s[i])
for len(stk) > 0 && stk[len(stk)-1] <= mi {
ans = append(ans, stk[len(stk)-1])
stk = stk[:len(stk)-1]
}
}
return string(ans)
}
2448. 使数组相等的最小开销 ​
题目、示例与约束
给你两个下标从 0 开始的数组 nums 和 cost ,分别包含 n 个 正 整数。
你可以执行下面操作 任意 次:
- 将
nums中 任意 元素增加或者减小1。
对第 i 个元素执行一次操作的开销是 cost[i] 。
请你返回使 nums 中所有元素 相等 的 最少 总开销。
示例 1:
输入:nums = [1,3,5,2], cost = [2,3,1,14] 输出:8 解释:我们可以执行以下操作使所有元素变为 2 : - 增加第 0 个元素 1 次,开销为 2 。 - 减小第 1 个元素 1 次,开销为 3 。 - 减小第 2 个元素 3 次,开销为 1 + 1 + 1 = 3 。 总开销为 2 + 3 + 3 = 8 。 这是最小开销。
示例 2:
输入:nums = [2,2,2,2,2], cost = [4,2,8,1,3] 输出:0 解释:数组中所有元素已经全部相等,不需要执行额外的操作。
提示:
n == nums.length == cost.length1 <= n <= 1051 <= nums[i], cost[i] <= 106- 测试用例确保输出不超过 253-1。
方法一:前缀和 + 排序 + 枚举 · Go 实现
思考
把所有数变成同一 的代价是 , 不能对每个可能 暴力。按 排序后,代价由左右加权和组成,可用 与 的前缀和在枚举 时 求出。
最优 必取某个 (分段线性),故枚举排序后的每个位置取最小代价。
我们记数组 nums 所有元素为 ,数组 cost 所有元素为 。我们不妨令 ,即数组 nums 升序排列。
假设我们将数组 nums 中的元素全部变为 ,那么我们需要的总开销为:
其中 为 中小于等于 的元素个数。
我们可以使用前缀和的方法,计算 和 ,以及 和 。
然后我们枚举 ,计算上述四个前缀和,得到上述的总开销,取其中的最小值即可。
时间复杂度 。其中 为数组 nums 的长度。主要是排序的时间复杂度。
func minCost(nums []int, cost []int) int64 {
n := len(nums)
type pair struct{ a, b int }
arr := make([]pair, n)
for i, a := range nums {
b := cost[i]
arr[i] = pair{a, b}
}
sort.Slice(arr, func(i, j int) bool { return arr[i].a < arr[j].a })
f := make([]int, n+1)
g := make([]int, n+1)
for i := 1; i <= n; i++ {
a, b := arr[i-1].a, arr[i-1].b
f[i] = f[i-1] + a*b
g[i] = g[i-1] + b
}
var ans int64 = 1e18
for i := 1; i <= n; i++ {
a := arr[i-1].a
l := a*g[i-1] - f[i-1]
r := f[n] - f[i] - a*(g[n]-g[i])
ans = min(ans, int64(l+r))
}
return ans
}
方法二:排序 + 中位数 · Go 实现
思考
方法一枚举了每个 作为目标。将 视为 的出现次数后,加权中位数即为最优目标,只需找到前缀权超过一半的那个 ,再算一次总代价。
我们还可以把 看作是 的出现次数,那么中位数下标是 。把所有数变成中位数,一定是最优的。
时间复杂度 。其中 为数组 nums 的长度。主要是排序的时间复杂度。
相似题目:
func minCost(nums []int, cost []int) int64 {
n := len(nums)
type pair struct{ a, b int }
arr := make([]pair, n)
mid := 0
for i, a := range nums {
b := cost[i]
mid += b
arr[i] = pair{a, b}
}
mid /= 2
sort.Slice(arr, func(i, j int) bool { return arr[i].a < arr[j].a })
s, ans := 0, 0
for _, e := range arr {
x, c := e.a, e.b
s += c
if s > mid {
for _, t := range arr {
ans += abs(t.a-x) * t.b
}
break
}
}
return int64(ans)
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
2449. 使数组相似的最少操作次数 ​
题目、示例与约束
给你两个正整数数组 nums 和 target ,两个数组长度相等。
在一次操作中,你可以选择两个 不同 的下标 i 和 j ,其中 0 <= i, j < nums.length ,并且:
- 令
nums[i] = nums[i] + 2且 - 令
nums[j] = nums[j] - 2。
如果两个数组中每个元素出现的频率相等,我们称两个数组是 相似 的。
请你返回将 nums 变得与 target 相似的最少操作次数。测试数据保证 nums 一定能变得与 target 相似。
示例 1:
输入:nums = [8,12,6], target = [2,14,10] 输出:2 解释:可以用两步操作将 nums 变得与 target 相似: - 选择 i = 0 和 j = 2 ,nums = [10,12,4] 。 - 选择 i = 1 和 j = 2 ,nums = [10,14,2] 。 2 次操作是最少需要的操作次数。
示例 2:
输入:nums = [1,2,5], target = [4,1,3] 输出:1 解释:一步操作可以使 nums 变得与 target 相似: - 选择 i = 1 和 j = 2 ,nums = [1,4,3] 。
示例 3:
输入:nums = [1,1,1,1,1], target = [1,1,1,1,1] 输出:0 解释:数组 nums 已经与 target 相似。
提示:
n == nums.length == target.length1 <= n <= 1051 <= nums[i], target[i] <= 106nums一定可以变得与target相似。
方法一:奇偶分类 + 排序 · Go 实现
思考
每次 不改变奇偶,故奇数只能与奇数配对。 ,将两数组按「奇偶、数值」排序后逐对相减,总绝对差除以 即为操作次数(一次操作同时改变两个位置各 )。
注意到,由于每次操作,元素的值只会增加 或减少 ,因此,元素的奇偶性不会改变。
因此,我们可以将数组 和 分别按奇偶性分为两组,分别记为 和 ,以及 和 。
那么,我们只需要将 中的元素与 中的元素配对,将 中的元素与 中的元素配对,然后进行操作。配对的过程中,我们可以使用贪心的策略,每次将 中较小的元素与 中较小的元素配对,这样可以保证操作的次数最少。这里可以直接通过排序来实现。
由于每次操作,都可以将对应位置的元素差值减少 ,因此,我们累计每个对应位置的差值,最后除以 即可得到答案。
时间复杂度 ,其中 为数组 的长度。
func makeSimilar(nums []int, target []int) int64 {
sort.Ints(nums)
sort.Ints(target)
a1, a2, b1, b2 := []int{}, []int{}, []int{}, []int{}
for _, v := range nums {
if v%2 == 0 {
a1 = append(a1, v)
} else {
a2 = append(a2, v)
}
}
for _, v := range target {
if v%2 == 0 {
b1 = append(b1, v)
} else {
b2 = append(b2, v)
}
}
ans := 0
for i := 0; i < len(a1); i++ {
ans += abs(a1[i] - b1[i])
}
for i := 0; i < len(a2); i++ {
ans += abs(a2[i] - b2[i])
}
return int64(ans / 4)
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
2457. 美丽整数的最小增量 ​
题目、示例与约束
给你两个正整数 n 和 target 。
如果某个整数每一位上的数字相加小于或等于 target ,则认为这个整数是一个 美丽整数 。
找出并返回满足 n + x 是 美丽整数 的最小非负整数 x 。生成的输入保证总可以使 n 变成一个美丽整数。
示例 1:
输入:n = 16, target = 6 输出:4 解释:最初,n 是 16 ,且其每一位数字的和是 1 + 6 = 7 。在加 4 之后,n 变为 20 且每一位数字的和变成 2 + 0 = 2 。可以证明无法加上一个小于 4 的非负整数使 n 变成一个美丽整数。
示例 2:
输入:n = 467, target = 6 输出:33 解释:最初,n 是 467 ,且其每一位数字的和是 4 + 6 + 7 = 17 。在加 33 之后,n 变为 500 且每一位数字的和变成 5 + 0 + 0 = 5 。可以证明无法加上一个小于 33 的非负整数使 n 变成一个美丽整数。
示例 3:
输入:n = 1, target = 1 输出:0 解释:最初,n 是 1 ,且其每一位数字的和是 1 ,已经小于等于 target 。
提示:
1 <= n <= 10121 <= target <= 150- 生成的输入保证总可以使
n变成一个美丽整数。
方法一:贪心 · Go 实现
思考
,要加尽可能小的 使数位和 。把最低非零位进位到更高位,数位和会下降,相当于把后缀变成 并入上一位。
循环:若 的数位和仍大,找到最低非零位所在十幂 ,令 变为「该位以上加一再乘 」减去 。直至数位和合格。
我们定义函数 表示一个整数 的每一位数字之和,那么题目求的是 的最小非负整数 。
如果 的每一位数字之和大于 ,那么我们可以循环通过以下操作,将 的每一位数字之和减小到小于等于 :
- 找到 的最低位的非零数字,将其减小到 ,并将其高一位的数字加 ;
- 更新 ,继续上述操作,直到 的每一位数字之和小于等于 。
循环结束,返回 即可。
我们可以举个例子,假设 , ,那么 的变化过程如下:
时间复杂度 ,其中 给题目给定的整数。空间复杂度 。
func makeIntegerBeautiful(n int64, target int) (x int64) {
f := func(x int64) (y int) {
for ; x > 0; x /= 10 {
y += int(x % 10)
}
return
}
for f(n+x) > target {
y := n + x
var p int64 = 10
for y%10 == 0 {
y /= 10
p *= 10
}
x = (y/10+1)*p - n
}
return
}
2486. 追加字符以获得子序列 ​
题目、示例与约束
给你两个仅由小写英文字母组成的字符串 s 和 t 。
现在需要通过向 s 末尾追加字符的方式使 t 变成 s 的一个 子序列 ,返回需要追加的最少字符数。
子序列是一个可以由其他字符串删除部分(或不删除)字符但不改变剩下字符顺序得到的字符串。
示例 1:
输入:s = "coaching", t = "coding"
输出:4
解释:向 s 末尾追加字符串 "ding" ,s = "coachingding" 。
现在,t 是 s ("coachingding") 的一个子序列。
可以证明向 s 末尾追加任何 3 个字符都无法使 t 成为 s 的一个子序列。
示例 2:
输入:s = "abcde", t = "a"
输出:0
解释:t 已经是 s ("abcde") 的一个子序列。
示例 3:
输入:s = "z", t = "abcde"
输出:5
解释:向 s 末尾追加字符串 "abcde" ,s = "zabcde" 。
现在,t 是 s ("zabcde") 的一个子序列。
可以证明向 s 末尾追加任何 4 个字符都无法使 t 成为 s 的一个子序列。
提示:
1 <= s.length, t.length <= 105s和t仅由小写英文字母组成
方法一:双指针 · Go 实现
思考
只能在 末尾追加,使 成为子序列,追加量即 未能被 匹配的后缀长度。 ,双指针在 上贪心匹配 的前缀,返回 。
我们定义两个指针 和 ,分别指向字符串 和 的首字符。遍历字符串 ,如果 ,则将 向后移动一位。最终返回 ,其中 是字符串 的长度。
时间复杂度 ,其中 和 分别是字符串 和 的长度。空间复杂度 。
func appendCharacters(s string, t string) int {
n, j := len(t), 0
for _, c := range s {
if j < n && byte(c) == t[j] {
j++
}
}
return n - j
}
2498. 青蛙过河 II ​
题目、示例与约束
给你一个下标从 0 开始的整数数组 stones ,数组中的元素 严格递增 ,表示一条河中石头的位置。
一只青蛙一开始在第一块石头上,它想到达最后一块石头,然后回到第一块石头。同时每块石头 至多 到达 一次。
一次跳跃的 长度 是青蛙跳跃前和跳跃后所在两块石头之间的距离。
- 更正式的,如果青蛙从
stones[i]跳到stones[j],跳跃的长度为|stones[i] - stones[j]|。
一条路径的 代价 是这条路径里的 最大跳跃长度 。
请你返回这只青蛙的 最小代价 。
示例 1:

输入:stones = [0,2,5,6,7] 输出:5 解释:上图展示了一条最优路径。 这条路径的代价是 5 ,是这条路径中的最大跳跃长度。 无法得到一条代价小于 5 的路径,我们返回 5 。
示例 2:

输入:stones = [0,3,9] 输出:9 解释: 青蛙可以直接跳到最后一块石头,然后跳回第一块石头。 在这条路径中,每次跳跃长度都是 9 。所以路径代价是 max(9, 9) = 9 。 这是可行路径中的最小代价。
提示:
2 <= stones.length <= 1050 <= stones[i] <= 109stones[0] == 0stones中的元素严格递增。
方法一:脑筋急转弯 · Go 实现
思考
去程与回程必须访问所有石块且方向各一次,最小化最大跳距。 ,最优是间隔跳:去程走偶数下标,回程走奇数(或反之),最大跨度即相邻隔一块的距离,还要计入第一步 。
要使得跳跃过程中的每一步的最大跳跃长度最小,我们应该将跳跃过程切分成尽可能多的连续的步骤。通过观察,间隔跳跃可以获取最优解。
时间复杂度 ,空间复杂度 。其中 为数组 stones 的长度。
func maxJump(stones []int) int {
ans := stones[1] - stones[0]
for i := 2; i < len(stones); i++ {
ans = max(ans, stones[i]-stones[i-2])
}
return ans
}
2499. 让数组不相等的最小总代价 ​
题目、示例与约束
给你两个下标从 0 开始的整数数组 nums1 和 nums2 ,两者长度都为 n 。
每次操作中,你可以选择交换 nums1 中任意两个下标处的值。操作的 开销 为两个下标的 和 。
你的目标是对于所有的 0 <= i <= n - 1 ,都满足 nums1[i] != nums2[i] ,你可以进行 任意次 操作,请你返回达到这个目标的 最小 总代价。
请你返回让 nums1 和 nums2 满足上述条件的 最小总代价 ,如果无法达成目标,返回 -1 。
示例 1:
输入:nums1 = [1,2,3,4,5], nums2 = [1,2,3,4,5] 输出:10 解释: 实现目标的其中一种方法为: - 交换下标为 0 和 3 的两个值,代价为 0 + 3 = 3 。现在 nums1 = [4,2,3,1,5] 。 - 交换下标为 1 和 2 的两个值,代价为 1 + 2 = 3 。现在 nums1 = [4,3,2,1,5] 。 - 交换下标为 0 和 4 的两个值,代价为 0 + 4 = 4 。现在 nums1 = [5,3,2,1,4] 。 最后,对于每个下标 i ,都有 nums1[i] != nums2[i] 。总代价为 10 。 还有别的交换值的方法,但是无法得到代价和小于 10 的方案。
示例 2:
输入:nums1 = [2,2,2,1,3], nums2 = [1,2,2,3,3] 输出:10 解释: 实现目标的一种方法为: - 交换下标为 2 和 3 的两个值,代价为 2 + 3 = 5 。现在 nums1 = [2,2,1,2,3] 。 - 交换下标为 1 和 4 的两个值,代价为 1 + 4 = 5 。现在 nums1 = [2,3,1,2,2] 。 总代价为 10 ,是所有方案中的最小代价。
示例 3:
输入:nums1 = [1,2,2], nums2 = [1,2,2] 输出:-1 解释: 不管怎么操作,都无法满足题目要求。 所以返回 -1 。
提示:
n == nums1.length == nums2.length1 <= n <= 1051 <= nums1[i], nums2[i] <= n
方法一:贪心 · Go 实现
思考
相等位置必须换出去,下标之和先计入这些强制交换。若某值在强制位置中超过一半,则无法在内部两两交换消化,还得到其他位置找与该值无关的一对,再累加那些下标。
先统计 的次数;若最大频次 满足 ,超额 必须向外交换。再扫 且两端都不是该主导值的位置。仍有剩余则无解。
我们先同时遍历数组 nums1 和 nums2,统计相同位置上的值相同的个数 same,这些位置上的值必须交换,因此,将这些位置下标累加到答案中。另外,用数组或哈希表 cnt 统计这些相同值的出现次数。
如果所有相同值的出现次数均不超过 same 的一半,那么意味着,我们可以在其内部,通过两两交换,使得对应位置上的值不同,而这些交换,已经在上面累加下标时计入了答案中了,无需额外的代价。否则,如果某个值的出现次数超过 same 的一半,那么对于这个值就是多出的个数,我们需要在数组的其他位置上找到合适的,进行交换。这里我们可以直接遍历一遍数组得出。
如果最终还有剩余位置未能交换,说明无法达成目标,返回 即可,否则返回答案。
时间复杂度 ,空间复杂度 。其中 是数组 nums1 或 nums2 的长度。
func minimumTotalCost(nums1 []int, nums2 []int) (ans int64) {
same, n := 0, len(nums1)
cnt := make([]int, n+1)
for i, a := range nums1 {
b := nums2[i]
if a == b {
same++
ans += int64(i)
cnt[a]++
}
}
var m, lead int
for i, v := range cnt {
if t := v*2 - same; t > 0 {
m = t
lead = i
break
}
}
for i, a := range nums1 {
b := nums2[i]
if m > 0 && a != b && a != lead && b != lead {
ans += int64(i)
m--
}
}
if m > 0 {
return -1
}
return ans
}
2517. 礼盒的最大甜蜜度 ​
题目、示例与约束
给你一个正整数数组 price ,其中 price[i] 表示第 i 类糖果的价格,另给你一个正整数 k 。
商店组合 k 类 不同 糖果打包成礼盒出售。礼盒的 甜蜜度 是礼盒中任意两种糖果 价格 绝对差的最小值。
返回礼盒的 最大 甜蜜度。
示例 1:
输入:price = [13,5,1,8,21,2], k = 3 输出:8 解释:选出价格分别为 [13,5,21] 的三类糖果。 礼盒的甜蜜度为 min(|13 - 5|, |13 - 21|, |5 - 21|) = min(8, 8, 16) = 8 。 可以证明能够取得的最大甜蜜度就是 8 。
示例 2:
输入:price = [1,3,1], k = 2 输出:2 解释:选出价格分别为 [1,3] 的两类糖果。 礼盒的甜蜜度为 min(|1 - 3|) = min(2) = 2 。 可以证明能够取得的最大甜蜜度就是 2 。
示例 3:
输入:price = [7,7,7,7], k = 2 输出:0 解释:从现有的糖果中任选两类糖果,甜蜜度都会是 0 。
提示:
2 <= k <= price.length <= 1051 <= price[i] <= 109
方法一:贪心 + 二分查找 · Go 实现
思考
礼盒的甜蜜度是所选 个价格中任意两两差的最小值,要求该最小值尽量大。枚举子集在 时不可行。
若甜蜜度 可达,则更小的值也可达,故对 二分。排序后从左到右贪心:与上一所选价格之差至少为 就选,能选出 个则 可行。上界为最大价与最小价之差。
我们注意到,如果一个甜蜜度为 的礼盒是可行的,那么甜蜜度小于 的礼盒也是可行的,这存在着单调性,因此我们可以使用二分查找的方法,找到最大的可行甜蜜度。
我们首先将数组 排序,然后定义二分查找的左边界 , 。每一次,我们计算出当前的中间值 ,以 作为甜蜜度,判断是否可行。若可行,那么我们将左边界 更新为 ,否则将右边界 更新为 。最后返回 即可。
那么问题的关键转化为:判断一个甜蜜度是否可行,我们通过函数 来实现。函数 的执行逻辑如下:
定义一个变量 表示当前已经选取的糖果的数量,初始值为 ,定义一个变量 表示上一个选取的糖果的价格,初始值为 。然后我们遍历排好序的数组 ,对于每一个糖果的价格 ,如果 ,那么我们就选取这个糖果,将 更新为 ,并将 加一。最后判断 是否大于等于 ,如果是,那么返回 ,否则返回 。
时间复杂度 ,空间复杂度 。其中 是数组 的长度;而 是数组 中的最大值,本题中 。
func maximumTastiness(price []int, k int) int {
sort.Ints(price)
return sort.Search(price[len(price)-1], func(x int) bool {
cnt, pre := 0, -x
for _, cur := range price {
if cur-pre >= x {
pre = cur
cnt++
}
}
return cnt < k
}) - 1
}
2528. 最大化城市的最小电量 ​
题目、示例与约束
给你一个下标从 0 开始长度为 n 的整数数组 stations ,其中 stations[i] 表示第 i 座城市的供电站数目。
每个供电站可以在一定 范围 内给所有城市提供电力。换句话说,如果给定的范围是 r ,在城市 i 处的供电站可以给所有满足 |i - j| <= r 且 0 <= i, j <= n - 1 的城市 j 供电。
|x|表示x的 绝对值 。比方说,|7 - 5| = 2,|3 - 10| = 7。
一座城市的 电量 是所有能给它供电的供电站数目。
政府批准了可以额外建造 k 座供电站,你需要决定这些供电站分别应该建在哪里,这些供电站与已经存在的供电站有相同的供电范围。
给你两个整数 r 和 k ,如果以最优策略建造额外的发电站,返回所有城市中,最小电量的最大值是多少。
这 k 座供电站可以建在多个城市。
示例 1:
输入:stations = [1,2,4,5,0], r = 1, k = 2 输出:5 解释: 最优方案之一是把 2 座供电站都建在城市 1 。 每座城市的供电站数目分别为 [1,4,4,5,0] 。 - 给城市 0 供电的供电站数目为 1 + 4 = 5 。 - 给城市 1 供电的供电站数目为 1 + 4 + 4 = 9 。 - 给城市 2 供电的供电站数目为 4 + 4 + 5 = 13 。 - 给城市 3 供电的供电站数目为 5 + 4 = 9 。 - 给城市 4 供电的供电站数目为 5 + 0 = 5 。 供电站数目最少是 5 。 无法得到更优解,所以我们返回 5 。
示例 2:
输入:stations = [4,4,4,4], r = 0, k = 3 输出:4 解释: 无论如何安排,总有一座城市的供电站数目是 4 ,所以最优解是 4 。
提示:
n == stations.length1 <= n <= 1050 <= stations[i] <= 1050 <= r <= n - 10 <= k <= 109
方法一:二分查找 + 差分数组 + 贪心 · Go 实现
思考
一座电站覆盖半径 内的城市,还可再新建至多 座,目标是最大化“供电最少的那座城市”的电量。 ,不能对每个 的分配方案搜索。
最小电量对新建数量单调,故二分目标值 。差分数组先算出每座城市的初始供电。检验时从左到右:当前城市不足 则把缺口建在能覆盖它的最右侧,使后续城市尽量受益,并用差分数组记录这段增量。缺口超过剩余 则 不可行。
根据题目描述,最小供电站数目随着 值的增大而增大,因此,我们可以用二分查找,找到一个最大的最小供电站数目,并且需要额外建造的供电站不超过 座。
我们先利用差分数组以及前缀和算出初始时每座城市的供电站数目,记录在数组 中,其中 表示第 座城市的供电站数目。
接下来,我们定义二分查找的左边界为 ,右边界为 。然后实现一个 函数,用于判断是否城市供电站数目的最小值是否可以为 ,使得额外建造的供电站不超过 座。
函数 的实现逻辑是:
遍历每座城市,如果当前城市 的供电站数目小于 ,此时我们可以贪心地在尽可能右边的位置上建造供电站,位置 ,这样可以使得供电站覆盖尽可能多的城市。过程中我们可以借助差分数组,给一段连续的位置加上某个值。如果需要额外建造的供电站数量超过 ,那么 不满足条件,返回 false。否则遍历结束后,返回 true。
时间复杂度 ,空间复杂度 。其中 为城市数量,而 我们固定取 。
func maxPower(stations []int, r int, k int) int64 {
n := len(stations)
d := make([]int, n+1)
s := make([]int, n+1)
for i, v := range stations {
left, right := max(0, i-r), min(i+r, n-1)
d[left] += v
d[right+1] -= v
}
s[0] = d[0]
for i := 1; i < n+1; i++ {
s[i] = s[i-1] + d[i]
}
check := func(x, k int) bool {
d := make([]int, n+1)
t := 0
for i := range stations {
t += d[i]
dist := x - (s[i] + t)
if dist > 0 {
if k < dist {
return false
}
k -= dist
j := min(i+r, n-1)
left, right := max(0, j-r), min(j+r, n-1)
d[left] += dist
d[right+1] -= dist
t += dist
}
}
return true
}
left, right := 0, 1<<40
for left < right {
mid := (left + right + 1) >> 1
if check(mid, k) {
left = mid
} else {
right = mid - 1
}
}
return int64(left)
}
2530. 执行 K 次操作后的最大分数 ​
题目、示例与约束
给你一个下标从 0 开始的整数数组 nums 和一个整数 k 。你的 起始分数 为 0 。
在一步 操作 中:
- 选出一个满足
0 <= i < nums.length的下标i, - 将你的 分数 增加
nums[i],并且 - 将
nums[i]替换为ceil(nums[i] / 3)。
返回在 恰好 执行 k 次操作后,你可能获得的最大分数。
向上取整函数 ceil(val) 的结果是大于或等于 val 的最小整数。
示例 1:
输入:nums = [10,10,10,10,10], k = 5 输出:50 解释:对数组中每个元素执行一次操作。最后分数是 10 + 10 + 10 + 10 + 10 = 50 。
示例 2:
输入:nums = [1,10,3,3,3], k = 3 输出:17 解释:可以执行下述操作: 第 1 步操作:选中 i = 1 ,nums 变为 [1,4,3,3,3] 。分数增加 10 。 第 2 步操作:选中 i = 1 ,nums 变为 [1,2,3,3,3] 。分数增加 4 。 第 3 步操作:选中 i = 2 ,nums 变为 [1,2,1,3,3] 。分数增加 3 。 最后分数是 10 + 4 + 3 = 17 。
提示:
1 <= nums.length, k <= 1051 <= nums[i] <= 109
方法一:优先队列(大根堆) · Go 实现
思考
每次把当前最大值 加入分数并改成 ,重复 次。若每次扫描数组找最大, 与 达 时为平方级。
大根堆始终给出当前最大者:弹出 ,累加后再把 压回。Python 用相反数模拟大根堆,共 次对数操作。
要使得分数最大化,我们需要在每一步操作中,选择元素值最大的元素进行操作。因此,我们可以使用优先队列(大根堆)来维护当前元素值最大的元素。
每次从优先队列中取出元素值最大的元素 ,将答案加上 ,并将 替换为 ,加入优先队列。重复 次后,将答案返回即可。
时间复杂度 ,空间复杂度 或 。其中 为数组 的长度。
func maxKelements(nums []int, k int) (ans int64) {
h := hp{nums}
heap.Init(&h)
for ; k > 0; k-- {
ans += int64(h.IntSlice[0])
h.IntSlice[0] = (h.IntSlice[0] + 2) / 3
heap.Fix(&h, 0)
}
return
}
type hp struct{ sort.IntSlice }
func (h hp) Less(i, j int) bool { return h.IntSlice[i] > h.IntSlice[j] }
func (hp) Push(any) {}
func (hp) Pop() (_ any) { return }
2541. 使数组中所有元素相等的最小操作数 II ​
题目、示例与约束
给你两个整数数组 nums1 和 nums2 ,两个数组长度都是 n ,再给你一个整数 k 。你可以对数组 nums1 进行以下操作:
- 选择两个下标
i和j,将nums1[i]增加k,将nums1[j]减少k。换言之,nums1[i] = nums1[i] + k且nums1[j] = nums1[j] - k。
如果对于所有满足 0 <= i < n 都有 num1[i] == nums2[i] ,那么我们称 nums1 等于 nums2 。
请你返回使 nums1 等于 nums2 的 最少 操作数。如果没办法让它们相等,请你返回 -1 。
示例 1:
输入:nums1 = [4,3,1,4], nums2 = [1,3,7,1], k = 3 输出:2 解释:我们可以通过 2 个操作将 nums1 变成 nums2 。 第 1 个操作:i = 2 ,j = 0 。操作后得到 nums1 = [1,3,4,4] 。 第 2 个操作:i = 2 ,j = 3 。操作后得到 nums1 = [1,3,7,1] 。 无法用更少操作使两个数组相等。
示例 2:
输入:nums1 = [3,8,5,2], nums2 = [2,4,1,6], k = 1 输出:-1 解释:无法使两个数组相等。
提示:
n == nums1.length == nums2.length2 <= n <= 1050 <= nums1[i], nums2[j] <= 1090 <= k <= 105
方法一:一次遍历 · Go 实现
思考
一次操作把某一位加 并把另一位减 ,即总量守恒。若某位差值不能被 整除,或 却仍有不等,则无法对齐。
统计需要“加 ”的次数 与“减 ”的次数 。操作成对出现,故仅当 时可行,答案为 。
我们用两个变量 和 分别记录将 中的元素增加 和减少 的次数。
我们遍历两个数组,如果两个指针指向的元素相等,则继续;如果两个指针指向的元素不相等,则如果 等于 或者两个元素之差不能被 整除,则返回 ;否则将两个元素之差除以 得到操作数 ,如果 小于 ,则将 加到 上,否则将 加到 上。
最后如果 和 相等,则返回 ,否则返回 。
时间复杂度 ,其中 是两个数组的长度。空间复杂度 。
func minOperations(nums1 []int, nums2 []int, k int) int64 {
var a, b int64
for i, x := range nums1 {
y := nums2[i]
if x == y {
continue
}
if k == 0 || (x-y)%k != 0 {
return -1
}
t := (x - y) / k
if t < 0 {
a += int64(-t)
} else {
b += int64(t)
}
}
if a == b {
return a
}
return -1
}
2542. 最大子序列的分数 ​
题目、示例与约束
给你两个下标从 0 开始的整数数组 nums1 和 nums2 ,两者长度都是 n ,再给你一个正整数 k 。你必须从 nums1 中选一个长度为 k 的 子序列 对应的下标。
对于选择的下标 i0 ,i1 ,..., ik - 1 ,你的 分数 定义如下:
nums1中下标对应元素求和,乘以nums2中下标对应元素的 最小值 。- 用公式表示:
(nums1[i0] + nums1[i1] +...+ nums1[ik - 1]) * min(nums2[i0] , nums2[i1], ... ,nums2[ik - 1])。
请你返回 最大 可能的分数。
一个数组的 子序列 下标是集合 {0, 1, ..., n-1} 中删除若干元素得到的剩余集合,也可以不删除任何元素。
示例 1:
输入:nums1 = [1,3,3,2], nums2 = [2,1,3,4], k = 3 输出:12 解释: 四个可能的子序列分数为: - 选择下标 0 ,1 和 2 ,得到分数 (1+3+3) * min(2,1,3) = 7 。 - 选择下标 0 ,1 和 3 ,得到分数 (1+3+2) * min(2,1,4) = 6 。 - 选择下标 0 ,2 和 3 ,得到分数 (1+3+2) * min(2,3,4) = 12 。 - 选择下标 1 ,2 和 3 ,得到分数 (3+3+2) * min(1,3,4) = 8 。 所以最大分数为 12 。
示例 2:
输入:nums1 = [4,2,3,1,1], nums2 = [7,5,10,9,6], k = 1 输出:30 解释: 选择下标 2 最优:nums1[2] * nums2[2] = 3 * 10 = 30 是最大可能分数。
提示:
n == nums1.length == nums2.length1 <= n <= 1050 <= nums1[i], nums2[j] <= 1051 <= k <= n
方法一:排序 + 优先队列(小根堆) · Go 实现
思考
从下标中选 个,得分是 所选之和乘以 所选的最小值。枚举全部 元子集不可行。
若最小值被定为某个 ,则只能在 的位置中选 个 。按 降序扫描时,最小值恰好是当前的 ,前面的候选都合法。小根堆保留最大的 个 ,堆满时用和乘 更新答案并弹出最小者,为后续腾出位置。
将 nums2 与 nums1 按照 nums2 降序排序,然后从前往后遍历,维护一个小根堆,堆中存储 nums1 中的元素,堆中元素个数不超过 个,同时维护一个变量 ,表示堆中元素的和,遍历过程中不断更新答案。
时间复杂度 ,空间复杂度 。其中 为数组 nums1 的长度。
func maxScore(nums1 []int, nums2 []int, k int) int64 {
type pair struct{ a, b int }
nums := []pair{}
for i, a := range nums1 {
b := nums2[i]
nums = append(nums, pair{a, b})
}
sort.Slice(nums, func(i, j int) bool { return nums[i].b > nums[j].b })
q := hp{}
var ans, s int
for _, e := range nums {
a, b := e.a, e.b
s += a
heap.Push(&q, a)
if q.Len() == k {
ans = max(ans, s*b)
s -= heap.Pop(&q).(int)
}
}
return int64(ans)
}
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
}
2548. 填满背包的最大价格 🔒 ​
题目、示例与约束
给定一个二维整数数组 items ,其中 items[i] = [pricei, weighti] 表示第 i 个物品的价格和重量。
还给定一个 正 整数容量 capacity 。
每个物品可以分成两个部分,比率为 part1 和 part2 ,其中 part1 + part2 == 1 。
- 第一个物品的重量是
weighti * part1,价格是pricei * part1。 - 同样,第二个物品的重量是
weighti * part2,价格是pricei * part2。
使用给定的物品,返回填满容量为 capacity 的背包所需的 最大总价格 。如果无法填满背包,则返回 -1 。与实际答案的差距在 10-5 以内的 实际答案 将被视为接受。
示例 1 :
输入:items = [[50,1],[10,8]], capacity = 5 输出:55.00000 解释: 我们将第二个物品分成两个部分,part1 = 0.5,part2 = 0.5。 第一个物品的价格和重量分别为 5 和 4 。同样地,第二个物品的价格和重量也是 5 和 4 。 经过操作后,数组 items 变为 [[50,1],[5,4],[5,4]] 。 为了填满容量为 5 的背包,我们取价格为 50 的第一个元素和价格为 5 的第二个元素。 可以证明,55.0 是我们可以达到的最大总价值。
示例 2 :
输入:items = [[100,30]], capacity = 50 输出:-1.00000 解释:无法用给定的物品装满背包。
提示:
1 <= items.length <= 105items[i].length == 21 <= pricei, weighti <= 1041 <= capacity <= 109
方法一:贪心 + 排序 · Go 实现
思考
物品可按重量比例取用,容量固定,要最大化总价;装不满则返回 。这是分数背包:单位价格高的应优先取。
按 (即单位价的倒数)升序等价于按单位价降序。依次取 ,累加对应价格;容量仍剩余说明总重量不足。
我们将物品按照单位价格从大到小排序,然后依次取出物品,直到背包装满。
若最后背包未装满,则返回 ,否则返回总价格。
时间复杂度 ,空间复杂度 。其中 为物品数量。
func maxPrice(items [][]int, capacity int) (ans float64) {
sort.Slice(items, func(i, j int) bool { return items[i][1]*items[j][0] < items[i][0]*items[j][1] })
for _, e := range items {
p, w := e[0], e[1]
v := min(w, capacity)
ans += float64(v) / float64(w) * float64(p)
capacity -= v
}
if capacity > 0 {
return -1
}
return
}
2551. 将珠子放入背包中 ​
题目、示例与约束
你有 k 个背包。给你一个下标从 0 开始的整数数组 weights ,其中 weights[i] 是第 i 个珠子的重量。同时给你整数 k 。
请你按照如下规则将所有的珠子放进 k 个背包。
- 没有背包是空的。
- 如果第
i个珠子和第j个珠子在同一个背包里,那么下标在i到j之间的所有珠子都必须在这同一个背包中。 - 如果一个背包有下标从
i到j的所有珠子,那么这个背包的价格是weights[i] + weights[j]。
一个珠子分配方案的 分数 是所有 k 个背包的价格之和。
请你返回所有分配方案中,最大分数 与 最小分数 的 差值 为多少。
示例 1:
输入:weights = [1,3,5,1], k = 2 输出:4 解释: 分配方案 [1],[3,5,1] 得到最小得分 (1+1) + (3+1) = 6 。 分配方案 [1,3],[5,1] 得到最大得分 (1+3) + (5+1) = 10 。 所以差值为 10 - 6 = 4 。
示例 2:
输入:weights = [1, 3], k = 2 输出:0 解释:唯一的分配方案为 [1],[3] 。 最大最小得分相等,所以返回 0 。
提示:
1 <= k <= weights.length <= 1051 <= weights[i] <= 109
方法一:问题转化 + 排序 · Go 实现
思考
把大理石按顺序分成 段,一段代价是两端重量之和,求最大总分与最小总分之差。首尾两颗始终计入,中间每切一刀在相邻对 上加一次。
因此切点互不影响。把所有相邻和排序,最大的 个之和减去最小的 个之和即为两端固定贡献抵消后的差值。
我们可以将问题转化为:将数组 weights 分成 个连续的子数组,也就是说,我们要找到 个分割点,每个分割点的价格是分割点左右两个元素的和,求最大的 个分割点的价格之和与最小的 个分割点的价格之和的差值,即为答案。
因此,我们可以处理数组 weights,将其转化为一个长度为 的数组 arr,其中 arr[i] = weights[i] + weights[i+1],然后对数组 arr 进行排序,最后求出最大的 个分割点的价格之和与最小的 个分割点的价格之和的差值即可。
时间复杂度 ,空间复杂度 。其中 为数组 weights 的长度。
func putMarbles(weights []int, k int) (ans int64) {
n := len(weights)
arr := make([]int, n-1)
for i, w := range weights[:n-1] {
arr[i] = w + weights[i+1]
}
sort.Ints(arr)
for i := 0; i < k-1; i++ {
ans += int64(arr[n-2-i] - arr[i])
}
return
}
2554. 从一个范围内选择最多整数 I ​
题目、示例与约束
给你一个整数数组 banned 和两个整数 n 和 maxSum 。你需要按照以下规则选择一些整数:
- 被选择整数的范围是
[1, n]。 - 每个整数 至多 选择 一次 。
- 被选择整数不能在数组
banned中。 - 被选择整数的和不超过
maxSum。
请你返回按照上述规则 最多 可以选择的整数数目。
示例 1:
输入:banned = [1,6,5], n = 5, maxSum = 6 输出:2 解释:你可以选择整数 2 和 4 。 2 和 4 在范围 [1, 5] 内,且它们都不在 banned 中,它们的和是 6 ,没有超过 maxSum 。
示例 2:
输入:banned = [1,2,3,4,5,6,7], n = 8, maxSum = 1 输出:0 解释:按照上述规则无法选择任何整数。
示例 3:
输入:banned = [11], n = 7, maxSum = 50 输出:7 解释:你可以选择整数 1, 2, 3, 4, 5, 6 和 7 。 它们都在范围 [1, 7] 中,且都没出现在 banned 中,它们的和是 28 ,没有超过 maxSum 。
提示:
1 <= banned.length <= 1041 <= banned[i], n <= 1041 <= maxSum <= 109
方法一:贪心 + 枚举 · Go 实现
思考
从 中选尽可能多的互异整数,避开 ,且和不超过 。选小数更利于多选,故从 起贪心。
时枚举每个候选,用集合判禁并累加,和一旦超限即停。
我们用变量 表示当前已经选择的整数的和,用变量 表示当前已经选择的整数的个数。将数组 banned 转换为哈希表,方便判断某个整数是否不可选。
接下来,我们从 开始枚举整数 ,如果 且 不在 banned 中,那么我们就可以选择整数 ,并将 和 分别加上 和 。
最终,我们返回 即可。
时间复杂度 ,空间复杂度 。其中 为给定的整数。
func maxCount(banned []int, n int, maxSum int) (ans int) {
ban := map[int]bool{}
for _, x := range banned {
ban[x] = true
}
s := 0
for i := 1; i <= n && s+i <= maxSum; i++ {
if !ban[i] {
ans++
s += i
}
}
return
}
方法二:贪心 + 二分查找 · Go 实现
思考
方法一的时间随 线性增长,在 更大的变体中不可用。禁选点把 切成若干连续段,段内前缀和是等差数列,可二分这段能取多少个。从左到右取满能取的,直到额度用尽。
如果 很大,那么方法一中的枚举会超时。
我们可以在数组 banned 中加入 和 ,并将数组 banned 去重且移除大于 的元素,然后进行排序。
接下来,我们枚举数组 banned 中的每两个相邻元素 和 ,那么可选的整数范围就是 。二分枚举我们在此区间内能够选择的元素个数,找到最大的可选元素个数,然后将其加到 中。同时在 maxSum 中减去这些元素的和。如果 maxSum 小于 ,那么我们跳出循环。返回答案即可。
时间复杂度 ,空间复杂度 。其中 为数组 banned 的长度。
相似题目:
func maxCount(banned []int, n int, maxSum int) (ans int) {
banned = append(banned, []int{0, n + 1}...)
sort.Ints(banned)
ban := []int{}
for i, x := range banned {
if (i > 0 && x == banned[i-1]) || x > n+1 {
continue
}
ban = append(ban, x)
}
for k := 1; k < len(ban); k++ {
i, j := ban[k-1], ban[k]
left, right := 0, j-i-1
for left < right {
mid := (left + right + 1) >> 1
if (i+1+i+mid)*mid/2 <= maxSum {
left = mid
} else {
right = mid - 1
}
}
ans += left
maxSum -= (i + 1 + i + left) * left / 2
if maxSum <= 0 {
break
}
}
return
}
2557. 从一个范围内选择最多整数 II 🔒 ​
题目、示例与约束
给你一个整数数组 banned 和两个整数 n 和 maxSum 。你需要按照以下规则选择一些整数:
- 被选择整数的范围是
[1, n]。 - 每个整数 至多 选择 一次 。
- 被选择整数不能在数组
banned中。 - 被选择整数的和不超过
maxSum。
请你返回按照上述规则 最多 可以选择的整数数目。
示例 1:
输入:banned = [1,4,6], n = 6, maxSum = 4 输出:1 解释:你可以选择整数 3 。 3 在范围 [1, 6] 内,且不在 banned 中,所选整数的和为 3 ,也没有超过 maxSum 。
示例 2:
输入:banned = [4,3,5,6], n = 7, maxSum = 18 输出:3 解释:你可以选择整数 1, 2 和 7 。 它们都在范围 [1, 7] 中,且都没出现在 banned 中,所选整数的和为 10 ,没有超过 maxSum 。
提示:
1 <= banned.length <= 1051 <= banned[i] <= n <= 1091 <= maxSum <= 1015
方法一:去重 + 排序 + 二分查找 · Go 实现
思考
题意与“范围 I”相同,但 与 大到不能从 枚举到 。
把 与 并入禁选集合并排序后,相邻禁选点之间是连续可选段。段内前 个数的和为等差公式,对 二分即可在对数时间内取满当前额度,从左向右直到和用尽。
我们可以在数组 banned 中加入 和 ,将数组 banned 去重并排序。
接下来,我们枚举数组 banned 中的每两个相邻元素 和 ,那么可选的整数范围就是 。二分枚举我们在此区间内能够选择的元素个数,找到最大的可选元素个数,然后将其加到 中。同时在 maxSum 中减去这些元素的和。如果 maxSum 小于 ,那么我们跳出循环。返回答案即可。
时间复杂度 ,空间复杂度 。其中 为数组 banned 的长度。
func maxCount(banned []int, n int, maxSum int64) (ans int) {
banned = append(banned, []int{0, n + 1}...)
sort.Ints(banned)
ban := []int{}
for i, x := range banned {
if i > 0 && x == banned[i-1] {
continue
}
ban = append(ban, x)
}
for k := 1; k < len(ban); k++ {
i, j := ban[k-1], ban[k]
left, right := 0, j-i-1
for left < right {
mid := (left + right + 1) >> 1
if int64((i+1+i+mid)*mid/2) <= maxSum {
left = mid
} else {
right = mid - 1
}
}
ans += left
maxSum -= int64((i + 1 + i + left) * left / 2)
if maxSum <= 0 {
break
}
}
return
}
2561. 重排水果 ​
题目、示例与约束
你有两个果篮,每个果篮中有 n 个水果。给你两个下标从 0 开始的整数数组 basket1 和 basket2 ,用以表示两个果篮中每个水果的交换成本。你想要让两个果篮相等。为此,可以根据需要多次执行下述操作:
- 选中两个下标
i和j,并交换basket1中的第i个水果和basket2中的第j个水果。 - 交换的成本是
min(basket1i,basket2j)。
根据果篮中水果的成本进行排序,如果排序后结果完全相同,则认为两个果篮相等。
返回使两个果篮相等的最小交换成本,如果无法使两个果篮相等,则返回 -1 。
示例 1:
输入:basket1 = [4,2,2,2], basket2 = [1,4,1,2] 输出:1 解释:交换 basket1 中下标为 1 的水果和 basket2 中下标为 0 的水果,交换的成本为 1 。此时,basket1 = [4,1,2,2] 且 basket2 = [2,4,1,2] 。重排两个数组,发现二者相等。
示例 2:
输入:basket1 = [2,3,4,1], basket2 = [3,2,5,1] 输出:-1 解释:可以证明无法使两个果篮相等。
提示:
basket1.length == basket2.length1 <= basket1.length <= 1051 <= basket1i,basket2i <= 109
方法一:贪心 + 构造 · Go 实现
思考
交换两篮中的水果使两边多重集相同,一次代价为二者较小值。先对每个取值做差:一侧多出的必须与另一侧多出的对换。差为奇数则无解。
需要换出的水果按值排序后,较小的一半应与较大的一半配对。直接交换代价是较小者本身;也可借全局最小水果 中转两次,代价 。对较小一半取 之和。
我们可以先将两个数组中共有的元素去掉,对于剩下的每个数字,其出现的次数必须为偶数,否则无法构造出相同的数组。不妨记去除共有元素后,两数组分别为 和 。
接下来,我们考虑如何进行交换。
如果我们要交换 中最小的数,那么我们要在 中找到最大的数,与其进行交换;同理,如果我们要交换 中最小的数,那么我们要在 中找到最大的数,与其进行交换。可以通过排序来实现。
不过,还有另一种交换的方案,我们可以借助一个最小的数 作为过渡元素,将 中的数先与 进行交换,再将 与 中的数进行交换。这样,交换的成本为 。
在代码实现上,我们可以直接将数组 和 合并成数组 ,然后对数组 进行排序。接下来枚举前一半的数,计算每次的最小成本,累加到答案中即可。
时间复杂度 ,空间复杂度 。其中 为数组长度。
func minCost(basket1 []int, basket2 []int) (ans int64) {
cnt := map[int]int{}
for i, a := range basket1 {
cnt[a]++
cnt[basket2[i]]--
}
mi := 1 << 30
nums := []int{}
for x, v := range cnt {
if v%2 != 0 {
return -1
}
for i := abs(v) / 2; i > 0; i-- {
nums = append(nums, x)
}
mi = min(mi, x)
}
sort.Ints(nums)
m := len(nums)
for i := 0; i < m/2; i++ {
ans += int64(min(nums[i], mi*2))
}
return
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
2566. 替换一个数字后的最大差值 ​
题目、示例与约束
给你一个整数 num 。你知道 Danny Mittal 会偷偷将 0 到 9 中的一个数字 替换 成另一个数字。
请你返回将 num 中 恰好一个 数字进行替换后,得到的最大值和最小值的差为多少。
注意:
- 当 Danny 将一个数字
d1替换成另一个数字d2时,Danny 需要将num中所有d1都替换成d2。 - Danny 可以将一个数字替换成它自己,也就是说
num可以不变。 - Danny 可以将数字分别替换成两个不同的数字分别得到最大值和最小值。
- 替换后得到的数字可以包含前导 0 。
- Danny Mittal 获得周赛 326 前 10 名,让我们恭喜他。
示例 1:
输入:num = 11891 输出:99009 解释: 为了得到最大值,我们将数字 1 替换成数字 9 ,得到 99899 。 为了得到最小值,我们将数字 1 替换成数字 0 ,得到 890 。 两个数字的差值为 99009 。
示例 2:
输入:num = 90 输出:99 解释: 可以得到的最大值是 99(将 0 替换成 9),最小值是 0(将 9 替换成 0)。 所以我们得到 99 。
提示:
1 <= num <= 108
方法一:贪心 · Go 实现
思考
把某一种数字一律改成另一个数字,分别得到尽可能大与尽可能小的结果,求二者之差。位数很少,枚举被改数字与目标数字亦可,但最优改法是确定的。
最小数把最高位那种数字全部改成 ;最大数把从左起第一个非 的数字全部改成 。若已全是 ,最大值就是自身。
我们先将数字转为字符串 。
要得到最小值,我们只需要将找到字符串 的第一个数字 ,然后把字符串中所有的 替换成 即可。
要得到最大值,我们需要找到字符串 中第一个不是 的数字 ,然后把字符串中所有的 替换成 即可。
最后返回最大值和最小值的差即可。
时间复杂度 ,空间复杂度 。其中 为数字 的值。
func minMaxDifference(num int) int {
s := []byte(strconv.Itoa(num))
first := s[0]
for i := range s {
if s[i] == first {
s[i] = '0'
}
}
mi, _ := strconv.Atoi(string(s))
t := []byte(strconv.Itoa(num))
for i := range t {
if t[i] != '9' {
second := t[i]
for j := i; j < len(t); j++ {
if t[j] == second {
t[j] = '9'
}
}
mx, _ := strconv.Atoi(string(t))
return mx - mi
}
}
return num - mi
}
2567. 修改两个元素的最小分数 ​
题目、示例与约束
给你一个下标从 0 开始的整数数组 nums 。
nums的 最小 得分是满足0 <= i < j < nums.length的|nums[i] - nums[j]|的最小值。nums的 最大 得分是满足0 <= i < j < nums.length的|nums[i] - nums[j]|的最大值。nums的分数是 最大 得分与 最小 得分的和。
我们的目标是最小化 nums 的分数。你 最多 可以修改 nums 中 2 个元素的值。
请你返回修改 nums 中 至多两个 元素的值后,可以得到的 最小分数 。
|x| 表示 x 的绝对值。
示例 1:
输入:nums = [1,4,3]
输出:0
解释:将 nums[1] 和 nums[2] 的值改为 1 ,nums 变为 [1,1,1] 。|nums[i] - nums[j]| 的值永远为 0 ,所以我们返回 0 + 0 = 0 。
示例 2:
输入:nums = [1,4,7,8,5] 输出:3 解释: 将 nums[0] 和 nums[1] 的值变为 6 ,nums 变为 [6,6,7,8,5] 。 最小得分是 i = 0 且 j = 1 时得到的 |nums[i] - nums[j]| = |6 - 6| = 0 。 最大得分是 i = 3 且 j = 4 时得到的 |nums[i] - nums[j]| = |8 - 5| = 3 。 最大得分与最小得分之和为 3 。这是最优答案。
提示:
3 <= nums.length <= 1051 <= nums[i] <= 109
方法一:排序 + 贪心 · Go 实现
思考
数组得分是“相邻差的最小值”加“最大最小之差”。可改至多两个元素。把某元素改成已有值即可让低分变成 ,于是得分只剩极差。
排序后,改两个数等价于丢掉两端各若干个极值。三种丢掉方式覆盖全部最优:丢掉两个最小、各丢一端、丢掉两个最大,取三种极差的最小者。
根据题意我们知道,最小得分实际上是排序数组相邻两个元素的最小差值,最大得分是排序数组首尾元素的差值。数组 的分数是最小得分与最大得分的和。
因此,我们可以先对数组进行排序。由于题目允许我们修改数组中最多两个元素的值,我们可以通过修改一个数,让其跟数组中的另一个数相同,使得最小得分为 ,那么数组 的分数实际上就是最大得分。我们可以选择进行如下修改之一:
- 修改最小的两个数为 ,那么最大得分为 ;
- 修改最小的一个数为 ,最大的一个数为 ,那么最大得分为 ;
- 修改最大的两个数为 ,那么最大得分为 。
最后,我们返回上述三种修改的得分的最小值即可。
时间复杂度 ,空间复杂度 。其中 为数组 的长度。
相似题目:
func minimizeSum(nums []int) int {
sort.Ints(nums)
n := len(nums)
return min(nums[n-1]-nums[2], min(nums[n-2]-nums[1], nums[n-3]-nums[0]))
}
继续阅读
- 实践案例
- 实践案例
- 实践案例