动态规划
1. 打印字符串的所有子序列
如题,比如有字符串
abc
,则它的子序列有" "
,"a"
,"b"
,"c"
,"ac"
,"ab"
,"bc"
,"abc"
public class Solution_PrintAllSub {
public static void printAllSub(char[] str, int i, String res){
if(i == str.length){
System.out.println(res);
return;
}
//结果字符串初始化为空
//遍历数组,每遇到一个元素,有两个选择:
//1.结果字符串中不加入该元素
//2.结果字符串中加入该元素
//数组遍历完毕,分别打印所得到的结果字符串。
printAllSub(str, i + 1, res);
printAllSub(str, i + 1, res + str[i]);
}
public static void main(String[] args) {
String str = "abc";
printAllSub(str.toCharArray(), 0, "");
}
}
2. 子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
import java.util.ArrayList;
import java.util.List;
class Solution {
public static List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> lists = new ArrayList<>();
List<Integer> list = new ArrayList<>();
process(nums, 0, list, lists);
return lists;
}
public static void process(int[] nums, int i, List<Integer> list, List<List<Integer>> lists){
if(i == nums.length){
lists.add(list);
}else{
process(nums, i + 1, list, lists);
List<Integer> temp = new ArrayList<>(list);
temp.add(nums[i]);
process(nums, i + 1, temp, lists);
}
}
public static void main(String[] args) {
int[] nums = new int[]{1,2,3};
List<List<Integer>> lists = subsets(nums);
}
}
注:list.add(nums[i])
返回的是一个布尔类型,不能直接写process(nums, i + 1, list.add(nums[i]), lists);
如果先添加数据,再进入递归
process(nums, i + 1, list, lists);
list.add(nums[i]);
process(nums, i + 1, list, lists);
会导致递归栈中list的值发生异常改变,list的值不应该改变,那么怎么向下传递呢?
可以复制list到一个辅助链表,但 List对象如果直接使用“=”进行赋值,如List<Integer> temp = list
,如果修改temp的话,还是会修改list,因为这种方式是将temp的地址指向了list。那怎么复制呢?
List<Integer> temp = new ArrayList<>(list);//得到一个tmep,地址不同,内容相同。
所以process
函数最终代码为
public static void process(int[] nums, int i, List<Integer> list, List<List<Integer>> lists){
if(i == nums.length){
lists.add(list);
}else{
process(nums, i + 1, list, lists);
List<Integer> temp = new ArrayList<>(list);
temp.add(nums[i]);
process(nums, i + 1, temp, lists);
}
}
3. 打印一个字符串的全排列
思路:将字符串转换成char类型的数组,先将数组排序,然后使用process函数,对[begin, end]区间的元素进行排列,每次交换arr[begin - 1]与arr[i] (begin <= i <= end)元素,不同的交换构成不同的前缀。前缀固定后,在对后续元素递归即可。
import java.util.ArrayList;
import static java.util.Arrays.sort;
class Solution {
//每次增加固定前缀的个数,对后面的进行全排列
public static ArrayList<String> Permutation(String str) {
ArrayList<String> lists = new ArrayList<String>();
char[] arr = str.toCharArray();
sort(arr);//使用工具类java.util.Arrays进行排序
process(lists, arr, 0, arr.length - 1);
return lists;
}
public static void process(ArrayList<String> lists, char[] arr, int begin, int end){
if(begin == end){
String temp = String.valueOf(arr);
if(!lists.contains(temp)){
lists.add(temp);
}
return;
}else{
if(arr[begin] == arr[begin + 1]){//数组已排好序, 有重复字符的话,直接将其计入前缀
process(lists, arr, begin + 1, end);
}else{
process(lists, arr, begin + 1, end);
for(int i = begin + 1; i <= end; i++){
swap(arr, begin, i);
process(lists, arr, begin + 1, end);
swap(arr, begin, i);
}
}
}
}
public static void swap(char[] arr, int m, int n){
char temp = arr[m];
arr[m] = arr[n];
arr[n] = temp;
}
public static void main(String[] args){
String str = "bba";
ArrayList<String> lists = Permutation(str);
for(String s: lists){
System.out.println(s);
}
}
}
4. 母牛的故事
f(n) = f(n - 1) + f(n - 3)
:今年母牛的数量=去年牛的数量+三年前牛的数量
5. 暴力递归转动态规划
5.1 最小路径和
题目:给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小
**说明:**每次只能向下或者向右移动一步。
示例:
输入: [ [1,3,1], [1,5,1], [4,2,1] ] 输出: 7 解释: 因为路径 1→3→1→1→1 的总和最小。
暴力递归解法
思路:
- 从矩阵的左上角开始走,每到达一个位置,当前路径和加上此矩阵元素
arr[i][j]
。 - 然后有两条路径可走:即向右和向下。(和第一个问题很像,每一步都有两种选择,0-1背包)
- 当到达最后一行时只能向右走,到达最后一列时只能 向下走。到达右下角,加上该元素,返回总结果。
class Solution {
public static int minPathSum(int[][] grid) {
return process(grid, 0, 0, 0);
}
public static int process(int[][]grid, int i, int j, int count){//从[0,0]开始时,路径和count为0
if(i == grid.length - 1 && j == grid[0].length - 1){
return count + grid[i][j];
}
if(i == grid.length - 1){//到达最后一行,只能向右移动
return process(grid, i, j + 1, count + grid[i][j]);
}
if(j == grid[0].length - 1){//到达最后一列,只能向下移动
return process(grid, i + 1, j, count + grid[i][j]);
}
int down = process(grid, i, j + 1, count + grid[i][j]);
int right = process(grid, i + 1, j, count + grid[i][j]);
return Math.min(down, right);
}
public static void main(String[] args){
int[][] grid = {
{1,2,3,0},
{2,4,4,0},
{3,1,1,0}
};
System.out.println(minPathSum(grid));
}
}
这种解法很慢,因为有很多重复的操作。假设从arr[i][j]
到右下角用函数f(i,j)
表示。则f(0,1)
的后续操作有两种,分别是f(1, 1)
和f(0, 2)
;而f(1,0)
的后续操作有两种,分别是f(1, 1)
和f(2,0)
。这样的话f(1, 1)
的操作就被执行了两次。
动态规划解法
==!!!==之所以该暴力递归能转化为动态规划,是因为每一步的操作都是一样的,而且对于一个固定的位置arr[i,j]
,从左上角到该位置的最短路径和也是固定的。从而可以创建一个同大小的矩阵arr
,将该数据记录下来。其中arrd[i][j]
表示原矩阵中从左上角到grid[i][j]
的最短路径。
arr[i][j] = grid[i][j] + Math.min(arr[i - 1][j], arr[i][j - 1]);
public class Solution_MinPathSum {
public static int minPathSum(int[][] grid){
if(grid == null || grid.length == 0 || (grid.length == 1 && grid[0].length == 0)){
return 0;
}
int row = grid.length;
int col = grid[0].length;
int[][] arr = new int[row][col];
arr[0][0] = grid[0][0];
//矩阵第一行和第一列比较特殊
for(int i = 1; i < row; i++){//对第一列进行赋值
arr[i][0] = grid[i][0] + arr[i - 1][0];
}
for(int j = 1; j < col; j++){//对第一行进行赋值
arr[0][j] = grid[0][j] + arr[0][j - 1];
}
for(int i = 1; i < row; i++){
for(int j = 1; j < col; j++){
arr[i][j] = grid[i][j] + Math.min(arr[i][j - 1], arr[i - 1][j]);
}
}
return arr[row - 1][col - 1];
}
public static void main(String[] args){
int[][] grid = {
{1,2,3,0},
{2,0,4,2},
{3,1,3,0}
};
System.out.println(minPathSum(grid));
}
}
5.2 数组中能否使累加和等于aim
题目:给定一个全为正数的数组arr,和一个正数aim。如果可以任意选择arr中的数字,能不能累加到aim,返回true或false。
暴力递归解法
思路:从i = 0
开始,遍历数组,每遇到一个元素,有两个选择:加上这个数或者不加。遍历完整个数组,如果数组长度为n
,则总共有
2
n
2^ n
2n种结果。暴力递归解法:把数组遍历完成,判断最终这
2
n
2^ n
2n种结果中是否含有aim
。
public static boolean function(int[] arr, int aim, int i, int sum){
if(i == arr.length){
return sum == aim;
}else{
return function(arr, aim, i + 1, sum) || function(arr, aim, i + 1, sum + arr[i]);
}
}
动态规划解法
函数f(i, sum)
表示:遍历到第i位元素,后续操作能否将sum
累加到aim
。毋庸置疑,f(arr.length - 1, aim)
的值为true
。
而且由暴力递归加法可知,f(i, sum)
的值由f(i + 1, sum)
和f(i + 1, sum + arr[i])
确定。
因此可以构造一个布尔类型的矩阵,列数为aim + 1
,行数为arr.length + 1
。假设f(arr.length, aim)
为true,则可以从该矩阵的右下角倒推出f(0,0)
的值。
class Solution {
public static boolean function(int[] arr, int aim){
int row = arr.length + 1;
int col = aim + 1;
boolean[][] grid = new boolean[row][col];
//将最后一行,除了最右下角,都设为false
grid[row - 1][col - 1] = true;
for(int i = 0; i < col - 1; i++){
grid[row - 1][i] = false;
}
//然后由倒数第二行开始,逐层向上,将grid矩阵填充完整
for(int i = row - 2; i >= 0; i--){
for(int j = 0; j < col; j++){
if(j + arr[i] <= aim){
grid[i][j] = grid[i + 1][j] || grid[i + 1][j + arr[i]];
}else{
grid[i][j] = grid[i + 1][j];
}
}
}
//打印grid数组
// for(int i = 0; i < row; i++){
// for(int j = 0; j < col; j++){
// System.out.print(grid[i][j] + " ");
// }
// System.out.println();
// }
//grid数组填充完毕,grid[0][0]的值即为结果
return grid[0][0];
}
public static void main(String[] args){
int[] arr = {1,3,0,4,2};
System.out.println(function(arr, 5));
}
}
5.3 换钱的方法数
暴力递归解法
arr=[100,50,20,10,5,1],aim=1000。
-
假设使用一张100元,转换成arr=[50,20,10,5,1],aim=900,计算此结果res1。
-
假设使用两张100元,转换成arr=[50,20,10,5,1],aim=800,计算此结果res2。
-
假设使用三张100元,转换成arr=[50,20,10,5,1],aim=700,计算此结果res3。
-
…
-
直到aim=0。此时使用10张100元,转换成arr=[50,20,10,5,1],aim=0,此结果res_n=1。
最终结果res = res1 + res2 + res3 + resn
public static int coins1(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
return process1(arr, 0, aim);
}
public static int process1(int[] arr, int index, int aim) {
int res = 0;
if (index == arr.length) {
res = aim == 0 ? 1 : 0;
} else {
for (int i = 0; arr[index] * i <= aim; i++) {
res += process1(arr, index + 1, aim - arr[index] * i);
}
}
return res;
}
动态规划解法
暴力递归解法太过暴力,有很多重复的操作,如下
arr=[100,50,20,10,5,1],aim=1000。
-
假设使用2张100元,0张50元,转换成arr=[50,20,10,5,1],aim=800
-
假设使用1张100元,2张50元,转换成arr=[50,20,10,5,1],aim=800
-
假设使用0张100元,4张50元,转换成arr=[50,20,10,5,1],aim=800
-
…
如上所示,没出现一次arr=[50,20,10,5,1],aim=800,对要对其进行计算。
我们把已经计算的结果存入一个哈希表里。哈希对的key为Index_aim
,value为process1(arr, index, aim)
的值。这将省去大量重复操作
public static int coins2(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
return process2(arr, 0, aim);
}
public static HashMap<String, Integer> map = new HashMap<>();
public static int process2(int[] arr, int index, int aim) {
int res = 0;
if (index == arr.length) {
res = aim == 0 ? 1 : 0;
} else {
for (int i = 0; arr[index] * i <= aim; i++) {
nextAim = aim - arr[index] * i;
nextKey = String.valueOf(index + 1) + "_" + String.valueOf(nextAim);
if(map.contains(nextKey)){
res += map.get(nextKey);
}else{
res += process2(arr, index + 1, nextAim);
}
}
}
map.put(String.valueOf(index) + "_" + String.valueOf(aim), res);
return res;
}
动态规划解法进阶版
上述动态规划版本已经很优秀了,我们可以将其推广,构建一个二维的动态规划数组。
假设有arr=[5,3,2],aim=10。数组元素dp[index][aim]
表示,使用arr[index],arr[index + 1],..., arr[arr.length -1]
,能够凑出aim的方案有多少种。dp[0][aim]
即为最终结果。
如dp[0][10]
表示使用5,3,2能凑出10元的方案有多少种。
对于dp[arr.length][aim]
表示,把所有钱都用上进行组合,剩余需要凑的钱为aim
- 如果aim=0,表示刚好能凑出来,即
dp[arr.length][0] = 1
, - 如果aim!=0,表示钱无论怎么凑,剩下的目标钱数都不为0,即凑不出来,结果为0。
arr=[5,3,2],aim=10,dp数组如下
dp[index][aim] = dp[index+1][aim-arr[index]] + dp[index+1][aim-2*arr[index]] + ...
如dp[1][8]
表示使用3元和2元能凑出8元的方案,
- 使用1张3元,则只需再凑出5元就行,此时的方案为
dp[2][5]
- 使用2张3元,则只需再凑出2元就行,此时的方案为
dp[2][2]
故dp[1][8] = dp[2][8] + dp[2][5] + dp[2][2] = 1 + 0 + 1 = 2
。
而dp[1][5]
表示使用3元和2元能凑出5元的方案,
- 使用1张3元,则只需再凑出5元就行,此时的方案为
dp[2][5]
- 使用2张3元,则只需再凑出2元就行,此时的方案为
dp[2][2]
可以看出,dp[1][8] = dp[2][8] + (dp[2][5] + dp[2][2]) =dp[2][8] + dp[1][5]= 1 + 1 = 2
因此可以有递推关系dp[index][j] = dp[index+1][j] + dp[index][j - arr[index]]
因此,我们可以使用如上的地推关系,快速地把二维表填满,计算到dp[0][10]
即为结果
代码如下
public static int coins(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
//将二维表最左一列都赋值为1
int[][] dp = new int[arr.length + 1][aim + 1];
for (int i = 0; i <= arr.length; i++) {
dp[i][0] = 1;
}
//将二维表最后一行赋值
for(int j = 1; j <= aim; j++){
dp[arr.length][j] = 0;
}
for (int i = arr.length; i >= 0; i--) {
for (int j = 1; j <= aim; j++) {
//dp[index][j] = dp[index+1][j] + dp[index][j - arr[index]]
dp[i][j] = dp[i + 1][j];
dp[i][j] += j - arr[i] >= 0 ? dp[i][j - arr[i]] : 0;
}
}
return dp[0][aim];
}
5.4 排成一条线的纸牌博弈等问题
题目:有一个整型数组A,代表数值不同的纸牌排成一条线。玩家a和玩家b依次拿走每张纸牌,规定玩家a先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家a和玩家b都绝顶聪明,他们总会采用最优策略。请返回最后获胜者的分数。
给定纸牌序列A及序列的大小n,请返回最后分数较高者得分数(相同则返回任意一个分数)。保证A中的元素均小于等于1000。且A的大小小于等于300。
测试样例:
[1,2,100,4], 4
返回:101
暴力递归解法
定义递归函数f(i,j),表示如果arr[i…j]这个排列上的纸牌被绝顶聪明的人拿走,最终能够获得什么分数。
定义递归函数s(i,j),表示如果a[i…j]这个排列上的纸牌被绝顶聪明的人后拿,最终能获得什么分数。
首先来分析,具体过程如下:
-
如果i==j(只有一张纸牌),会被先拿纸牌的人拿走,所以返回arr[i];
-
如果i!=j,先拿纸牌的人有两种选择,要么拿走arr[i],要么拿走arr[j];
如果拿走arr[i],剩下arr[i+1,j]。对于arr[i+1,j]中的纸牌,当前玩家成了后拿的人,因此他后续能获得的分数为。s(i+1, j)
。如果拿走arr[j],那么剩下arr[i,j-1],当前玩家后续能获得的分数为s[i,j-1]
。
作为绝顶聪明的人,必然会在两种决策中选择最优的。所以返回max{arr[i]+s(i+1, j),arr[j]+s(i, j-1)}
然后来分析s(i,j)
:
-
如果i == j,后拿纸牌的人什么也拿不到,返回0
-
如果i != j,玩家的对手会先拿纸牌。对手要么先拿走a[i],要么先拿走arr[j]。如果对手拿走arr[i],那么排列剩下arr[i+1,j];如果对手拿走arr[j],剩下arr[i,j-1]。对手也是绝顶聪明的人,所以也会把最差的情况留给玩家因此返回
min{f(i+1,j),f(i,j-1)}
public static int win1(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
//先拿的人不一定最后一定赢,比如1,100,2
return Math.max(f(arr, 0, arr.length - 1), s(arr, 0, arr.length - 1));
}
//先拿
public static int f(int[] arr, int i, int j) {
if (i == j) {
return arr[i];
}
return Math.max(arr[i] + s(arr, i + 1, j), arr[j] + s(arr, i, j - 1));
}
//后拿
public static int s(int[] arr, int i, int j) {
if (i == j) {
return 0;
}
return Math.min(f(arr, i + 1, j), f(arr, i, j - 1));
}
动态规划解法
假设有数组arr={1,100,2,3}
由上面的代码可以看出,f(i, j)
依赖s(i+1, j)
和s(i, j-1)
,s(i, j)依赖f(i+1, j)和f(i, j-1)
而最后的结果又由f(i, j)
和s(i, j)
同时决定。所以可以构造出两个表,f表和s表。
先根据暴力代码填充不需要依赖其他信息的值,即对角线上的值。
由s(i, j) = Math.min(f(arr, i + 1, j), f(arr, i, j - 1))
可得
s(0,1) = min(f(1,1), f(0, 0)) = 1
s(1,2) = min(f(2,2), f(1, 1)) = 2
s(2,3) = min(f(3,3), f(3, 3)) = 2
故可得S表
由f(i,j) = Math.max(arr[i] + s(arr, i + 1, j), arr[j] + s(arr, i, j - 1))
f(0, 1) = max(arr[0] + s(1,1), arr[1] + s(0,0)) = max(1, 100) = 100
f(1, 2) = max(arr[1] + s(2,2), arr[2] + s(1,1)) = max(100, 2) = 100
f(2, 3) = max(arr[2] + s(3,3), arr[3] + s(2,2)) = max(2, 3) = 3
故可得S表如下
两个表按照依赖关系循环利用,就可得
比较f(0,3)
和s(0,3)
,故返回最大值为103
代码实现
package class06;
import java.util.Scanner;
class Solution_MaxCardSum{
public static int maxSum(int[] arr){
int length = arr.length;
int[][] f = new int[length][length];
int[][] s = new int[length][length];
//对角线赋值
for(int i = 0; i < length; i++){
f[i][i] = arr[i];
s[i][i] = 0;
}
//循环利用两者的关系赋值
for(int j = 1; j < length; j++){
int i = 0;
int k = j;
while(k < length){
s[i][k] = Math.min(f[i+1][k], f[i][k-1]);
f[i][k] = Math.max(arr[i] + s[i+1][k], arr[k] + s[i][k-1]);
i++;
k++;
}
}
return Math.max(f[0][length - 1], s[0][length - 1]);
}
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
int n = 0;
System.out.println("请输入数组的长度");
n = sc.nextInt();
System.out.println("请输入数组中的元素");
int[] arr = new int[n];
for(int i = 0; i < n; i++){
if(sc.hasNext()){
arr[i] = sc.nextInt();
}
}
System.out.print("最后的结果为:");
System.out.println(maxSum(arr));
}
}
5.5 机器人行走
题目:给定一个从1到N的有序序列,序列中的元素每次递增1。机器人在M位置上,机器人可以走P步,每次只能往走一步,求机器人走到K位置上的方式有几种。
暴力递归解法
//m:当前机器人的位置
//n:序列为(1,2,3,...n-1,n)
//机器人可以走p步
//机器人最终在k位置上
public static int getWays(int m, int n, int p, int k){
if(m < 1 || m > n || n < 2 || p < 1 || k < 1 || k > n){
return 0;
}
int res = 0;
if(p == 0){
return m == k ? 1 : 0;
}
if(m == 1){
res = getWays(m + 1, n, p - 1, k);
}else if(m == n){
res = getWays(m - 1, n, p - 1, k);
}else{
res = getWays(m + 1, n, p - 1, k) + getWays(m - 1, n, p - 1, k);
}
return res;
}
动态规划解法
在暴力递归解法中,变化的参数为m和p,可以根据这两个变量做动态规划
假设:k=7,n=10。可做如下动态规划表
先将矩阵第一行赋值,即还剩余0步时,机器人刚好到k位置,所以记为1。
p=1时,根据res = getWays(m + 1, n, p - 1, k) + getWays(m - 1, n, p - 1, k)
,由第一行可推第二行。其实该关系就是杨辉三角
最后的结果从最后一行看,如果机器人初始位置为4,可以走5步,那么走到7的方式有5种。