小C选点 | 豆包MarsCode AI刷题
问题描述
小C在坐标轴上有 n 个点,她想从中选出 m 个点,使得这些点之间的两两距离都不超过 k。你的任务是帮助小C计算出有多少种选点的方案。由于答案可能非常大,请将答案对 10e9+7 取模。
例如:给定点的坐标为 [1, 2, 3, 4]
,并且 k*=3,你可以任选两个点,它们的距离都不会超过 k,因此总共有6种选点方案。
测试样例
样例1:
输入:
n = 4 ,m = 2 ,k = 3 ,x = [1, 2, 3, 4]
输出:6
样例2:
输入:
n = 4 ,m = 2 ,k = 2 ,x = [1, 2, 3, 4]
输出:5
样例3:
输入:
n = 5 ,m = 3 ,k = 5 ,x = [1, 3, 6, 7, 9]
输出:`3
思路分析
核心思路
-
组合生成:
- 我们需要从 n 个点中选出 m 个点,使用递归深度优先搜索(DFS)枚举所有可能的 m 点组合。
-
合法性验证:
- 对每个生成的组合,检查是否满足点与点之间的距离要求(两两距离小于等于 k)。
-
结果统计:
- 对于每种合法组合,计入结果,同时对 10e9+7 取模以避免溢出。
代码解析
-
主函数 (
countValidCombinations
) :- 输入 n,m,k,xn, m, k, xn,m,k,x。
- 对点坐标数组 xxx 排序,便于后续处理。
- 调用递归函数
findCombinations
。
-
递归函数 (
findCombinations
) :- 使用深度优先搜索(DFS)枚举所有可能的 mmm-点组合。
- 每次递归选择当前点(或不选择当前点)。
- 当组合长度达到 mmm 时,调用
isValidCombination
检查组合是否满足条件。
-
验证函数 (
isValidCombination
) :- 遍历当前组合中两两点的距离,确保每对点的距离都不超过 kkk。
-
测试用例:
- 提供了几个示例,覆盖了问题中给定的测试场景。
1. 主函数逻辑
主函数solution
负责初始化参数和调用递归函数以生成所有组合:
public static int solution(int n, int m, int k, int[] x) {
final int MOD = 1_000_000_007;
// 排序坐标数组,方便后续距离计算
Arrays.sort(x);
// 存储结果计数
int result = 0;
// 使用递归生成所有组合并验证
List<Integer> currentCombination = new ArrayList<>();
result = findCombinations(0, 0, n, m, k, x, currentCombination, result, MOD);
return result;
}
关键点:
- 数组排序:将点按坐标升序排列,便于更高效的距离计算。
- 递归调用:通过
findCombinations
递归生成所有组合并验证其合法性。
2. 递归生成组合
函数findCombinations
负责生成所有可能的点集合,并对每个集合进行验证:
private static int findCombinations(int index, int depth, int n, int m, int k, int[] x,
List<Integer> currentCombination, int result, int MOD) {
// 如果已经选了 m 个点
if (depth == m) {
if (isValidCombination(currentCombination, k)) {
result = (result + 1) % MOD;
}
return result;
}
// 如果已经没有剩余的点可选
if (index == n) {
return result;
}
// 当前点加入组合
currentCombination.add(x[index]);
result = findCombinations(index + 1, depth + 1, n, m, k, x, currentCombination, result, MOD);
// 当前点不加入组合
currentCombination.remove(currentCombination.size() - 1);
result = findCombinations(index + 1, depth, n, m, k, x, currentCombination, result, MOD);
return result;
}
逻辑解析:
-
递归结束条件:
- 如果已经选够了 m 个点,则验证当前组合是否合法。
- 如果所有点都被尝试过,则直接返回。
-
递归选择过程:
- 每个点可以被选中(加入组合)或不被选中(跳过)。
- 递归分支分别处理这两种情况。
-
结果计数:
- 对于每种合法组合,
result
自增,同时对 109+710^9 + 7109+7 取模以避免溢出。
- 对于每种合法组合,
3. 合法性验证
函数isValidCombination
负责验证当前组合是否满足题目要求:
private static boolean isValidCombination(List<Integer> combination, int k) {
int size = combination.size();
for (int i = 0; i < size; i++) {
for (int j = i + 1; j < size; j++) {
if (Math.abs(combination.get(i) - combination.get(j)) > k) {
return false;
}
}
}
return true;
}
逻辑解析:
- 遍历组合中的所有点对,检查每对点的距离是否小于等于 k。
- 如果存在任何一对点的距离大于 k,则组合非法,返回
false
。 - 如果所有点对均满足条件,返回
true
。
实现代码
import java.util.*;
public class Main {
// 主函数,用于计算满足条件的组合数
public static int solution(int n, int m, int k, int[] x) {
final int MOD = 1_000_000_007;
// 排序坐标数组
Arrays.sort(x);
// 存储结果计数
int result = 0;
// 使用递归生成所有组合并验证
List<Integer> currentCombination = new ArrayList<>();
result = findCombinations(0, 0, n, m, k, x, currentCombination, result, MOD);
return result;
}
// 递归生成组合并验证每种组合是否满足条件
private static int findCombinations(int index, int depth, int n, int m, int k, int[] x,
List<Integer> currentCombination, int result, int MOD) {
// 如果已经选了 m 个点
if (depth == m) {
if (isValidCombination(currentCombination, k)) {
result = (result + 1) % MOD;
}
return result;
}
// 如果已经没有剩余的点可选
if (index == n) {
return result;
}
// 当前点加入组合
currentCombination.add(x[index]);
result = findCombinations(index + 1, depth + 1, n, m, k, x, currentCombination, result, MOD);
// 当前点不加入组合
currentCombination.remove(currentCombination.size() - 1);
result = findCombinations(index + 1, depth, n, m, k, x, currentCombination, result, MOD);
return result;
}
// 验证当前组合是否满足条件
private static boolean isValidCombination(List<Integer> combination, int k) {
int size = combination.size();
for (int i = 0; i < size; i++) {
for (int j = i + 1; j < size; j++) {
if (Math.abs(combination.get(i) - combination.get(j)) > k) {
return false;
}
}
}
return true;
}
// 测试用例
public static void main(String[] args) {
System.out.println(solution(4, 2, 3, new int[]{1, 2, 3, 4}) == 6);
System.out.println(solution(4, 2, 2, new int[]{1, 2, 3, 4}) == 5);
System.out.println(solution(5, 3, 5, new int[]{1, 3, 6, 7, 9}) == 3);
}
}
总结
1. 递归与回溯
-
核心思想:将问题分解为子问题,通过递归逐步尝试所有可能的解决方案,同时在遇到无效路径时及时回退,避免重复计算。
-
实现方式:
- 枚举所有可能的点组合,递归地选择当前点或跳过当前点。
- 当组合长度满足条件时,检查其是否有效。
-
适用场景:
- 枚举所有可能的子集、路径或组合(如排列组合问题、棋盘问题等)。
2. 剪枝优化
-
核心思想:在递归过程中,如果当前状态已经无法满足问题要求,提前结束递归,避免不必要的计算。
-
实现方式:
- 若组合长度超过目标长度 m,直接返回。
- 若剩余点数量不足以填满组合,提前终止递归。
-
适用场景:
- 状态空间较大、需要通过条件限制减少搜索范围的问题。
3. 合法性验证(约束检查)
-
核心思想:在生成每一个可能的组合后,检查其是否满足题目规定的约束条件。
-
实现方式:
- 遍历当前组合中的所有点对,计算其距离是否符合条件。
-
适用场景:
- 所有需要验证条件是否满足的问题(如背包问题、图路径问题等)。
4. 分治与组合生成
-
核心思想:将大的问题分解为小的问题,通过分而治之的方法生成所有可能的组合。
-
实现方式:
- 递归地选择点或跳过点,将大问题拆分为更小的子问题。
-
适用场景:
- 需要生成子集、排列或组合的场景。
结合思想的总结
小C选点问题综合运用了递归、回溯、合法性验证、剪枝优化等编程思想,形成了一种系统的解题方法。这些思想的结合具有以下特点:
- 全面性:通过递归枚举所有可能的组合,确保解法的完整性。
- 高效性:借助剪枝优化和排序,将搜索范围尽可能缩小。
- 可扩展性:各个模块(递归、验证、计数)独立,实现代码结构清晰且易于修改。
这种思想不仅适用于本问题,也可以推广到其他涉及组合生成和条件验证的复杂问题中。