You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

整数无序分拆:求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;
            }
        }
    }
}
代码关键细节解释
  1. 非递增约束:每次递归时,下一个数的maxNum设为当前选的数,确保后面的数不会比前面的大,避免生成重复排列的分拆。
  2. 剪枝优化
    • 计算upper时,限制当前数的最大值:既要保证不超过前一个数,也要保证剩下的数能凑够至少1的总和,避免无效递归。
    • 一旦找到目标方案,立即终止所有递归,提升效率。
  3. 计数逻辑:每找到一个符合条件的分拆(个数为n且和为s),计数器加1,当计数器等于k时,保存当前路径作为结果。
验证你的示例

输入11 6 5时,程序会按非递增顺序生成前5种分拆:

  1. 6+1+1+1+1+1
  2. 5+2+1+1+1+1
  3. 4+3+1+1+1+1
  4. 4+2+2+1+1+1
  5. 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

火山引擎 最新活动