动态规划通过把一个复杂问题分阶段进行简化,逐步简化为多个简单的小问题。大事化小,小事化了。
动态规划问题中包含三个重要概念
- 最优子结构(理解为对问题合理拆分的第一步)
- 边界(最简单不用计算就可以得出结果的那一步)
- 状态转移公式(每一阶段和下一阶段的关系)
对于动态规划的讲解:https://www.sohu.com/a/153858619_466939#comment_area
动态规划问题需要满足的条件
- 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理
- 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关
- 有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
leetcode 70.Climbing Stairs
https://leetcode.com/problems/climbing-stairs/description/
You are climbing a stair case. It takes n steps to reach to the top.
Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?
Note: Given n will be a positive integer.
Example 1:
Input: 2
Output: 2
Explanation: There are two ways to climb to the top.
1. 1 step + 1 step
2. 2 steps
Example 2:
Input: 3
Output: 3
Explanation: There are three ways to climb to the top.
1. 1 step + 1 step + 1 step
2. 1 step + 2 steps
3. 2 steps + 1 step
爬楼梯,可以走一步或者2步,问一共有多少种方法,爬到第n节台阶相当于是从第n-1节爬一步或者是从第n-2节爬两步这两者的和,边界是如果只走一层台阶或者是只走两层台阶。
class Solution {
public int climbStairs(int n) {
//动态规划 爬楼梯问题
if(n==1)
return 1;
if(n==2)
return 2;
int[] climb=new int[n+1];
climb[0]=0;
climb[1]=1;
climb[2]=2;
for(int i=3;i<=n;i++){
climb[i]=climb[i-1]+climb[i-2];
}
return climb[n];
}
}
leetcode 198. House Robber
https://leetcode.com/problems/house-robber/description/
You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security system connected and it will automatically contact the police if two adjacent houses were broken into on the same night.
Given a list of non-negative integers representing the amount of money of each house, determine the maximum amount of money you can rob tonight without alerting the police.
Example 1:
Input: [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3).
Total amount you can rob = 1 + 3 = 4.
Example 2:
Input: [2,7,9,3,1]
Output: 12
Explanation: Rob house 1 (money = 2), rob house 3 (money = 9) and rob house 5 (money = 1).
Total amount you can rob = 2 + 9 + 1 = 12.
最终获得的钱取决于抢不抢当前的房子以及各种抢之前的房子得到了多少钱
如果不抢这个房子,那它前面的房子可以被抢也可以不被抢
class Solution {
public int rob(int[] nums) {
if(nums.length==0)
return 0;
//抢劫这间房子获得的钱
int[] robbed=new int[nums.length];
//不抢这间房子获得的钱
int[] nrobbed=new int[nums.length];
robbed[0]=nums[0];
nrobbed[0]=0;
for(int i=1;i<nums.length;i++){
//如果当前的房子被抢了的话,那么它前面的房子一定不能被墙
robbed[i]=nums[i]+nrobbed[i-1];
//如果不抢当前的房子,那么它前面的那座房子可以被抢也可以不被抢
nrobbed[i]=Math.max(robbed[i-1],nrobbed[i-1]);
}
return Math.max(robbed[nums.length-1],nrobbed[nums.length-1]);
}
}
leetcode 53. Maximum Subarray
https://leetcode.com/problems/maximum-subarray/description/
Given an integer array nums
, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.
Example:
Input: [-2,1,-3,4,-1,2,1,-5,4],
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.
Follow up:
If you have figured out the O(n) solution, try coding another solution using the divide and conquer approach, which is more subtle.
class Solution {
public int maxSubArray(int[] nums) {
//采用动态规划的方法,可以选择它本身或者是加上前面的值,每一步都找最优解
int result=0;
if(nums.length==0)
return result;
int max[]=new int[nums.length];
max[0]=nums[0];
result=max[0];
for(int i=1;i<nums.length;i++){
max[i]=Math.max(max[i-1]+nums[i],nums[i]);
result=Math.max(result,max[i]);
}
return result;
}
}
仍然需要注意一些特别的边界条件。
leetcode 309. Best Time to Buy and Sell Stock with Cooldown
https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/
Say you have an array for which the ith element is the price of a given stock on day i.
Design an algorithm to find the maximum profit. You may complete as many transactions as you like (ie, buy one and sell one share of the stock multiple times) with the following restrictions:
- You may not engage in multiple transactions at the same time (ie, you must sell the stock before you buy again).
- After you sell your stock, you cannot buy stock on next day. (ie, cooldown 1 day)
Example:
Input: [1,2,3,0,2]
Output: 3
Explanation: transactions = [buy, sell, cooldown, buy, sell]
一道动态规划问题,对股票进行买卖操作,但特殊的一点是中间有一个冷却期,即在卖出股票的第二天不能买入股票
对于状态转移方程一直没有搞明白,看到一篇文章采用状态机的方法说明了各个状态之间的转移关系,非常清晰明了
https://www.cnblogs.com/jdneo/p/5228004.html
s0代表未买入的状态,s1代表买入未卖出的状态,s2代表已卖出的状态
可得状态转移方程:
s0[i]=Math.max(s0[i-1],s2[i-1]);
s1[i]=Math.max(s1[i-1],s0[i-1]-prices[i]);
s2[i]=s1[i-1]+prices[i];
class Solution {
public int maxProfit(int[] prices) {
//好吧 要开始上这道middle了 简单题全都搞完了
if(prices.length<=1)
return 0;
int[] s0=new int[prices.length];//未买入的状态
int[] s1=new int[prices.length];//买入后待卖出的状态
int[] s2=new int[prices.length];//卖出后的状态
s0[0]=0;
s1[0]=-prices[0];
s2[0]=Integer.MIN_VALUE;
for(int i=1;i<prices.length;i++){
s0[i]=Math.max(s0[i-1],s2[i-1]);
s1[i]=Math.max(s1[i-1],s0[i-1]-prices[i]);
s2[i]=s1[i-1]+prices[i];
}
return Math.max(s0[prices.length-1],s2[prices.length-1]);
}
}
leetcode 322. Coin Change
You are given coins of different denominations and a total amount of money amount. Write a function to compute the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1
.
Example 1:
Input: coins =[1, 2, 5]
, amount =11
Output:3
Explanation: 11 = 5 + 5 + 1
Example 2:
Input: coins =[2]
, amount =3
Output: -1
Note:
You may assume that you have an infinite number of each kind of coin.
题目要求选出最少的硬币组合的个数,每一个总额产生的结果都是最优的,当前状态不会影响之前的状态信息,并且用重叠性,符合动态规划的条件。
class Solution {
private int getNum(int pos,int[] coins,int[] nums){
//例如5=1+4=2+3
int result=nums[pos];
if(pos==0)
return nums[pos];
int low=0,high=pos-1;
while(low<=high){
result=Math.min(nums[low]+nums[high],result);
++low;
--high;
}
return result;
}
public int coinChange(int[] coins, int amount) {
//如果金额总额小于等于0,则只需要0个硬币即可
if(amount<=0)
return 0;
int[] nums=new int[amount];
//对每个总额需要的数字赋初值
//若恰是硬币的金额且此硬币的面值比总金额小,那么赋值为1
//否则赋值为amount+1,用于最终检测是否可以找到合适的组合方式
for(int i=0;i<coins.length;i++){
if(coins[i]<=amount){
nums[coins[i]-1]=1;
}
}
for(int i=0;i<amount;i++){
if(nums[i]!=1)
nums[i]=amount+1;
}
//从前到后得到每个金额最少使用的硬币的数量
for(int i=0;i<amount;i++){
nums[i]=getNum(i,coins,nums);
}
//如果硬币数量比amount还大的话则返回-1
if(nums[amount-1]<=amount)
return nums[amount-1];
else
return -1;
}
}
前面添加的代码段不能删掉也是很烦了。
solution中使用的方法也是动态规划,但动态规划的递推关系不太一样。为:
F(n)=F(n-s)+1
st.n-s>0
s为coins中的元素
leetcode 338. Counting Bits
Given a non negative integer number num. For every numbers i in the range 0 ≤ i ≤ num calculate the number of 1's in their binary representation and return them as an array.
Example 1:
Input: 2
Output: [0,1,1]
Example 2:
Input: 5
Output: [0,1,1,2,1,2]
Follow up:
- It is very easy to come up with a solution with run time O(n*sizeof(integer)). But can you do it in linear time O(n) /possibly in a single pass?
- Space complexity should be O(n).
- Can you do it like a boss? Do it without using any builtin function like __builtin_popcount in c++ or in any other language.
题目要求输出转化为二进制的结果中1的个数,明确讲出不能一个数一个数的遍历得出结果,此问题符合动态规划的三个条件,于是找出如下规律
代码如下:
class Solution {
public int[] countBits(int num) {
int[] result=new int[num+1];
result[0]=0;
if(num==0) return result;
result[1]=1;
if(num==1) return result;
int index=2;
for(int i=1;;i++){
int j=0;
while(j<(int)(Math.pow(2,i))){
result[index]=result[j]+1;
index++;
j++;
if(index>num) return result;
}
}
//unreachable statement
//return result;
}
}
在discuss中看到一个解决方法,太神奇了吧,omg
public int[] countBits(int num) {
int[] f = new int[num + 1];
for (int i=1; i<=num; i++) f[i] = f[i >> 1] + (i & 1);
return f;
}
原理是:
f[1] = (f[0]==0) + (1%1==1) = 1
f[11] = (f[1]==1) + (11%1==1) = 2
f[110] = (f[11]==2) + (110%1==0) = 2
f[1101] = (f[110] ==2) + (1101%1==1) =3;
...
将数转化为二进制发现其中的规律,并通过位运算得到结果,比自己单纯的找规律要好太多了!
leetcode 221. Maximal Square
Given a 2D binary matrix filled with 0's and 1's, find the largest square containing only 1's and return its area.
Example:
Input:
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
Output: 4
题目要求找到二维数组中由1组成的最小正方形的面积,此题目依旧符合动态规划的三个条件,每一个都是最优,当前的不影响之前的,会重复利用。
开辟一个与所给矩阵大小相同的矩阵空间,记录此点能够产生的最大正方形的边长,从第一行开始,从左到右,逐步推导,最左边的一列和最上面的一行的边长肯定是它本身,对于内部的点,若是0,则边长也为0,若为1的话,则考虑周围(即左方,上方,左上方是否边长也为1或者更大,取它们之间的最小值加1围成更大的正方形,并且永远记录这些记录边长的值中的最大值,(记录的是这个位置往左上看的正方形),最终通过最大边长计算最大面积。
有一点需要注意的是,在题目中没有体现出来,但在给的方法中体现出来了,传入的矩阵是char类型的,需要转换为int类型的值才能得到正确结果!!!
class Solution {
public int maximalSquare(char[][] matrix) {
if(matrix.length==0) return 0;
if(matrix[0].length==0) return 0;
int result=0;
//记录char类型转化为int类型的数组
int[][] matrix2=new int[matrix.length][matrix[0].length];
int[][] count=new int[matrix.length][matrix[0].length];
for(int i=0;i<matrix2.length;i++){
for(int j=0;j<matrix2[0].length;j++){
//将char类型的数字转化为int类型的数字
matrix2[i][j]=matrix[i][j]-'0';
if(i==0||j==0){
count[i][j]=matrix2[i][j];
}
else{
if(matrix2[i][j]==0)
count[i][j]=0;
else
count[i][j]=Math.min(count[i][j-1],Math.min(count[i-1][j],count[i-1][j-1]))+matrix2[i][j];
}
//System.out.println(count[i][j]);
result=Math.max(result,count[i][j]);
}
}
return result*result;
}
}
leetcode 152. Maximum Product Subarray
Given an integer array nums
, find the contiguous subarray within an array (containing at least one number) which has the largest product.
Example 1:
Input: [2,3,-2,4]
Output: 6
Explanation: [2,3] has the largest product 6.
Example 2:
Input: [-2,0,-1]
Output: 0
Explanation: The result cannot be 2, because [-2,-1] is not a subarray.
题目要求是找出数组中连续的子子数组的最大乘积(只需要位置相邻即可,不需要数字的大小连续!!隔了一晚上自己才审明白题目!!!)
由于数组中存在负数,最大值可能会变成最小值,所以要维护一个当前的最大值和当前的最小值两个表。
单单从动态规划的角度考虑的话可能会思考是否需要扩展数组,情况不免就很复杂了,看到一篇文章说的很好,需要从大局上思考,就像物理上的运动过程很杂乱,但是如果从功,能的角度考虑可能就豁然开朗了,写程序也是如此呀~
class Solution {
public int maxProduct(int[] nums) {
if(nums.length<1)
return 0;
int[] max=new int[nums.length];
int[] min=new int[nums.length];
max[0]=nums[0];
min[0]=nums[0];
int ans=nums[0];
for(int i=1;i<nums.length;i++){
max[i]=Math.max(max[i-1]*nums[i],Math.max(min[i-1]*nums[i],nums[i]));
min[i]=Math.min(max[i-1]*nums[i],Math.min(min[i-1]*nums[i],nums[i]));
ans=Math.max(ans,max[i]);
}
return ans;
}
}
由于此问题只使用到前一个位置的最大值和最小值,所以其实不用开辟一片空间,只需要维护之前的最大值和之前的最小值即可。
class Solution {
public int maxProduct(int[] nums) {
if(nums.length<1)
return 0;
int premax=nums[0];
int premin=nums[0];
int curmax=nums[0];
int curmin=nums[0];
int ans=nums[0];
for(int i=1;i<nums.length;i++){
curmax=Math.max(premax*nums[i],Math.max(premin*nums[i],nums[i]));
curmin=Math.min(premax*nums[i],Math.min(premin*nums[i],nums[i]));
premax=curmax;
premin=curmin;
ans=Math.max(ans,curmax);
}
return ans;
}
}
另外,在一片博客中看到说这个题目的测试用例还是很仁慈的,未考虑:
如果出现0-1之间的小数,上述的两种方式依旧成立,但是如果通过考虑细节是否扩展数组那就相当麻烦了;乘法会出现溢出的情况,需要用类似Java中的bigInteger类进行处理。
leetcode 64. Minimum Path Sum
Given a m x n grid filled with non-negative numbers, find a path from top left to bottom right which minimizes the sum of all numbers along its path.
Note: You can only move either down or right at any point in time.
Example:
Input: [ [1,3,1], [1,5,1], [4,2,1] ] Output: 7 Explanation: Because the path 1→3→1→1→1 minimizes the sum.
找到达终点的最短路径,这个题目和之前的找全部由1组成的最小的正方形很相似,也是从上到下,从左往右维护一个矩阵。相对还简单一些,不可以斜着走
class Solution {
public int minPathSum(int[][] grid) {
int[][] step=new int[grid.length][grid[0].length];
if(grid.length<1) return 0;
if(grid[0].length<1) return 0;
for(int i=0;i<grid.length;i++){
for(int j=0;j<grid[0].length;j++){
//first row
if(i==0){
//first row,first column
if(j==0)
step[i][j]=grid[i][j];
else
step[i][j]=step[i][j-1]+grid[i][j];
}
else{
//first column
if(j==0)
step[i][j]=step[i-1][j]+grid[i][j];
else
step[i][j]=(int)Math.min(step[i-1][j],step[i][j-1])+grid[i][j];
}
}
}
return step[grid.length-1][grid[0].length-1];
}
}