递归和动态规划专题(二)—-剑指offer+左程云算法
【前言】1.动态规划算法是从暴力搜索算法优化过来的,如果我们不清楚暴力搜索的过程,就难以理解动态规划的实现,当我们了解了动态规划算法的基本原理的文字概述,实现条件之后,这时可能并不是太理解这种思想,去面对实际问题的时候也是无从下手,这个时候我们不能停留在文字层面上,而应该去学习经典动态规划算法的实现,然后倒回来看这些概念,便会恍然大悟。
2.动态规划算法的难点在于 从实际问题中抽象出动态规划表dp,dp一般是一个数组,可能是一维的也可能是二维的,也可能是其他的数据结构。
(一).矩阵的最小路径和
【题目】给定一个矩阵m,从左上角开始每次只能向右走或者向下走,最后达到右下角的位置,路径中所有数字累加起来就是路径和,返回所有路径的最小路径和,如果给定的m如下,那么路径1,3,1,0,6,1,0就是最小路径和,返回12.
1 3 5 9
8 1 3 4
5 0 6 1
8 8 4 0
这是经典的动态规划问题:假设m是m行n列的矩阵,那么我们用dp[m][n]来抽象这个问题,dp[i][j]表示的是从原点到i,j位置的最短路径和。我们首先计算第一行和第一列,直接累加即可,那么对于其他位置,要么是从它左边的位置达到,要么是从上边的位置达到,我们取左边和上边的较小值,然后加上当前的路径值,就是达到当前点的最短路径。也就是说问题分为三种情况:第一行dp[0][j] 第一列 dp[i][0] 和非第一行第一列dp[i][j]. dp矩阵称为动态规划表!
import java.util.*;
public int minPathSum1(int[][] m){
if(m==null||m.length==0||m[0]==null||m[0].length==null){
return 0;
}
rows=m.length;
cols=m[0].length;
int[][] dp=new int[rows][cols];
dp[0][0]=m[0][0];
//第一行都是前面的累加
for(int j=1;j0][j] = dp[0][j-1]+m[0][j];
}
//第一列都是上面的累加
for(int i=1;i0] = dp[i-1][0]+m[i][0];
}
//构建动态规划表dp
for(int i=2;ifor(int j=2;j1][j],dp[i][j-1])+m[i][j];
}
}
return dp[rows-1][cols-1];
}
该方法时间复杂度为O(N*M),空间复杂度由于新建了动态规划表dp,所以为O(N*M)!但是动态规划可以通过空间压缩的方法将额外空间复杂度变成O(min{M,N}),也就是不使用M*N的dp矩阵,而是使用arr[]一维数组。
import java.util.*;
public int minPathSum1(int[][] m){
if(m==null||m.length==0||m[0]==null||m[0].length==null){
return 0;
}
int more = Math.max(m.length,m[0].length);//行数和列数较多的那个
int less = Math.min(m.length,m[0].length);//行数和列数较少的那个
int[] arr= new int[less];
boolean rowMore = m.length==more;//判断是行数多还是列数多
arr[0]==m[0][0];
//让第一行或第一列赋值给arr数组
for(int i=1;i1]+(rowMore ? m[0][i]:m[i][0]);
}
//arr数组往下滚 从arr[0]开始逐渐得到该行的最短路径;
for(int i=1;i0]=arr[0]+(rowMore ? m[i][0]:m[0][i]);
for(int j=1;jmin(arr[j-1],arr[j])+(rowMore?m[i][j]:m[j][i]);
}
}
return arr[less-1];
}
(二).换钱的方法
【题目】给定数组arr,arr中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim代表要找的钱数,求换钱有多少种方法。
【举例】
arr=[5,10,25,1],aim=0。组成0元的方法有1种,就是所有面值的货币都不用。所以返回1。
arr=[5,10,25,1],aim=15。组成15元的方法有6种,分别为3张5元,1张10元+1张5元,1张10元+5张1元,10张1元+1张5元,2张5元+5张1元,15张1元。所以返回6。
arr=[3,5],aim=2。任何方法都无法组成2元。所以返回0。
这个题目讲的是从暴力递归到动态规划:
递归 –> 记忆化搜索(备忘录方法)– dp –> dp状态合并!在这里直接给出动态规划基础方法及动态规划的优化方法!
【分析】生成行数为N,列数为aim+1的动态规划表矩阵dp,dp[i][j]的含义表示使用arr[0…i]种货币组成要找的数j有多少种?
1.完全不用arr[i],用arr[0….i-1]货币组成j;
2.用一张arr[i],用arr[0….i-1]货币组成j-arr[i];
………
k+1.用k张arr[i],用arr[0….i-1]货币组成j-k*arr[i] (j-k*arr[i]>=0);
【注意】通过分析我们可以发现上述的第三种情况可以简化为dp[i][j] = dp[i-1][j]+dp[i][j-arr[i]];最终代码如下,时间复杂度为O(N*aim),空间复杂度为O(N*aim);
import java.util.*;
public class Exchange {
public int countWays(int[] penny, int n, int aim) {
if(penny==null||penny.length==0||aim==0){
return 0;
}
int[][] dp=new int[penny.length][aim+1];
//第一列,均为0;
for(int i=0;i0 ]=1;
}
//第一行,k*arr[0]为0;
for(int j=0;j*penny[0]1;j++){
dp[0][j*penny[0]]=1;
}
for(int i=1;ifor(int j=1;j1;j++){
dp[i][j]=dp[i-1][j];
dp[i][j]+=j-penny[i]>=0?dp[i][j-penny[i]]:0;
}
}
return dp[penny.length-1][aim];
}
}
【注意】通过动态规划的空间压缩,把dp矩阵用dp一维矩阵表示,滚下一行!使额外空间复杂度变为O(aim)方法。
import java.util.*;
public class Exchange {
public int countWays(int[] penny, int n, int aim) {
if(penny==null||penny.length==0||aim==0){
return 0;
}
int[] dp=new int[aim+1];
for(int i=0;i*penny[0]<=aim;i++){
dp[i*dp[0]]=1;
}
for(int i=1;ifor(int j=1;j<=aim;j++){
dp[j]+=j-penny[i]>=0?dp[j-penny[i]]:0;
}
}
return dp[aim];
}
}
(三).记录几道《剑指offer》中与连续子序列(连续子数组)有关的题目!
【题目】HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。你会不会被他忽悠住?(子向量的长度至少是1)
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
//记住:整数最大值为:2^31-1 0x7FFFFFFF 最小值:-2^31 0x80000000
if(array==null||array.length==0){
return 0;
}
int sum=0;
int greatSum=0x80000000;
for(int i=0;i//如果sum小于0,把之前累加的都去除
if(sum<=0){
sum=array[i];
}else{
sum+=array[i];
}
if(sum>greatSum){
greatSum = sum;
}
}
return greatSum;
}
}
【题目】小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列?
Good Luck!
import java.util.ArrayList;
public class Solution {
public ArrayList > FindContinuousSequence(int sum) {
//和为S的连续正数序列;
ArrayList > arrayList = new ArrayList > ();
if(sum<3){
return arrayList;
}
int small=1;
int big=2;
int middle = (1+sum)/2;
int s=small+big;
//至少包括俩个数,所以由此可以把small
while(smallif (s==sum){
arrayList.add(Sequence(small,big));
}
//当系列和大于sum以及small仍小于middle时;
while(s>sum&&smallif(s==sum){
arrayList.add(Sequence(small,big));
}
}
big++;
s+=big;
}
return arrayList;
}
public ArrayList Sequence(int small,int big){
ArrayList list = new ArrayList();
if(smallfor(int i=small;i<=big;i++){
list.add(i);
}
}
return list;
}
}
【题目】输入一个递增排序的数组和一个数字S,在数组中查找两个数,是的他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
import java.util.ArrayList;
public class Solution {
public ArrayList FindNumbersWithSum(int [] array,int sum) {
//因为递增排序,所以考虑从左右俩边开始往中间遍历;
ArrayList list= new ArrayList();
if(array==null||array.length<2){
return list;
}
int i=0;
int j=array.length-1;
while(iint s=array[i]+array[j];
if(s>sum){
j--;
}else if(selse if(s==sum){
list.add(array[i]);
list.add(array[j]);
return list;
}
}
return list;
}
}
(四).最长上升子序列问题(LIS)
【注意】为什么上面第三点要记录《剑指offer》里的子序列题目呢?因为我刷到这道题时发现思路和上面的几道题相似!然而非也,我错以为最长上升子序列为最长连续上升子序列,所以最优解仍然需要使用动态规划!
如果为最长连续上升子序列:
import java.util.*;
public class LongestIncreasingSubsequence {
public int getLIS(int[] A, int n) {
//记录上升子序列的长度
int count=1;
if(A==null||n==0){
return 0;
}
int i=0;
int greatNum=1;
for(int j=0;j1;j++){
if(A[j]>=A[j+1]){
count=1;
}else{
count++;
}
if(count>greatNum){
greatNum=count;
}
}
return greatNum;
}
}
按照题意,使用动态规划的解法:
import java.util.*;
public class LongestIncreasingSubsequence {
public int getLIS(int[] A, int n) {
// write code here
if (n <= 0)
return -1;
int[] dp = new int[n];
dp[0] = 1;
for (int i = 1; i < n; i++) {
int j = i - 1;
int max = 1;
while (j >= 0) {
if (A[i] > A[j]) {
if (dp[j] + 1 > max)
max = dp[j] + 1;
}
j--;
}
dp[i] = max;
}
int max = 1;
for (int i = 0; i < dp.length; i++) {
if (dp[i] > max)
max = dp[i];
}
return max;
}
}
****要回家啦,未完待续**