目录
🌞动态规划概念
动态规划是分治思想的延伸,通俗一点来说就是大事化小,小事化无的艺术。
在将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并供后面处理更大规模的问题时直接使用这些结果。
🌻动态规划具备了以下三个特点
1.把原来的问题分解成了几个子问题
2.所有的子问题都只需要解决一次
3.储存子问题的解
🌻动态规划的本质
1.对问题状态的定义
2.状态转移方程的定义
🌻动态规划的问题一般从以下四个角度考虑
1.状态定义
2.状态间的转移方程定义
3.状态的初始化
4.返回结果
💧动态规划例题
🌂一维数组相关:
☔斐波那契数列
这是一道最经典的动态规划入门题,我们可以用递归和迭代两种思路去解答,也可以利用动态规划的思路。
public static int fibc(int n){
if(n==0||n==1){
return n;
}
int fn=0;
int fn1=1;
int fn2=0;
for(int i=2;i<=n;++i){
fn=fn1+fn2;
fn2=fn1;
fn1=fn;
}
return fn;
}
☔股票的最大利润
假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
例如:[7,1,5,3,6,4] 5
最大利润为在最低点1买入,在之后的最高点6卖出,最大利润为5。
class Solution {
public int maxProfit(int[] prices) {
if(prices.length==0){
return 0;
}
//定义股票的最小价格作为买入点
int min=prices[0];
//定义数组array[i]存放股票在i点的最大利润
int[] array = new int[prices.length];
//初始化在第0点时利润为0
array[0]=0;
for(int i=1;i<prices.length;++i){
if(prices[i]<=prices[i-1]){ //股票跌了,此时更新买入点,但是最大利润不变
min=Math.min(prices[i],min);
array[i]=array[i-1];
}else{ //股票涨了,此时更新最大利润
array[i]=Math.max(prices[i]-min,array[i-1]);
}
}
//返回结果
return array[prices.length-1];
}
}
☔连续子数组的最大和
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。
例如:[-2,1,-3,4,-1,2,1,-5,4] 6
连续子数组 [4,-1,2,1] 的和最大,为6。
class Solution {
public int maxSubArray(int[] nums) {
int[] array = new int[nums.length];
//定义一个数组储存0到i中子数组和的最大值
array[0]=nums[0];
//返回的max为array[0]到array[n]里的最大值
int max=array[0];
for(int i=1;i<nums.length;++i){
//如果array[i-1]是负数,那么array[i]要更新为nums[i],否则更新为array[i-1]+nums[i]
array[i]=Math.max(nums[i]+array[i-1],nums[i]);
//更新最大值
max=Math.max(array[i],max);
}
return max;
}
}
🌂字符串相关:
☔字符串分割
给定一个字符串s和一簇单词dict,判断s是否可以用空格分割成一个单词序列,是的单词序列中所有的单词都是dict中的单词。
例如:s = "leetcode"; dict = ["leet","code"];
返回true
public boolean workBreak(String s,Set<String> dict){
boolean[] canBreak = new boolean[s.length()+1];
//设置初始状态为true
canBreak[0] = true;
for(int i=0;i<=s.length();++i){
for(int j=0;j<i;++j){
//如果前j个字符符合条件,并且第j个到第i个字符组成的单词也在字典里面,则F(i)=true
if(canBreak[j]&&dict.contains(s.sunstring(j,i))){
canBreak[i]=true;
break;
}
}
}
//返回最后一个字符的状态
return canBreak[s.length()];
}
☔回文串分割
给出一个字符串s,分割s使得分割出的每一个子串都是回文串
计算将字符串s分割成会问分割结果的最小切割数
public class Solution {
//判断回文串函数
public boolean isPal(String s, int start, int end){
while(start < end){
if(s.charAt(start) != s.charAt(end))
return false;
++start;
--end;
}
return true;
}
public int minCut(String s) {
int len = s.length();
int[] minCut = new int[len + 1];
//因为每一个字符都是一个单独的回文串,那么我们初始化为最大的分割次数(字符串个数减一)
for(int i = 0; i <= len; ++i){
minCut[i] = i - 1;
}
for(int i = 1; i <= len; ++i){
for(int j = 0; j < i; ++j){
if(isPal(s, j,i - 1)){
//如果是回文串,那就更新最小分割次数
minCut[i] = Math.min(minCut[i], minCut[j] + 1);
}
}
}
//返回结果
return minCut[len];
}
}
我们发现,判断是否为回文串的函数也可以利用动态规划问题来解决
因此,代码可以优化为:
public class Solution {
public boolean[][] getMat(String s){
int len = s.length();
//利用一个二维数组来储存s(i,j)是否为回文串
boolean[][] Mat = new boolean[len][len];
for(int i = len - 1; i >= 0; --i){
for(int j = i; j < len; ++j){
if(j == i){
//单个字符为true
Mat[i][j] = true;
}else if(j == i + 1){
//只需判断s(i)和s(j)想不想等
if(s.charAt(i) == s.charAt(j)){
Mat[i][j] = true;
}else{
Mat[i][j] = false;
}
}else{
//利用(i+1,j-1),并且判断s(i)和s(j)是否相等,就能知道(i,j)是否为回文串
Mat[i][j] = (s.charAt(i) == s.charAt(j)) && Mat[i + 1][j - 1];
}
}
}
return Mat;
}
public int minCut(String s) {
int len = s.length();
boolean[][] Mat = getMat(s);
int[] minCut = new int[len + 1];
for(int i = 0; i <= len; ++i){
minCut[i] = i - 1;
}
for(int i = 1; i <= len; ++i){
for(int j = 0; j < i; ++j){
if(Mat[j][i - 1]){
minCut[i] = Math.min(minCut[i], minCut[j] + 1);
}
}
}
return minCut[len];
}
}
☔编辑距离
给定两个单词word1和word2,请计算将word1转换为word2至少需要多少步操作。
你可以对一个单词执行以下三种操作
1.在单词中插入一个字符
2.删除单词中的一个字符
3.替换单词中的一个字符
public class Solution {
public int minDistance(String word1, String word2) {
int len1 = word1.length();
int len2 = word2.length();
int[][] minDis = new int[len1 + 1][len2 + 1];
//word1和word2有一个为0的时候,最小编辑次数就是i,可以依此进行初始化
for(int i = 0; i <= len1; ++i){
minDis[i][0] = i;
}
for(int i = 0; i <= len2; ++i){
minDis[0][i] = i;
}
for(int i = 1; i <= len1; ++i){
for(int j = 1; j <= len2; ++j){
//先对插入和删除情况进行判断
minDis[i][j] = 1 + Math.min(minDis[i - 1][j], minDis[i][j - 1]);
//再对修改这种情况进行判断
if(word1.charAt(i - 1) == word2.charAt(j - 1)){
minDis[i][j] = Math.min(minDis[i][j],minDis[i - 1][j - 1]);
}else{
minDis[i][j] = Math.min(minDis[i][j],minDis[i - 1][j - 1] + 1);
}
}
}
//返回结果
return minDis[len1][len2];
}
}
☔不同的子序列
给定两个字符串S和T,返回S子序列等于T的不同子序列的个数有多少个?
字符串的子序列是由原来的字符串删除一些字符,也可以不删除,在不改变相对位置的情况下的剩余字符
例如:S = "nowcccoder"; T = "nowccoder";
返回3
public class Solution {
public int numDistinct(String S, String T) {
int sLen = S.length();
int tLen = T.length();
int[][] numDis = new int[sLen + 1][tLen + 1];
//都为0的时候,返回结果为1
numDis[0][0] = 1;
//字符串S为0的时候,返回结果为0
for(int i = 1; i <= tLen; ++i){
numDis[0][i] = 0;
}
//字符串T为0的时候,返回结果为1
for(int i = 1; i <= sLen; ++i){
numDis[i][0] = 1;
}
for(int i = 1; i <= sLen; ++i){
for(int j = 1; j <= tLen; ++j){
if(S.charAt(i - 1) == T.charAt(j - 1)){
//如果S(i-1)==T(j-1),可以选择使用和不使用两种
numDis[i][j] = numDis[i - 1][j] + numDis[i - 1][j - 1];
}else{
//如果不等于那只能接着往后走了
numDis[i][j] = numDis[i - 1][j];
}
}
}
//返回结果
return numDis[sLen][tLen];
}
}
🌂矩阵与路径问题:
☔三角形中的最小路径和
给出一个三角形,计算从三角形顶部到底部的最小路径和,每一步都可以移动到下面一行相邻的数字。
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
int n = triangle.size();
int[][] array = new int[n][n];
//初始化为顶部路径
array[0][0] = triangle.get(0).get(0);
for (int i = 1; i < n; ++i) {
for (int j = 0; j <= i; ++j) {
//在三角形最左端的情况
if(j==0){
array[i][j] = array[i-1][0] + triangle.get(i).get(0);
//在三角形最右边的情况
}else if(j==i){
array[i][j] = array[i-1][j-1] + triangle.get(i).get(j);
}else{
array[i][j] = Math.min(array[i-1][j-1],array[i-1][j])+triangle.get(i).get(j);
}
}
}
//处理最后一行
int minTotal = array[n - 1][0];
for (int i = 1; i < n; ++i) {
minTotal = Math.min(minTotal, array[n - 1][i]);
}
//返回结果
return minTotal;
}
}
☔路径的数目&最小路径和&礼物的最大价值
这三种题目其实都是一种解法,所以就把他们放在一起了👀
而且这些题都很简单,就不详细解释了😃
路径的数目
一个机器人位于一个m✖n网格的左上角,机器人每次只能向下或者向右移动一步。机器人试图到达网格的右下角,问总共有多少条不同的路径?
class Solution {
public int uniquePaths(int m, int n) {
int[][] pathNum = new int[m][n];
初始化一直向下和一直向右的路径数目为1
for(int i=0;i<m;++i){
pathNum[i][0]=1;
}
for(int j=0;j<n;++j){
pathNum[0][j]=1;
}
for(int i=1;i<m;++i){
for(int j=1;j<n;++j){
//因为只能向右向下走,所以(i,j)路径等于(i-1,j)和(i,j-1)路径和
pathNum[i][j]=pathNum[i-1][j]+pathNum[i][j-1];
}
}
//返回重点处的路径数目
return pathNum[m-1][n-1];
}
}
最小路径和
给定一个包含非负整数的m✖n网格gird,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
class Solution {
public int minPathSum(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
for(int j = 1; j < n; j++){
grid[0][j] += grid[0][j - 1];
}
for(int i = 1; i < m; i++){
grid[i][0] += grid[i - 1][0];
}
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
grid[i][j] += Math.min(grid[i][j - 1], grid[i - 1][j]);
}
}
return grid[m - 1][n - 1];
}
}
礼物的最大价值
在以恶搞m✖n的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格,直到到达棋盘的右下角。给定一个棋盘以及上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
class Solution {
public int maxValue(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
for(int j = 1; j < n; j++){
grid[0][j] += grid[0][j - 1];
}
for(int i = 1; i < m; i++){
grid[i][0] += grid[i - 1][0];
}
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
grid[i][j] += Math.max(grid[i][j - 1], grid[i - 1][j]);
}
}
return grid[m - 1][n - 1];
}
}
☔背包问题
有n个物品和一个大小为m的背包,给定数组A表示每个物品的大小和数组V表示每个物品的价值,问最多能装入背包的总价值是多大?
public class Solution {
public int backPackII(int m, int[] A, int[] V) {
int n = A.length;
//从前i个商品中选择,背包大小为j时的最大价值
int[][] maxValue = new int[n + 1][m + 1];
//初始化
for(int i = 0; i <= n; ++i){
maxValue[i][0] = 0;
}
for(int i = 1; i <= m; ++i){
maxValue[0][i] = 0;
}
for(int i = 1; i <= n; ++i){
for(int j = 1; j <= m; ++j){
//第i个物品装不下
if(A[i - 1] > j){
maxValue[i][j] = maxValue[i - 1][j];
//第i个物品装得下
}else{
maxValue[i][j] = Math.max(maxValue[i - 1][j - A[i - 1]]+ V[i - 1], maxValue[i - 1][j]);
}
}
}
//返回结果
return maxValue[n][m];
}
}
我们发现,二维数组中的 i 没有用到之前的数据,所以可以优化为一维数组
public class Solution {
public int backPackII(int m, int[] A, int[] V) {
int n = A.length;
int[] maxValue = new int[m + 1];
for(int i = 0; i <= m; ++i){
maxValue[i] = 0;
}
for(int i = 1; i <= n; ++i){
for(int j = m; j > 0; --j){
if(A[i - 1] <= j){
maxValue[j] = Math.max(maxValue[j - A[i - 1]] + V[i - 1], maxValue[j]);
}
}
}
return maxValue[m];
}
}
😀总结
动态规划问题没有固定的解题模板,需要具体问题具体分析。
我们在分析问题过程中发现,动态规划问题,最难定义的就是问题的状态,那么,动态规划问题中的状态如何定义呢?
🌈动态规划状态定义
状态来源:从问题中抽象状态,一维状态或者二维状态
抽象状态:每一个状态对应一个子问题
🌈状态的形式可以定义很多,如何验证状态的合理性
1.某一个状态的解或者多个状态处理之后能否对应问题的最终解
2.状态之间要可以形成递推关系
🌈常见问题的状态
字符串:状态一般对应子串,状态中每次一般增加一个新的字符
矩阵:二维状态 👉 优化 👉 一维状态