普通视图

发现新文章,点击刷新页面。
今天 — 2025年6月29日LeetCode 每日一题题解

每日一题-满足条件的子序列数目🟡

2025年6月29日 00:00

给你一个整数数组 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 <= 105
  • 1 <= nums[i] <= 106
  • 1 <= target <= 106

排序+相向双指针(Python/Java/C++/Go)

作者 endlesscheng
2025年6月16日 09:22

推荐先完成本题的简单版本2824. 统计和小于目标的下标对数目

虽然本题要求的是子序列,但由于我们只关心子序列的最小值和最大值,并不关心元素的位置,所以可以先把 $\textit{nums}$ 排序,从而方便计算。

从小到大排序后,对于任意子序列,第一个数一定是最小的,最后一个数一定是最大的。

注意:子序列的最小值和最大值可以是同一个数,此时子序列长度为 $1$。

分类讨论:

  • 如果 $\textit{nums}[0] + \textit{nums}[n-1] > \textit{target}$,这意味着 $\textit{nums}[n-1]$ 太大了,与最小的 $\textit{nums}[0]$ 相加不满足要求,与更大的 $\textit{nums}[1],\textit{nums}[2],\ldots, \textit{nums}[n-1]$ 相加,更不满足要求。所以 $\textit{nums}[n-1]$ 不可能作为子序列的最大值。去掉 $\textit{nums}[n-1]$,问题变成剩余 $n-1$ 个数中的满足要求的子序列的数目。
  • 如果 $\textit{nums}[0] + \textit{nums}[n-1] \le \textit{target}$,这意味着 $\textit{nums}[0]$ 可以作为子序列的最小值,不仅与 $\textit{nums}[n-1]$ 相加满足要求,与更小的 $\textit{nums}[n-2],\textit{nums}[n-3],\ldots, \textit{nums}[0]$ 相加,也满足要求。换句话说,如果选 $\textit{nums}[0]$ 作为最小值,那么其余 $n-1$ 个数,每个数可以选也可以不选,每个数有 $2$ 种方案,一共有 $2^{n-1}$ 种方案(乘法原理),加到答案中。接下来,去掉 $\textit{nums}[0]$,计算剩余 $n-1$ 个数中的满足要求的子序列的数目。

一般地:

  1. 初始化 $\textit{left}=0$,$\textit{right}=n-1$,分别表示剩余元素中的最小下标和最大下标。
  2. 如果 $\textit{nums}[\textit{left}] + \textit{nums}[\textit{right}] > \textit{target}$,这意味着 $\textit{nums}[\textit{right}]$ 太大了,不仅与剩余元素中最小的 $\textit{nums}[\textit{left}]$ 相加不满足要求,与更大的 $\textit{nums}[\textit{left}+1],\textit{nums}[\textit{left}+2],\ldots, \textit{nums}[\textit{right}]$ 相加,更不满足要求。所以 $\textit{nums}[\textit{right}]$ 不可能作为剩余元素中的子序列最大值。去掉 $\textit{nums}[\textit{right}]$(也就是把 $\textit{right}$ 减一),问题变成 $[\textit{left},\textit{right}-1]$ 中满足要求的子序列数目。
  3. 如果 $\textit{nums}[\textit{left}] + \textit{nums}[\textit{right}] \le \textit{target}$,这意味着 $\textit{nums}[\textit{left}]$ 可以作为子序列的最小值,不仅与 $\textit{nums}[\textit{right}]$ 相加满足要求,与更小的 $\textit{nums}[\textit{right}-1],\textit{nums}[\textit{right}-2],\ldots, \textit{nums}[\textit{left}]$ 相加,也满足要求。换句话说,如果选 $\textit{nums}[\textit{left}]$ 作为最小值,那么其余下标在 $[\textit{left}+1,\textit{right}]$ 中的这 $\textit{right}-\textit{left}$ 个数可选可不选,有 $2^{\textit{right}-\textit{left}}$ 种方案,加到答案中。接下来,去掉 $\textit{nums}[\textit{left}]$(也就是把 $\textit{left}$ 加一),问题变成 $[\textit{left}+1,\textit{right}]$ 中满足要求的子序列数目。
  4. 循环直到没有剩余元素,即 $\textit{left}>\textit{right}$。

代码实现时,可以预处理 $2$ 的幂。为什么可以在中途取模,原理见 模运算的世界:当加减乘除遇上取模

###py

MOD = 1_000_000_007
MX = 100_000

pow2 = [1] * MX  # pow2[i] = 2 ** i % MOD
for i in range(1, MX):
    pow2[i] = pow2[i - 1] * 2 % MOD

class Solution:
    def numSubseq(self, nums: List[int], target: int) -> int:
        nums.sort()
        ans = 0
        left, right = 0, len(nums) - 1
        while left <= right:  # 可以相等,此时子序列的最小最大是同一个数
            if nums[left] + nums[right] <= target:
                # nums[left] 可以作为子序列的最小值
                # 其余下标在 [left+1,right] 中的数选或不选都可以
                ans += pow2[right - left]
                left += 1
            else:
                # nums[right] 太大了,即使与剩余元素的最小值 nums[left] 相加也不满足要求
                right -= 1
        return ans % MOD

###java

class Solution {
    private static final int MOD = 1_000_000_007;
    private static final int[] pow2 = new int[100_000]; // 2^i
    private static boolean initialized = false;

    // 这样写比 static block 快
    private void init() {
        if (initialized) {
            return;
        }
        initialized = true;

        pow2[0] = 1;
        for (int i = 1; i < pow2.length; i++) {
            pow2[i] = pow2[i - 1] * 2 % MOD;
        }
    }

    public int numSubseq(int[] nums, int target) {
        init();
        Arrays.sort(nums);
        long ans = 0;
        int left = 0;
        int right = nums.length - 1;
        while (left <= right) { // 可以相等,此时子序列的最小最大是同一个数
            if (nums[left] + nums[right] <= target) {
                // nums[left] 可以作为子序列的最小值
                // 其余下标在 [left+1,right] 中的数选或不选都可以
                ans += pow2[right - left];
                left++;
            } else {
                // nums[right] 太大了,即使与剩余元素的最小值 nums[left] 相加也不满足要求
                right--;
            }
        }
        return (int) (ans % MOD);
    }
}

###cpp

const int MOD = 1'000'000'007;
const int MX = 100'000;

int pow2[MX]; // 2^i

auto init = [] {
    pow2[0] = 1;
    for (int i = 1; i < MX; i++) {
        pow2[i] = pow2[i - 1] * 2 % MOD;
    }
    return 0;
}();

class Solution {
public:
    int numSubseq(vector<int>& nums, int target) {
        ranges::sort(nums);
        long long ans = 0;
        int left = 0, right = nums.size() - 1;
        while (left <= right) { // 可以相等,此时子序列的最小最大是同一个数
            if (nums[left] + nums[right] <= target) {
                // nums[left] 可以作为子序列的最小值
                // 其余下标在 [left+1,right] 中的数选或不选都可以
                ans += pow2[right - left];
                left++;
            } else {
                // nums[right] 太大了,即使与剩余元素的最小值 nums[left] 相加也不满足要求
                right--;
            }
        }
        return ans % MOD;
    }
};

###go

const mod = 1_000_000_007

var pow2 = [100_000]int{1} // 2^i

func init() {
for i := 1; i < len(pow2); i++ {
pow2[i] = pow2[i-1] * 2 % mod
}
}

func numSubseq(nums []int, target int) (ans int) {
slices.Sort(nums)
left, right := 0, len(nums)-1
for left <= right {
if nums[left]+nums[right] <= target { // 可以相等,此时子序列的最小最大是同一个数
// nums[left] 可以作为子序列的最小值 
// 其余下标在 [left+1,right] 中的数选或不选都可以
ans += pow2[right-left]
left++
} else {
// nums[right] 太大了,即使与剩余元素的最小值 nums[left] 相加也不满足要求
right--
}
}
return ans % mod
}

复杂度分析

不计入预处理的时间和空间。

  • 时间复杂度:$\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{nums}$ 的长度。瓶颈在排序上。
  • 空间复杂度:$\mathcal{O}(1)$。忽略排序时的栈开销。

思考题

改成最大元素与最小元素之差 $\le \textit{target}$ 呢?

总结

如果是两数之和 $\le$(或者 $=$、$\ge$)问题,其中一个数变小,另一个数变大,通常用相向双指针解决。

如果是两数之差 $\le$(或者 $=$、$\ge$)问题,其中一个数变大,另一个数也变大,通常用同向双指针解决。

相似题目

见下面双指针题单的「§3.1 相向双指针」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

满足条件的子序列数目

2020年7月1日 00:18

方法一:计算贡献

算法

题目要求我们统计符合「最小元素与最大元素的和小于或等于 $\rm target$」的非空子序列的个数。我们可以关注到两个点:

  • 「子序列」是不要求连续的;

  • 在这题中,我们只关心这个子序列的最小值和最大值,而不关心元素的相对顺序。

这道题我们显然不能枚举出所有的子序列然后进行判断,但是我们可以转化成求出「从原序列中选出一些元素来构成符合条件的子序列」的方案数。假如我们可以固定住这个子序列的最小值 $v_{\min}$,那么这个子序列最大值 $v_{\max}$ 一定小于等于 ${\rm target} - v_{\min}$,我们得到这样一个不等式:

$$ v_{\min} \leq v_{\max} \leq {\rm target} - v_{\min}$$

于是可以得到这样一个关系 $2 \times v_{\min} \leq {\rm target}$,也即 $v_{\min} \leq \lfloor \frac{\rm target}{2} \rfloor$,这个结论在后续的过程中会使用到。

再回到刚刚的假设,如果我们固定住 $v_{\min}$,我们可以求出 $v_{\max}$ 的最大可能值为 ${\rm target} - v_{\min}$。我们尝试枚举 $v_{\max}$,它可能是是序列中在区间 $[v_{\min}, {\rm target} - v_{\min}]$ 中的任意一个元素,例如原序列为 ${ 5, 1, 8, 2, 9 }$,$\rm target = 7$,$v_{\min} = 1$ 的时候,$[v_{\min}, {\rm target} - v_{\min}]$ 就是 $[1, 6]$,对应可能的 $v_{\max}$ 为 $1$ 或 $2$ 或 $5$。因为 $1$ 是我们假设「固定的」,所以我们认为 $1$ 必须出现在我们选出的子序列当中作为最小值,而 $2$ 和 $5$ 可出现也可不出现在最终的子序列当中,所以,如果 $1$ 以最小值的形式出现在我们选出的子序列中的话,一共有 $4$ 种选法:

  • $1$
  • $1, 2$
  • $1, 5$
  • $1, 2, 5$

这里的 $4 = 2^2$,即 $2$ 和 $5$ 这两个数每个数都有「出现在子序列中」和「不出现在子序列中」两种状态。这可以看作 $v_{\min} = 1$ 的情况下对答案的贡献,那么我们只要枚举所有的合法的 $v_{\min}$,把它们对答案的贡献求和,就可以计算出总的方案数。

由于我们刚刚得到了 $2 \times v_{\min} \leq {\rm target}$, 于是我们很容易枚举 $v_{\min}$,只要找到原序列中所有满足这个条件的元素,都可以作为 $v_{\min}$。那我们怎么找出符合条件的 $v_{\max}$ 的个数呢?我们可以对原序列排序之后做二分查找,就可以得到区间 $[v_{\min}, {\rm target} - v_{\min}]$ 中数的个数 $x$,但是由于 $v_{\min}$ 是必取的,所以这里的贡献应该是 $2^{x - 1}$。因为「我们只关心这个子序列的最小值和最大值,而不关心元素的相对顺序」,所以我们才可以重新排序。

具体地,我们可以先预处理出所有 $2^i \bmod (10^9 + 7)$ 的值,然后对原序列进行排序。排序之后,我们顺序枚举所有合法的 $v_{\min}$,对于每个 $v_{\min}$,二分出最大的 $v_{\max}$ 的位置,这个时候 $v_{\min}$ 和 $v_{\max}$最大值下标的差的绝对值为 $x$,当前的贡献就是 $2^x$。(思考:为什么不是 $2^{x - 1}$ ?)

这个时候也许有同学会提问:为什么这里用的是预处理,而不是直接对每个位置算一次快速幂呢?这是个好问题。其实这样做也是可以的,但是快速幂求到单个 $2^i$ 的时间代价是 $O(\log i) = O(\log n)$,假设序列一共有 $n$ 个数,最坏情况下这里的总代价是 $O(n \log n)$;而由于这里要用到的 $2^i$ 底数不变,可以用递推的方法在 $O(n)$ 时间内预处理出所有的 $2^i$,均摊到每个位置是 $O(1)$ 的,预处理和查询的总代价为 $O(n)$。所以这里预处理所耗费的时间更小。

