DP定义:动态规划是分治思想的延伸,通俗一点来说就是大事化小,小事化无的艺术。在将大问题化解为小问题的分支过程中,保存对这些小问题已经处理好的结果,并供后面处理更大规模的问题时直接使用这些结果。
动态规划的特点:
- 把原来的问题分解成了几个相似的子问题
- 所有的子问题都只需要解决一次
- 储存子问题的解
动态规划的本质,是对问题状态的定义和状态转移方程的定义(状态与状态之间的递推关系),动规一般都需要和数组搭配使用来保存中间结果。
动态规划问题一般从以下四个角度考虑:
- 状态定义(子问题)
- 状态间的转移方程定义
- 状态的初始化
- 返回结果
状态定义的要求:定义的状态一定要形成递推关系。
适用场景:最大/最小值 、可行不可行、是不是、方案个数等
实践出真知:
一、斐波那契数列
现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1)。
- 状态定义:F(i):斐波那契第 i 项的值
- 状态间的转移方程定义:F(i) = F(i-1) + F(i-2)
- 状态的初始化:F(0)=0,F(1)=1,F(2)=1
- 返回结果:F(n):斐波那契第 n 项的值
public class Solution {
public int Fibonacci(int n) {
//保存中间值
// if(n==0) return 0;
// if(n==1 || n==2) return 1;
// int[] f=new int[n+1];//因为从第0项开始,所以给个n+1的空间
// f[1]=1;//状态初始化
// for(int i=2;i<=n;i++){
// f[i]=f[i-1]+f[i-2];//状态转移方程
// }
// return f[n];//返回结果
//不保存中间值
if(n==0) return 0;
if(n==1 || n==2) return 1;
int one=0;
int two=1;
int three=1;
for(int i=2;i<=n;i++){
three=one+two;
one=two;
two=three;
}
return three;
}
}
二、变态跳台阶
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
- 状态定义:F(i):跳上第 i 级台阶需要的方法数
- 状态间的转移方程定义:F(i)=F(i-1)*2
- 状态的初始化:F(1)=1
- 返回结果:F(n):跳上第 n 级台阶需要的方法数
public class Solution {
public int JumpFloorII(int target) {
if(target<=0) return 0;
int f1 = 1;
int ret = 1;
for(int i=2;i<=target;i++){
ret*=2;
}
return ret;
}
}
三、矩形覆盖
我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?
同样的解法:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
- 状态定义:F(i):用 i 个 2*1 的小矩形无重复覆盖一个 2*i 的矩形的方法个数
- 状态间的转移方程定义:F(i) = F(i-1) + F(i-2)
- 状态的初始化:F(1) = 1,F(2) = 2
- 返回结果:F(n):方法个数
public class Solution {
public int RectCover(int target) {
if(target < 3) return target;
int f1=1,f2=2,fn=0;
for(int i=3;i<=target;i++){
fn=f1+f2;
f1=f2;
f2=fn;
}
return fn;
}
}
四、牛妹的蛋糕
第一天牛妹吃掉蛋糕总数三分之一(向下取整)多一个,第二天又将剩下的蛋糕吃掉三分之一(向下取整)多一个,以后每天吃掉前一天剩下的三分之一(向下取整)多一个,到第n天准备吃的时候只剩下一个蛋糕。牛妹想知道第一天开始吃的时候蛋糕一共有多少呢?
假设今天有 x 个,昨天有 y 个,则可得 x = y-(y/3+1),即 y = 3(x+1)/2。
public class Solution {
public int cakeNumber (int n) {
int ret=1;
for(int i=n;i>1;i--){
ret=(ret+1)*3/2;
}
return ret;
}
}
五、连续子数组的最大和
在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)、
- 状态定义:F(i):以第 i 个元素结尾的最大连续和
- 状态间的转移方程:
- 状态的初始化:F(0)=array[0]
- 返回结果:max(F(i)),其中 i < n
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int curSum=array[0],maxSum=array[0];
for(int i=1;i<array.length;i++){
curSum=Math.max(array[i],curSum+array[i]);
maxSum=Math.max(curSum,maxSum);
}
return maxSum;
}
}
六、单词分割
给定一个字符串s和一组单词dict,判断s是否可以用空格分割成一个单词序列,使得单词序列中所有的单词都是dict中的单词(序列可以包含一个或多个单词)。例如:
给定s=“leetcode”;
dict=["leet", "code"].
返回true,因为"leetcode"可以被分割成"leet code".
import java.util.Set;
public class Solution {
public boolean wordBreak(String s, Set<String> dict) {
if(s.length()==0) return false;
boolean[] ret=new boolean[s.length()+1];
ret[0]=true;
for(int i=1;i<=s.length();i++){
for(int j=i-1;j>=0;j--){
if(ret[j]&&dict.contains(s.substring(j,i))){
ret[i]=true;
break;
}
}
}
return ret[s.length()];
}
}
七、三角形最小路径和
给出一个三角形,计算从三角形顶部到底部的最小路径和,每一步都可以移动到下面一行相邻的数字,
例如,给出的三角形如下:
[[2],[3,4],[6,5,7],[4,1,8,3]]
最小的从顶部到底部的路径和是2 + 3 + 5 + 1 = 11。
自顶向下求解: 求出每个位置的路径和,最后遍历一遍二维数组最后一行找出最小值即可。每个位置的路径和等于它本身加上上面元素的和。这个位置列坐标 j 有如下可能:
- j = 0,则 path[i][0] = path[i-1][0] + triangle[i][0]
- i == j,则 path[i][j] = path[i-1][j-1] + triangle[i][i]
- 剩余情况:path[i][j] = min(path[i-1][j-1] ,path[i-1][j] ) + triangle[i][j]
import java.util.ArrayList;
public class Solution {
public int minimumTotal(ArrayList<ArrayList<Integer>> triangle) {
int row=triangle.size();
int[][] minPath=new int[row][row];
minPath[0][0]=triangle.get(0).get(0);
for(int i=1;i<row;i++){
for(int j=0;j<=i;j++){
if(j==0) minPath[i][j]=triangle.get(i).get(0)+minPath[i-1][0];
else if(i==j) minPath[i][j]=triangle.get(i).get(i)+minPath[i-1][j-1];
else minPath[i][j]=triangle.get(i).get(j)+Math.min(minPath[i-1][j],minPath[i-1][j-1]);
}
}
int min=minPath[row-1][0];
for(int i=1;i<row;i++){
min=Math.min(minPath[row-1][i],min);
}
return min;
}
}
自底向上求解:
import java.util.ArrayList;
public class Solution {
public int minimumTotal(ArrayList<ArrayList<Integer>> triangle) {
int row=triangle.size();
int[][] minPath=new int[row][row];
for(int i=0;i<row;i++){// 初始化最后一行
minPath[row-1][i]=triangle.get(row-1).get(i);
}
for(int i=row-2;i>=0;i--){
for(int j=0;j<=i;j++){
minPath[i][j]=triangle.get(i).get(j)+Math.min(minPath[i+1][j],minPath[i+1][j+1]);
}
}
return minPath[0][0];
}
}
八、带权值的最小路径和
给定一个由非负整数填充的m x n的二维数组,现在要从二维数组的左上角走到右下角,请找出路径上的所有数字之和最小的路径。注意:你每次只能向下或向右移动。
import java.util.*;
public class Solution {
public int minPathSum (int[][] grid) {
int row=grid.length,col=grid[0].length;
int[][] arr=new int[row][col];
for(int i=0;i<row;i++){
for(int j=0;j<col;j++){
if(i==0&&j==0){
arr[0][0]=grid[0][0];
}else if(i==0){
arr[i][j]=grid[i][j]+arr[i][j-1];
}else if(j==0){
arr[i][j]=grid[i][j]+arr[i-1][j];
}else{
arr[i][j]=grid[i][j]+Math.min(arr[i-1][j],arr[i][j-1]);
}
}
}
return arr[row-1][col-1];
}
}
九、unique-path(1)
一个机器人在m×n大小的地图的左上角(起点,下图中的标记“start"的位置)。机器人每次向下或向右移动。机器人要到达地图的右下角。(终点,下图中的标记“Finish"的位置)。可以有多少种不同的路径从起点走到终点?
import java.util.*;
public class Solution {
public int uniquePaths (int m, int n) {
int[][] arr=new int[m][n];
for(int i=1;i<n;i++){//两个for循环进行初始化
arr[0][i]=1;
}
for(int i=0;i<m;i++){
arr[i][0]=1;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
arr[i][j]=arr[i-1][j]+arr[i][j-1];//状态转移方程,这一步可以有前面这两部走过来
}
}
return arr[m-1][n-1];//返回结果
}
}
unique-path(2)
如果在图中加入了一些障碍,有多少不同的路径?分别用0和1代表空区域和障碍:
import java.util.*;
public class Solution {
/**
*
* @param obstacleGrid int整型二维数组
* @return int整型
*/
public int uniquePathsWithObstacles (int[][] obstacleGrid) {
if(obstacleGrid.length==0) return 0;
int row=obstacleGrid.length;
int col=obstacleGrid[0].length;
int[][] path=new int[row][col];
for(int i=0;i<row;i++){
if(obstacleGrid[i][0]==0) path[i][0]=1;
else break;//从(i,0)后面的点都走不到
}
for(int j=0;j<col;j++){
if(obstacleGrid[0][j]==0) path[0][j]=1;
else break;//从(j,0)后面的点都走不到
}
for(int i=1;i<row;i++){
for(int j=1;j<col;j++){
if(obstacleGrid[i][j]==0)
path[i][j]=path[i-1][j]+path[i][j-1];
}
}
return path[row-1][col-1];
}
}
十、背包问题
有 n
个物品和一个大小为 m
的背包. 给定数组 A
表示每个物品的大小和数组 V
表示每个物品的价值。问最多能装入背包的总价值是多大?
A[i], V[i], n, m
均为整数- 你不能将物品进行切分
- 你所挑选的要装入背包的物品的总大小不能超过
m
- 每个物品只能取一次
public class Solution {
public int backPackII(int m, int[] A, int[] V) {
int size=A.length;
int[][] arr=new int[size+1][m+1];
for(int i=1;i<=size;i++){
for(int j=1;j<=m;j++){
if(A[i-1]>j){
arr[i][j]=arr[i-1][j];
}else{
arr[i][j]=Math.max(arr[i-1][j],arr[i-1][j-A[i-1]]+V[i-1]);
}
}
}
return arr[size][m];
}
}