前言
继续做几道动态规划的题目,从中继续体会动态规划。最后一道n皇后的问题无法使用动态规划。
一、累加和最小(不限集合个数)
题目
* 给定一个正数数组arr,
* 请把arr中所有的数分成两个集合,尽量让两个集合的累加和接近
* 返回:
* 最接近的情况下,较小集合的累加和
分析
先算出总累加和,分成两个集合后,我们只要转化成新组成集合的累加和最接近总累加和的一半的问题
递归代码
public static int right(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
//求出总累加和
int sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
return process(arr, 0, sum / 2);
}
/**
* 返回:arr[index...]从index位置及其往后,最接近rest的累加和
*
* @param arr
* @param index 数组的下标
* @param rest 剩余要组成最接近的数
* @return
*/
public static int process(int[] arr, int index, int rest) {
//base case
if (index == arr.length) {
return 0;
}
//普遍情况,从左往右模型,要和不要
//index位置不要
int p1 = process(arr, index + 1, rest);
//index 位置要
int p2 = 0;
if (arr[index] <= rest) {
p2 = arr[index] + process(arr, index + 1, rest - arr[index]);
}
return Math.max(p1, p2);
}
举个例子,当arr[5,2,3,.....] 总累加和为10
第一种递归的选择:只选择下标0的位置5,然后下标1,2不选择,此时就是要求arr下标从3开始最接近5的选择
第二张递归的选择:下标0不选择,然后下标1,2选择,此时就是求arr下标从3开始最接近5的选择
通过以上分析可以知道存在重复解,需要改动态规划
动态规划版本
public static int dp(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
int sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
sum /= 2;
int N = arr.length;
int[][] dp = new int[N + 1][sum + 1];
for (int index = N - 1; index >= 0; index--) {
for (int rest = 0; rest <= sum; rest++) {
//index位置不要
int p1 = dp[index + 1][rest];
//index 位置要
int p2 = 0;
if (arr[index] <= rest) {
p2 = arr[index] + dp[index + 1][rest - arr[index]];
}
dp[index][rest] = Math.max(p1, p2);
}
}
return dp[0][sum];
}
二、累加和最小(限制集合个数)
题目:
* 给定一个正数数组arr,请把arr中所有的数分成两个集合
* 如果arr长度为偶数,两个集合包含数的个数要一样多
* 如果arr长度为奇数,两个集合包含数的个数必须只差一个
* 请尽量让两个集合的累加和接近
* 返回:
* 最接近的情况下,较小集合的累加和
分析:
基于上一道题目,在递归入参增加集合个数限制即可
递归代码:
public static int right(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
int sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
if (arr.length % 2 == 0) {
//偶数情况
return process(arr, 0, arr.length / 2, sum / 2);
} else {
//奇数情况
return Math.max(process(arr, 0, arr.length / 2, sum / 2), process(arr, 0, arr.length / 2 + 1, sum / 2));
}
}
/**
* 返回arr[index...]从index位置及其往后,满足累加picks个数情况下,最接近rest目标的累加和
*
* @param arr
* @param index 数组下标
* @param picks 还剩下多少个数需要选择
* @param rest 距离目标累加和
* @return
*/
public static int process(int[] arr, int index, int picks, int rest) {
//base case
if (index == arr.length) {
return picks == 0 ? 0 : -1;
}
//普遍情况
//index位置不要
int p1 = process(arr, index + 1, picks, rest);
//index位置要
int p2 = -1;
if (arr[index] <= rest && picks > 0) {
int next = process(arr, index + 1, picks - 1, rest - arr[index]);
if (next != -1) {
p2 = arr[index] + next;
}
}
return Math.max(p1, p2);
}
动态规划代码
public static int dp(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
int sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
int N = arr.length;
int M = (N + 1) / 2;
sum /= 2;
int[][][] dp = new int[N + 1][M + 1][sum + 1];
//默认都是-1
for (int index = 0; index <= N; index++) {
for (int picks = 0; picks <= M; picks++) {
for (int rest = 0; rest <= sum; rest++) {
dp[index][picks][rest] = -1;
}
}
}
//base case
for (int rest = 0; rest <= sum; rest++) {
dp[N][0][rest] = 0;
}
for (int index = N - 1; index >= 0; index--) {
for (int picks = 0; picks <= M; picks++) {
for (int rest = 0; rest <= sum; rest++) {
//index位置不要
int p1 = dp[index + 1][picks][rest];
//index位置要
int p2 = -1;
if (arr[index] <= rest && picks > 0) {
int next = dp[index + 1][picks - 1][rest - arr[index]];
if (next != -1) {
p2 = arr[index] + next;
}
}
dp[index][picks][rest] = Math.max(p1, p2);
}
}
}
if (N % 2 == 0) {
//偶数情况
return dp[0][N / 2][sum];
} else {
//奇数情况
return Math.max(dp[0][N / 2][sum], dp[0][N / 2 + 1][sum]);
}
}
三、n皇后
题目:
* N皇后问题是指在N*N的棋盘上要摆N个皇后,
* 要求任何两个皇后不同行、不同列, 也不在同一条斜线上
* 给定一个整数n,返回n皇后的摆法有多少种。n=1,返回1
* n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0
* n=8,返回92
分析:
每行每列都去尝试,同时需要保证不同列和左右斜对角线
递归版本:
public static int right(int n) {
if (n <= 0) {
return 0;
}
int[] record = new int[n];
return process1(0, record, n);
}
/**
* 返回摆放n个皇后共有多少种方法数
*
* @param i 第几行
* @param record 记录每行皇后摆放位置
* @param n 摆放几个皇后
* @return
*/
public static int process1(int i, int[] record, int n) {
//base case
if (i == n) {
return 1;
}
int ways = 0;
//递归求每一列皇后放置的情况
for (int j = 0; j < n; j++) {
//剪枝
if (isValid(record, i, j)) {
record[i] = j;
ways += process1(i + 1, record, n);
}
}
return ways;
}
/**
* 返回在0...i行的皇后摆放是否合法
* 不能同列
* 不能左右斜线
*
* @param record 记录数
* @param i 行
* @param j 列
* @return
*/
public static boolean isValid(int[] record, int i, int j) {
for (int k = 0; k < i; k++) {
if (j == record[k] || Math.abs(j - record[k]) == Math.abs(i - k)) {
return false;
}
}
return true;
}
因为不存在重复解,所以就不需要改动态规划
总结
不是所有的递归都需要改成动态规划,只有当递归中存在重复解的时候才有必要去改写动态规划
、