在实现中,我们会用到取模来防止答案过大而溢出,这里需要用这样的小技巧来处理:

$$
(a + b) \bmod P = [(a \bmod P) + (b \bmod P)] \bmod P \
(a \times b) \bmod P = [(a \bmod P) \times (b \bmod P)] \bmod P
$$

代码如下。注意如果使用 Java,需要自己实现二分查找的方法。

代码

###C++

class Solution {
public:
    static constexpr int P = int(1E9) + 7;
    static constexpr int MAX_N = int(1E5) + 5;

    int f[MAX_N];

    void pretreatment() {
        f[0] = 1;
        for (int i = 1; i < MAX_N; ++i) {
            f[i] = (long long)f[i - 1] * 2 % P;
        }
    }

    int numSubseq(vector<int>& nums, int target) {
        pretreatment();

        sort(nums.begin(), nums.end());

        int ans = 0;
        for (int i = 0; i < nums.size() && nums[i] * 2 <= target; ++i) {
            int maxValue = target - nums[i];
            int pos = upper_bound(nums.begin(), nums.end(), maxValue) - nums.begin() - 1;
            int contribute = (pos >= i) ? f[pos - i] : 0;
            ans = (ans + contribute) % P;
        }

        return ans;
    }
};

###Java

class Solution {
    static final int P = 1000000007;
    static final int MAX_N = 100005;

    int[] f = new int[MAX_N];

    public int numSubseq(int[] nums, int target) {
        pretreatment();

        Arrays.sort(nums);

        int ans = 0;
        for (int i = 0; i < nums.length && nums[i] * 2 <= target; ++i) {
            int maxValue = target - nums[i];
            int pos = binarySearch(nums, maxValue) - 1;
            int contribute = (pos >= i) ? f[pos - i] : 0;
            ans = (ans + contribute) % P;
        }

        return ans;
    }

    public void pretreatment() {
        f[0] = 1;
        for (int i = 1; i < MAX_N; ++i) {
            f[i] = (f[i - 1] << 1) % P;
        }
    }

    public int binarySearch(int[] nums, int target) {
        int low = 0, high = nums.length;
        while (low < high) {
            int mid = (high - low) / 2 + low;
            if (mid == nums.length) {
                return mid;
            }
            int num = nums[mid];
            if (num <= target) {
                low = mid + 1;
            } else {
                high = mid;
            }
        }
        return low;
    }
}

###Python

class Solution:
    def numSubseq(self, nums: List[int], target: int) -> int:
        n = len(nums)
        P = 10**9 + 7
        f = [1] + [0] * (n - 1)
        for i in range(1, n):
            f[i] = f[i - 1] * 2 % P

        nums.sort()
        ans = 0
        for i, num in enumerate(nums):
            if nums[i] * 2 > target:
                break
            maxValue = target - nums[i]
            pos = bisect.bisect_right(nums, maxValue) - 1
            contribute = f[pos - i] if pos >= i else 0
            ans += contribute
        
        return ans % P

###C#

public class Solution {
    private const int P = 1000000007;
    private const int MAX_N = 100005;

    public int NumSubseq(int[] nums, int target) {
        int[] f = new int[MAX_N];
        f[0] = 1;
        for (int i = 1; i < MAX_N; ++i) {
            f[i] = (f[i - 1] << 1) % P;
        }
        Array.Sort(nums);
        int ans = 0;
        for (int i = 0; i < nums.Length && nums[i] * 2 <= target; ++i) {
            int maxValue = target - nums[i];
            int pos = BinarySearch(nums, maxValue) - 1;
            int contribute = (pos >= i) ? f[pos - i] : 0;
            ans = (ans + contribute) % P;
        }

        return ans;
    }

    private int BinarySearch(int[] nums, int target) {
        int low = 0, high = nums.Length;
        while (low < high) {
            int mid = (high - low) / 2 + low;
            if (mid == nums.Length) {
                return mid;
            }
            int num = nums[mid];
            if (num <= target) {
                low = mid + 1;
            } else {
                high = mid;
            }
        }
        return low;
    }
}

###Go

func numSubseq(nums []int, target int) int {
    n := len(nums)
P := int(1e9 + 7)
f := make([]int, n)
f[0] = 1
for i := 1; i < n; i++ {
f[i] = (f[i - 1] * 2) % P
}
sort.Ints(nums)
ans := 0
for i, num := range nums {
if num * 2 > target {
break
}
maxValue := target - num
pos := sort.SearchInts(nums, maxValue + 1) - 1
contribute := 0
if pos >= i {
contribute = f[pos - i]
}
ans = (ans + contribute) % P
}

return ans
}

###C

#define P 1000000007
#define MAX_N 100005

int binarySearch(int* nums, int numsSize, int target) {
    int low = 0, high = numsSize;
    while (low < high) {
        int mid = (high - low) / 2 + low;
        if (mid == numsSize) {
            return mid;
        }
        int num = nums[mid];
        if (num <= target) {
            low = mid + 1;
        } else {
            high = mid;
        }
    }
    return low;
}

int cmp(const void *a, const void *b) {
    return *(int *)a - *(int *)b;
}

int numSubseq(int* nums, int numsSize, int target) {
    int f[MAX_N];
    f[0] = 1;
    for (int i = 1; i < MAX_N; ++i) {
        f[i] = (f[i - 1] << 1) % P;
    }
    qsort(nums, numsSize, sizeof(int), cmp);
    
    int ans = 0;
    for (int i = 0; i < numsSize && nums[i] * 2 <= target; ++i) {
        int maxValue = target - nums[i];
        int pos = binarySearch(nums, numsSize, maxValue) - 1;
        int contribute = (pos >= i) ? f[pos - i] : 0;
        ans = (ans + contribute) % P;
        printf("ans = %d\n", ans);
    }
    return ans;
}

###JavaScript

const P = 1000000007;
const MAX_N = 100005;

var numSubseq = function(nums, target) {
    let f = new Array(MAX_N).fill(0);
    f[0] = 1;
    for (let i = 1; i < MAX_N; ++i) {
        f[i] = (f[i - 1] << 1) % P;
    }

    nums.sort((a, b) => a - b);
    let ans = 0;
    for (let i = 0; i < nums.length && nums[i] * 2 <= target; ++i) {
        let maxValue = target - nums[i];
        let pos = binarySearch(nums, maxValue) - 1;
        let contribute = (pos >= i) ? f[pos - i] : 0;
        ans = (ans + contribute) % P;
    }

    return ans;
}

function binarySearch(nums, target) {
    let low = 0, high = nums.length;
    while (low < high) {
        let mid = Math.floor((high - low) / 2) + low;
        if (mid === nums.length) {
            return mid;
        }
        let num = nums[mid];
        if (num <= target) {
            low = mid + 1;
        } else {
            high = mid;
        }
    }
    return low;
}

###TypeScript

const P = 1000000007;
const MAX_N = 100005;

function numSubseq(nums: number[], target: number): number {
    let f: number[] = new Array(MAX_N).fill(0);
    f[0] = 1;
    for (let i = 1; i < MAX_N; ++i) {
        f[i] = (f[i - 1] << 1) % P;
    }
    nums.sort((a, b) => a - b);

    let ans = 0;
    for (let i = 0; i < nums.length && nums[i] * 2 <= target; ++i) {
        let maxValue = target - nums[i];
        let pos = binarySearch(nums, maxValue) - 1;
        let contribute = (pos >= i) ? f[pos - i] : 0;
        ans = (ans + contribute) % P;
    }

    return ans;
}

function binarySearch(nums: number[], target: number): number {
    let low = 0, high = nums.length;
    while (low < high) {
        let mid = Math.floor((high - low) / 2) + low;
        if (mid === nums.length) {
            return mid;
        }
        let num = nums[mid];
        if (num <= target) {
            low = mid + 1;
        } else {
            high = mid;
        }
    }
    return low;
}

###Rust

impl Solution {
    pub fn num_subseq(nums: Vec<i32>, target: i32) -> i32 {
        const P: i32 = 1_000_000_007;
        const MAX_N: usize = 100_005;
        let mut f = vec![0; MAX_N];
        f[0] = 1;
        for i in 1..MAX_N {
            f[i] = (f[i - 1] << 1) % P;
        }
        let mut nums = nums;
        nums.sort();

        let mut ans = 0;
        for i in 0..nums.len() {
            if nums[i] * 2 > target {
                break;
            }
            let max_value = target - nums[i];
            let pos = Self::binary_search(&nums, max_value) - 1;
            let contribute = if pos >= i { f[pos - i] } else { 0 };
            ans = (ans + contribute) % P;
        }

        ans
    }

    fn binary_search(nums: &Vec<i32>, target: i32) -> usize {
        let mut low = 0;
        let mut high = nums.len();
        while low < high {
            let mid = (high - low) / 2 + low;
            if mid == nums.len() {
                return mid;
            }
            let num = nums[mid];
            if num <= target {
                low = mid + 1;
            } else {
                high = mid;
            }
        }
        low
    }
}

复杂度

假设这里元素的个数为 $n$。

  • 时间复杂度:$O(n \log n)$。预处理的时间代价为 $O(n)$;排序的时间代价为 $O(n \log n)$;单次二分的时间代价为 $O(\log n)$,所以二分的总代价为 $O(n \log n)$;计算贡献的单次代价为 $O(1)$,总代价为 $O(n)$。故渐进时间复杂度为 $O(n \log n)$。

  • 空间复杂度:$O(n)$。预处理的空间代价为 $O(n)$。

python 排序+双指针

作者 irruma
2020年6月28日 13:39

这题一开始我打leetcode周赛的时候用的回溯算法,结果时间超了,那么表示暴力法是过不了的,所以这题需要找规律,然后用数学的方式解决。

思路:该题目只需要子数组的最小值+最大值<=target,因此,如果有个滑动窗口,滑动窗口内的最小值+最大值小于等于target的话,那么这个子数组进行排列组合,在包含最小值的情况下,排列组合的结果都符合题意。

如:用list=[min,num1,num2,num3,num4,max]表示min+max<=target的滑动窗口,这里n=len(list),n==6,包含最小值的子数组为:
[min]、[min,num1]、[min,num2]、[min,num1,num2,num3]、[min,num1,max]等等,这样的子数组有2^(n-1)个。(即[num1,num2,num3,num4,max]的全排列(2^(n-1))-1个,加上只有[min]的子数组,加起来共2^(n-1)个)

由上我们可知,我们需要首先对数组进行从小到大排序,使用双指针找到满足条件的每一对最小值和最大值,累加滑动窗口的排列组合数量就可以了。

如:经排序后的数组为[num1,num2,num3,num4,num5,num6],output=0,双指针的索引left=0 right=5

第一步,若 num1+num5<=targer,这里num1和num5是一对最小值和最大值,output=output+2^4

第二步,双指针向内移动,left=left+1 -> num2+num5>target -> right=right-1 -> num2+num4<=target, 这里num2和num4 是一对最小值和最大值, output=output+2^2

如此迭代至 left>right 的时候,即可结束。

具体代码如下

class Solution:
    def numSubseq(self, nums: List[int], target: int) -> int:
        nums.sort()
        if nums[0] * 2 > target:
            return 0
            
        left = 0
        right = len(nums) - 1
        res = 0
        while left <= right:
            if nums[left] + nums[right] <= target:
                res += 2**(right-left)
                left += 1
            else:
                right -= 1
        return res%(10**9+7)

8000ms居然都过了,我原来的回溯真不知道要多久。。

image.png

昨天 — 2025年6月28日LeetCode 每日一题题解

每日一题-找到和最大的长度为 K 的子序列🟢

2025年6月28日 00:00

给你一个整数数组 nums 和一个整数 k 。你需要找到 nums 中长度为 k 的 子序列 ,且这个子序列的 和最大 

请你返回 任意 一个长度为 k 的整数子序列。

子序列 定义为从一个数组里删除一些元素后,不改变剩下元素的顺序得到的数组。

 

示例 1:

输入:nums = [2,1,3,3], k = 2
输出:[3,3]
解释:
子序列有最大和:3 + 3 = 6 。

示例 2:

输入:nums = [-1,-2,3,4], k = 3
输出:[-1,3,4]
解释:
子序列有最大和:-1 + 3 + 4 = 6 。

示例 3:

输入:nums = [3,4,3,3], k = 2
输出:[3,4]
解释:
子序列有最大和:3 + 4 = 7 。
另一个可行的子序列为 [4, 3] 。

 

提示:

  • 1 <= nums.length <= 1000
  • -105 <= nums[i] <= 105
  • 1 <= k <= nums.length

找到和最大的长度为 K 的子序列

2021年12月12日 16:17

方法一:排序

