小C选点 | 豆包MarsCode AI刷题

44 阅读6分钟

小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

思路分析

核心思路
  1. 组合生成:

    • 我们需要从 n 个点中选出 m 个点,使用递归深度优先搜索(DFS)枚举所有可能的 m 点组合。
  2. 合法性验证:

    • 对每个生成的组合,检查是否满足点与点之间的距离要求(两两距离小于等于 k)。
  3. 结果统计:

    • 对于每种合法组合,计入结果,同时对 10e9+7 取模以避免溢出。

代码解析

  1. 主函数 (countValidCombinations) :

    • 输入 n,m,k,xn, m, k, xn,m,k,x。
    • 对点坐标数组 xxx 排序,便于后续处理。
    • 调用递归函数 findCombinations
  2. 递归函数 (findCombinations) :

    • 使用深度优先搜索(DFS)枚举所有可能的 mmm-点组合。
    • 每次递归选择当前点(或不选择当前点)。
    • 当组合长度达到 mmm 时,调用 isValidCombination 检查组合是否满足条件。
  3. 验证函数 (isValidCombination) :

    • 遍历当前组合中两两点的距离,确保每对点的距离都不超过 kkk。
  4. 测试用例:

    • 提供了几个示例,覆盖了问题中给定的测试场景。
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;
 }

逻辑解析

  1. 递归结束条件

    • 如果已经选够了 m 个点,则验证当前组合是否合法。
    • 如果所有点都被尝试过,则直接返回。
  2. 递归选择过程

    • 每个点可以被选中(加入组合)或不被选中(跳过)。
    • 递归分支分别处理这两种情况。
  3. 结果计数

    • 对于每种合法组合,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选点问题综合运用了递归、回溯、合法性验证、剪枝优化等编程思想,形成了一种系统的解题方法。这些思想的结合具有以下特点:

  1. 全面性:通过递归枚举所有可能的组合,确保解法的完整性。
  2. 高效性:借助剪枝优化和排序,将搜索范围尽可能缩小。
  3. 可扩展性:各个模块(递归、验证、计数)独立,实现代码结构清晰且易于修改。

这种思想不仅适用于本问题,也可以推广到其他涉及组合生成和条件验证的复杂问题中。