LeetCode Go · 二分查找(3) ​
来源与许可
题目版权归 LeetCode;题解来自 Doocs/leetcode,按 CC BY-SA 4.0 共享。补充 Go 代码来自灵茶山艾府、halfrost,保留 MIT 许可及逐题署名。本站提取 Go、调整分类及折叠排版;标注“本站整理”的 Go 解法依据题意与参考思路补写,按 CC BY-SA 4.0 共享。收录日期:2026-10-31;主题源版本:91ef8c241a5e。
本卷 30 题 · 点击题目展开,先独立思考,再查看解法。
1099. 小于 K 的两数之和 🔒 ​
题目、示例与约束
给你一个整数数组 nums 和整数 k ,返回最大和 sum ,满足存在 i < j 使得 nums[i] + nums[j] = sum 且 sum < k 。如果没有满足此等式的 i,j 存在,则返回 -1 。
示例 1:
输入:nums = [34,23,1,24,75,33,54,8], k = 60 输出:58 解释: 34 和 24 相加得到 58,58 小于 60,满足题意。
示例 2:
输入:nums = [10,20,30], k = 15 输出:-1 解释: 我们无法找到和小于 15 的两个元素。
提示:
1 <= nums.length <= 1001 <= nums[i] <= 10001 <= k <= 2000
方法一:排序 + 二分查找 · Go 实现
思考
小于 的最大两数之和,枚举一对是平方级。排序后对每个 找小于 的最大右端,可用二分。
对下标 ,在 上
bisect_left定位 ,前一个位置若仍大于 则更新答案。无合法对时保持 。
我们可以先对数组 进行排序,初始化答案为 。
接下来,我们枚举数组中的每个元素 ,并在数组中寻找满足 的最大的 。这里我们可以使用二分查找来加速寻找过程。如果找到了这样的 ,那么我们就可以更新答案,即 。
枚举结束后,返回答案即可。
时间复杂度 ,空间复杂度 。其中 是数组 的长度。
func twoSumLessThanK(nums []int, k int) int {
sort.Ints(nums)
ans := -1
for i, x := range nums {
j := sort.SearchInts(nums[i+1:], k-x) + i
if v := nums[i] + nums[j]; i < j && ans < v {
ans = v
}
}
return ans
}
方法二:排序 + 双指针 · Go 实现
思考
每个 一次二分共 。两端相向是同一单调性:和偏小则左端右移以增大,和达到 则右端左移。
双指针一次扫完所有互异下标对的帕累托前沿,常数更小。
与方法一类似,我们可以先对数组 进行排序,初始化答案为 。
接下来,我们使用双指针 和 分别指向数组的左右两端,每次判断 是否小于 ,如果小于 ,那么我们就可以更新答案,即 ,并将 右移一位,否则将 左移一位。
枚举结束后,返回答案即可。
时间复杂度 ,空间复杂度 。其中 是数组 的长度。
func twoSumLessThanK(nums []int, k int) int {
sort.Ints(nums)
ans := -1
for i, j := 0, len(nums)-1; i < j; {
if s := nums[i] + nums[j]; s < k {
ans = max(ans, s)
i++
} else {
j--
}
}
return ans
}
1150. 检查一个数是否在数组中占绝大多数 🔒 ​
题目、示例与约束
给出一个按 非递减 顺序排列的数组 nums,和一个目标数值 target。假如数组 nums 中绝大多数元素的数值都等于 target,则返回 True,否则请返回 False。
所谓占绝大多数,是指在长度为 N 的数组中出现必须 超过 N/2 次。
示例 1:
输入:nums = [2,4,5,5,5,5,5,6,6], target = 5 输出:true 解释: 数字 5 出现了 5 次,而数组的长度为 9。 所以,5 在数组中占绝大多数,因为 5 次 > 9/2。
示例 2:
输入:nums = [10,100,101,101], target = 101 输出:false 解释: 数字 101 出现了 2 次,而数组的长度是 4。 所以,101 不是 数组占绝大多数的元素,因为 2 次 = 4/2。
提示:
1 <= nums.length <= 10001 <= nums[i] <= 10^91 <= target <= 10^9
方法一:二分查找 · Go 实现
思考
有序数组中 的出现次数等于其左右边界之差。两次二分分别得第一个 与第一个 的下标,差大于 则为多数元素。线性计数在 较大时没有必要。
我们注意到,数组 中的元素是非递减的,也就是说,数组 中的元素单调递增。因此,我们可以使用二分查找的方法,找到数组 中第一个大于等于 的元素的下标 ,以及第一个大于 的元素的下标 。如果 ,则说明数组 中的元素 出现的次数超过了数组长度的一半,因此返回 ,否则返回 。
时间复杂度 ,空间复杂度 。其中 为数组 的长度。
func isMajorityElement(nums []int, target int) bool {
left := sort.SearchInts(nums, target)
right := sort.SearchInts(nums, target+1)
return right-left > len(nums)/2
}
方法二:二分查找(优化) · Go 实现
思考
方法一用两次二分定位区间。若 出现超过一半,则 处仍应等于 。一次
bisect_left后再做一次下标检查即可,少一次二分。
方法一中,我们使用了两次二分查找,分别找到数组 中第一个大于等于 的元素的下标 ,以及第一个大于 的元素的下标 。但是,我们可以使用一次二分查找,找到数组 中第一个大于等于 的元素的下标 ,然后判断 是否等于 ,如果相等,说明数组 中的元素 出现的次数超过了数组长度的一半,因此返回 ,否则返回 。
时间复杂度 ,空间复杂度 。其中 为数组 的长度。
func isMajorityElement(nums []int, target int) bool {
n := len(nums)
left := sort.SearchInts(nums, target)
right := left + n/2
return right < n && nums[right] == target
}
1170. 比较字符串最小字母出现频次 ​
题目、示例与约束
定义一个函数 f(s),统计 s 中(按字典序比较)最小字母的出现频次 ,其中 s 是一个非空字符串。
例如,若 s = "dcce",那么 f(s) = 2,因为字典序最小字母是 "c",它出现了 2 次。
现在,给你两个字符串数组待查表 queries 和词汇表 words 。对于每次查询 queries[i] ,需统计 words 中满足 f(queries[i]) < f(W) 的 词的数目 ,W 表示词汇表 words 中的每个词。
请你返回一个整数数组 answer 作为答案,其中每个 answer[i] 是第 i 次查询的结果。
示例 1:
输入:queries = ["cbd"], words = ["zaaaz"]
输出:[1]
解释:查询 f("cbd") = 1,而 f("zaaaz") = 3 所以 f("cbd") < f("zaaaz")。
示例 2:
输入:queries = ["bbb","cc"], words = ["a","aa","aaa","aaaa"]
输出:[1,2]
解释:第一个查询 f("bbb") < f("aaaa"),第二个查询 f("aaa") 和 f("aaaa") 都 > f("cc")。
提示:
1 <= queries.length <= 20001 <= words.length <= 20001 <= queries[i].length, words[i].length <= 10queries[i][j]、words[i][j]都由小写英文字母组成
方法一:排序 + 二分查找 · Go 实现
思考
每个查询要数 中 值严格更大的个数。对每个查询扫描全部 为 。先把每个 算出并排序,查询时二分第一个大于 的位置,右侧长度即为答案。 只依赖最小字母的次数,对短串线性可求。
我们先按照题目描述,实现函数 ,函数返回字符串 中按字典序比较最小字母的出现频次。
接下来,我们将 中的每个字符串 都计算出 ,并将其排序,存放在数组 中。
然后,我们遍历 中的每个字符串 ,在 中二分查找第一个大于 的位置 ,则 中下标 及其后面的元素都满足 ,那么当前查询的答案就是 。
时间复杂度 ,空间复杂度 。其中 和 分别是数组 和 的长度,而 是字符串的最大长度。
func numSmallerByFrequency(queries []string, words []string) (ans []int) {
f := func(s string) int {
cnt := [26]int{}
for _, c := range s {
cnt[c-'a']++
}
for _, x := range cnt {
if x > 0 {
return x
}
}
return 0
}
n := len(words)
nums := make([]int, n)
for i, w := range words {
nums[i] = f(w)
}
sort.Ints(nums)
for _, q := range queries {
x := f(q)
ans = append(ans, n-sort.SearchInts(nums, x+1))
}
return
}
1198. 找出所有行中最小公共元素 🔒 ​
题目、示例与约束
给你一个 m x n 的矩阵 mat,其中每一行的元素均符合 严格递增 。请返回 所有行中的 最小公共元素 。
如果矩阵中没有这样的公共元素,就请返回 -1。
示例 1:
输入:mat = [[1,2,3,4,5],[2,4,5,8,10],[3,5,7,9,11],[1,3,5,7,9]] 输出:5
示例 2:
输入:mat = [[1,2,3],[2,3,4],[2,3,5]] 输出: 2
提示:
m == mat.lengthn == mat[i].length1 <= m, n <= 5001 <= mat[i][j] <= 104mat[i]已按严格递增顺序排列。
方法一:计数 · Go 实现
思考
每行严格递增,故同一数字在一行至多一次。按行扫描并计数,某值次数到达行数即出现在每一行。行内从左到右保证较小值先被加满,因此第一次凑齐行数的值就是最小公共元素;若无则返回 。
我们用一个长度为 的数组 统计每个数出现的次数。顺序遍历矩阵中的每个数,将其出现次数加一。当某个数的出现次数等于矩阵的行数时,说明该数在每一行都出现过,即为最小公共元素,返回该数即可。
若遍历结束后没有找到最小公共元素,则返回 。
时间复杂度 ,空间复杂度 。其中 和 分别是矩阵的行数和列数。
func smallestCommonElement(mat [][]int) int {
cnt := [10001]int{}
for _, row := range mat {
for _, x := range row {
cnt[x]++
if cnt[x] == len(mat) {
return x
}
}
}
return -1
}
1201. 丑数 III ​
题目、示例与约束
丑数是可以被 a 或 b 或 c 整除的 正整数 。
给你四个整数:n 、a 、b 、c ,请你设计一个算法来找出第 n 个丑数。
示例 1:
输入:n = 3, a = 2, b = 3, c = 5 输出:4 解释:丑数序列为 2, 3, 4, 5, 6, 8, 9, 10... 其中第 3 个是 4。
示例 2:
输入:n = 4, a = 2, b = 3, c = 4 输出:6 解释:丑数序列为 2, 3, 4, 6, 8, 9, 10, 12... 其中第 4 个是 6。
示例 3:
输入:n = 5, a = 2, b = 11, c = 13 输出:10 解释:丑数序列为 2, 4, 6, 8, 10, 11, 12, 13... 其中第 5 个是 10。
提示:
1 <= n, a, b, c <= 1091 <= a * b * c <= 1018- 本题结果在
[1, 2 * 109]的范围内
方法一:二分查找 + 容斥原理 · Go 实现
思考
按从小到大枚举丑数需要生成约 个候选, 可达 ,无法在时限内完成。
小于等于 的丑数个数随 单调不减,因此第 个丑数是使该计数至少为 的最小 。计数本身可用容斥在 内求出:并上 的倍数,再减去两两最小公倍数的倍数,加上三者最小公倍数的倍数。
为此在 上二分 ,中点用容斥判断是否已覆盖至少 个丑数。答案范围由题目保证,无需枚举具体丑数。
我们可以将题目转换为:找到最小的正整数 ,使得小于等于 的丑数个数恰好为 个。
对于一个正整数 ,能被 整除的数有 个,能被 整除的数有 个,能被 整除的数有 个,能被 和 同时整除的数有 个,能被 和 同时整除的数有 个,能被 和 同时整除的数有 个,能被 , 和 同时整除的数有 个。根据容斥原理,小于等于 的丑数个数为:
我们可以使用二分查找的方法找到最小的正整数 ,使得小于等于 的丑数个数恰好为 个。
定义二分查找的左边界为 ,右边界为 ,其中 是题目给定的最大值。在二分查找的每一步中,我们找出中间数 ,如果小于等于 的丑数个数大于等于 ,那么说明最小的正整数 落在 区间内,否则落在 区间内。在二分查找的过程中,我们需要不断更新小于等于 的丑数个数,直到找到最小的正整数 。
时间复杂度 ,其中 。空间复杂度 。
func nthUglyNumber(n int, a int, b int, c int) int {
ab, bc, ac := lcm(a, b), lcm(b, c), lcm(a, c)
abc := lcm(ab, c)
var l, r int = 1, 2e9
for l < r {
mid := (l + r) >> 1
if mid/a+mid/b+mid/c-mid/ab-mid/bc-mid/ac+mid/abc >= n {
r = mid
} else {
l = mid + 1
}
}
return l
}
func gcd(a, b int) int {
if b == 0 {
return a
}
return gcd(b, a%b)
}
func lcm(a, b int) int {
return a * b / gcd(a, b)
}
1208. 尽可能使字符串相等 ​
题目、示例与约束
给你两个长度相同的字符串,s 和 t。
将 s 中的第 i 个字符变到 t 中的第 i 个字符需要 |s[i] - t[i]| 的开销(开销可能为 0),也就是两个字符的 ASCII 码值的差的绝对值。
用于变更字符串的最大预算是 maxCost。在转化字符串时,总开销应当小于等于该预算,这也意味着字符串的转化可能是不完全的。
如果你可以将 s 的子字符串转化为它在 t 中对应的子字符串,则返回可以转化的最大长度。
如果 s 中没有子字符串可以转化成 t 中对应的子字符串,则返回 0。
示例 1:
输入:s = "abcd", t = "bcdf", maxCost = 3 输出:3 解释:s 中的 "abc" 可以变为 "bcd"。开销为 3,所以最大长度为 3。
示例 2:
输入:s = "abcd", t = "cdef", maxCost = 3
输出:1
解释:s 中的任一字符要想变成 t 中对应的字符,其开销都是 2。因此,最大长度为 1。
示例 3:
输入:s = "abcd", t = "acde", maxCost = 0 输出:1 解释:a -> a, cost = 0,字符串未发生变化,所以最大长度为 1。
提示:
1 <= s.length <= 105t.length == s.length0 <= maxCost <= 106s和t都只含小写英文字母。
方法一:前缀和 + 二分查找 · Go 实现
思考
子串费用是对应位置 ASCII 差的绝对值和。 ,枚举全部 子串不可行。
固定长度时,存在费用不超过 的子串具有单调性:长度 可行则 也可行。任意区间费用可由前缀和 求出。
为此先建差分数组的前缀和,再对长度二分,并用滑动窗口式枚举验证。二分利用单调性,前缀和去掉内层求和。
我们可以创建一个长度为 的数组 ,其中 表示字符串 的前 个字符与字符串 的前 个字符的 ASCII 码值的差的绝对值之和。这样,我们就可以通过 来计算字符串 的第 个字符到第 个字符的 ASCII 码值的差的绝对值之和,其中 。
注意到长度具有单调性,即如果存在长度为 的子串满足条件,那么长度为 的子串也一定满足条件。因此,我们可以使用二分查找的方法来求解最大长度。
我们定义函数 ,表示是否存在长度为 的子串满足条件。在该函数中,我们只需要枚举所有长度为 的子串,判断其是否满足条件即可。如果存在满足条件的子串,那么函数返回 true,否则返回 false。
接下来,我们定义二分查找的左边界 为 ,右边界 为 。在每一步中,我们令 ,如果函数 的返回值为 true,那么我们将左边界更新为 ,否则我们将右边界更新为 。在二分查找结束后,我们得到的左边界即为答案。
时间复杂度 ,空间复杂度 。其中 为字符串 的长度。
func equalSubstring(s string, t string, maxCost int) int {
n := len(s)
f := make([]int, n+1)
for i, a := range s {
f[i+1] = f[i] + abs(int(a)-int(t[i]))
}
check := func(x int) bool {
for i := 0; i+x-1 < n; i++ {
if f[i+x]-f[i] <= maxCost {
return true
}
}
return false
}
l, r := 0, n
for l < r {
mid := (l + r + 1) >> 1
if check(mid) {
l = mid
} else {
r = mid - 1
}
}
return l
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
方法二:双指针 · Go 实现
思考
方法一额外需要 前缀数组,总时间为 。费用非负,窗口右端右移时费用只增,左端右移时费用只减,因此可用双指针在一次扫描中维护最大可行窗口,线性时间且常数空间。
我们可以维护两个指针 和 ,初始时 ;维护一个变量 ,表示下标区间 之间的 ASCII 码值的差的绝对值之和。在每一步中,我们将 向右移动一位,然后更新 。如果 ,那么我们就循环将 向右移动一位,并且减少 的值,直到 。然后我们更新答案,即 。
最后返回答案即可。
时间复杂度 ,其中 为字符串 的长度。空间复杂度 。
func equalSubstring(s string, t string, maxCost int) (ans int) {
var cost, l int
for r := range s {
cost += abs(int(s[r]) - int(t[r]))
for ; cost > maxCost; l++ {
cost -= abs(int(s[l]) - int(t[l]))
}
ans = max(ans, r-l+1)
}
return
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
方法三:双指针的另一种写法 · Go 实现
思考
方法二在费用超标时可能把窗口缩短后再拉长,并显式维护答案。题目只要最大长度,可让左指针至多前进、窗口单调变长,最终 即为该最大长度,省去对 的更新。
在方法二中,双指针维护的区间可能变短,也可能变长,由于题目只需要求出最大长度,我们可以维护一个单调变长的区间。
具体地,我们用两个指针 和 指向区间的左右端点,初始时 。在每一步中,我们将 向右移动一位,然后更新 。如果 ,那么我们就将 向右移动一位,并且减少 的值。
最后返回 即可。
时间复杂度 ,其中 为字符串 的长度。空间复杂度 。
func equalSubstring(s string, t string, maxCost int) int {
n := len(s)
var cost, l int
for r := range s {
cost += abs(int(s[r]) - int(t[r]))
if cost > maxCost {
cost -= abs(int(s[l]) - int(t[l]))
l++
}
}
return n - l
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
1213. 三个有序数组的交集 🔒 ​
题目、示例与约束
给出三个均为 严格递增排列 的整数数组 arr1,arr2 和 arr3。返回一个由 仅 在这三个数组中 同时出现 的整数所构成的有序数组。
示例 1:
输入: arr1 = [1,2,3,4,5], arr2 = [1,2,5,7,9], arr3 = [1,3,4,5,8] 输出: [1,5] 解释: 只有 1 和 5 同时在这三个数组中出现.
示例 2:
输入: arr1 = [197,418,523,876,1356], arr2 = [501,880,1593,1710,1870], arr3 = [521,682,1337,1395,1764] 输出: []
提示:
1 <= arr1.length, arr2.length, arr3.length <= 10001 <= arr1[i], arr2[i], arr3[i] <= 2000
方法一:计数 · Go 实现
思考
三数组均已有序且长度不超过 ,值域在 以内。将三个数组一并计数后,值为 的元素即公共元素;因各数组内部元素互不相同,该计数不会被同一数组重复抬高。按 的顺序输出即可保持升序。
遍历三个数组,统计每个数字出现的次数,然后遍历任意一个数组,若某个数字出现的次数为 ,则将其加入结果数组。
时间复杂度 ,空间复杂度 。其中 和 分别为数组的长度和数组中数字的范围。
func arraysIntersection(arr1 []int, arr2 []int, arr3 []int) (ans []int) {
cnt := [2001]int{}
for _, x := range arr1 {
cnt[x]++
}
for _, x := range arr2 {
cnt[x]++
}
for _, x := range arr3 {
cnt[x]++
if cnt[x] == 3 {
ans = append(ans, x)
}
}
return
}
方法二:二分查找 · Go 实现
思考
计数需要与值域成正比的额外数组。三个数组已有序,可在 、 上二分判断 的每个值是否存在,额外空间降为常数,时间变为 。
遍历第一个数组,对于其中的每个数字,使用二分查找在第二个数组和第三个数组中查找该数字,若都找到,则将该数字加入结果数组。
时间复杂度 ,空间复杂度 。其中 为数组的长度。
func arraysIntersection(arr1 []int, arr2 []int, arr3 []int) (ans []int) {
for _, x := range arr1 {
i := sort.SearchInts(arr2, x)
j := sort.SearchInts(arr3, x)
if i < len(arr2) && j < len(arr3) && arr2[i] == x && arr3[j] == x {
ans = append(ans, x)
}
}
return
}
1231. 分享巧克力 🔒 ​
题目、示例与约束
你有一大块巧克力,它由一些甜度不完全相同的小块组成。我们用数组 sweetness 来表示每一小块的甜度。
你打算和 K 名朋友一起分享这块巧克力,所以你需要将切割 K 次才能得到 K+1 块,每一块都由一些 连续 的小块组成。
为了表现出你的慷慨,你将会吃掉 总甜度最小 的一块,并将其余几块分给你的朋友们。
请找出一个最佳的切割策略,使得你所分得的巧克力 总甜度最大,并返回这个 最大总甜度。
示例 1:
输入:sweetness = [1,2,3,4,5,6,7,8,9], K = 5 输出:6 解释:你可以把巧克力分成 [1,2,3], [4,5], [6], [7], [8], [9]。
示例 2:
输入:sweetness = [5,6,7,8,9,1,2,3,4], K = 8 输出:1 解释:只有一种办法可以把巧克力分成 9 块。
示例 3:
输入:sweetness = [1,2,2,1,2,2,1,2,2], K = 2 输出:5 解释:你可以把巧克力分成 [1,2,2], [1,2,2], [1,2,2]。
提示:
0 <= K < sweetness.length <= 10^41 <= sweetness[i] <= 10^5
方法一:二分查找 + 贪心 · Go 实现
思考
要把巧克力分成 段并最大化自己那一段的最小甜度。 ,枚举切点不可行。若最小甜度 可达,则更小的阈值也可达,具有单调性。
判定时从左到右累加,每凑满 就切一刀;能切出多于 段(自己再加 位朋友)则 可行。在 上二分最大可行 。贪心切分保证判定正确:尽早成段只会留下更多剩余去凑后面的段。
我们注意到,如果我们能吃到一块甜度为 的巧克力,那么甜度小于等于 的巧克力也都能吃到。这存在着单调性,因此,我们可以使用二分查找,找到最大的满足条件的 。
我们定义二分查找的左边界 ,右边界 。每一次,我们取 和 的中间值 ,然后判断能否吃到一块甜度为 的巧克力。如果能吃到,那么我们就尝试吃掉甜度更大的巧克力,即令 ;否则,我们就尝试吃掉甜度更小的巧克力,即令 。在二分查找结束后,我们返回 即可。
问题的关键在于,我们如何判断能否吃到一块甜度为 的巧克力。我们可以使用贪心的思想,从左到右遍历数组,每次累加当前的甜度,当累加的甜度大于等于 时,那么巧克力数 加 ,并将累加的甜度清零。最后判断 是否大于 即可。
时间复杂度 ,空间复杂度 。其中 是数组的长度。
func maximizeSweetness(sweetness []int, k int) int {
l, r := 0, 0
for _, v := range sweetness {
r += v
}
check := func(x int) bool {
s, cnt := 0, 0
for _, v := range sweetness {
s += v
if s >= x {
s = 0
cnt++
}
}
return cnt > k
}
for l < r {
mid := (l + r + 1) >> 1
if check(mid) {
l = mid
} else {
r = mid - 1
}
}
return l
}
1237. 找出给定方程的正整数解 ​
题目、示例与约束
给你一个函数 f(x, y) 和一个目标结果 z,函数公式未知,请你计算方程 f(x,y) == z 所有可能的正整数 数对 x 和 y。满足条件的结果数对可以按任意顺序返回。
尽管函数的具体式子未知,但它是单调递增函数,也就是说:
f(x, y) < f(x + 1, y)f(x, y) < f(x, y + 1)
函数接口定义如下:
interface CustomFunction {
public:
// Returns some positive integer f(x, y) for two positive integers x and y based on a formula.
int f(int x, int y);
};
你的解决方案将按如下规则进行评判:
- 判题程序有一个由
CustomFunction的9种实现组成的列表,以及一种为特定的z生成所有有效数对的答案的方法。 - 判题程序接受两个输入:
function_id(决定使用哪种实现测试你的代码)以及目标结果z。 - 判题程序将会调用你实现的
findSolution并将你的结果与答案进行比较。 - 如果你的结果与答案相符,那么解决方案将被视作正确答案,即
Accepted。
示例 1:
输入:function_id = 1, z = 5 输出:[[1,4],[2,3],[3,2],[4,1]] 解释:function_id = 1 暗含的函数式子为 f(x, y) = x + y 以下 x 和 y 满足 f(x, y) 等于 5: x=1, y=4 -> f(1, 4) = 1 + 4 = 5 x=2, y=3 -> f(2, 3) = 2 + 3 = 5 x=3, y=2 -> f(3, 2) = 3 + 2 = 5 x=4, y=1 -> f(4, 1) = 4 + 1 = 5
示例 2:
输入:function_id = 2, z = 5 输出:[[1,5],[5,1]] 解释:function_id = 2 暗含的函数式子为 f(x, y) = x * y 以下 x 和 y 满足 f(x, y) 等于 5: x=1, y=5 -> f(1, 5) = 1 * 5 = 5 x=5, y=1 -> f(5, 1) = 5 * 1 = 5
提示:
1 <= function_id <= 91 <= z <= 100- 题目保证
f(x, y) == z的解处于1 <= x, y <= 1000的范围内。 - 在
1 <= x, y <= 1000的前提下,题目保证f(x, y)是一个 32 位有符号整数。
方法一:枚举 + 二分查找 · Go 实现
思考
对两个自变量均严格递增, 且解在 。双重线性枚举最坏 次调用,可以接受但可再降。固定 后 单调,可在 上二分定位等于 的位置。
对每个 二分 ,命中则记录。单调性把内层从线性变成对数。
根据题目我们可以知道,函数 是单调递增函数,因此,我们可以枚举 ,然后在 中二分查找 ,使得 。如果找到了,就将 加入答案中。
时间复杂度 ,其中 是 的值,空间复杂度 。
/**
* This is the declaration of customFunction API.
* @param x int
* @param x int
* @return Returns f(x, y) for any given positive integers x and y.
* Note that f(x, y) is increasing with respect to both x and y.
* i.e. f(x, y) < f(x + 1, y), f(x, y) < f(x, y + 1)
*/
func findSolution(customFunction func(int, int) int, z int) (ans [][]int) {
for x := 1; x <= 1000; x++ {
y := 1 + sort.Search(999, func(y int) bool { return customFunction(x, y+1) >= z })
if customFunction(x, y) == z {
ans = append(ans, []int{x, y})
}
}
return
}
方法二:双指针 · Go 实现
思考
每个 各做一次二分,调用次数仍带对数因子。从 出发:值偏小只能增大 (再减小 会更小),值偏大只能减小 。相等则收入并同时移动两端。总调用 级,利用了双单调性。
我们可以定义两个指针 和 ,初始时 , 。
- 如果 ,我们将 加入答案中,然后 , ;
- 如果 ,此时对任意的 ,都有 ,因此我们不能将 减小,只能将 增大,所以 ;
- 如果 ,此时对任意的 ,都有 ,因此我们不能将 增大,只能将 减小,所以 。
循环结束后,返回答案。
时间复杂度 ,其中 是 的值,空间复杂度 。
/**
* This is the declaration of customFunction API.
* @param x int
* @param x int
* @return Returns f(x, y) for any given positive integers x and y.
* Note that f(x, y) is increasing with respect to both x and y.
* i.e. f(x, y) < f(x + 1, y), f(x, y) < f(x, y + 1)
*/
func findSolution(customFunction func(int, int) int, z int) (ans [][]int) {
x, y := 1, 1000
for x <= 1000 && y > 0 {
t := customFunction(x, y)
if t < z {
x++
} else if t > z {
y--
} else {
ans = append(ans, []int{x, y})
x, y = x+1, y-1
}
}
return
}
1283. 使结果不超过阈值的最小除数 ​
题目、示例与约束
给你一个整数数组 nums 和一个正整数 threshold ,你需要选择一个正整数作为除数,然后将数组里每个数都除以它,并对除法结果求和。
请你找出能够使上述结果小于等于阈值 threshold 的除数中 最小 的那个。
每个数除以除数后都向上取整,比方说 7/3 = 3 , 10/2 = 5 。
题目保证一定有解。
示例 1:
输入:nums = [1,2,5,9], threshold = 6 输出:5 解释:如果除数为 1 ,我们可以得到和为 17 (1+2+5+9)。 如果除数为 4 ,我们可以得到和为 7 (1+1+2+3) 。如果除数为 5 ,和为 5 (1+1+1+2)。
示例 2:
输入:nums = [2,3,5,7,11], threshold = 11 输出:3
示例 3:
输入:nums = [19], threshold = 5 输出:4
提示:
1 <= nums.length <= 5 * 10^41 <= nums[i] <= 10^6nums.length <= threshold <= 10^6
方法一:二分查找 · Go 实现
思考
除数越大,向上取整之和越小,存在单调性。 ,枚举除数不可行。在 上二分最小 ,使 。判定一遍线性扫描。
我们注意到,对于一个数字 ,如果将 中的每个数字都除以 的结果之和小于等于 ,那么所有大于 的值都满足条件。这存在着单调性,因此我们可以使用二分查找的方法找到最小的满足条件的 。
我们定义二分查找的左边界 , ,每次取 ,计算 中每个数字除以 的结果之和 ,如果 小于等于 ,那么说明 满足条件,我们将 更新为 ,否则将 更新为 。
最后返回 即可。
时间复杂度 ,其中 是数组 的长度,而 是数组 中的最大值。空间复杂度 。
func smallestDivisor(nums []int, threshold int) int {
return sort.Search(slices.Max(nums), func(v int) bool {
v++
s := 0
for _, x := range nums {
s += (x + v - 1) / v
}
return s <= threshold
}) + 1
}
1292. 元素和小于等于阈值的正方形的最大边长 ​
题目、示例与约束
给你一个大小为 m x n 的矩阵 mat 和一个整数阈值 threshold。
请你返回元素总和小于或等于阈值的正方形区域的最大边长;如果没有这样的正方形区域,则返回 0 。
示例 1:

输入:mat = [[1,1,3,2,4,3,2],[1,1,3,2,4,3,2],[1,1,3,2,4,3,2]], threshold = 4 输出:2 解释:总和小于或等于 4 的正方形的最大边长为 2,如图所示。
示例 2:
输入:mat = [[2,2,2,2,2],[2,2,2,2,2],[2,2,2,2,2],[2,2,2,2,2],[2,2,2,2,2]], threshold = 1 输出:0
提示:
m == mat.lengthn == mat[i].length1 <= m, n <= 3000 <= mat[i][j] <= 1040 <= threshold <= 105
方法一:二维前缀和 + 二分查找 · Go 实现
思考
正方形区域和随边长增大而增大(元素非负),存在单调性。 ,枚举边长再扫左上角若每次求和为 则过慢。二维前缀和使任意正方形求和 ,再对边长二分。
判定函数枚举左上角,用前缀和比较阈值。二分找最大可行边长。
我们可以先预处理得到二维前缀和数组 ,其中 表示矩阵 中从 到 的元素和,那么对于任意的正方形区域,我们都可以在 的时间内得到其元素和。
接下来,我们可以使用二分查找的方法得到最大的边长。我们枚举正方形的边长 ,然后枚举正方形的左上角位置 ,那么我们可以得到正方形的元素和 ,如果 ,那么说明存在边长为 的正方形区域的元素和小于或等于阈值,否则不存在。
时间复杂度 ,空间复杂度 。
func maxSideLength(mat [][]int, threshold int) int {
m, n := len(mat), len(mat[0])
s := make([][]int, m+1)
for i := range s {
s[i] = make([]int, n+1)
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + mat[i-1][j-1]
}
}
check := func(k int) bool {
for i := 0; i < m-k+1; i++ {
for j := 0; j < n-k+1; j++ {
if s[i+k][j+k]-s[i][j+k]-s[i+k][j]+s[i][j] <= threshold {
return true
}
}
}
return false
}
l, r := 0, min(m, n)
for l < r {
mid := (l + r + 1) >> 1
if check(mid) {
l = mid
} else {
r = mid - 1
}
}
return l
}
1300. 转变数组后最接近目标值的数组和 ​
题目、示例与约束
给你一个整数数组 arr 和一个目标值 target ,请你返回一个整数 value ,使得将数组中所有大于 value 的值变成 value 后,数组的和最接近 target (最接近表示两者之差的绝对值最小)。
如果有多种使得和最接近 target 的方案,请你返回这些整数中的最小值。
请注意,答案不一定是 arr 中的数字。
示例 1:
输入:arr = [4,9,3], target = 10 输出:3 解释:当选择 value 为 3 时,数组会变成 [3, 3, 3],和为 9 ,这是最接近 target 的方案。
示例 2:
输入:arr = [2,3,5], target = 10 输出:5
示例 3:
输入:arr = [60864,25176,27249,21296,20204], target = 56803 输出:11361
提示:
1 <= arr.length <= 10^41 <= arr[i], target <= 10^5
方法一:排序 + 前缀和 + 二分查找 + 枚举 · Go 实现
思考
若对每个候选 都扫描一遍数组求转变后的和,则复杂度为 。其中 、 ,难以稳定通过。
转变后的和只取决于「不超过 的部分原样保留、超过的部分一律改成 」。将数组排序后,分界点是一段前缀,可用二分定位,前缀和一次算出保留部分。因此按 到 枚举 ,每次 求转变和并维护与 的最小绝对差;差值相同时取更小的 。
我们注意到,题目中要把所有大于 value 的值变成 value,并且求和,因此我们可以考虑先对数组 arr 进行排序,然后求出前缀和数组 ,其中 表示数组前 个元素之和。
接下来,我们可以从小到大枚举所有 value 值,对于每个 value,我们可以通过二分查找找到数组中第一个大于 value 的元素的下标 ,此时数组中大于 value 的元素个数为 ,因此数组中小于等于 value 的元素个数为 ,此时数组中小于等于 value 的元素之和为 ,数组中大于 value 的元素之和为 ,因此数组中所有元素之和为 。如果 与 target 的差的绝对值小于当前的最小差值 diff,则更新 diff 和 ans。
枚举完所有 value 后,即可得到最终答案 ans。
时间复杂度 ,空间复杂度 。其中 为数组 arr 的长度。
func findBestValue(arr []int, target int) (ans int) {
sort.Ints(arr)
n := len(arr)
s := make([]int, n+1)
mx := slices.Max(arr)
for i, x := range arr {
s[i+1] = s[i] + x
}
diff := 1 << 30
for value := 0; value <= mx; value++ {
i := sort.SearchInts(arr, value+1)
d := abs(s[i] + (n-i)*value - target)
if diff > d {
diff = d
ans = value
}
}
return
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
1337. 矩阵中战斗力最弱的 K 行 ​
题目、示例与约束
给你一个大小为 m * n 的矩阵 mat,矩阵由若干军人和平民组成,分别用 1 和 0 表示。
请你返回矩阵中战斗力最弱的 k 行的索引,按从最弱到最强排序。
如果第 i 行的军人数量少于第 j 行,或者两行军人数量相同但 i 小于 j,那么我们认为第 i 行的战斗力比第 j 行弱。
军人 总是 排在一行中的靠前位置,也就是说 1 总是出现在 0 之前。
示例 1:
输入:mat = [[1,1,0,0,0], [1,1,1,1,0], [1,0,0,0,0], [1,1,0,0,0], [1,1,1,1,1]], k = 3 输出:[2,0,3] 解释: 每行中的军人数目: 行 0 -> 2 行 1 -> 4 行 2 -> 1 行 3 -> 2 行 4 -> 5 从最弱到最强对这些行排序后得到 [2,0,3,1,4]
示例 2:
输入:mat = [[1,0,0,0], [1,1,1,1], [1,0,0,0], [1,0,0,0]], k = 2 输出:[0,2] 解释: 每行中的军人数目: 行 0 -> 1 行 1 -> 4 行 2 -> 1 行 3 -> 1 从最弱到最强对这些行排序后得到 [0,2,3,1]
提示:
m == mat.lengthn == mat[i].length2 <= n, m <= 1001 <= k <= mmatrix[i][j]不是 0 就是 1
方法一 · Go 实现
思考
行按军人数量升序、行号升序取前 行。每行 在左、 在右,军人个数等于首次 的位置。将行反转后二分 的右界,得到军人数量;再按该数量对行号排序,截取前 个。
func kWeakestRows(mat [][]int, k int) []int {
m, n := len(mat), len(mat[0])
res := make([]int, m)
var idx []int
for i, row := range mat {
idx = append(idx, i)
left, right := 0, n
for left < right {
mid := (left + right) >> 1
if row[mid] == 0 {
right = mid
} else {
left = mid + 1
}
}
res[i] = left
}
sort.Slice(idx, func(i, j int) bool {
return res[idx[i]] < res[idx[j]] || (res[idx[i]] == res[idx[j]] && idx[i] < idx[j])
})
return idx[:k]
}
1346. 检查整数及其两倍数是否存在 ​
题目、示例与约束
给你一个整数数组 arr,请你检查是否存在两个整数 N 和 M,满足 N 是 M 的两倍(即,N = 2 * M)。
更正式地,检查是否存在两个下标 i 和 j 满足:
i != j0 <= i, j < arr.lengtharr[i] == 2 * arr[j]
示例 1:
输入:arr = [10,2,5,3] 输出:true 解释:N= 10是 M= 5 的两倍,即10 = 2 * 5 。
示例 2:
输入:arr = [7,1,14,11] 输出:true 解释:N= 14是 M= 7 的两倍,即14 = 2 * 7。
示例 3:
输入:arr = [3,1,7,11] 输出:false 解释:在该情况下不存在 N 和 M 满足 N = 2 * M 。
提示:
2 <= arr.length <= 500-10^3 <= arr[i] <= 10^3
方法一:哈希表 · Go 实现
思考
是否存在 使 。双重循环在 可过,但一遍哈希即可:扫描时若 或(偶数时) 已出现则成功,否则把 放入集合。集合只含左侧元素,自然保证下标不同。
我们定义一个哈希表 ,用于记录访问过的元素。
遍历数组 ,对于每个元素 ,如果 的两倍或者 的一半在哈希表 中,那么返回 true。否则将 加入哈希表 。
若遍历结束后没有找到满足条件的元素,返回 false。
时间复杂度 ,空间复杂度 。其中 为数组 的长度。
func checkIfExist(arr []int) bool {
s := map[int]bool{}
for _, x := range arr {
if s[x*2] || (x%2 == 0 && s[x/2]) {
return true
}
s[x] = true
}
return false
}
1351. 统计有序矩阵中的负数 ​
题目、示例与约束
给你一个 m * n 的矩阵 grid,矩阵中的元素无论是按行还是按列,都以非严格递减顺序排列。 请你统计并返回 grid 中 负数 的数目。
示例 1:
输入:grid = [[4,3,2,-1],[3,2,1,-1],[1,1,-1,-2],[-1,-1,-2,-3]] 输出:8 解释:矩阵中共有 8 个负数。
示例 2:
输入:grid = [[3,2],[1,0]] 输出:0
提示:
m == grid.lengthn == grid[i].length1 <= m, n <= 100-100 <= grid[i][j] <= 100
进阶:你可以设计一个时间复杂度为 O(n + m) 的解决方案吗?
方法一:从左下角开始遍历 · Go 实现
思考
每行每列非严格递减,统计负数个数。逐格扫描为 。左下角是「当前行最小、当前列最大」的分界:该格非负则本行左侧皆非负,右移;为负则本行从这里到右端全负,计入 后上移。行列各走一遍,复杂度 。
由于矩阵是按行和按列非严格递减排序的,我们可以从矩阵的左下角开始遍历,记当前位置为 。
如果当前位置的元素大于等于 ,说明该行的前面所有元素也都大于等于 ,因此我们将列索引 向右移动一位,即 。
如果当前位置的元素小于 ,说明该列的当前元素以及其右侧的所有元素都是负数,因此我们可以将负数的数量增加 (其中 是矩阵的列数),然后将行索引 向上移动一位,即 。
我们重复上述过程,直到行索引 小于 或列索引 大于等于 。最终,负数的数量即为答案。
时间复杂度 ,其中 和 分别是矩阵的行数和列数。空间复杂度 。
func countNegatives(grid [][]int) (ans int) {
m := len(grid)
n := len(grid[0])
i := m - 1
j := 0
for i >= 0 && j < n {
if grid[i][j] >= 0 {
j++
} else {
ans += n - j
i--
}
}
return
}
1385. 两个数组间的距离值 ​
题目、示例与约束
给你两个整数数组 arr1 , arr2 和一个整数 d ,请你返回两个数组之间的 距离值 。
「距离值」 定义为符合此距离要求的元素数目:对于元素 arr1[i] ,不存在任何元素 arr2[j] 满足 |arr1[i]-arr2[j]| <= d 。
示例 1:
输入:arr1 = [4,5,8], arr2 = [10,9,1,8], d = 2 输出:2 解释: 对于 arr1[0]=4 我们有: |4-10|=6 > d=2 |4-9|=5 > d=2 |4-1|=3 > d=2 |4-8|=4 > d=2 所以 arr1[0]=4 符合距离要求 对于 arr1[1]=5 我们有: |5-10|=5 > d=2 |5-9|=4 > d=2 |5-1|=4 > d=2 |5-8|=3 > d=2 所以 arr1[1]=5 也符合距离要求 对于 arr1[2]=8 我们有: |8-10|=2 <= d=2 |8-9|=1 <= d=2 |8-1|=7 > d=2 |8-8|=0 <= d=2 存在距离小于等于 2 的情况,不符合距离要求 故而只有 arr1[0]=4 和 arr1[1]=5 两个符合距离要求,距离值为 2
示例 2:
输入:arr1 = [1,4,2,3], arr2 = [-4,-3,6,10,20,30], d = 3 输出:2
示例 3:
输入:arr1 = [2,1,100,3], arr2 = [-5,-2,10,-3,7], d = 6 输出:1
提示:
1 <= arr1.length, arr2.length <= 500-10^3 <= arr1[i], arr2[j] <= 10^30 <= d <= 100
方法一:排序 + 二分查找 · Go 实现
思考
统计 中与 所有元素距离都大于 的个数。双重扫描为 。对 排序后,每个 只需看是否存在落在 内的值:二分第一个 的位置,若越界或该值已大于 ,则 合法。
我们可以先对数组 排序,然后对于数组 中的每个元素 ,使用二分查找,找到数组 中第一个大于等于 的元素,如果元素存在,且小于等于 ,则说明不符合距离要求,否则说明符合距离要求。我们将符合距离要求的元素个数累加,即为答案。
时间复杂度 ,空间复杂度 。其中 和 分别是数组 和 的长度。
func findTheDistanceValue(arr1 []int, arr2 []int, d int) (ans int) {
sort.Ints(arr2)
for _, x := range arr1 {
i := sort.SearchInts(arr2, x-d)
if i == len(arr2) || arr2[i] > x+d {
ans++
}
}
return
}
1428. 至少有一个 1 的最左端列 🔒 ​
题目、示例与约束
行排序二进制矩阵 表示所有元素都是 0 或 1,并且矩阵的每一行都以非递减排序。
给定一个 行排序二进制矩阵 binaryMatrix,返回至少包含一个 1 的 最左端列 的索引(从 0 开始)。如果这样的列不存在,返回 -1。
您不能直接访问该二进制矩阵。你只可以通过 BinaryMatrix 接口来访问。
BinaryMatrix.get(row, col)返回位于索引(row, col)(从 0 开始)的元素。BinaryMatrix.dimensions()返回含有 2 个元素的列表[rows, cols],表示这是一个rows * cols的矩阵。
如果提交的答案调用 BinaryMatrix.get 超过 1000 次,则该答案会被判定为错误答案。提交任何试图规避判定机制的答案将会被取消资格。
下列示例中, mat 为给定的二进制矩阵。您不能直接访问该矩阵。
示例 1:

输入: mat = [[0,0],[1,1]] 输出: 0
示例 2:

输入: mat = [[0,0],[0,1]] 输出: 1
示例 3:

输入: mat = [[0,0],[0,0]] 输出: -1
提示:
rows == mat.lengthcols == mat[i].length1 <= rows, cols <= 100mat[i][j]只会是0或1。mat[i]已按非递减顺序排序。
方法一:二分查找 · Go 实现
思考
矩阵每行非递减,且访问次数受限,不能整表扫描。每一行中第一个 的位置可用二分在 次
get内求出。对所有行取这些列号的最小值;若没有任何 ,则返回 。
我们先调用 BinaryMatrix.dimensions() 得到矩阵的行数 和列数 ,然后对于每一行,我们使用二分查找来找到最左边的 所在的列数 ,找出所有行中最小的满足 的值即为答案。如果不存在这样的列,则返回 。
时间复杂度 ,其中 和 分别是矩阵的行数和列数。需要遍历每一行,每一行内使用二分查找,时间复杂度为 。空间复杂度 。
/**
* // This is the BinaryMatrix's API interface.
* // You should not implement it, or speculate about its implementation
* type BinaryMatrix struct {
* Get func(int, int) int
* Dimensions func() []int
* }
*/
func leftMostColumnWithOne(binaryMatrix BinaryMatrix) int {
e := binaryMatrix.Dimensions()
m, n := e[0], e[1]
ans := n
for i := 0; i < m; i++ {
l, r := 0, n
for l < r {
mid := (l + r) >> 1
if binaryMatrix.Get(i, mid) == 1 {
r = mid
} else {
l = mid + 1
}
}
ans = min(ans, l)
}
if ans >= n {
return -1
}
return ans
}
1439. 有序矩阵中的第 k 个最小数组和 ​
题目、示例与约束
给你一个 m * n 的矩阵 mat,以及一个整数 k ,矩阵中的每一行都以非递减的顺序排列。
你可以从每一行中选出 1 个元素形成一个数组。返回所有可能数组中的第 k 个 最小 数组和。
示例 1:
输入:mat = [[1,3,11],[2,4,6]], k = 5 输出:7 解释:从每一行中选出一个元素,前 k 个和最小的数组分别是: [1,2], [1,4], [3,2], [3,4], [1,6]。其中第 5 个的和是 7 。
示例 2:
输入:mat = [[1,3,11],[2,4,6]], k = 9 输出:17
示例 3:
输入:mat = [[1,10,10],[1,4,5],[2,3,6]], k = 7 输出:9 解释:从每一行中选出一个元素,前 k 个和最小的数组分别是: [1,1,2], [1,1,3], [1,4,2], [1,4,3], [1,1,6], [1,5,2], [1,5,3]。其中第 7 个的和是 9 。
示例 4:
输入:mat = [[1,1,10],[2,2,9]], k = 7 输出:12
提示:
m == mat.lengthn == mat.length[i]1 <= m, n <= 401 <= k <= min(200, n ^ m)1 <= mat[i][j] <= 5000mat[i]是一个非递减数组
方法一:逐行遍历 + 排序 · Go 实现
思考
每行选一个数求和, 种组合而 。只需保留前 小的部分和。行已有序,与下一行相加时只需用到该行前 个元素。
设 为已处理行的前 小和,与当前行做笛卡尔加后排序截断前 项。处理完所有行后 即为答案。
根据题目描述,我们需要找出前 行的所有可能数组中的第 个最小数组和。
如果我们能够找出前 行的所有可能数组中的前 个最小数组和,那么我们可以将第 行的每个元素与前 行的前 个最小数组和相加,将得到的所有结果排序后,取前 个最小值,即为前 行的所有可能数组中的前 个最小值。
因此,我们可以定义一个数组 ,用于存储此前遍历到的行的前 个最小数组和,初始时 只有一个元素 。
然后,我们遍历 的每一行 ,将 中的每个元素与 中的每个元素相加,将得到的所有结果排序后,取前 个最小值作为新的 。继续遍历下一行,直到遍历完所有行。
最后返回 中的第 个数(下标 )即可。
时间复杂度 ,空间复杂度 。其中 和 分别是矩阵的行数和列数。
func kthSmallest(mat [][]int, k int) int {
pre := []int{0}
for _, row := range mat {
cur := []int{}
for _, a := range pre {
for _, b := range row {
cur = append(cur, a+b)
}
}
sort.Ints(cur)
pre = cur[:min(k, len(cur))]
}
return pre[k-1]
}
1482. 制作 m 束花所需的最少天数 ​
题目、示例与约束
给你一个整数数组 bloomDay,以及两个整数 m 和 k 。
现需要制作 m 束花。制作花束时,需要使用花园中 相邻的 k 朵花 。
花园中有 n 朵花,第 i 朵花会在 bloomDay[i] 时盛开,恰好 可以用于 一束 花中。
请你返回从花园中摘 m 束花需要等待的 最少 的天数。如果不能摘到 m 束花则返回 -1。
示例 1:
输入:bloomDay = [1,10,3,10,2], m = 3, k = 1 输出:3 解释:让我们一起观察这三天的花开过程,x 表示花开,而 _ 表示花还未开。 现在需要制作 3 束花,每束只需要 1 朵。 1 天后:[x, _, _, _, _] // 只能制作 1 束花 2 天后:[x, _, _, _, x] // 只能制作 2 束花 3 天后:[x, _, x, _, x] // 可以制作 3 束花,答案为 3
示例 2:
输入:bloomDay = [1,10,3,10,2], m = 3, k = 2 输出:-1 解释:要制作 3 束花,每束需要 2 朵花,也就是一共需要 6 朵花。而花园中只有 5 朵花,无法满足制作要求,返回 -1 。
示例 3:
输入:bloomDay = [7,7,7,7,12,7,7], m = 2, k = 3 输出:12 解释:要制作 2 束花,每束需要 3 朵。 花园在 7 天后和 12 天后的情况如下: 7 天后:[x, x, x, x, _, x, x] 可以用前 3 朵盛开的花制作第一束花。但不能使用后 3 朵盛开的花,因为它们不相邻。 12 天后:[x, x, x, x, x, x, x] 显然,我们可以用不同的方式制作两束花。
示例 4:
输入:bloomDay = [1000000000,1000000000], m = 1, k = 1 输出:1000000000 解释:需要等 1000000000 天才能采到花来制作花束
示例 5:
输入:bloomDay = [1,10,2,9,3,8,4,7,5,6], m = 4, k = 2 输出:9
提示:
bloomDay.length == n1 <= n <= 10^51 <= bloomDay[i] <= 10^91 <= m <= 10^61 <= k <= n
方法一:二分查找 · Go 实现
思考
天数越大越容易凑齐 束,具有单调性。 、 ,对天数二分。判定时从左到右数连续已开且长度为 的段数是否至少 。找不到则返回 。
根据题目描述,如果一个天数 可以满足制作 束花,那么对任意 ,也一定可以满足制作 束花。因此,我们可以使用二分查找的方法找到最小的满足制作 束花的天数。
我们记花园中最大的开花天数为 ,接下来,我们定义二分查找的左边界 ,右边界 。
然后,我们进行二分查找,对于每一个中间值 ,我们判断是否可以制作 束花。如果可以,我们将右边界 更新为 ,否则,我们将左边界 更新为 。
最终,当 时,结束二分查找。此时如果 ,说明无法制作 束花,返回 ,否则返回 。
因此,问题转换为判断一个天数 是否可以制作 束花。
我们可以使用一个函数 来判断是否可以制作 束花。具体地,我们从左到右遍历花园中的每一朵花,如果当前花开的天数小于等于 ,我们将当前花加入到当前花束中,否则,我们将当前花束清空。当当前花束中的花的数量等于 时,我们将当前花束的数量加一,并清空当前花束。最后,我们判断当前花束的数量是否大于等于 ,如果是,说明可以制作 束花,否则,说明无法制作 束花。
时间复杂度 ,其中 和 分别为花园中的花的数量和最大的开花天数,本题中 。空间复杂度 。
func minDays(bloomDay []int, m int, k int) int {
mx := slices.Max(bloomDay)
if l := sort.Search(mx+2, func(days int) bool {
cnt, cur := 0, 0
for _, x := range bloomDay {
if x <= days {
cur++
if cur == k {
cnt++
cur = 0
}
} else {
cur = 0
}
}
return cnt >= m
}); l <= mx {
return l
}
return -1
}
1498. 满足条件的子序列数目 ​
题目、示例与约束
给你一个整数数组 nums 和一个整数 target 。
请你统计并返回 nums 中能满足其最小元素与最大元素的 和 小于或等于 target 的 非空 子序列的数目。
由于答案可能很大,请将结果对 109 + 7 取余后返回。
示例 1:
输入:nums = [3,5,6,7], target = 9 输出:4 解释:有 4 个子序列满足该条件。 [3] -> 最小元素 + 最大元素 <= target (3 + 3 <= 9) [3,5] -> (3 + 5 <= 9) [3,5,6] -> (3 + 6 <= 9) [3,6] -> (3 + 6 <= 9)
示例 2:
输入:nums = [3,3,6,8], target = 10 输出:6 解释:有 6 个子序列满足该条件。(nums 中可以有重复数字) [3] , [3] , [3,3], [3,6] , [3,6] , [3,3,6]
示例 3:
输入:nums = [2,3,3,4,6,7], target = 12 输出:61 解释:共有 63 个非空子序列,其中 2 个不满足条件([6,7], [7]) 有效序列总数为(63 - 2 = 61)
提示:
1 <= nums.length <= 1051 <= nums[i] <= 1061 <= target <= 106
方法一:排序 + 二分查找 · Go 实现
思考
子序列只关心最小与最大,排序不改变可组成的最值对。 ,枚举最小值 ,二分最大合法 ,中间元素可任意选,方案为 。
预处理二的幂以免重复计算。若 ,更大的最小值已不可能。
由于题目中描述的是子序列,并且涉及到最小元素与最大元素的和,因此我们可以先对数组 进行排序。
然后我们枚举最小元素 ,对于每个 ,我们可以在 到 中找到最大元素 ,使得 ,此时满足条件的子序列数目为 ,其中 表示从 到 的所有子序列的数目。我们将所有的子序列数目累加即可。
时间复杂度 ,空间复杂度 。其中 为数组 的长度。
func numSubseq(nums []int, target int) (ans int) {
sort.Ints(nums)
n := len(nums)
f := make([]int, n+1)
f[0] = 1
const mod int = 1e9 + 7
for i := 1; i <= n; i++ {
f[i] = f[i-1] * 2 % mod
}
for i, x := range nums {
if x*2 > target {
break
}
j := sort.SearchInts(nums[i+1:], target-x+1) + i
ans = (ans + f[j-i]) % mod
}
return
}
1508. 子数组和排序后的区间和 ​
题目、示例与约束
给你一个数组 nums ,它包含 n 个正整数。你需要计算所有非空连续子数组的和,并将它们按升序排序,得到一个新的包含 n * (n + 1) / 2 个数字的数组。
请你返回在新数组中下标为 left 到 right (下标从 1 开始)的所有数字和(包括左右端点)。由于答案可能很大,请你将它对 10^9 + 7 取模后返回。
示例 1:
输入:nums = [1,2,3,4], n = 4, left = 1, right = 5 输出:13 解释:所有的子数组和为 1, 3, 6, 10, 2, 5, 9, 3, 7, 4 。将它们升序排序后,我们得到新的数组 [1, 2, 3, 3, 4, 5, 6, 7, 9, 10] 。下标从 le = 1 到 ri = 5 的和为 1 + 2 + 3 + 3 + 4 = 13 。
示例 2:
输入:nums = [1,2,3,4], n = 4, left = 3, right = 4 输出:6 解释:给定数组与示例 1 一样,所以新数组为 [1, 2, 3, 3, 4, 5, 6, 7, 9, 10] 。下标从 le = 3 到 ri = 4 的和为 3 + 3 = 6 。
示例 3:
输入:nums = [1,2,3,4], n = 4, left = 1, right = 10 输出:50
提示:
1 <= nums.length <= 10^3nums.length == n1 <= nums[i] <= 1001 <= left <= right <= n * (n + 1) / 2
方法一:模拟 · Go 实现
思考
需要把所有子数组和排序后,求第 到第 项之和。子数组有 个, ,若再对每个询问区间重新求和会重复劳动,但本题只询问一次。
枚举左端点并维护向右延伸的前缀和,即可在 内列出全部子数组和;排序后对指定闭区间求和并对 取模。 的量级可以接受。
我们可以按照题目的要求,生成数组 ,然后对数组进行排序,最后求出 范围的所有元素的和,得到结果。
时间复杂度 ,空间复杂度 。其中 为题目给定的数组长度。
func rangeSum(nums []int, n int, left int, right int) (ans int) {
var arr []int
for i := 0; i < n; i++ {
s := 0
for j := i; j < n; j++ {
s += nums[j]
arr = append(arr, s)
}
}
sort.Ints(arr)
const mod int = 1e9 + 7
for _, x := range arr[left-1 : right] {
ans = (ans + x) % mod
}
return
}
1533. 找到最大整数的索引 🔒 ​
题目、示例与约束
我们有这样一个整数数组 arr ,除了一个最大的整数外,其他所有整数都相等。你不能直接访问该数组,你需要通过 API ArrayReader 来间接访问,这个 API 有以下成员函数:
int compareSub(int l, int r, int x, int y):其中0 <= l, r, x, y < ArrayReader.length(),l <= r且x <= y。这个函数比较子数组arr[l..r]与子数组arr[x..y]的和。该函数返回:- 1 若
arr[l]+arr[l+1]+...+arr[r] > arr[x]+arr[x+1]+...+arr[y]。 - 0 若
arr[l]+arr[l+1]+...+arr[r] == arr[x]+arr[x+1]+...+arr[y]。 - -1 若
arr[l]+arr[l+1]+...+arr[r] < arr[x]+arr[x+1]+...+arr[y]。
- 1 若
int length():返回数组的长度。
你最多可以调用函数 compareSub() 20 次。你可以认为这两个函数的时间复杂度都为 O(1) 。
返回 arr 中最大整数的索引。
示例 1:
输入: arr = [7,7,7,7,10,7,7,7] 输出: 4 解释: API 的调用如下: reader.compareSub(0, 0, 1, 1) // 返回 0。比较子数组 (0, 0) 与子数组 (1, 1) (即比较 arr[0] 和 arr[1])。 因此我们知道 arr[0] 和 arr[1] 不包含最大元素。 reader.compareSub(2, 2, 3, 3) // 返回 0。我们可以排除 arr[2] 和 arr[3]。 reader.compareSub(4, 4, 5, 5) // 返回 1。因此,可以确定 arr[4] 是数组中最大的元素。 注意,我们只调用了 3 次 compareSub,所以这个答案是有效的。
示例 2:
输入: nums = [6,6,12] 输出: 2
提示:
2 <= arr.length <= 5 * 1051 <= arr[i] <= 100arr中除一个最大元素外,其余所有元素都相等。
进阶
- 如果
arr中有两个整数比其他数大呢? - 如果有一个数比其他数大,另一个数比其他数小呢?
方法一 · Go 实现
思考
数组除一个较大值外全相等,只能通过比较两段区间和来定位,且长度可达 ,线性次比较不够,必须对数次缩小范围。
将当前区间近似三等分:比较左段 与中段 。和相等则大数在剩下的右段;左段更大则落在左段;否则落在中段。每次丢掉约三分之一,直至左右端重合。
/**
* // This is the ArrayReader's API interface.
* // You should not implement it, or speculate about its implementation
* type ArrayReader struct {
* }
* // Compares the sum of arr[l..r] with the sum of arr[x..y]
* // return 1 if sum(arr[l..r]) > sum(arr[x..y])
* // return 0 if sum(arr[l..r]) == sum(arr[x..y])
* // return -1 if sum(arr[l..r]) < sum(arr[x..y])
* func (this *ArrayReader) compareSub(l, r, x, y int) int {}
*
* // Returns the length of the array
* func (this *ArrayReader) length() int {}
*/
func getIndex(reader *ArrayReader) int {
left, right := 0, reader.length()-1
for left < right {
t1, t2, t3 := left, left+(right-left)/3, left+(right-left)/3*2+1
cmp := reader.compareSub(t1, t2, t2+1, t3)
if cmp == 0 {
left = t3 + 1
} else if cmp == 1 {
right = t2
} else {
left, right = t2+1, t3
}
}
return left
}
1539. 第 k 个缺失的正整数 ​
题目、示例与约束
给你一个 严格升序排列 的正整数数组 arr 和一个整数 k 。
请你找到这个数组里第 k 个缺失的正整数。
示例 1:
输入:arr = [2,3,4,7,11], k = 5 输出:9 解释:缺失的正整数包括 [1,5,6,8,9,10,12,13,...] 。第 5 个缺失的正整数为 9 。
示例 2:
输入:arr = [1,2,3,4], k = 2 输出:6 解释:缺失的正整数包括 [5,6,7,...] 。第 2 个缺失的正整数为 6 。
提示:
1 <= arr.length <= 10001 <= arr[i] <= 10001 <= k <= 1000- 对于所有
1 <= i < j <= arr.length的i和j满足arr[i] < arr[j]
进阶:
你可以设计一个时间复杂度小于 O(n) 的算法解决此问题吗?
方法一:二分查找 · Go 实现
思考
有序正数组中求第 个缺失正整数。线性扫描在 与 同阶时可行,但缺失个数 随 单调,适合二分。
若 ,答案就是 。否则二分第一个「缺失数 」的位置 ,其左侧 处已缺失 个,再向后补 与该值的差即可。
func findKthPositive(arr []int, k int) int {
if arr[0] > k {
return k
}
left, right := 0, len(arr)
for left < right {
mid := (left + right) >> 1
if arr[mid]-mid-1 >= k {
right = mid
} else {
left = mid + 1
}
}
return arr[left-1] + k - (arr[left-1] - (left - 1) - 1)
}
1552. 两球之间的磁力 ​
题目、示例与约束
在代号为 C-137 的地球上,Rick 发现如果他将两个球放在他新发明的篮子里,它们之间会形成特殊形式的磁力。Rick 有 n 个空的篮子,第 i 个篮子的位置在 position[i] ,Morty 想把 m 个球放到这些篮子里,使得任意两球间 最小磁力 最大。
已知两个球如果分别位于 x 和 y ,那么它们之间的磁力为 |x - y| 。
给你一个整数数组 position 和一个整数 m ,请你返回最大化的最小磁力。
示例 1:

输入:position = [1,2,3,4,7], m = 3 输出:3 解释:将 3 个球分别放入位于 1,4 和 7 的三个篮子,两球间的磁力分别为 [3, 3, 6]。最小磁力为 3 。我们没办法让最小磁力大于 3 。
示例 2:
输入:position = [5,4,3,2,1,1000000000], m = 2 输出:999999999 解释:我们使用位于 1 和 1000000000 的篮子时最小磁力最大。
提示:
n == position.length2 <= n <= 10^51 <= position[i] <= 10^9- 所有
position中的整数 互不相同 。 2 <= m <= position.length
方法一:二分查找 · Go 实现
思考
在篮子上放 个球,最大化任意两球间距的最小值。位置坐标达 ,不能按间距枚举所有放法; ,需要更快的判定。
间距越大能放下的球越少,存在单调性。排序后二分间距 ,从左到右贪心:与上一球距离至少为 才放入。用「无法放下 个」作为二分键,第一个失败的 的前驱即为最大可行间距。
我们注意到,任意两球间的最小磁力越大,能够放入的球的数量就越少,这存在着单调性。我们可以使用二分查找,找到最大的最小磁力,使得能够放入的球的数量不小于 。
我们首先对篮子的位置进行排序,然后使用二分查找的方法,定义二分查找的左边界 ,右边界 ,其中 为篮子的数量。在每次二分查找的过程中,我们计算取中值 ,然后判断是否存在一种放置球的方法,使得能够放入的球的数量不小于 。
问题转换为判断一个给定的最小磁力 是否能够放入 个球。我们可以从左到右遍历篮子的位置,如果上一个球的位置与当前篮子的位置的距离大于等于 ,则说明可以在当前篮子放置一个球。最后判断放置的球的数量是否不小于 即可。
时间复杂度 ,空间复杂度 。其中 和 分别为篮子的数量和篮子的位置的最大值。
func maxDistance(position []int, m int) int {
sort.Ints(position)
return sort.Search(position[len(position)-1], func(f int) bool {
prev := position[0]
cnt := 1
for _, curr := range position {
if curr-prev >= f {
cnt++
prev = curr
}
}
return cnt < m
}) - 1
}
1562. 查找大小为 M 的最新分组 ​
题目、示例与约束
给你一个数组 arr ,该数组表示一个从 1 到 n 的数字排列。有一个长度为 n 的二进制字符串,该字符串上的所有位最初都设置为 0 。
在从 1 到 n 的每个步骤 i 中(假设二进制字符串和 arr 都是从 1 开始索引的情况下),二进制字符串上位于位置 arr[i] 的位将会设为 1 。
给你一个整数 m ,请你找出二进制字符串上存在长度为 m 的一组 1 的最后步骤。一组 1 是一个连续的、由 1 组成的子串,且左右两边不再有可以延伸的 1 。
返回存在长度 恰好 为 m 的 一组 1 的最后步骤。如果不存在这样的步骤,请返回 -1 。
示例 1:
输入:arr = [3,5,1,2,4], m = 1 输出:4 解释: 步骤 1:"00100",由 1 构成的组:["1"] 步骤 2:"00101",由 1 构成的组:["1", "1"] 步骤 3:"10101",由 1 构成的组:["1", "1", "1"] 步骤 4:"11101",由 1 构成的组:["111", "1"] 步骤 5:"11111",由 1 构成的组:["11111"] 存在长度为 1 的一组 1 的最后步骤是步骤 4 。
示例 2:
输入:arr = [3,1,5,4,2], m = 2 输出:-1 解释: 步骤 1:"00100",由 1 构成的组:["1"] 步骤 2:"10100",由 1 构成的组:["1", "1"] 步骤 3:"10101",由 1 构成的组:["1", "1", "1"] 步骤 4:"10111",由 1 构成的组:["1", "111"] 步骤 5:"11111",由 1 构成的组:["11111"] 不管是哪一步骤都无法形成长度为 2 的一组 1 。
示例 3:
输入:arr = [1], m = 1 输出:1
示例 4:
输入:arr = [2,1], m = 2 输出:2
提示:
n == arr.length1 <= n <= 10^51 <= arr[i] <= narr中的所有整数 互不相同1 <= m <= arr.length
方法一:并查集 · Go 实现
思考
按 的顺序把 变成 ,求最后一次出现长度为 的连续 段的时刻。 ,每步重建全串不可行。 时整段一次填满,答案为 。
并查集维护当前 连通块的大小。新点与左或右已填邻居合并前,若邻居块长度恰为 ,则该时刻仍有一段长度为 (即将被合并破坏),记下步骤。合并后更新块大小。
正向遍历 ,利用并查集动态维护每组 的长度。
时间复杂度 。
相似题目:
func findLatestStep(arr []int, m int) int {
n := len(arr)
if m == n {
return n
}
p := make([]int, n)
size := make([]int, n)
vis := make([]bool, n)
for i := range p {
p[i] = i
size[i] = 1
}
var find func(int) int
find = func(x int) int {
if p[x] != x {
p[x] = find(p[x])
}
return p[x]
}
union := func(a, b int) {
pa, pb := find(a), find(b)
if pa == pb {
return
}
p[pa] = pb
size[pb] += size[pa]
}
ans := -1
for i, v := range arr {
v--
if v > 0 && vis[v-1] {
if size[find(v-1)] == m {
ans = i
}
union(v, v-1)
}
if v < n-1 && vis[v+1] {
if size[find(v+1)] == m {
ans = i
}
union(v, v+1)
}
vis[v] = true
}
return ans
}
方法二:动态维护区间端点的长度 · Go 实现
思考
并查集查找带来额外对数因子,而合并只发生在区间端点。在每个 段的左右端记下长度,新点根据左右邻居的端点长度拼出新段,并写回新端点。判断与更新都是 ,总时间线性。
我们其实并不需要去通过查找并查集来获取每个区间长度,我们只需要在每个区间端点处记录每个区间长度,由于合并的时候只会访问区间端点,所以合并区间的时候修改端点区间长度即可。
时间复杂度 。
func findLatestStep(arr []int, m int) int {
n := len(arr)
if m == n {
return n
}
cnt := make([]int, n+2)
ans := -1
for i, v := range arr {
l, r := cnt[v-1], cnt[v+1]
if l == m || r == m {
ans = i
}
cnt[v-l], cnt[v+r] = l+r+1, l+r+1
}
return ans
}
1574. 删除最短的子数组使剩余数组有序 ​
题目、示例与约束
给你一个整数数组 arr ,请你删除一个子数组(可以为空),使得 arr 中剩下的元素是 非递减 的。
一个子数组指的是原数组中连续的一个子序列。
请你返回满足题目要求的最短子数组的长度。
示例 1:
输入:arr = [1,2,3,10,4,2,3,5] 输出:3 解释:我们需要删除的最短子数组是 [10,4,2] ,长度为 3 。剩余元素形成非递减数组 [1,2,3,3,5] 。 另一个正确的解为删除子数组 [3,10,4] 。
示例 2:
输入:arr = [5,4,3,2,1] 输出:4 解释:由于数组是严格递减的,我们只能保留一个元素。所以我们需要删除长度为 4 的子数组,要么删除 [5,4,3,2],要么删除 [4,3,2,1]。
示例 3:
输入:arr = [1,2,3] 输出:0 解释:数组已经是非递减的了,我们不需要删除任何元素。
示例 4:
输入:arr = [1] 输出:0
提示:
1 <= arr.length <= 10^50 <= arr[i] <= 10^9
方法一:双指针 + 二分查找 · Go 实现
思考
删除一段连续子数组后使剩余部分非递减,求最短删除长度。 ,不能枚举删除区间。剩余数组只能是「一段前缀加一段后缀」,且前缀的末元不超过后缀的首元。
先找出最长非递减前缀 与后缀 。整段有序则答案为 ;否则至少可删掉整段后缀或整段前缀。再枚举前缀右端 ,在后缀中二分第一个 的位置 ,删除 并取最短。
我们先找出数组的最长非递减前缀和最长非递减后缀,分别记为 和 。
如果 ,说明数组本身就是非递减的,返回 。
否则,我们可以选择删除右侧后缀,也可以选择删除左侧前缀,因此初始时答案为 。
接下来,我们枚举左侧前缀的最右端点 ,对于每个 ,我们可以通过二分查找,在 中找到第一个大于等于 的位置,记为 ,此时我们可以删除 ,并且更新答案 。继续枚举 ,最终得到答案。
时间复杂度 ,其中 为数组长度。空间复杂度 。
func findLengthOfShortestSubarray(arr []int) int {
n := len(arr)
i, j := 0, n-1
for i+1 < n && arr[i] <= arr[i+1] {
i++
}
for j-1 >= 0 && arr[j-1] <= arr[j] {
j--
}
if i >= j {
return 0
}
ans := min(n-i-1, j)
for l := 0; l <= i; l++ {
r := j + sort.SearchInts(arr[j:], arr[l])
ans = min(ans, r-l-1)
}
return ans
}
方法二:双指针 · Go 实现
思考
方法一对每个 单独二分,多出 因子。前缀与后缀都有序,随着 增大,对应的 只增不减,右指针单调右移即可,总时间降为线性。
与方法一类似,我们先找出数组的最长非递减前缀和最长非递减后缀,分别记为 和 。
如果 ,说明数组本身就是非递减的,返回 。
否则,我们可以选择删除右侧后缀,也可以选择删除左侧前缀,因此初始时答案为 。
接下来,我们枚举左侧前缀的最右端点 ,对于每个 ,我们直接利用双指针找到第一个大于等于 的位置,记为 ,此时我们可以删除 ,并且更新答案 。继续枚举 ,最终得到答案。
时间复杂度 ,其中 为数组长度。空间复杂度 。
func findLengthOfShortestSubarray(arr []int) int {
n := len(arr)
i, j := 0, n-1
for i+1 < n && arr[i] <= arr[i+1] {
i++
}
for j-1 >= 0 && arr[j-1] <= arr[j] {
j--
}
if i >= j {
return 0
}
ans := min(n-i-1, j)
r := j
for l := 0; l <= i; l++ {
for r < n && arr[r] < arr[l] {
r += 1
}
ans = min(ans, r-l-1)
}
return ans
}
1608. 特殊数组的特征值 ​
题目、示例与约束
给你一个非负整数数组 nums 。如果存在一个数 x ,使得 nums 中恰好有 x 个元素 大于或者等于 x ,那么就称 nums 是一个 特殊数组 ,而 x 是该数组的 特征值 。
注意: x 不必 是 nums 的中的元素。
如果数组 nums 是一个 特殊数组 ,请返回它的特征值 x 。否则,返回 -1 。可以证明的是,如果 nums 是特殊数组,那么其特征值 x 是 唯一的 。
示例 1:
输入:nums = [3,5] 输出:2 解释:有 2 个元素(3 和 5)大于或等于 2 。
示例 2:
输入:nums = [0,0] 输出:-1 解释:没有满足题目要求的特殊数组,故而也不存在特征值 x 。 如果 x = 0,应该有 0 个元素 >= x,但实际有 2 个。 如果 x = 1,应该有 1 个元素 >= x,但实际有 0 个。 如果 x = 2,应该有 2 个元素 >= x,但实际有 0 个。 x 不能取更大的值,因为 nums 中只有两个元素。
示例 3:
输入:nums = [0,4,3,0,4] 输出:3 解释:有 3 个元素大于或等于 3 。
示例 4:
输入:nums = [3,6,7,7,0] 输出:-1
提示:
1 <= nums.length <= 1000 <= nums[i] <= 1000
方法一:暴力枚举 · Go 实现
思考
特数 只能落在 ,而 ,对每个候选 扫描数组统计 的个数即可在 内判定。
若不存在这样的 ,按题意返回 。
实现上直接枚举 ,用一次线性计数比较是否等于 。
我们在 范围内枚举 ,然后统计数组中大于等于 的元素个数,记为 。若存在 与 相等,直接返回 。
时间复杂度 ,其中 是数组的长度。空间复杂度 。
func specialArray(nums []int) int {
for x := 1; x <= len(nums); x++ {
cnt := 0
for _, v := range nums {
if v >= x {
cnt++
}
}
if cnt == x {
return x
}
}
return -1
}
方法二:排序 + 二分查找 · Go 实现
思考
方法一对每个 都整表扫描。排序之后, 的个数等于 减去第一个不小于 的下标,可用二分在 内求得。
总复杂度降为 ,在 更大时更稳妥,本题规模下两者均可通过。
我们也可以先对 nums 进行排序。
接下来同样枚举 ,利用二分查找,找到 nums 中第一个大于等于 的元素,快速统计出 nums 中大于等于 的元素个数。
时间复杂度 ,空间复杂度 。其中 是数组的长度。
func specialArray(nums []int) int {
sort.Ints(nums)
n := len(nums)
for x := 1; x <= n; x++ {
left, right := 0, n
for left < right {
mid := (left + right) >> 1
if nums[mid] >= x {
right = mid
} else {
left = mid + 1
}
}
cnt := n - left
if cnt == x {
return x
}
}
return -1
}
1618. 找出适应屏幕的最大字号 🔒 ​
题目、示例与约束
给定一个字符串 text。并能够在 宽为 w 高为 h 的屏幕上显示该文本。
字体数组中包含按升序排列的可用字号,您可以从该数组中选择任何字体大小。
您可以使用FontInfo接口来获取任何可用字体大小的任何字符的宽度和高度。
FontInfo接口定义如下:
interface FontInfo {
// 返回 fontSize 大小的字符 ch 在屏幕上的宽度。
// 每调用该函数复杂度为 O(1)
public int getWidth(int fontSize, char ch);
// 返回 fontSize 大小的任意字符在屏幕上的高度。
// 每调用该函数复杂度为 O(1)
public int getHeight(int fontSize);
}
一串字符的文本宽度应该是 每一个字符 在对应字号(fontSize)下返回的宽度getWidth(fontSize, text[i])的 总和 。对应字号的文本高度可由 getHeight(fontSize) 计算得到。
请注意:文本最多只能排放一排
如果使用相同的参数调用 getHeight 或 getWidth ,则可以保证 FontInfo 将返回相同的值。
同时,对于任何字体大小的 fontSize 和任何字符 ch :
getHeight(fontSize) <= getHeight(fontSize+1)getWidth(fontSize, ch) <= getWidth(fontSize+1, ch)
返回可用于在屏幕上显示文本的最大字体大小。如果文本不能以任何字体大小显示,则返回-1。
示例 1:
输入: text = "helloworld", w = 80, h = 20, fonts = [6,8,10,12,14,16,18,24,36] 输出: 6
示例 2:
输入: text = "leetcode", w = 1000, h = 50, fonts = [1,2,4] 输出: 4
示例 3:
输入: text = "easyquestion", w = 100, h = 100, fonts = [10,15,20,25] 输出: -1
注意:
1 <= text.length <= 50000text只包含小写字母1 <= w <= 1071 <= h <= 1041 <= fonts.length <= 1051 <= fonts[i] <= 105fonts已经按升序排序,且不包含重复项。
方法一:Go 解法(本站整理) · Go 实现
基于 Doocs 二分参考改写,并先统计字符频次,避免每次判定扫描全文。fits(s) 要求字高不超过 h,且每个字符宽度乘次数的总和不超过 w;单调性允许查找最后一个可行字号。n 为文本长度,f 为字号数,时间 O(n + 26 log f),空间 O(1)。宽度乘法用 int64。平台接口 FontInfo 提供 GetWidth(fontSize int, ch byte) int 和 GetHeight(fontSize int) int;代码用 Go interface 表达接口,测试 mock 的单调尺寸与官方样例结果兼容,真实尺寸由平台提供。
出处:Doocs 原题与题解,固定提交 91ef8c241a5eaabf96f9ca8d45cfb1a338bcb011;题意、参考及本 Go 改写采用 CC BY-SA 4.0。
// 题意与参考:Doocs LeetCode,CC BY-SA 4.0。
// https://github.com/doocs/leetcode/blob/91ef8c241a5eaabf96f9ca8d45cfb1a338bcb011/solution/1600-1699/1618.Maximum%20Font%20to%20Fit%20a%20Sentence%20in%20a%20Screen/README.md
// Go 实现与测试沿用 CC BY-SA 4.0;来源性质见配套说明。
type FontInfo interface {
GetWidth(int, byte) int
GetHeight(int) int
}
func maxFont(text string, w, h int, fonts []int, fontInfo FontInfo) int {
var counts [26]int64
for i := 0; i < len(text); i++ {
counts[text[i]-'a']++
}
fits := func(size int) bool {
if fontInfo.GetHeight(size) > h {
return false
}
var width int64
for c, n := range counts {
if n > 0 {
width += n * int64(fontInfo.GetWidth(size, byte(c)+'a'))
if width > int64(w) {
return false
}
}
}
return true
}
lo, hi := 0, len(fonts)
for lo < hi {
mid := lo + (hi-lo)/2
if fits(fonts[mid]) {
lo = mid + 1
} else {
hi = mid
}
}
if lo == 0 {
return -1
}
return fonts[lo-1]
}
1658. 将 x 减到 0 的最小操作数 ​
题目、示例与约束
给你一个整数数组 nums 和一个整数 x 。每一次操作时,你应当移除数组 nums 最左边或最右边的元素,然后从 x 中减去该元素的值。请注意,需要 修改 数组以供接下来的操作使用。
如果可以将 x 恰好 减到 0 ,返回 最小操作数 ;否则,返回 -1 。
示例 1:
输入:nums = [1,1,4,2,3], x = 5 输出:2 解释:最佳解决方案是移除后两个元素,将 x 减到 0 。
示例 2:
输入:nums = [5,6,7,8,9], x = 4 输出:-1
示例 3:
输入:nums = [3,2,20,1,1,3], x = 10 输出:5 解释:最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0 。
提示:
1 <= nums.length <= 1051 <= nums[i] <= 1041 <= x <= 109
方法一:哈希表 + 前缀和 · Go 实现
思考
每次只能删左右端点,使删除之和为 且次数最少,等价于中间留下和为 的最长子数组。 达 ,需要线性查找该子数组。
前缀和哈希:记下和第一次出现的下标,当 已出现则更新最长长度,答案为 减该长度。
根据题目描述,我们需要移除数组 左右两端的元素,使得移除的元素和等于 ,且移除的元素个数最少。我们可以将问题转化为:找到数组 中最长的连续子数组,使得子数组的和 。这样,我们就可以将问题转化为求解数组 中和为 的最长连续子数组的长度 ,答案即为 。
我们初始化 ,然后使用哈希表 来存储前缀和,键为前缀和,值为前缀和对应的下标。
遍历数组 ,对于当前元素 ,计算前缀和 ,如果 不在哈希表中,则将 加入哈希表;如果 在哈希表中,则更新 。
最后,如果 ,则返回 ,否则返回 。
时间复杂度 ,空间复杂度 。其中 为数组 的长度。
func minOperations(nums []int, x int) int {
s := -x
for _, v := range nums {
s += v
}
vis := map[int]int{0: -1}
mx, t := -1, 0
for i, v := range nums {
t += v
if _, ok := vis[t]; !ok {
vis[t] = i
}
if j, ok := vis[t-s]; ok {
mx = max(mx, i-j)
}
}
if mx == -1 {
return -1
}
return len(nums) - mx
}
方法二:双指针 · Go 实现
思考
方法一用哈希表。元素均为正,前缀和严格递增,可用双指针把窗口和压到 ,省掉额外空间。
基于方法一的分析,我们需要求解数组 中和为 的最长连续子数组的长度 。由于数组 中的元素都是正整数,数组的前缀和只会单调递增,因此我们可以使用双指针来求解。
我们初始化指针 ,前缀和 ,最长连续子数组的长度 。
遍历数组 ,对于当前元素 ,计算前缀和 ,如果 ,则移动指针 ,直到 。如果 ,则更新 。
最后,如果 ,则返回 ,否则返回 。
时间复杂度 ,其中 为数组 的长度。空间复杂度 。
func minOperations(nums []int, x int) int {
s := -x
for _, v := range nums {
s += v
}
mx, t, j := -1, 0, 0
for i, v := range nums {
t += v
for ; j <= i && t > s; j++ {
t -= nums[j]
}
if t == s {
mx = max(mx, i-j+1)
}
}
if mx == -1 {
return -1
}
return len(nums) - mx
}
1712. 将数组分成三个子数组的方案数 ​
题目、示例与约束
我们称一个分割整数数组的方案是 好的 ,当它满足:
- 数组被分成三个 非空 连续子数组,从左至右分别命名为
left,mid,right。 left中元素和小于等于mid中元素和,mid中元素和小于等于right中元素和。
给你一个 非负 整数数组 nums ,请你返回 好的 分割 nums 方案数目。由于答案可能会很大,请你将结果对 109 + 7 取余后返回。
示例 1:
输入:nums = [1,1,1] 输出:1 解释:唯一一种好的分割方案是将 nums 分成 [1] [1] [1] 。
示例 2:
输入:nums = [1,2,2,2,5,0] 输出:3 解释:nums 总共有 3 种好的分割方案: [1] [2] [2,2,5,0] [1] [2,2] [2,5,0] [1,2] [2,2] [5,0]
示例 3:
输入:nums = [3,2,1] 输出:0 解释:没有好的分割方案。
提示:
3 <= nums.length <= 1050 <= nums[i] <= 104
方法一:前缀和 + 二分查找 · Go 实现
思考
三段非空且前缀和满足 。两重枚举分割点为 , 不可行。
元素非负,前缀和单调。固定左段右端 后, 的右端落在一段连续区间,可用二分求出。
条件化为 与 。对每个 做两次二分,方案数对 取模。
我们先预处理出数组 的前缀和数组 ,其中 表述数组 前 个元素之和。
由于数组 的元素都是非负整数,因此前缀和数组 是一个单调递增数组。
我们在 的范围内枚举 left 子数组所能到达的下标 ,然后利用前缀和数组单调递增的特性,通过二分查找的方式找到 mid 子数组分割的合理范围,记为 ,累加方案数 。
二分细节上,子数组分割必须满足 ,并且 。即 ,且 。
最后,将方案数对 取模后返回即可。
时间复杂度 。其中 为数组 的长度。
func waysToSplit(nums []int) (ans int) {
const mod int = 1e9 + 7
n := len(nums)
s := make([]int, n)
s[0] = nums[0]
for i := 1; i < n; i++ {
s[i] = s[i-1] + nums[i]
}
for i := 0; i < n-2; i++ {
j := sort.Search(n-1, func(h int) bool { return h > i && s[h] >= (s[i]<<1) })
k := sort.Search(n-1, func(h int) bool { return h >= j && s[h] > (s[n-1]+s[i])>>1 })
ans = (ans + k - j) % mod
}
return
}
继续阅读
- 实践案例
- 实践案例
- 实践案例