思路与算法

数组 $\textit{nums}$ 中和最大的长度为 $K$ 的子序列一定是由 $\textit{nums}$ 中最大的 $K$ 个数组成的。为了使得排序寻找最大的 $K$ 个数后,还能按照它们在原数组 $\textit{nums}$ 中的顺序组成目标子序列,我们建立辅助数组 $\textit{vals}$,它的第 $i$ 个元素 $(i, \textit{nums}[i])$ 包含下标 $i$ 本身,以及数组中的对应数值 $\textit{nums}[i]$。

首先,我们将辅助数组按照原数组中的数值 $\textit{nums}[i]$ 为关键字降序排序,排序后的前 $K$ 个元素对应原数组的数值即为原数组 $\textit{nums}$ 中最大的 $K$ 个数,对应的下标即为它们在 $\textit{nums}$ 中的下标。随后,我们将这 $K$ 个元素按照下标 $i$ 为关键字升序排序,排序后的 $K$ 个数值保持了它们在原数组中的顺序,我们用新的数组顺序记录这些数值,该数组即为 $\textit{nums}$ 中和最大的长度为 $K$ 的子序列。我们返回该数组作为答案。

代码

###C++

class Solution {
public:
    vector<int> maxSubsequence(vector<int>& nums, int k) {
        int n = nums.size();
        vector<pair<int, int>> vals;   // 辅助数组
        for (int i = 0; i < n; ++i) {
            vals.emplace_back(i, nums[i]);
        }
        // 按照数值降序排序
        sort(vals.begin(), vals.end(), [&](auto x1, auto x2) {
            return x1.second > x2.second;
        });
        // 取前 k 个并按照下标升序排序
        sort(vals.begin(), vals.begin() + k);
        vector<int> res;   // 目标子序列
        for (int i = 0; i < k; ++i) {
            res.push_back(vals[i].second);
        }
        return res;
    }
};

###Python

class Solution:
    def maxSubsequence(self, nums: List[int], k: int) -> List[int]:
        n = len(nums)
        vals = [[i, nums[i]] for i in range(n)]   # 辅助数组
        # 按照数值降序排序
        vals.sort(key = lambda x: -x[1])
        # 取前 k 个并按照下标升序排序
        vals = sorted(vals[:k])
        res = [val for idx, val in vals]   # 目标子序列
        return res

###Java

class Solution {
    public int[] maxSubsequence(int[] nums, int k) {
        int n = nums.length;
        int[][] vals = new int[n][2]; // 二维数组存储索引和值
        for (int i = 0; i < n; ++i) {
            vals[i][0] = i;      // 存储索引
            vals[i][1] = nums[i]; // 存储值
        }
        // 按照数值降序排序
        Arrays.sort(vals, (x1, x2) -> Integer.compare(x2[1], x1[1]));
        // 取前 k 个并按照下标升序排序
        Arrays.sort(vals, 0, k, (x1, x2) -> Integer.compare(x1[0], x2[0]));
        int[] res = new int[k]; // 目标子序列
        for (int i = 0; i < k; ++i) {
            res[i] = vals[i][1];
        }
        return res;
    }
}

###C#

public class Solution {
    public int[] MaxSubsequence(int[] nums, int k) {
        int n = nums.Length;
        int[,] vals = new int[n, 2]; // 二维数组存储索引和值
        for (int i = 0; i < n; ++i) {
            vals[i, 0] = i;      // 存储索引
            vals[i, 1] = nums[i]; // 存储值
        }
        // 按照数值降序排序
        var sortedVals = Enumerable.Range(0, n)
            .Select(i => new { Index = vals[i, 0], Value = vals[i, 1] })
            .OrderByDescending(x => x.Value)
            .ToArray();
        // 取前 k 个并按照下标升序排序
        var topK = sortedVals.Take(k)
            .OrderBy(x => x.Index)
            .ToArray();
        int[] res = new int[k]; // 目标子序列
        for (int i = 0; i < k; ++i) {
            res[i] = topK[i].Value;
        }
        return res;
    }
}

###Go

func maxSubsequence(nums []int, k int) []int {
    n := len(nums)
vals := make([][]int, n) // 辅助数组
for i := 0; i < n; i++ {
vals[i] = []int{i, nums[i]}
}
// 按照数值降序排序
sort.Slice(vals, func(i, j int) bool {
return vals[i][1] > vals[j][1]
})
// 取前 k 个并按照下标升序排序
sort.Slice(vals[:k], func(i, j int) bool {
return vals[i][0] < vals[j][0]
})
res := make([]int, k) // 目标子序列
for i := 0; i < k; i++ {
res[i] = vals[i][1]
}
return res
}

###C

typedef struct {
    int index;
    int value;
} Pair;

int compareValueDesc(const void* a, const void* b) {
    return ((Pair*)b)->value - ((Pair*)a)->value;
}

int compareIndexAsc(const void* a, const void* b) {
    return ((Pair*)a)->index - ((Pair*)b)->index;
}

int* maxSubsequence(int* nums, int numsSize, int k, int* returnSize) {
    Pair* vals = (Pair*)malloc(numsSize * sizeof(Pair)); // 辅助数组
    for (int i = 0; i < numsSize; ++i) {
        vals[i].index = i;
        vals[i].value = nums[i];
    }
    // 按照数值降序排序
    qsort(vals, numsSize, sizeof(Pair), compareValueDesc);
    // 取前 k 个并按照下标升序排序
    qsort(vals, k, sizeof(Pair), compareIndexAsc);
    int* res = (int*)malloc(k * sizeof(int)); // 目标子序列
    for (int i = 0; i < k; ++i) {
        res[i] = vals[i].value;
    }
    *returnSize = k;
    free(vals);
    return res;
}

###JavaScript

var maxSubsequence = function(nums, k) {
    const n = nums.length;
    const vals = []; // 辅助数组
    for (let i = 0; i < n; ++i) {
        vals.push({ index: i, value: nums[i] }); // 存储索引和值
    }
    // 按照数值降序排序
    vals.sort((x1, x2) => x2.value - x1.value);
    // 取前 k 个并按照下标升序排序
    const topK = vals.slice(0, k); // 获取前 k 个元素
    topK.sort((x1, x2) => x1.index - x2.index); // 对前 k 个元素按索引升序排序
    const res = []; // 目标子序列
    for (let i = 0; i < k; ++i) {
        res.push(topK[i].value); // 将排序后的值加入结果
    }
    return res;
};

###TypeScript

function maxSubsequence(nums: number[], k: number): number[] {
    const n = nums.length;
    const vals: { index: number; value: number }[] = []; // 辅助数组
    for (let i = 0; i < n; ++i) {
        vals.push({ index: i, value: nums[i] }); // 存储索引和值
    }
    // 按照数值降序排序
    vals.sort((x1, x2) => x2.value - x1.value);
    // 取前 k 个并按照下标升序排序
    const topK = vals.slice(0, k); // 获取前 k 个元素
    topK.sort((x1, x2) => x1.index - x2.index); // 对前 k 个元素按索引升序排序
    const res: number[] = []; // 目标子序列
    for (let i = 0; i < k; ++i) {
        res.push(topK[i].value); // 将排序后的值加入结果
    }
    return res;
};

###Rust

impl Solution {
    pub fn max_subsequence(nums: Vec<i32>, k: i32) -> Vec<i32> {
        let n = nums.len();
        let mut vals: Vec<(usize, i32)> = Vec::new(); // 辅助数组
        for i in 0..n {
            vals.push((i, nums[i]));
        }
        // 按照数值降序排序
        vals.sort_by(|x1, x2| x2.1.cmp(&x1.1));
        // 取前 k 个并按照下标升序排序
        vals[0..k as usize].sort_by(|x1, x2| x1.0.cmp(&x2.0));
        let mut res: Vec<i32> = Vec::new(); // 目标子序列
        for i in 0..k as usize {
            res.push(vals[i].1);
        }
        res
    }
}

复杂度分析

  • 时间复杂度:$O(n \log n)$,其中 $n$ 为 $\textit{nums}$ 的长度。即为对辅助数组排序的时间复杂度。

  • 空间复杂度:$O(n)$,即为辅助数组的空间开销。

C语言,两次排序

作者 liu-xiang-3
2021年12月12日 09:26

思路:

  1. 先按照大小排序,再按照下标排序;
  2. 按顺序输出前k个最大的值;
/* 按大小从大到小排序 */
int cmpByValue(const void *a, const void *b) {
    return ((int*)b)[0] - ((int*)a)[0];
}

/* 按下标从小到大排序 */
int cmpByIdx(const void *a, const void *b) {
    return ((int*)a)[1] - ((int*)b)[1];
}

int* maxSubsequence(int* nums, int numsSize, int k, int* returnSize){
    int arr[numsSize][2];
    /* 存入值和下标 */
    for (int i = 0; i < numsSize; i++) {
        arr[i][0] = nums[i];
        arr[i][1] = i;
    }
    /* 先按照大小排序, 再对前k个按照下标排序 */
    qsort(arr, numsSize, sizeof(arr[0]), cmpByValue);
    qsort(arr, k, sizeof(arr[0]), cmpByIdx);

    /* 按顺序输出前k个最大的值 */
    int *ans = (int*)malloc(sizeof(int) * k);
    for (int i = 0; i < k; i++) {
        ans[i] = arr[i][0];
    }
    *returnSize = k;
    return ans;
}

简单题,简单做(Python/Java/C++/C/Go/JS/Rust)

作者 endlesscheng
2021年12月12日 00:12

问题相当于选 $\textit{nums}$ 中最大的 $k$ 个数,这样总和才能最大。

但子序列的顺序是不能变的,怎么办?

  1. 创建一个下标数组 $\textit{idx} = [0,1,2,\ldots,n-1]$,根据 $\textit{nums}[\textit{idx}[i]]$ 对 $\textit{idx}$ 排序,这样就知道最大的 $k$ 个数在哪里了。
  2. 排序前 $k$ 大元素的下标。
  3. 最后,获取这些下标对应的元素值,即为子序列。
class Solution:
    def maxSubsequence(self, nums: List[int], k: int) -> List[int]:
        idx = sorted(range(len(nums)), key=lambda i: nums[i])  # 创建下标数组,对下标数组排序
        idx = sorted(idx[-k:])  # 取前 k 大元素的下标,排序
        return [nums[i] for i in idx]  # 取出 nums 的子序列
class Solution {
    public int[] maxSubsequence(int[] nums, int k) {
        // 创建下标数组,对下标数组排序
        Integer[] idx = new Integer[nums.length];
        Arrays.setAll(idx, i -> i);
        Arrays.sort(idx, (i, j) -> nums[j] - nums[i]);

        // 对前 k 个下标排序
        // 注:排序 int[] 比排序 Integer[] 快 2ms
        int[] ans = new int[k];
        for (int i = 0; i < k; i++) {
            ans[i] = idx[i];
        }
        Arrays.sort(ans);

        // 取出 nums 的子序列
        for (int i = 0; i < k; i++) {
            ans[i] = nums[ans[i]];
        }
        return ans;
    }
}
class Solution {
public:
    vector<int> maxSubsequence(vector<int>& nums, int k) {
        // 创建下标数组,对下标数组排序
        vector<int> idx(nums.size());
        ranges::iota(idx, 0);
        ranges::sort(idx, {}, [&](int i) { return -nums[i]; });

        // 对前 k 个下标排序
        idx.resize(k);
        ranges::sort(idx);

        // 取出 nums 的子序列
        for (int& i : idx) {
            i = nums[i];
        }
        return idx;
    }
};
class Solution {
public:
    vector<int> maxSubsequence(vector<int>& nums, int k) {
        int n = nums.size();
        vector<int> idx(n);
        ranges::iota(idx, 0);
        ranges::nth_element(idx, idx.begin() + k, {}, [&](int i) { return -nums[i]; });

        vector<int8_t> in_seq(n);
        for (int i = 0; i < k; i++) {
            in_seq[idx[i]] = true; // 标记前 k 大元素的下标
        }

        idx.resize(k);
        int j = 0;
        for (int i = 0; i < n; i++) {
            if (in_seq[i]) {
                idx[j++] = nums[i];
            }
        }
        return idx;
    }
};
int* _nums;

int cmp_by_num(const void* a, const void* b) {
    return _nums[*(int*)b] - _nums[*(int*)a];
}

int cmp(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}

