【算法】整数拆分问题

1. 概述

给定一个整数n,将n拆分,问一共有多少种拆分方法。

整数拆分是很经典的问题。也是很经典的 “动态规划” 问题。

问题分析

1 = 1

2 = 2 
  = 1 + 1
  
3 = 3 
  = 2 + 1 
  = 1 + 1 + 1
  
4 = 4 
  = 3 + 1 
  = 2 + 2 
  = 2 + 1 + 1 
  = 1 + 1 + 1 + 1
  
5 = 5 
  = 4 + 1  
  = 3 + 2 
  = 3 + 1 + 1 
  = 2 + 2 + 1 
  = 2 + 1 + 1 + 1 
  = 1 + 1 + 1 + 1 + 1 
  
6 = 6
  = 5 + 1
  = 4 + 2
  = 4 + 1 + 1
  = 3 + 3
  = 3 + 2 + 1
  = 3 + 1 + 1 + 1
  = 2 + 2 + 2
  = 2 + 2 + 1 + 1
  = 2 + 1 + 1 + 1 + 1
  = 1 + 1 + 1 + 1 + 1 + 1

.......


2. 拆分方法

2.1 方法一 —— 递归法

若设f(n, m)表示将n拆分,拆分得到的序列中最大值不超过m。

递归分析

  • n == 1 或者 m == 1
    • n == 1, 即待拆分的n为1,只能拆分为 {1}
    • m == 1, 即拆分n得到的序列最大值为1,那么这个拆分得到的序列只能为{1, 1, 1, …, 1},n个1,只有这1种拆分方法;
  • 其他情况:
    • n == m 时,分为两种情况:
      a) 拆分得到的序列包含m,即序列只包含一个n —— {n},拆分结束;
      b) 拆分得到的序列不包含m,也即后面的拆分过程中拆得的最大整数只可能是m - 1,因此拆分问题可以递归到 f(n, m - 1);
      综上:n == m时的拆分方案共有:1 + f(n, m - 1)
    • n < m 时,拆分的方案共有: f(n, n)
      因为拆分不能拆分出负数出来,拆分得到的最大值只能为n,因此转移到f(n, n);
    • n > m 时
      n > m时的拆分,也分为两种情况:
      a) 拆分得到的序列包含m,那么待拆分的数就为n - m,下一次可以拆分得到的最大值仍为m,因此转移到f(n - m, m);
      b) 拆分得到的序列不包含m,那么待拆分的数还是n,下一次可以拆分得到的最大值变为了m - 1 (本次拆分不包含m,下一次也肯定就不会包含m了)
代码及测试结果

只求出整数拆分情况的总数

package cn.pku.edu.algorithm.leetcode.plus;

/**
 * @author yumu
 * @date 2022/8/17
 */
public class IntegerSplitCount {

    public int split(int n) {
        return helper(n, n);
    }

    private int helper(int n, int m) {
        if (n == 1 || m == 1) {
            return 1;
        }
        else {
            if (n == m) {
                return 1 + helper(n, m - 1);
            } else if (n < m) {
                return helper(n, n);
            } else {
                return helper(n - m, m) + helper(n, m - 1);
            }
        }
    }

    public static void main(String[] args) {
        IntegerSplitCount integerSplitCount = new IntegerSplitCount();
        int n = 6;
        int res = integerSplitCount.split(n);
        System.out.println(res);
    }
}

求出所有的整数拆分结果

import java.util.ArrayList;
import java.util.List;

/**
 * @author yumu
 * @date 2022/8/16
 */
public class IntegerSplit {

    public List<List<Integer>> split(int n) {
        List<List<Integer>> res = new ArrayList<>();
        helper(n, n, new ArrayList<>(), res);
        return res;
    }

    private void helper(int n, int m, List<Integer> out, List<List<Integer>> res) {
        if (n == 1 || m == 1) {
            if (n == 1) {
                out.add(1);
                res.add(new ArrayList<>(out));
                out.remove(out.size() - 1);
            } else {
                for (int i = 1; i <= n; i++) out.add(1);
                res.add(new ArrayList<>(out));
                for (int i = 1; i <= n; i++) out.remove(out.size() - 1);
            }
        } else {
            if (n == m) {
                out.add(m);
                res.add(new ArrayList<>(out));
                out.remove(out.size() - 1);
                helper(n, m - 1, out, res);
            } else if (n < m) {
                helper(n, n, out, res);
            } else {
                out.add(m);
                helper(n - m, m, out, res);
                out.remove(out.size() - 1);
                helper(n, m - 1, out, res);
            }
        }
    }

