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了)
- n == 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
- n == 1,只能拆分为{1},因此
dp[1][m] = 1
; - m == 1,只能拆分为{1, 1, …, 1},因此
dp[n][1] = 1
;
- 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]
- n == m 时
代码及测试
/**
* @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);
}
}
}

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