leetcode
leetcode 2251 ~ 2300
好子序列的个数

好子序列的个数

难度:

标签:

题目描述

代码结果

运行时间: 77 ms, 内存: 17.0 MB


/*
题目思路:
给定一个数组 nums 和一个整数 k,要求找出数组中所有长度为 k 的子序列,使得子序列中最大元素和最小元素之差不超过 1。
使用 Java Stream API,我们可以更简洁地实现这一功能。
1. 首先,我们需要生成所有长度为 k 的子序列。
2. 对于每一个生成的子序列,判断其最大值和最小值之差是否不超过 1。
3. 统计所有符合条件的子序列的数量。
*/

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class GoodSubsequencesStream {
    public static int countGoodSubsequences(int[] nums, int k) {
        List<List<Integer>> subsequences = generateSubsequences(nums, k);
        return (int) subsequences.stream()
                .filter(subseq -> {
                    int max = subseq.stream().mapToInt(v -> v).max().orElse(Integer.MIN_VALUE);
                    int min = subseq.stream().mapToInt(v -> v).min().orElse(Integer.MAX_VALUE);
                    return max - min <= 1;
                })
                .count();
    }

    private static List<List<Integer>> generateSubsequences(int[] nums, int k) {
        return IntStream.range(0, 1 << nums.length)
                .mapToObj(i -> IntStream.range(0, nums.length)
                        .filter(j -> (i & (1 << j)) != 0)
                        .mapToObj(j -> nums[j])
                        .collect(Collectors.toList()))
                .filter(list -> list.size() == k)
                .collect(Collectors.toList());
    }

    public static void main(String[] args) {
        int[] nums = {1, 2, 3, 4};
        int k = 3;
        System.out.println(countGoodSubsequences(nums, k)); // 示例输出
    }
}

解释

方法:

这个题解通过预计算阶乘和阶乘的逆来计算组合数C(n, k),用于计算每个字符出现次数的所有可能的组合方式。解法首先统计字符串中每个字符的出现次数,并将这些数值排序存储。对每种可能的出现次数cnt从最大值向1迭代,计算在至少有cnt个字符的情况下,可以形成的所有子序列的方式,并累加到答案中。这种方法通过动态规划的方式,利用组合数学的知识来高效计算结果。

时间复杂度:

O(N + M*K)

空间复杂度:

O(N)

代码细节讲解

🦆
为什么在计算组合数C(n, k)时需要使用逆阶乘,直接使用阶乘相除会有什么问题?
在计算组合数C(n, k)时使用逆阶乘而不是直接使用阶乘相除的主要原因是计算机在处理模运算(特别是大数)时的限制。直接使用阶乘相除可能导致中间结果非常大,从而溢出或失去精度。而模运算的性质要求我们不能直接在模数下进行除法,因此需要使用逆元来代替除法操作。逆元是基于费马小定理的一个概念,当模数为质数时,一个数a的逆元是a^(p-2) mod p,这可以保证在模运算环境下的乘法和除法结果正确。使用逆阶乘数组,我们可以有效地预计算出所有可能需要的逆元,并用它们来计算组合数,确保计算过程在MOD下的正确性和效率。
🦆
解法中提到的字符频率为何需要排序?排序的目的是什么,对算法的影响如何?
在解法中对字符频率进行排序是为了优化计算过程,确保在从最大频率cnt开始向下迭代时的计算效率。通过排序,算法可以从最高的频率开始向下计算每个频率层级的可能子序列数,尽可能快地累加较大的组合数,并在计算过程中避免重复的组合数计算。这种方法利用了从高到低的频率分布,使得每次迭代计算的子序列都是基于当前还未计算过的频率,从而提高算法的总体效率和减少不必要的计算。
🦆
在计算组合数时,您检查了n < 0, k < 0 或 n < k的情况,这些条件是如何影响组合数计算的?
在组合数学中,C(n, k)定义为从n个不同元素中选择k个元素的方式数。当k > n时,显然无法从n个元素中选出更多的元素,因此C(n, k)为0。同理,n或k为负数在数学上没有实际意义,因此这些情况下组合数也应当为0。这些边界条件的检查是为了确保组合数函数在逻辑上是健全的,并防止程序在运行时产生非法的数学计算或错误的结果。
🦆
在主函数中减去1的操作是为了去除哪种特殊情况?为什么要特别去除这种情况?
在主函数中减去1是为了排除所有字符都不被选择的情况,即空子序列的情况。在计算每种字符的频率可能的组合时,包括了每个字符都不被选中的情况(即C(v, 0) = 1),这意味着当所有字符都不被选择时,会有一个额外的情况被计算进去,即空子序列。因此,为了仅统计有效的子序列(至少包含一个字符的子序列),需要在最后的累加结果中减去这个空子序列的情况。

相关问题