int* maxSubsequence(int* nums, int numsSize, int k, int* returnSize) {
    _nums = nums;

    // 创建下标数组,对下标数组排序
    int* idx = malloc(sizeof(int) * numsSize);
    for (int i = 0; i < numsSize; i++) {
        idx[i] = i;
    }
    qsort(idx, numsSize, sizeof(int), cmp_by_num);

    // 对前 k 个下标排序
    qsort(idx, k, sizeof(int), cmp);

    // 取出 nums 的子序列
    for (int i = 0; i < k; i++) {
        idx[i] = nums[idx[i]];
    }

    *returnSize = k;
    return idx;
}
func maxSubsequence(nums []int, k int) []int {
// 创建下标数组,对下标数组排序
idx := make([]int, len(nums))
for i := range idx {
idx[i] = i
}
slices.SortFunc(idx, func(i, j int) int { return nums[j] - nums[i] })

// 对前 k 个下标排序
idx = idx[:k]
slices.Sort(idx)

// 取出 nums 的子序列
for i, j := range idx {
idx[i] = nums[j]
}
return idx
}
var maxSubsequence = function(nums, k) {
    // 创建下标数组,对下标数组排序
    let idx = [...nums.keys()]
    idx.sort((i, j) => nums[j] - nums[i]);

    // 对前 k 个下标排序
    idx.length = k;
    idx.sort((a, b) => a - b);

    // 取出 nums 的子序列
    return idx.map(i => nums[i]);
};
impl Solution {
    pub fn max_subsequence(nums: Vec<i32>, k: i32) -> Vec<i32> {
        // 创建下标数组,对下标数组排序
        let mut idx = (0..nums.len()).collect::<Vec<_>>();
        idx.sort_unstable_by_key(|&i| -nums[i]);

        // 对前 k 个下标排序
        idx.truncate(k as usize);
        idx.sort_unstable();

        // 取出 nums 的子序列
        idx.into_iter().map(|i| nums[i]).collect()
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log n)$ 或 $\mathcal{O}(n)$,其中 $n$ 是 $\textit{nums}$ 的长度。使用快速选择可以做到 $\mathcal{O}(n)$,见 C++ 代码。
  • 空间复杂度:$\mathcal{O}(n)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

昨天以前LeetCode 每日一题题解

[Python3/Java/C++/Go/TypeScript] 一题一解:BFS(清晰题解)

作者 lcbin
2025年6月27日 07:28

方法一:BFS

我们可以先统计字符串中每个字符出现的次数,然后将出现次数大于等于 $k$ 的字符按从小到大的顺序存入一个列表 $\textit{cs}$ 中。接下来,我们可以使用 BFS 来枚举所有可能的子序列。

我们定义一个队列 $\textit{q}$,初始时将空字符串放入队列中。然后,我们从队列中取出一个字符串 $\textit{cur}$,并尝试将每个字符 $c \in \textit{cs}$ 添加到 $\textit{cur}$ 的末尾,形成一个新的字符串 $\textit{nxt}$。如果 $\textit{nxt}$ 是一个重复 $k$ 次的子序列,我们就将其加入到答案中,并将 $\textit{nxt}$ 放入队列中继续处理。

我们需要一个辅助函数 $\textit{check(t, k)}$ 来判断字符串 $\textit{t}$ 是否是字符串 $s$ 的一个重复 $k$ 次的子序列。具体地,我们可以使用两个指针来遍历字符串 $s$ 和 $\textit{t}$,如果在遍历过程中能够找到 $\textit{t}$ 的所有字符,并且能够重复 $k$ 次,那么就返回 $\textit{true}$,否则返回 $\textit{false}$。

###python

class Solution:
    def longestSubsequenceRepeatedK(self, s: str, k: int) -> str:
        def check(t: str, k: int) -> bool:
            i = 0
            for c in s:
                if c == t[i]:
                    i += 1
                    if i == len(t):
                        k -= 1
                        if k == 0:
                            return True
                        i = 0
            return False

        cnt = Counter(s)
        cs = [c for c in ascii_lowercase if cnt[c] >= k]
        q = deque([""])
        ans = ""
        while q:
            cur = q.popleft()
            for c in cs:
                nxt = cur + c
                if check(nxt, k):
                    ans = nxt
                    q.append(nxt)
        return ans

###java

class Solution {
    private char[] s;

    public String longestSubsequenceRepeatedK(String s, int k) {
        this.s = s.toCharArray();
        int[] cnt = new int[26];
        for (char c : this.s) {
            cnt[c - 'a']++;
        }

        List<Character> cs = new ArrayList<>();
        for (char c = 'a'; c <= 'z'; ++c) {
            if (cnt[c - 'a'] >= k) {
                cs.add(c);
            }
        }
        Deque<String> q = new ArrayDeque<>();
        q.offer("");
        String ans = "";
        while (!q.isEmpty()) {
            String cur = q.poll();
            for (char c : cs) {
                String nxt = cur + c;
                if (check(nxt, k)) {
                    ans = nxt;
                    q.offer(nxt);
                }
            }
        }
        return ans;
    }

    private boolean check(String t, int k) {
        int i = 0;
        for (char c : s) {
            if (c == t.charAt(i)) {
                i++;
                if (i == t.length()) {
                    if (--k == 0) {
                        return true;
                    }
                    i = 0;
                }
            }
        }
        return false;
    }
}

###cpp

class Solution {
public:
    string longestSubsequenceRepeatedK(string s, int k) {
        auto check = [&](const string& t, int k) -> bool {
            int i = 0;
            for (char c : s) {
                if (c == t[i]) {
                    i++;
                    if (i == t.size()) {
                        if (--k == 0) {
                            return true;
                        }
                        i = 0;
                    }
                }
            }
            return false;
        };
        int cnt[26] = {};
        for (char c : s) {
            cnt[c - 'a']++;
        }

        vector<char> cs;
        for (char c = 'a'; c <= 'z'; ++c) {
            if (cnt[c - 'a'] >= k) {
                cs.push_back(c);
            }
        }

        queue<string> q;
        q.push("");
        string ans;
        while (!q.empty()) {
            string cur = q.front();
            q.pop();
            for (char c : cs) {
                string nxt = cur + c;
                if (check(nxt, k)) {
                    ans = nxt;
                    q.push(nxt);
                }
            }
        }
        return ans;
    }
};

###go

func longestSubsequenceRepeatedK(s string, k int) string {
check := func(t string, k int) bool {
i := 0
for _, c := range s {
if byte(c) == t[i] {
i++
if i == len(t) {
k--
if k == 0 {
return true
}
i = 0
}
}
}
return false
}

cnt := [26]int{}
for i := 0; i < len(s); i++ {
cnt[s[i]-'a']++
}

cs := []byte{}
for c := byte('a'); c <= 'z'; c++ {
if cnt[c-'a'] >= k {
cs = append(cs, c)
}
}

q := []string{""}
ans := ""
for len(q) > 0 {
cur := q[0]
q = q[1:]
for _, c := range cs {
nxt := cur + string(c)
if check(nxt, k) {
ans = nxt
q = append(q, nxt)
}
}
}
return ans
}

###ts

function longestSubsequenceRepeatedK(s: string, k: number): string {
    const check = (t: string, k: number): boolean => {
        let i = 0;
        for (const c of s) {
            if (c === t[i]) {
                i++;
                if (i === t.length) {
                    k--;
                    if (k === 0) {
                        return true;
                    }
                    i = 0;
                }
            }
        }
        return false;
    };

    const cnt = new Array(26).fill(0);
    for (const c of s) {
        cnt[c.charCodeAt(0) - 97]++;
    }

    const cs: string[] = [];
    for (let i = 0; i < 26; ++i) {
        if (cnt[i] >= k) {
            cs.push(String.fromCharCode(97 + i));
        }
    }

    const q: string[] = [''];
    let ans = '';
    while (q.length > 0) {
        const cur = q.shift()!;
        for (const c of cs) {
            const nxt = cur + c;
            if (check(nxt, k)) {
                ans = nxt;
                q.push(nxt);
            }
        }
    }

    return ans;
}

有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~

每日一题-重复 K 次的最长子序列🔴

2025年6月27日 00:00

给你一个长度为 n 的字符串 s ,和一个整数 k 。请你找出字符串 s重复 k 次的 最长子序列

子序列 是由其他字符串删除某些(或不删除)字符派生而来的一个字符串。

如果 seq * ks 的一个子序列,其中 seq * k 表示一个由 seq 串联 k 次构造的字符串,那么就称 seq 是字符串 s 中一个 重复 k 的子序列。

  • 举个例子,"bba" 是字符串 "bababcba" 中的一个重复 2 次的子序列,因为字符串 "bbabba" 是由 "bba" 串联 2 次构造的,而 "bbabba" 是字符串 "bababcba" 的一个子序列。

返回字符串 s重复 k 次的最长子序列  。如果存在多个满足的子序列,则返回 字典序最大 的那个。如果不存在这样的子序列,返回一个 字符串。

 

示例 1:

example 1

输入:s = "letsleetcode", k = 2
输出:"let"
解释:存在两个最长子序列重复 2 次:let" 和 "ete" 。
"let" 是其中字典序最大的一个。

示例 2:

输入:s = "bb", k = 2
输出:"b"
解释:重复 2 次的最长子序列是 "b" 。

示例 3:

输入:s = "ab", k = 2
输出:""
解释:不存在重复 2 次的最长子序列。返回空字符串。

 

提示:

