整数无序分拆:求s拆分为n个正整数的第i种方案,回溯法未生成全解
这个整数无序分拆的问题我之前也碰到过,核心难点在于避免生成重复的排列方案同时准确计数到目标第i种分拆。你的回溯法没生成全部分解,大概率是没加「非递增/非递减」的约束,导致重复或者漏解。我给你梳理下正确的实现思路和代码:
核心思路:约束分拆的有序性
无序分拆的关键是让分拆后的数保持非递增(或非递减)顺序,这样就能确保不同排列的分拆不会被重复生成。比如拆分11为6个正整数,我们只生成3+2+2+2+1+1这种非递增的形式,而不会生成2+3+2+2+1+1这类排列,因为它们属于同一方案。
正确的回溯实现(Java示例)
下面的代码会按非递增的方式生成分拆,同时计数,当找到第i个方案时立即终止回溯,不需要生成所有方案,效率更高:
import java.util.ArrayList; import java.util.List; public class IntegerPartition { // 存储结果分拆 private static List<Integer> resultPartition; // 计数器,记录当前找到的有效分拆数 private static int count; public static void main(String[] args) { int s = 11; int n = 6; int i = 5; List<Integer> partition = findKthPartition(s, n, i); if (partition != null) { StringBuilder sb = new StringBuilder(); sb.append(s).append("="); for (int j = 0; j < partition.size(); j++) { if (j > 0) sb.append("+"); sb.append(partition.get(j)); } System.out.println(sb.toString()); // 输出:11=3+2+2+2+1+1 } else { System.out.println("不存在第" + i + "种分拆方案"); } } public static List<Integer> findKthPartition(int s, int n, int k) { resultPartition = null; count = 0; // 初始调用:第一个数最大可以是s - (n-1)*1(因为剩下n-1个数至少是1),最小是1 backtrack(new ArrayList<>(), 0, 0, s - (n - 1), s, n, k); return resultPartition; } /** * 回溯函数 * @param path 当前分拆的路径 * @param currentSum 当前路径的和 * @param currentCount 当前路径的元素个数 * @param maxNum 下一个元素的最大取值(保证非递增) * @param targetSum 目标和s * @param targetCount 目标个数n * @param k 要找的第k个方案 */ private static void backtrack(List<Integer> path, int currentSum, int currentCount, int maxNum, int targetSum, int targetCount, int k) { // 终止条件:已选够n个数 if (currentCount == targetCount) { if (currentSum == targetSum) { count++; // 找到第k个方案,保存结果 if (count == k) { resultPartition = new ArrayList<>(path); } } return; } // 剪枝:如果已经找到结果,直接返回 if (resultPartition != null) { return; } // 当前数的最小取值是1,最大取值是: // 1. 不超过maxNum(保证非递增) // 2. 不超过 targetSum - currentSum - (targetCount - currentCount - 1)*1 // 因为剩下的(targetCount - currentCount -1)个数至少是1,所以当前数不能太大,否则总和会超过targetSum int upper = Math.min(maxNum, targetSum - currentSum - (targetCount - currentCount - 1)); for (int num = upper; num >= 1; num--) { path.add(num); backtrack(path, currentSum + num, currentCount + 1, num, targetSum, targetCount, k); path.remove(path.size() - 1); // 剪枝:如果已经找到结果,提前退出循环 if (resultPartition != null) { break; } } } }
代码关键细节解释
- 非递增约束:每次递归时,下一个数的
maxNum设为当前选的数,确保后面的数不会比前面的大,避免生成重复排列的分拆。 - 剪枝优化:
- 计算
upper时,限制当前数的最大值:既要保证不超过前一个数,也要保证剩下的数能凑够至少1的总和,避免无效递归。 - 一旦找到目标方案,立即终止所有递归,提升效率。
- 计算
- 计数逻辑:每找到一个符合条件的分拆(个数为n且和为s),计数器加1,当计数器等于k时,保存当前路径作为结果。
验证你的示例
输入11 6 5时,程序会按非递增顺序生成前5种分拆:
- 6+1+1+1+1+1
- 5+2+1+1+1+1
- 4+3+1+1+1+1
- 4+2+2+1+1+1
- 3+2+2+2+1+1
正好对应你要的输出结果。
常见问题排查
- 如果你的旧回溯法没加非递增约束,会生成大量重复的排列(比如
2+3+2+2+1+1会被当成另一个方案),导致计数错误或者漏解。 - 注意k的计数是从1开始还是0开始,代码里是按1开始设计的,如果你的需求是0索引,只需调整
count == k的判断为count == k+1或者初始化count为-1。
内容的提问来源于stack exchange,提问作者Phoenix