    public static void main(String[] args) {
        IntegerSplit integerSplit = new IntegerSplit();
        int n = 6;
        List<List<Integer>> res = integerSplit.split(n);
        for (int i = 0; i < res.size(); i++) {
            System.out.print(n + " = ");
            List<Integer> out = res.get(i);
            System.out.print(out.get(0));
            for (int j = 1; j < out.size(); j++) {
                System.out.print(" + " + out.get(j));
            }
            System.out.println();
        }
    }
}

得到的拆分序列如下:


2.2 方法二 —— 动态规划方法

动态规划是在上面递归思路上的优化,减少了重复计算。

dp[n][m] 表示将 n 拆分,其中拆分得的最大的数不超过 m;

状态转移分析;

  • n == 1 或者 m == 1
    1. n == 1,只能拆分为{1},因此 dp[1][m] = 1
    2. m == 1,只能拆分为{1, 1, …, 1},因此 dp[n][1] = 1
  • 其他
    • n == m 时
      拆分包含m,即只能拆分为 {m},只有这一种拆分方案;
      拆分不包含m,即之后的拆分最大能拆分得到的整数是m-1,因此转移到了 dp[n][m - 1]
      综上,n == m时的转移方程为:
      dp[n][m] = 1 + dp[n][m - 1]
    • n < m 时
      不可能拆分出负数,因此转移到 dp[n][n],所以状态转移方程为:
      dp[n][m] = dp[n][n]
    • n > m 时,分为两种情况:
      拆分包含m,转移为dp[n - m][m]
      拆分不包含m,那么后面的拆分得到的最大整数只可能是m - 1,因此转移到 dp[n][m - 1]
      综上,n < m时的转移方程为:
      dp[n][m] = dp[n - m][n] + d\[n][m - 1]

代码及测试
/**
 * @author yumu
 * @date 2022/8/17
 */
public class IntegerSplitCount {

    public int split(int n) {
        // dp[n][m] 表示对整数n进行拆分,且拆分出的最大整数是m,一共的拆分方案数
        int[][] dp = new int[n + 1][n + 1];
        for (int i = 1; i < n; i++) {
            dp[i][1] = 1;  // 拆分得的最大整数是1,那么只能有1种拆分方案
            dp[1][i] = 1;  // 待拆分的整数是1,那么只能有一种拆分方案
        }
        for (int i = 1; i < n + 1; i++) {
            for (int j = 1; j < n + 1; j++) {
                if (i == 1 || j == 1) {
                    dp[i][j] = 1;
                } else {
                    if (i == j) {
                        dp[i][j] = 1 + dp[i][j - 1];
                    } else if (i < j) {
                        dp[i][j] = dp[i][i];
                    } else {
                        dp[i][j] = dp[i - j][j] + dp[i][j - 1];
                    }
                }
            }
        }
        return dp[n][n];
    }

    public static void main(String[] args) {
        IntegerSplitCount integerSplitCount = new IntegerSplitCount();
        int n = 6;
        int res = integerSplitCount.split(n);
        System.out.println(res);
    }
}

若要求出全部的拆分方案,还是只能通过递归方法获取到,动态规划只能获取到方案数。


3. 相关改编问题

在这里插入图片描述
从题目中可以看到,需要在完整的整数拆分中出去全部为同一个元素的情况。
因此可以首先除去拆分为全1和拆分为自身的2种情况。
然后,如果n能够被某个整数整除,也需要除去,因为n可以被拆分为全是这个元素。

代码及测试

/**
 * @author yumu
 * @date 2022/8/17
 */
public class IntegerSplitPlus {
    public int split(int n) {
        if (n == 1 || n == 2) return 0;
        // dp[n][m] 表示对整数n进行拆分,且拆分出的最大整数是m,一共的拆分方案数
        int[][] dp = new int[n + 1][n + 1];
        for (int i = 1; i < n + 1; i++) {
            for (int j = 1; j < n + 1; j++) {
                if (i == 1 || j == 1) {
                    dp[i][j] = 1;
                } else {
                    if (i == j) {
                        dp[i][j] = 1 + dp[i][j - 1];
                    } else if (i < j) {
                        dp[i][j] = dp[i][i];
                    } else {
                        dp[i][j] = dp[i - j][j] + dp[i][j - 1];
                    }
                }
            }
        }
        int res = dp[n][n];
        res -= 2; // 除去全为1和自身的情况
        // 需要出去全部为同一个元素的情况
        // 即若n能够被一个小于它的整数整除,那么res就需要减1,因为n可以被拆分成全部为这个整数的情况
        int temp = (int) Math.sqrt((double) n) + 1;
        for (int i = 2; i <= temp; i++) {
            if (n % i == 0) res--;
        }
        return res;
    }

    public static void main(String[] args) {
        IntegerSplitPlus integerSplitPlus = new IntegerSplitPlus();
        for (int n = 1; n <= 6; n++) {
            int res = integerSplitPlus.split(n);
            System.out.println(res);
        }
    }
}

可以看到,拆分方案数与题目显示的一致。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值