  • n == s.length
  • 2 <= k <= 2000
  • 2 <= n < k * 8
  • s 由小写英文字母组成

最简洁易懂的方法:利用字母频率

作者 fuchen2050
2021年9月19日 16:08

如果一个字母频率为freq,那么其可能参与组成的子串最多为freq//k个,因此我们只需要统计s中各个字母出现的频率,进行倒序排列便于后续能够直接筛选出首字母最大的子串,然后频率满足要求的字母组合起来成为新的串hot

接着我们求出hot全部子串的全排列,然后依次判断是否属于s,第一个满足要求的即为所求

class Solution:
    def longestSubsequenceRepeatedK(self, s: str, k: int) -> str:
        num = Counter(s)
        hot = ''.join(ele * (num[ele] // k) for ele in sorted(num, reverse=True))
        for i in range(len(hot), 0, -1):
            for item in permutations(hot, i):
                word = ''.join(item)
                ss = iter(s)
                if all(c in ss for c in word * k):
                    return word
        return ''

注意在判断是否属于s时,利用iter()函数生成迭代器是个非常巧妙的选择,比直接for循环判断要更加简洁高效

枚举排列 + 判断子序列(Python/Java/C++/Go)

作者 endlesscheng
2021年9月19日 12:26

分析

如果 $\textit{seq} * k$ 是 $s$ 的子序列,必须满足:

  • $\textit{seq}$ 中的每个字母在 $s$ 中至少出现 $k$ 次。

示例 1 的 $s=\texttt{letsleetcode}$,$k=2$,其中 $\texttt{s},\texttt{c},\texttt{o},\texttt{d}$ 这些只出现一次的字母,一定不在 $\textit{seq}$ 中(因为 $\textit{seq}$ 要重复 $k=2$ 次,$1$ 个字母无法重复 $2$ 次)。只有 $\texttt{l},\texttt{e},\texttt{t}$ 这些至少出现 $k$ 次的字母,才可能在 $\textit{seq}$ 中。

分析到这里,题目的这个奇怪的数据范围 $n < 8k$ 就有用了。

设有 $x$ 个字母至少出现 $k$ 次,那么

$$
kx\le n
$$

$$
x \le \dfrac{n}{k} < 8
$$

所以至多有 $7$ 个字母至少出现 $k$ 次。

枚举排列

枚举从 $7$ 个字母中选出 $i=1,2,3,4,5,6,7$ 个字母的排列,枚举次数为

$$
A_7^1 + A_7^2 + A_7^3 + A_7^4 + A_7^5 + A_7^6 + A_7^7 = 13699
$$

这不多,在时间范围内可以接受。

在示例 1 中,至少出现 $k=2$ 次的字母有 $\texttt{l},\texttt{e},\texttt{t}$。其中字母 $\texttt{e}$ 出现了 $4$ 次,这意味着 $\textit{seq}$ 中可以有 $\left\lfloor\dfrac{4}{k}\right\rfloor=2$ 个 $\texttt{e}$。所以我们枚举的是 $a=[\texttt{l},\texttt{e},\texttt{e},\texttt{t}]$ 的 47. 全排列 II我的题解。注意排列中有重复元素。

判断子序列

设当前枚举的排列为 $\textit{seq}$,我们需要判断 $\textit{seq} * k$ 是否为 $s$ 的子序列。

做法见 392. 判断子序列我的题解

优化前

class Solution:
    # 392. 判断子序列
    # 返回 seq 是否为 s 的子序列
    def isSubsequence(self, seq: str, s: str) -> bool:
        it = iter(s)
        return all(c in it for c in seq)  # in 会消耗迭代器

    def longestSubsequenceRepeatedK(self, s: str, k: int) -> str:
        cnt = Counter(s)
        a = [ch for ch, freq in cnt.items() for _ in range(freq // k)]
        a.sort(reverse=True)  # 排序后,下面会按照字典序从大到小枚举排列

        for i in range(len(a), 0, -1):  # 长的优先
            for perm in permutations(a, i):  # 枚举 a 的长为 i 的排列
                seq = ''.join(perm)
                if self.isSubsequence(seq * k, s):  # seq*k 是 s 的子序列
                    return seq
        return ''
class Solution {
    private char[] ans;
    private int ansLen = 0;

    public String longestSubsequenceRepeatedK(String S, int k) {
        char[] s = S.toCharArray();

        int[] cnt = new int[26];
        for (char c : s) {
            cnt[c - 'a']++;
        }

        StringBuilder tmp = new StringBuilder();
        // 倒序,这样我们可以优先枚举字典序大的排列
        for (int i = 25; i >= 0; i--) {
            String c = String.valueOf((char) ('a' + i));
            tmp.append(c.repeat(cnt[i] / k));
        }
        char[] a = tmp.toString().toCharArray();

        ans = new char[a.length];
        permute(a, k, s);

        return new String(ans, 0, ansLen);
    }

    // 47. 全排列 II
    // 枚举从 nums 中选任意个数的所有排列,处理枚举的排列
    private void permute(char[] nums, int k, char[] s) {
        int n = nums.length;
        char[] path = new char[n];
        boolean[] onPath = new boolean[n]; // onPath[j] 表示 nums[j] 是否已经填入排列
        dfs(0, nums, path, onPath, k, s);
    }

    private void dfs(int i, char[] nums, char[] path, boolean[] onPath, int k, char[] s) {
        // 处理当前排列 path
        process(path, i, k, s);

        if (i == nums.length) {
            return;
        }

        // 枚举 nums[j] 填入 path[pathLen]
        for (int j = 0; j < nums.length; j++) {
            // 如果 nums[j] 已填入排列,continue
            // 如果 nums[j] 和前一个数 nums[j-1] 相等,且 nums[j-1] 没填入排列,continue
            if (onPath[j] || j > 0 && nums[j] == nums[j - 1] && !onPath[j - 1]) {
                continue;
            }
            path[i] = nums[j]; // 填入排列
            onPath[j] = true; // nums[j] 已填入排列(注意标记的是下标,不是值)
            dfs(i + 1, nums, path, onPath, k, s); // 填排列的下一个数
            onPath[j] = false; // 恢复现场
            // 注意 path 无需恢复现场,直接覆盖 path[i] 就行
        }
    }

    private void process(char[] seq, int seqLen, int k, char[] s) {
        // 先比大小(时间复杂度低),再判断是否为子序列(时间复杂度高)
        if (seqLen > ansLen || seqLen == ansLen && compare(seq, ans, ansLen) > 0) {
            if (isSubsequence(seq, seqLen, k, s)) {
                System.arraycopy(seq, 0, ans, 0, seqLen);
                ansLen = seqLen;
            }
        }
    }

    // 比较 a 和 b 的字典序大小
    private int compare(char[] a, char[] b, int n) {
        for (int i = 0; i < n; i++) {
            if (a[i] != b[i]) {
                return a[i] - b[i];
            }
        }
        return 0;
    }

    // 392. 判断子序列
    // 返回 seq*k 是否为 s 的子序列
    private boolean isSubsequence(char[] seq, int n, int k, char[] s) {
        int i = 0;
        for (char c : s) {
            if (seq[i % n] == c) {
                i++;
                if (i == n * k) { // seq*k 的所有字符匹配完毕
                    return true; // seq*k 是 s 的子序列
                }
            }
        }
        return false;
    }
}
class Solution {
    // 47. 全排列 II
    // 枚举从 nums 中选任意个数的所有排列,用 f 处理枚举的排列
    void permuteFunc(const string& nums, auto&& f) {
        int n = nums.size();
        string path;
        vector<int8_t> on_path(n); // on_path[j] 表示 nums[j] 是否已经填入排列
        auto dfs = [&](this auto&& dfs) -> void {
            f(path);
            if (path.size() == n) {
                return;
            }
            // 枚举 nums[j] 填入 path[i]
            for (int j = 0; j < n; j++) {
                // 如果 nums[j] 已填入排列,continue
                // 如果 nums[j] 和前一个数 nums[j-1] 相等,且 nums[j-1] 没填入排列,continue
                if (on_path[j] || j > 0 && nums[j] == nums[j - 1] && !on_path[j - 1]) {
                    continue;
                }
                path += nums[j]; // 填入排列
                on_path[j] = true; // nums[j] 已填入排列(注意标记的是下标,不是值)
                dfs(); // 填排列的下一个数
                on_path[j] = false; // 恢复现场
                path.pop_back(); // 恢复现场
            }
        };
        dfs();
    };

    // 392. 判断子序列
    // 返回 seq*k 是否为 s 的子序列
    bool isSubsequence(const string& seq, int k, const string& s) {
        int n = seq.size();
        int i = 0;
        for (char c : s) {
            if (seq[i % n] == c) {
                i++;
                if (i == n * k) { // seq*k 的所有字符匹配完毕
                    return true; // seq*k 是 s 的子序列
                }
            }
        }
        return false;
    }

public:
    string longestSubsequenceRepeatedK(string s, int k) {
        int cnt[26]{};
        for (char c : s) {
            cnt[c - 'a']++;
        }

        string a;
        for (int i = 25; i >= 0; i--) { // 倒序,这样我们可以优先枚举字典序大的排列
            a.insert(a.end(), cnt[i] / k, 'a' + i);
        }

        string ans;
        permuteFunc(a, [&](const string& seq) {
            // 先比大小(时间复杂度低),再判断是否为子序列(时间复杂度高)
            if (seq.size() > ans.size() || seq.size() == ans.size() && seq > ans) {
                if (isSubsequence(seq, k, s)) {
                    ans = seq;
                }
            }
        });
        return ans;
    }
};
// 47. 全排列 II
// 枚举从 nums 中选任意个数的所有排列,用 f 处理枚举的排列
func permuteFunc[T comparable](nums []T, f func([]T)) {
    n := len(nums)
    path := []T{}
    onPath := make([]bool, n) // onPath[j] 表示 nums[j] 是否已经填入排列
    var dfs func()
    dfs = func() {
        f(path)
        if len(path) == n {
            return
        }
        // 枚举 nums[j] 填入 path[i]
        for j, on := range onPath {
            // 如果 nums[j] 已填入排列,continue
            // 如果 nums[j] 和前一个数 nums[j-1] 相等,且 nums[j-1] 没填入排列,continue
            if on || j > 0 && nums[j] == nums[j-1] && !onPath[j-1] {
                continue
            }
            path = append(path, nums[j])
            onPath[j] = true // nums[j] 已填入排列(注意标记的是下标,不是值)
            dfs() // 填排列的下一个数
            onPath[j] = false // 恢复现场
            path = path[:len(path)-1] // 恢复现场
        }
    }
    dfs()
}

// 392. 判断子序列
// 返回 seq*k 是否为 s 的子序列
func isSubsequence(seq []byte, k int, s string) bool {
    n := len(seq)
    i := 0
    for _, c := range s {
        if seq[i%n] == byte(c) {
            i++
            if i == n*k { // seq*k 的所有字符匹配完毕
                return true // seq*k 是 s 的子序列
            }
        }
    }
    return false
}

func longestSubsequenceRepeatedK(s string, k int) string {
    cnt := [26]int{}
    for _, c := range s {
        cnt[c-'a']++
    }
    a := []byte{}
    for i := 25; i >= 0; i-- { // 倒序,这样我们可以优先枚举字典序大的排列
        bs := []byte{'a' + byte(i)}
        a = append(a, bytes.Repeat(bs, cnt[i]/k)...)
    }

    ans := []byte{}
    permuteFunc(a, func(seq []byte) {
        // 先比大小(时间复杂度低),再判断是否为子序列(时间复杂度高)
        if len(seq) > len(ans) || len(seq) == len(ans) && bytes.Compare(seq, ans) > 0 {
            if isSubsequence(seq, k, s) {
                ans = slices.Clone(seq)
            }
        }
    })
    return string(ans)
}

优化:子序列自动机

用 392 题的进阶做法(子序列自动机),原理见 我的题解

class Solution:
    def longestSubsequenceRepeatedK(self, s: str, k: int) -> str:
        s = [ord(c) - ord('a') for c in s]  # 转成 0 到 25,这样下面无需频繁调用 ord

        # 392. 判断子序列(进阶做法)
        n = len(s)
        nxt = [[n] * 26 for _ in range(n + 1)]
        for i in range(n - 1, -1, -1):
            nxt[i][:] = nxt[i + 1]
            nxt[i][s[i]] = i

        def isSubsequence(seq: Tuple[int, ...]) -> bool:
            i = -1
            for _ in range(k):
                for c in seq:
                    i = nxt[i + 1][c]
                    if i == n:  # c 不在 s 中,说明 seq*k 不是 s 的子序列
                        return False
            return True  # seq*k 是 s 的子序列

        cnt = Counter(s)
        a = [ch for ch, freq in cnt.items() for _ in range(freq // k)]
        a.sort(reverse=True)  # 排序后,下面会按照字典序从大到小枚举排列

        for i in range(len(a), 0, -1):  # 长的优先
            for seq in permutations(a, i):  # 枚举 a 的长为 i 的排列
                if isSubsequence(seq):  # seq*k 是 s 的子序列
                    return ''.join(ascii_lowercase[c] for c in seq)
        return ''
class Solution {
    private char[] ans;
    private int ansLen = 0;

    public String longestSubsequenceRepeatedK(String S, int k) {
        char[] s = S.toCharArray();

        // 392. 判断子序列(进阶做法)
        int n = s.length;
        int[] cnt = new int[26];
        int[][] nxt = new int[n + 1][];
        nxt[n] = new int[26];
        Arrays.fill(nxt[n], n);
        for (int i = n - 1; i >= 0; i--) {
            int c = s[i] - 'a';
            nxt[i] = nxt[i + 1].clone();
            nxt[i][c] = i;
            cnt[c]++;
        }

        StringBuilder tmp = new StringBuilder();
        // 倒序,这样我们可以优先枚举字典序大的排列
        for (int i = 25; i >= 0; i--) {
            String c = String.valueOf((char) ('a' + i));
            tmp.append(c.repeat(cnt[i] / k));
        }
        char[] a = tmp.toString().toCharArray();

        ans = new char[a.length];
        permute(a, k, nxt);

        return new String(ans, 0, ansLen);
    }

    // 47. 全排列 II
    // 枚举从 nums 中选任意个数的所有排列,处理枚举的排列
    private void permute(char[] nums, int k, int[][] nxt) {
        int n = nums.length;
        char[] path = new char[n];
        boolean[] onPath = new boolean[n]; // onPath[j] 表示 nums[j] 是否已经填入排列
        dfs(0, nums, path, onPath, k, nxt);
    }

    private void dfs(int i, char[] nums, char[] path, boolean[] onPath, int k, int[][] nxt) {
        // 处理当前排列 path
        process(path, i, k, nxt);

        if (i == nums.length) {
            return;
        }

        // 枚举 nums[j] 填入 path[pathLen]
        for (int j = 0; j < nums.length; j++) {
            // 如果 nums[j] 已填入排列,continue
            // 如果 nums[j] 和前一个数 nums[j-1] 相等,且 nums[j-1] 没填入排列,continue
            if (onPath[j] || j > 0 && nums[j] == nums[j - 1] && !onPath[j - 1]) {
                continue;
            }
            path[i] = nums[j]; // 填入排列
            onPath[j] = true; // nums[j] 已填入排列(注意标记的是下标,不是值)
            dfs(i + 1, nums, path, onPath, k, nxt); // 填排列的下一个数
            onPath[j] = false; // 恢复现场
            // 注意 path 无需恢复现场,直接覆盖 path[i] 就行
        }
    }

    private void process(char[] seq, int seqLen, int k, int[][] nxt) {
        // 先比大小(时间复杂度低),再判断是否为子序列(时间复杂度高)
        if (seqLen > ansLen || seqLen == ansLen && compare(seq, ans, ansLen) > 0) {
            if (isSubsequence(seq, seqLen, k, nxt)) {
                System.arraycopy(seq, 0, ans, 0, seqLen);
                ansLen = seqLen;
            }
        }
    }

    // 比较 a 和 b 的字典序大小
    private int compare(char[] a, char[] b, int n) {
        for (int i = 0; i < n; i++) {
            if (a[i] != b[i]) {
                return a[i] - b[i];
            }
        }
        return 0;
    }

    // 392. 判断子序列
    // 返回 seq*k 是否为 s 的子序列
    private boolean isSubsequence(char[] seq, int n, int k, int[][] nxt) {
        int i = -1;
        while (k-- > 0) {
            for (int j = 0; j < n; j++) {
                char c = seq[j];
                i = nxt[i + 1][c - 'a'];
                if (i + 1 == nxt.length) { // c 不在 s 中,说明 seq*k 不是 s 的子序列
                    return false;
                }
            }
        }
        return true;
    }
}
class Solution {
    // 47. 全排列 II
    // 枚举从 nums 中选任意个数的所有排列,用 f 处理枚举的排列
    void permuteFunc(const string& nums, auto&& f) {
        int n = nums.size();
        string path;
        vector<int8_t> on_path(n); // on_path[j] 表示 nums[j] 是否已经填入排列
        auto dfs = [&](this auto&& dfs) -> void {
            f(path);
            if (path.size() == n) {
                return;
            }
            // 枚举 nums[j] 填入 path[i]
            for (int j = 0; j < n; j++) {
                // 如果 nums[j] 已填入排列,continue
                // 如果 nums[j] 和前一个数 nums[j-1] 相等,且 nums[j-1] 没填入排列,continue
                if (on_path[j] || j > 0 && nums[j] == nums[j - 1] && !on_path[j - 1]) {
                    continue;
                }
                path += nums[j]; // 填入排列
                on_path[j] = true; // nums[j] 已填入排列(注意标记的是下标,不是值)
                dfs(); // 填排列的下一个数
                on_path[j] = false; // 恢复现场
                path.pop_back(); // 恢复现场
            }
        };
        dfs();
    };

public:
    string longestSubsequenceRepeatedK(string s, int k) {
        // 392. 判断子序列(进阶做法)
        int n = s.size();
        vector<array<int, 26>> nxt(n + 1);
        ranges::fill(nxt[n], n);
        for (int i = n - 1; i >= 0; i--) {
            nxt[i] = nxt[i + 1];
            nxt[i][s[i] - 'a'] = i;
        }

        auto isSubsequence = [&](const string& seq, int k) -> bool {
            int i = -1;
            while (k--) {
                for (char c : seq) {
                    i = nxt[i + 1][c - 'a'];
                    if (i == n) { // c 不在 s 中,说明 seq*k 不是 s 的子序列
                        return false;
                    }
                }
            }
            return true;
        };

        int cnt[26]{};
        for (char c : s) {
            cnt[c - 'a']++;
        }

        string a;
        for (int i = 25; i >= 0; i--) { // 倒序,这样我们可以优先枚举字典序大的排列
            a.insert(a.end(), cnt[i] / k, 'a' + i);
        }

        string ans;
        permuteFunc(a, [&](const string& seq) {
            // 先比大小(时间复杂度低),再判断是否为子序列(时间复杂度高)
            if (seq.size() > ans.size() || seq.size() == ans.size() && seq > ans) {
                if (isSubsequence(seq, k)) {
                    ans = seq;
                }
            }
        });
        return ans;
    }
};
// 47. 全排列 II
// 枚举从 nums 中选任意个数的所有排列,用 f 处理枚举的排列
func permuteFunc[T comparable](nums []T, f func([]T)) {
    n := len(nums)
    path := []T{}
    onPath := make([]bool, n) // onPath[j] 表示 nums[j] 是否已经填入排列
    var dfs func()
    dfs = func() {
        f(path)
        if len(path) == n {
            return
        }
        // 枚举 nums[j] 填入 path[i]
        for j, on := range onPath {
            // 如果 nums[j] 已填入排列,continue
            // 如果 nums[j] 和前一个数 nums[j-1] 相等,且 nums[j-1] 没填入排列,continue
            if on || j > 0 && nums[j] == nums[j-1] && !onPath[j-1] {
                continue
            }
            path = append(path, nums[j])
            onPath[j] = true  // nums[j] 已填入排列(注意标记的是下标,不是值)
            dfs()             // 填排列的下一个数
            onPath[j] = false // 恢复现场
            path = path[:len(path)-1]
        }
    }
    dfs()
}

func longestSubsequenceRepeatedK(s string, k int) string {
    // 392. 判断子序列(进阶做法)
    n := len(s)
    nxt := make([][26]int, n+1)
    for j := range nxt[n] {
        nxt[n][j] = n
    }
    for i := n - 1; i >= 0; i-- {
        nxt[i] = nxt[i+1]
        nxt[i][s[i]-'a'] = i
    }
    isSubsequence := func(seq []byte) bool {
        i := -1
        for range k {
            for _, c := range seq {
                i = nxt[i+1][c-'a']
                if i == n { // c 不在 s 中,说明 seq*k 不是 s 的子序列
                    return false
                }
            }
        }
        return true
    }

    cnt := [26]int{}
    for _, c := range s {
        cnt[c-'a']++
    }
    a := []byte{}
    for i := 25; i >= 0; i-- { // 倒序,这样我们可以优先枚举字典序大的排列
        bs := []byte{'a' + byte(i)}
        a = append(a, bytes.Repeat(bs, cnt[i]/k)...)
    }

    ans := []byte{}
    permuteFunc(a, func(seq []byte) {
        // 先比大小(时间复杂度低),再判断是否为子序列(时间复杂度高)
        if len(seq) > len(ans) || len(seq) == len(ans) && bytes.Compare(seq, ans) > 0 {
            if isSubsequence(seq) {
                ans = slices.Clone(seq)
            }
        }
    })
    return string(ans)
}

复杂度分析

  • 时间复杂度:$\mathcal{O}((n/k)!\cdot n)$,其中 $n$ 是 $s$ 的长度。注意 $\sum_{i=0}^m A_m^i$ 就是全排列搜索树的节点个数,我在 排列型回溯【基础算法精讲 16】中精确地算出了全排列搜索树的节点个数为 $\left\lfloor e\cdot m!\right\rfloor$,其中 $e=2.718\cdots$ 为自然常数。比如 $m=7$ 时,$\sum_{i=0}^7 A_7^i = 13700 = \left\lfloor e\cdot 7!\right\rfloor$。当 $m=n/k$ 时,遍历这棵搜索树需要 $\mathcal{O}((n/k)!)$ 的时间,每个节点判断子序列需要 $\mathcal{O}(n)$ 的时间。
  • 空间复杂度:$\mathcal{O}(n|\Sigma|)$。其中 $|\Sigma|=26$ 是字符集合的大小。

相似题目

  1. 回溯题单的「§4.5 排列型回溯」。
  2. 双指针题单的「§4.2 判断子序列」。
  3. 字符串题单的「九、子序列自动机」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

一个猎奇一点的随机算法

作者 hqztrue
2021年9月19日 12:21

令参数 $\ell=\frac{n}{k}$,注意到题中的 $\ell$ 非常小,可以视为常数。不过根据个人习惯,下文分析复杂度的时候还是会将 $\ell$ 作为参数代入进行计算。
$O(\ell!\cdot n)$ 的搜索我猜肯定有人会写题解,用到的性质是最多只有 $\ell$ 个出现频率超过 $\frac{1}{\ell}$ 的候选字符,这里就不赘述了。下面介绍一个猎奇一点的随机算法:
考虑最优解中每个重复的子序列 $seq$ 的出现区间,平均来说长度不超过 $\ell$。根据 Markov's inequality,至少有 $\frac{k}{\ell}$ 个区间的长度不超过 $\ell+1$。那么如果我们从原数组中 $n-\ell$ 个可能的长度为 $\ell+1$ 的子区间中随机挑选一个,它完整包含子序列 $seq$ 的概率至少为 $\dfrac{k/\ell}{n-\ell}\geq \dfrac{1}{\ell^2}$。如果我们猜对了区间,就只需枚举该区间中所有 $2^{\ell+1}$ 个子序列进行贪心验证即可,每次验证需要 $O(n)$ 的时间。重复随机 $O(\ell^2\log \frac{1}{\epsilon})$ 次即可使错误率足够小。
这个算法的复杂度是 $O(2^\ell\cdot \mathrm{poly}(\ell)\cdot n\cdot \log \frac{1}{\epsilon})$,在 $\ell$ 较大时是比搜索的复杂度渐近更优的。(注意到根据斯特林公式有 $l!=O^*\left((\dfrac{\ell}{e})^\ell\right)$.)


以下代码没怎么进行剪枝优化,所以比较慢。比赛的时候随便狂调了几次重复次数就过了。仔细优化一下的话应该是稳过的。
无标题.png

const int L=8;
int n; char *a;
class Solution {
public:
bool test(string t,int k){
int m=t.size(),cnt=0,j=0;
char *b=&t[0];
for (int i=0;i<n;++i)
if (a[i]==b[j]){
if (j==m-1)j=0,++cnt;
else ++j;
}
return cnt>=k;
}
string longestSubsequenceRepeatedK(string s, int k) {
a=&s[0]; string ans;
n=s.size(); int l=min(n,L);
for (int T=1;T<=20;++T){
int p=rand()%(n-l+1);
for (int i=1;i<(1<<l);++i)
if (__builtin_popcount(i)>=ans.size()){
string b;
for (int j=0;j<l;++j)
if (i&(1<<j))b+=a[p+j];
if ((b.size()>ans.size()||b>ans)&&test(b,k))ans=b;
}
}
return ans;
}
};

用这个性质进行 dfs 剪枝,再调一下参,可以刷到第一。
2014.png{:width=400}

const int N=2005;
int c[255],_next[N][26],(*nxt)[26]=&_next[2],n,m,k;
char b[N];
string a,ans;
inline bool check(const string &t){
int m=t.size(),cur;
if (!m)return 1;
if (k>40){
bool flag=0;
for (int i=0;i<10;++i){
int p=rand()%n; cur=p-1;
for (auto &ch:t)cur=nxt[cur][ch-'a'];
if (cur>=0&&cur-p<=7){flag=1; break;}
}
if (!flag)return 0;
}
cur=-1;
for (int i=0;i<k;++i){
for (auto &ch:t)cur=nxt[cur][ch-'a'];
if (cur<0)return 0;
}
return 1;
}
void dfs(string &t){
if (!check(t))return;
if (t.size()>ans.size()||t.size()==ans.size()&&t>ans)ans=t;
for (int i=0;i<m;++i)if (!i||b[i]!=b[i-1]){
char ch=b[i]; --m;
for (int j=i;j<m;++j)b[j]=b[j+1];
t.push_back(ch);
dfs(t);
t.pop_back();
for (int j=m;j>i;--j)b[j]=b[j-1];
++m; b[i]=ch;
}
}
class Solution {
public:
string longestSubsequenceRepeatedK(string s, int k) {
m=0; ::k=k; a=ans="";
for (int i='a';i<='z';++i)c[i]=0;
for (auto &ch:s)++c[ch];
for (auto &ch:s)if (c[ch]>=k)a.push_back(ch);
n=a.size();
for (int i=0;i<26;++i)nxt[-2][i]=nxt[n-1][i]=-2;
for (int i=n-2;i>=-1;--i){
memcpy(nxt[i],nxt[i+1],sizeof(int)*26);
nxt[i][a[i+1]-'a']=i+1;
}
for (int i='a';i<='z';++i)
for (int j=0;j<c[i]/k;++j)b[m++]=i;
string _s; dfs(_s);
return ans;
}
};

每日一题-小于等于 K 的最长二进制子序列🟡

2025年6月26日 00:00

给你一个二进制字符串 s 和一个正整数 k 。

请你返回 s 的 最长 子序列的长度,且该子序列对应的 二进制 数字小于等于 k 。

注意:

  • 子序列可以有 前导 0 。
  • 空字符串视为 0 。
  • 子序列 是指从一个字符串中删除零个或者多个字符后,不改变顺序得到的剩余字符序列。

 

示例 1:

输入:s = "1001010", k = 5
输出:5
解释:s 中小于等于 5 的最长子序列是 "00010" ,对应的十进制数字是 2 。
注意 "00100" 和 "00101" 也是可行的最长子序列,十进制分别对应 4 和 5 。
最长子序列的长度为 5 ,所以返回 5 。

示例 2:

输入:s = "00101001", k = 1
输出:6
解释:"000001" 是 s 中小于等于 1 的最长子序列,对应的十进制数字是 1 。
最长子序列的长度为 6 ,所以返回 6 。

 

提示:

  • 1 <= s.length <= 1000
  • s[i] 要么是 '0' ,要么是 '1'
  • 1 <= k <= 109

贪心思想解决加强版

作者 nopenope
2022年6月19日 12:09

本题比较有用的是字符串长度扩展到 $10^6$ 时的做法。您不妨先思考一下。


好,下面是提示。

如果您会了或者想直接看答案,可以跳过。保证不看提示也能看懂正解。


提示一

$0$ 和 $1$ 中,谁才对数的大小造成贡献?

提示二

$0$ 比起 $1$ 来,有什么优越性?


提示三

假如没有 $1$,您会做吗?

提示四

假如只有一个 $1$ 呢?

假如只有两个 $1$ 呢?

假如……!


我们一一解答以上问题。

解释一

$1$。

解释二

$0$ 可以以前导 $0$ 的形式出现在数的前面,增加数的长度,同时保证数的大小不变。

解释三

全是前导〇嘛,全选上不就得了!

解释四

一个 $1$,那就讨论它选不选。

两个 $1$,讨论这两个 $1$ 分别选不选。

更多 $1$,$2^{length}$ 讨论选不选……?

显然我们需要更快的讨论方式。


正解

这里直接抛出结论:先选择所有的 $0$,再从低位到高位地贪心选择所有的 $1$,直到所构成的数超出 $k$。此时字符串长度即为答案。

例如,字符串 $s$ 为 $11001010$,$k$ 为 $16$。

  1. 先选择所有的 $0$,得 $(0000)_2=0\le k$。
  2. 选择最低位的未选择 $1$,得 $(00010)_2=2\le k$。
  3. 继续选择最低位的未选择 $1$,得 $(001010)_2=10\le k$。
  4. 继续选择最低位的未选择 $1$,得 $(1001010)_2=74> k$,结束并退出。

下面证明这样选择为什么是对的。考虑扰动法。

情况一:用一个未选择的 $1$ 替换选择的 $1$。

因为我们是从低到高地选择 $1$,所以未选择的一定是高位。低位 $1$ 一定比高位 $1$ 更优(因为对串长贡献相同,但是却会使得数更小),故不可能。

情况二:用一个未选择的 $1$ 替换选择的 $0$。

同上且更不可能。

综上是正确的贪心策略。代码就很简单了。

时间复杂度 $O(n)$。

点个赞吧。

UPD:一个理解上的问题。
有一种直观感受是,如果我删去一个低位的 $0$,相当于所有高位整体向右移动一位,对 k 的贡献是不确定的?
其实不然。别忘了我们是在使得字符串长度最长。考虑字符串长度相同的情况,就会发现删去最后一个必然导致的是前面补上一个 $0$ 或 $1$。整体右移一位其实是补上了一个前导 $0$,但是我们已经选择了所有的 $0$,故这个情况是不存在的。那么删去一个 $0$ 必然会导致补上一个 $1$,这个显然不优。

C++ Sol:

###cpp

class Solution {
public:
int longestSubsequence(string s, int k) {
int res=0;
for(char y:s) if(y=='0') ++res;
long long w=1,ans=0;
for(int i=s.length()-1; ~i; i--,w<<=1) {
if(s[i]=='1') ans+=w;
if(ans>k) return res;
if(s[i]=='1') ++res;
if(w>2e9) return res;
}
return res;
}
};

Java Sol:见评论区,感谢 @零点再睡觉。

脑筋急转弯(Python/Java/C++/Go/JS/Rust)

作者 endlesscheng
2022年6月19日 12:08

设 $k$ 的二进制长度为 $m$。比如 $k=4=100_{(2)}$,二进制长度 $m=3$。

核心思路

  1. 任意长为 $m-1$ 的子序列,数值一定小于 $k$。比如 $00$,$01$,$10$,$11$ 都小于 $100$。
  2. 前导零不影响数值。子序列越靠右,我们能添加的前导零就越多,子序列就越长。贪心地,选 $s$ 的后缀作为子序列。(为什么?理由见下面的问答)
  3. 如果 $s$ 的长为 $m$ 的后缀 $\le k$,那么就选长为 $m$ 的后缀作为子序列;否则选长为 $m-1$ 的后缀(一定小于 $k$)作为子序列。在此基础上,加上长为 $n-m$ 的前缀中的 $0$ 的个数,即为答案。

在示例 1 中,$s=1001010$,$k=101_{(2)}$,$s$ 的长为 $m=3$ 的后缀 $010\le k$。如果在 $010$ 的前面添加 $1$,就超过 $k$ 了。此外,由于我们选的是 $s$ 的后缀,可以往 $010$ 前面添加尽量多的前导零,从而增加子序列的长度,但不会增大子序列的数值。最终得到 $00 + 010 = 00010$,长为 $5$。

又比如 $s=1001110$,$k=101_{(2)}$,长为 $m$ 的后缀 $110>k$,但长为 $m-1$ 的后缀 $10$ 一定小于 $k$。在这个后缀的前面添加两个前导零,最终得到 $00 + 10 = 0010$,长为 $4$。

:在上例中,虽然 $110>k$,但我可以选其他长为 $m$ 的子序列呀?比如 $100$,$101$,长为 $m$ 且 $\le k$。为什么不考虑这样的子序列呢?

:这是因为,要想得到比后缀 $110$ 更小的长为 $m$ 的子序列,比如 $100$,$101$,这样的子序列必然包含「本应添加到后缀 $10$ 前的前导零」,这些长为 $m$ 的子序列,相比长为 $m-1$ 的后缀,长度仅仅增加了 $1$,但我们只需要在长为 $m-1$ 的后缀的基础上,再添加一个前导零,也能得到长为 $m$ 的子序列 $010$,且这个子序列的第一个字符是「尽量靠右」的,可以添加更多的前导零。所以这些长为 $m$ 的子序列,并不会比「一个 $0$ + 长为 $m-1$ 的后缀」这种构造方式更优。

class Solution:
    def longestSubsequence(self, s: str, k: int) -> int:
        n, m = len(s), k.bit_length()
        if n < m:  # int(s, 2) < k
            return n  # 全选
        ans = m if int(s[-m:], 2) <= k else m - 1  # 后缀长度
        return ans + s[:-m].count('0')  # 添加前导零
class Solution {
    public int longestSubsequence(String s, int k) {
        int n = s.length();
        int m = 32 - Integer.numberOfLeadingZeros(k); // k 的二进制长度
        if (n < m) {
            return n; // 全选
        }

        int sufVal = Integer.parseInt(s.substring(n - m), 2);
        int ans = sufVal <= k ? m : m - 1; // 后缀长度

        for (int i = 0; i < n - m; i++) {
            ans += '1' - s.charAt(i); // 添加前导零
        }
        return ans;
    }
}
class Solution {
public:
    int longestSubsequence(string s, int k) {
        int n = s.length();
        int m = bit_width((uint32_t) k);
        if (n < m) {
            return n; // 全选
        }
        int suf_val = stoi(s.substr(n - m), nullptr, 2);
        int ans = suf_val <= k ? m : m - 1; // 后缀长度
        return ans + count(s.begin(), s.end() - m, '0'); // 添加前导零
    }
};
int longestSubsequence(char* s, int k) {
    int n = strlen(s);
    int m = 32 - __builtin_clz(k); // k 的二进制长度
    if (n < m) {
        return n; // 全选
    }

    int suf_val = strtol(s + n - m, NULL, 2);
    int ans = suf_val <= k ? m : m - 1; // 后缀长度

    for (int i = 0; i < n - m; i++) {
        ans += '1' - s[i]; // 添加前导零
    }
    return ans;
}
func longestSubsequence(s string, k int) int {
n, m := len(s), bits.Len(uint(k))
if n < m {
return n // 全选
}
ans := m // 后缀长度
sufVal, _ := strconv.ParseInt(s[n-m:], 2, 0)
if int(sufVal) > k {
ans--
}
return ans + strings.Count(s[:n-m], "0") // 添加前导零
}
var longestSubsequence = function(s, k) {
    const n = s.length;
    const m = 32 - Math.clz32(k); // k 的二进制长度
    if (n < m) {
        return n; // 全选
    }

    const sufVal = parseInt(s.slice(n - m), 2);
    let ans = sufVal <= k ? m : m - 1; // 后缀长度

    for (let i = 0; i < n - m; i++) {
        if (s[i] === '0') {
            ans++; // 添加前导零
        }
    }
    return ans;
};
impl Solution {
    pub fn longest_subsequence(s: String, k: i32) -> i32 {
        let n = s.len();
        let m = (32 - k.leading_zeros()) as usize; // k 的二进制长度
        if n < m {
            return n as _; // 全选
        }

        let suf_val = i32::from_str_radix(&s[n - m..], 2).unwrap();
        let ans = if suf_val <= k { m } else { m - 1 }; // 后缀长度

        (ans + s[..n - m].bytes().filter(|&c| c == b'0').count()) as _ // 添加前导零
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 为 $s$ 的长度。这里是严格意义上的一次遍历,$s$ 的每个字符都会被恰好遍历一次。(C 语言要计算字符串长度,遍历两次)
  • 空间复杂度:$\mathcal{O}(n)$ 或 $\mathcal{O}(m)$ 或 $\mathcal{O}(1)$,取决于实现。手动遍历计算可以做到 $\mathcal{O}(1)$ 空间。

思考题

子序列改成子串,要怎么做?你能做到 $\mathcal{O}(n)$ 时间复杂度吗?

欢迎在评论区发表你的思路/代码。

专题训练

见下面贪心与思维题单的「§5.2 脑筋急转弯」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

『 贪心 』从低位到高位遍历s,判断1能否被保留

作者 flix
2022年6月19日 12:07

贪心:

  1. $s$ 中的 0 均可保留,仅需判断 $s$ 中的 1 能否被保留。

    说明:
    在任何情况下,与其删除某一位的 0,不如删除最高位的 1。也就是说在必要的删除操作下,删除某一位 0 所带来的“收益”不会高于删除最高位的 1
    例如,对于 $s$ = "1001010",要使得其子序列对应的值小于等于 $k=10$,那么删除任一位的 0,如 "1001010",都不如删除最高位的 1,即 "1001010"。

  2. 要使得 $s$ 的子序列最长,且该子序列对应的 二进制 数字小于等于 $k$,那么应优先保留较低位的 1


解题思路

从低位到高位遍历 $s$ (反向遍历 $s$),记需要移除的 1 的个数为 $removed$:

  1. 若当前位为 0,可保留;
  2. 若当前位为 1,分类判断:
    • 计入当前位 1,数字总和依然 $\le k$,可保留;
    • 计入当前位 1,数字总和 $> k$,不可保留,$removed + 1$。

最终返回 $s$ 的总长度减去需要删除的 1 的个数,即 $len(s) - removed$。


代码

###Python

class Solution:
    def longestSubsequence(self, s: str, k: int) -> int:
        
        n = len(s)
        summ = 0                            # 当前子序列的值
        removed = 0                         # 记录要移除的1的个数
        for i, ch in enumerate(s[::-1]):    # 贪心:从低位到高位遍历s
            if ch == '1':                   # 判断当前位的1能否被保留
                if summ >= k:               # 当前位的1无法保留
                    removed += 1
                else:
                    summ += (1<<i)
                    if summ > k:            # 当前位的1无法保留
                        removed += 1

        return n - removed

###Python

class Solution:
    def longestSubsequence(self, s: str, k: int) -> int:
        
        n = len(s)
        summ = 0                            # 当前子序列的值
        removed = 0                         # 记录要移除的1的个数
        for i, ch in enumerate(s[::-1]):    # 贪心:从低位到高位遍历s
            if ch == '1':                   # 判断当前位的1能否被保留
                if summ + (1<<i) > k:       # 当前位的1无法保留
                    removed += 1
                else:                       # 当前位的1可以保留
                    summ += (1<<i)
        
        return n - removed

改进

还可充分利用 $1 \le k \le 10^9$ 这一条件:
从低位到高位遍历 $s$ 时,当遍历到的 1 的位数 $\ge 30$ 时可直接删除该位的 1,因为 $2^{30}=1073741824>10^9 > 2^{29}$。
这对于 C++ 等 $\text{INT_MAX}\le 2^{31}-1$ 的语言来说也能避免整数溢出的情况。

###Python

class Solution:
    def longestSubsequence(self, s: str, k: int) -> int:
        
        n = len(s)
        summ = 0                            # 当前子序列的值
        removed = 0                         # 记录要移除的1的个数
        for i, ch in enumerate(s[::-1]):    # 贪心:从低位到高位遍历s
            if ch == '1':                   # 判断当前位的1能否被保留
                if i >= 30 or summ + (1<<i) > k:    # 当前位的1无法保留
                    removed += 1
                else:                       # 当前位的1可以保留
                    summ += (1<<i)

        return n - removed

###CPP

class Solution {
public:
    int longestSubsequence(string s, int k) {
        int n = s.size();
        int summ = 0;                       // 当前子序列的值
        int removed = 0;
        for (int i = n - 1; i >= 0; i--) {  // 反向遍历s
            if (s[i] == '1') {
                int offset = n-1-i;         // offset:从低位到高位的位数
                if (offset >= 30 || summ + (1 << offset) > k)  removed += 1;
                else  summ += 1 << (offset);
            }
        }
        return n - removed;
    }
};

**复杂度分析**
  • 时间复杂度:$O(n)$,其中 $n=len(s)$。
  • 空间复杂度:$O(1)$(不计反转 $s$)。

每日一题-两个有序数组的第 K 小乘积🔴

2025年6月25日 00:00
给你两个 从小到大排好序 且下标从 0 开始的整数数组 nums1 和 nums2 以及一个整数 k ,请你返回第 k (从 1 开始编号)小的 nums1[i] * nums2[j] 的乘积,其中 0 <= i < nums1.length 0 <= j < nums2.length 。

 

示例 1:

输入:nums1 = [2,5], nums2 = [3,4], k = 2
输出:8
解释:第 2 小的乘积计算如下:
- nums1[0] * nums2[0] = 2 * 3 = 6
- nums1[0] * nums2[1] = 2 * 4 = 8
第 2 小的乘积为 8 。

示例 2:

输入:nums1 = [-4,-2,0,3], nums2 = [2,4], k = 6
输出:0
解释:第 6 小的乘积计算如下:
- nums1[0] * nums2[1] = (-4) * 4 = -16
- nums1[0] * nums2[0] = (-4) * 2 = -8
- nums1[1] * nums2[1] = (-2) * 4 = -8
- nums1[1] * nums2[0] = (-2) * 2 = -4
- nums1[2] * nums2[0] = 0 * 2 = 0
- nums1[2] * nums2[1] = 0 * 4 = 0
第 6 小的乘积为 0 。

示例 3:

输入:nums1 = [-2,-1,0,1,2], nums2 = [-3,-1,2,4,5], k = 3
输出:-6
解释:第 3 小的乘积计算如下:
- nums1[0] * nums2[4] = (-2) * 5 = -10
- nums1[0] * nums2[3] = (-2) * 4 = -8
- nums1[4] * nums2[0] = 2 * (-3) = -6
第 3 小的乘积为 -6 。

 

提示:

  • 1 <= nums1.length, nums2.length <= 5 * 104
  • -105 <= nums1[i], nums2[j] <= 105
  • 1 <= k <= nums1.length * nums2.length
  • nums1 和 nums2 都是从小到大排好序的。

一题三解(双指针、解不等式、前缀和)

作者 newhar
2021年10月17日 22:21

解法框架:二分查找

做过类似题目(668. 乘法表中第k小的数 或者 719. 找出第 k 小的距离对)的不难发现这题的解法是二分查找。首先令 $f(x) =$ 满足 $nums_1[i] * nums_2[j] \le x$ 的数对个数,显然 $f(x)$ 是关于 $x$ 递增的,因此可以进行二分查找,找到第一个满足 $f(x) \ge k$ 的 $x$ 即可。

下面的给出三种求 $f(x)$ 的解法。

解法一:双指针 — 分类讨论

在类似题目 668. 乘法表中第k小的数719. 找出第 k 小的距离对 中,经典的解法是双指针,将单次 check 的时间复杂度降低到了 $O(n)$。那么这个题目呢?

首先,我们把 $nums_1$ 分成 $neg_1$ 和 $pos_1$,分别表示 $nums_1$ 的 负数部分非负数部分
把 $nums_2$ 分成 $neg_2$ 和 $pos_2$,分别表示 $nums_2$ 的 负数部分非负数部分

一图胜千言,下面用一幅图来解释双指针遍历的各种边界条件。

  • 情形一:$nums_1[i] \ge 0$, $nums_2[j] \ge 0$,分别对应 $pos_1$ 和 $pos_2$;

  • 情形二:$nums_1[i] < 0$, $nums_2[j] \ge 0$,分别对应 $neg_1$ 和 $pos_2$;

  • 情形三:$nums_1[i] \ge 0$, $nums_2[j] < 0$,分别对应 $pos_1$ 和 $neg_2$;

  • 情形四:$nums_1[i] < 0$, $nums_2[j] < 0$,分别对应 $neg_1$ 和 $neg_2$。

image.png

代码

###cpp

class Solution {
public:
    long long kthSmallestProduct(vector<int>& nums1, vector<int>& nums2, long long k) {
        vector<int> neg1, pos1, neg2, pos2;
        for(int v : nums1) (v < 0)? neg1.push_back(v) : pos1.push_back(v);
        for(int v : nums2) (v < 0)? neg2.push_back(v) : pos2.push_back(v);

        long long l = -1e10, r = 1e10;
        while(l < r) {
            long long m = (l + r) >> 1;
            long long cur = 0;
            for(int i = 0, j = (int)pos2.size() - 1; i < pos1.size(); ++i) {
                while(j >= 0 && 1ll * pos1[i] * pos2[j] > m) --j;
                cur += j + 1;
            }
            for(int i = 0, j = 0; i < neg1.size(); ++i) {
                while(j < pos2.size() && 1ll * neg1[i] * pos2[j] > m) ++j;
                cur += (int)pos2.size() - j;
            }
            for(int i = 0, j = 0; i < pos1.size(); ++i) {
                while(j < neg2.size() && 1ll * pos1[i] * neg2[j] <= m) ++j;
                cur += j;
            }
            for(int i = 0, j = (int)neg2.size() - 1; i < neg1.size(); ++i) {
                while(j >= 0 && 1ll * neg1[i] * neg2[j] <= m) --j;
                cur += (int)neg2.size() - 1 - j;
            }
            if(cur < k) l = m + 1;
            else r = m;
        }

        return l;
    }
};

如果不想思考那么多复杂的边界条件,那么下面这种双指针方法的思考量较小,可以一试。

解法一点五:双指针 — 等价转换

上面的分类讨论之所以麻烦,关键在于数字有正有负,需要分 4 种情况讨论。如果我们可以将 4 种情况都转化为一种情况呢?
答案是可以。

  • 如果 $nums_1[i] < 0, nums_2[j] \ge 0$,则 $nums_1[i] \times nums_2[j] \le x$
    等价于 $(-nums_1[i]) \times nums_2[j] \ge x$
  • 如果 $nums_1[i] \ge 0, nums_2[j] < 0$,则 $nums_1[i] \times nums_2[j] \le x$
    等价于 $nums_1[i] \times (-nums_2[j]) \ge x$
  • 如果 $nums_1[i] \ge 0, nums_2[j] < 0$,则 $nums_1[i] \times nums_2[j] \le x$
    等价于 $(-nums_1[i]) \times (-nums_2[j]) \le x$

这样我们就将有负数的情形二、三、四转化为全是非负整数的情形一了。注意,由于负数取反后,大小关系发生了逆转,所以需要将数组反转,以保持递增的顺序。

代码

###c++

class Solution {
public:
    long long calc(vector<int>& a, vector<int>& b, long long x, bool greater) {
        long long res = 0;
        if(!a.size() || !b.size()) return 0;
        for(int i = 0, j = (int)b.size() - 1, k = (int)b.size() - 1; i < a.size(); ++i) {
            while(j >= 0 && 1ll * a[i] * b[j] > x) --j;
            while(k >= 0 && 1ll * a[i] * b[k] >= x) --k;
            if(greater) res += (int)b.size() - 1 - k;
            else res += j + 1;
        }
        return res;
    }
    long long kthSmallestProduct(vector<int>& nums1, vector<int>& nums2, long long k) {
        vector<int> neg1, pos1, neg2, pos2;
        for(int v : nums1) (v < 0)? neg1.push_back(-v) : pos1.push_back(v);
        for(int v : nums2) (v < 0)? neg2.push_back(-v) : pos2.push_back(v);
        reverse(neg1.begin(), neg1.end());
        reverse(neg2.begin(), neg2.end());

        long long l = -1e10, r = 1e10;
        while(l < r) {
            long long m = (l + r) >> 1, cur = 0;
            cur += calc(pos1, pos2, m, false);
            cur += calc(neg1, pos2, -m, true);
            cur += calc(pos1, neg2, -m, true);
            cur += calc(neg1, neg2, m, false);
            if(cur < k) l = m + 1;
            else r = m;
        }

        return l;
    }
};

解法二:解不等式 + 嵌套二分查找

我们可以首先在 $nums_1$ 枚举数字 $a$,然后找出 $nums_2$ 中满足 $ab \le x$ 的数字的 $b$ 的个数即可。

如果 $a > 0$,则不等式 $ab \le x$ 成立的条件是 $\displaystyle{b \le \left\lfloor \frac{x}{a} \right\rfloor}$(向下取整);

如果 $a < 0$,则不等式 $ab \le x$ 成立的条件是 $\displaystyle{b \ge \left\lceil \frac{x}{a} \right\rceil}$(向上取整);

$a = 0$ 的情况比较特殊,此时若 $x \ge 0$,则 $ab \le x$ 恒成立;否则 $ab \le x$ 恒不成立。

由于 $nums_2$ 已经排好序,故我们对于 $nums_1$ 中的每个 $a$,只需要在 $nums_2$ 中二分查找小于等于(或大于等于)$x$ 的数量即可。

细节 + 小知识

如何实现向下取整呢?直接用整数除法 $\texttt{a/b}$ 不行吗?不幸的是,实际上 C/C++ 的除法实现是 向 0 取整 的。例如 $\texttt{-1 / 2}$ 的值是 $0$,而不是 $-1$。因此需要稍微改变一下思路。
思路一:采用浮点数 + floor 库函数实现向下取整;顺带用 ceil 库函数实现向上取整。

###c++

long long floorDiv(long long x, long long y) {
    return floor(x / (double)y + 1e-7);
}
long long ceilDiv(long long x, long long y) {
    return ceil(x / (double)y - 1e-7);
}

思路二:避免浮点数运算的实现:

###c++

long long floorDiv(long long x, long long y) {
    if(y < 0) x = -x, y = -y;
    if(x < 0) return (x - (y - 1)) / y;
    return x / y;
}
long long ceilDiv(long long x, long long y) {
    if(y < 0) x = -x, y = -y;
    if(x < 0) return x / y;
    return (x + (y - 1)) / y;
}

最终代码

class Solution {
public:
    long long floorDiv(long long x, long long y) {
        if(y < 0) x = -x, y = -y;
        if(x < 0) return (x - (y - 1)) / y;
        return x / y;
    }
    long long ceilDiv(long long x, long long y) {
        if(y < 0) x = -x, y = -y;
        if(x < 0) return x / y;
        return (x + (y - 1)) / y;
    }
    long long kthSmallestProduct(vector<int>& nums1, vector<int>& nums2, long long k) {
        long long l = -1e10, r = 1e10;
        while(l < r) {
            long long m = (l + r) >> 1;
            long long cur = 0;
            for(int v : nums1) {
                if(v < 0) {
                    cur += nums2.end() - lower_bound(nums2.begin(), nums2.end(), ceilDiv(m, v));
                } else if(v == 0) {
                    cur += (m >= 0)? nums2.size() : 0;
                } else {
                    cur += upper_bound(nums2.begin(), nums2.end(), floorDiv(m, v)) - nums2.begin();
                }
            }
            if(cur < k) l = m + 1;
            else r = m;
        }
        return l;
    }
};

解法三:前缀和

解法二中,我们也可以不用嵌套二分查找。注意到 $-10^5 \le nums_1[i], nums_2[i] \le 10^5$,我们完全可以开辟足够大的空间来统计 $nums_2$ 中各个数字的数量,然后采用前缀和的方式来统计 $nums_2$ 中 $\le x$ 的数字数目,这样可以免去二分查找。

代码

###c++

class Solution {
public:
    long long sums[200005] = {0};
    long long floorDiv(long long x, long long y) {
        if(y < 0) x = -x, y = -y;
        if(x < 0) return (x - (y - 1)) / y;
        return x / y;
    }
    long long ceilDiv(long long x, long long y) {
        if(y < 0) x = -x, y = -y;
        if(x < 0) return x / y;
        return (x + (y - 1)) / y;
    }
    long long kthSmallestProduct(vector<int>& nums1, vector<int>& nums2, long long k) {
        for(int v : nums2) sums[v + 100000]++;
        for(int i = 1; i <= 200000; ++i) sums[i] += sums[i-1];

        auto sum = [&](long long x) -> long long {
            if(x < -100000) return 0;
            if(x > 100000) return sums[200000];
            return sums[x + 100000];
        };

        long long l = -1e10, r = 1e10;
        while(l < r) {
            long long m = (l + r) >> 1;
            
            long long cnt = 0;
            for(int v : nums1) {
                if(v < 0) cnt += sum(100000) - sum(ceilDiv(m, v) - 1);
                if(v == 0 && m >= 0) cnt += nums2.size();
                if(v > 0) cnt += sum(floorDiv(m, v));
            }

            if(cnt < k) l = m + 1;
            else r = m;
        }

        return l;
    }
};
❌
❌