算法描述
通过把原问题分解为相对简单的子问题来求解复杂问题。动态规划常常适用于有重叠子问题和最优子结构性质的问题。
算法总体思想
- 动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题
- 与分治法的区别在于:适用于动态规划算法求解的问题,经分解得到的子问题往往不是互相独立的;若用分治法求解,则分解得到的子问题数目太多,导致最终解决原问题需指数时间, 原因在于:虽然子问题的数目常常只有多项式量级,但在用分治法求解时,有些子问题被重复计算了许多次
- 如果可以保存已解决的子问题的答案,就可以避免大量重复计算,从而得到多项式时间的算法
- 动态规划法的基本思路是:构造一张表(dp数组)来记录所有已解决的子问题的答案(无论算法形式如何,其填表格式是相同的)
算法的基本步骤
- 找出最优解的性质(分析其结构特征): 状态转移方程
- 递归地定义最优值(优化目标函数):当前状态
- 以自底向上的方式计算出最优值
- 根据计算最优值时得到的信息,构造最优解
算法的基本要素:
- 最优子结构
- 在分析问题的最优子结构性质时,所用的方法具有普遍性:首先假设由问题的最优解导出的子问题的解不是最优的;然后设法证明在该假设下可构造出比原问题最优解更好的解;通过矛盾法证明由最优解导出的子问题的解也是最优的
- 解题方法:利用问题的最优子结构性质,以自底向上的方式递归地从子问题的最优解逐步构造出整个问题的最优解
最优子结构是问题能用动态规划算法求解的前提:同一个问题可以有多种方式刻划它的最优子结构,有些表示方法的求解速度更快(空间占用小,问题的维度低)
- 重叠子问题
- 子问题的重叠性:采用递归算法求解问题时,产生的子问题并不总是独立的,有些子问题被反复计算多次,称为子问题的重叠性质
- 动态规划算法的特点:对每一个子问题只求解一次,并将结果保存在一个表格中;当再次需要求解该子问题时,可以用常数时间查表得出结果;通常独立的子问题个数随问题的规模呈多项式增长;因此采用动态规划算法求解此类问题只需要多项式时间,因而解题效率较高
- 备忘录方法
- 备忘录方法是动态规划算法的一种变形,它也用表格来保存已解决的子问题答案,以避免重复计算
- 与动态规划的区别在于,备忘录方法的递归方式是自顶向下的
- 备忘录方法的控制结构与直接递归方法的控制结构相同,区别在于备忘录方法为每个解过的子问题建立了备忘录,以备需要时查看,从而避免了相同子问题的重复求解!
算法示例:
当前面值所需要的最小硬币数量
package com.AdvancedDataStructure.DP;
/**
* @Created with IntelliJ IDEA
* @Description: DP解决所需价值的最小硬币数
* 有面值1,3,5分的三种硬币,现在给出一个价值c,问组成价值c最少需要的硬币数量。
* 假设c为11,当我们求c所需要硬币的最小数目时,我们需要知道c-1 c-3 c-5 价值时,
* 所需要的硬币数量。因此是一道典型的对子规模求解的问题。
* 有三个量我们是知道的,当c为1 3 5 .因此我们可以在他们的基础上进行求解,
* 由此得到dp方程 dp[c]=Max(dp[c-1],dp[c-3],dp[c-5])
* @Package: com.AdvancedDataStructure.DP
* @author: FLy-Fly-Zhang
* @Date: 2019/7/17
* @Time: 21:02
*/
public class DPdemo1 {
static int [] dp;
//dp递归
private static int minNum(int [] v,int num){
if(num<=0)
return 0;
//防止子问题重复求解
if(dp[num]!=0)
return dp[num];
//当前状态下dp所能得到的最大值
dp[num]=num;
for (int i = 0; i < v.length; i++) {
if(v[i]<num){
int n=minNum(v,num-v[i]);
if(n<dp[num])
dp[num]=n+1;
}
}
return dp[num];
}
public static void main(String[] args) {
int [] v={1,3,5};
int num=11; //面额
dp=new int[num+1]; //dp数组
System.out.println(minNum(v,num));
for (int i = 1; i <= num; i++) {
//当前面值最多需要i个一元硬币
dp[i]=i;
for (int j = 0; j < v.length; j++) {
if(i>=v[j]&&dp[i-v[j]]+1<dp[i]){
dp[i]=dp[i-v[j]]+1;
}
}
}
System.out.println(dp[num]);
}
}
连续数组最大和:
package com.AdvancedDataStructure.DP;
/**
* @Created with IntelliJ IDEA
* @Description: 最大连续子数组和,当所给整数均为负数时和为0.
* 我们想要求当前子数组的和,就需要得到前一个子数组的和,
* 最小到我们将每一个元素都认为是一个连续子数组,若连续子数组<0
* 则置为0。
* 由此我们可以得到方程:dp[i]=dp[i-1]+array[1]
* @Package: com.AdvancedDataStructure.DP
* @author: FLy-Fly-Zhang
* @Date: 2019/7/20
* @Time: 18:46
*/
public class DPDemo2 {
//常规dp数组
public static int maxSumSubSgament(int[] array){
if(array.length==0)
return 0;
int[] dp=new int[array.length];
//得到第一个dp状态
dp[0]=array[0];
if(array[0]<0){
dp[0]=0;
}
int maxSum=dp[0]; //记录当前最大的dp状态
for (int i = 1; i < array.length; i++) {
if(dp[i-1]+array[i]<0){
dp[i]=0;
}else{
dp[i]=dp[i-1]+array[i];
if(dp[i]>maxSum)
maxSum=dp[i];
}
}
return maxSum;
}
public static int maxSumSubSgament1(int[] array){
if(array==null||array.length==0)
return 0;
int maxSum=array[0]>0?array[0]:0;
int nowSum=maxSum;
for (int i = 1; i <array.length; i++) {
if(array[i]+nowSum>0){
nowSum=array[i]+nowSum;
}else{
nowSum=0;
}
maxSum=maxSum>nowSum?maxSum:nowSum;
}
return maxSum;
}
public static void main(String[] args) {
int [] arr={-2,11,-4,13,-5,-2};
System.out.println(maxSumSubSgament1(arr));
}
}
三角数组最大和
package com.AdvancedDataStructure.DP;
/**
* @Created with IntelliJ IDEA
* @Description: 有一个三角数组,如
* 5
* 12 6
* 7 13 18
* 12 14 10 9
* 从最上面开始,每层选择一个元素,每个元素可以向下,向左斜下,向右斜下
* 求最终选择出来的数字的最大值。
* 每个元素都有来自三个状态元素的值,在我们从上向下查找时,只需要找到三个
* 状态元素值最大的,并加上自己,那么就是他自己的状态。
* 由此我们可以得到一个状态转移方程
* dp[i][j]=Max(dp[i+1][j-1],dp[i+1][j1],dp[i+1][j+1])]+arr[i][j]
* 并且i=arr.length-1这一行的元素,它的三个状态方程都是0,因此这一行元素的
* dp状态就是已知的。
* @Package: com.AdvancedDataStructure.DP
* @author: FLy-Fly-Zhang
* @Date: 2019/7/20
* @Time: 19:16
*/
public class DPDemo3 {
private static int[][] dp;
//递归解法
private static int maxSum(int [][] arr,int i,int j){
//界限判断
if(i<0||i>=arr.length||j<0 || j>=arr[i].length){
return 0;
}
//防止子规模重复求解
if(dp[i][j]!=0)
return dp[i][j];
//row-1 行的子规模是已知的
if(i==arr.length-1){
dp[i][j]=arr[i][j];
return dp[i][j];
}
//找到子规模最大值
int down=maxSum( arr,i+1,j);
int right=maxSum( arr,i+1,j+1);
int left=maxSum( arr,i+1,j-1);
dp[i][j]=Math.max(Math.max(left,down),right)+arr[i][j];
return dp[i][j];
}
//非递归解法
private static int maxSum(int[][] arr){
//从已知的最小自规模开始。
for (int i = arr.length-1-1; i>=0; --i) {
for (int j = 0; j < arr[i].length; j++) {
//对前两个不用判断,因此row和col是等长的,
//当前循环row=row-1。因此是不会越界的。
int down=dp[i+1][j];
int right=dp[i+1][j+1];
int left=0;
if(j>0){
left=dp[i+1][j-1];
}
dp[i][j]=Math.max(Math.max(left,down),right)+arr[i][j];
}
}
return dp[0][0];
}
public static void main(String[] args) {
int [][] arr={
{5},
{12, 6},
{7,13,18},
{12,14,10,9}
};
dp=new int[arr.length][arr.length];
System.out.println(maxSum(arr, 0, 0));
//对已知dp状态进行赋值
int row=arr.length-1;
for (int i = 0; i < arr[row].length; i++) {
dp[row][i]=arr[row][i];
}
System.out.println(maxSum(arr));
System.out.println(num);
}
}
求两个序列的最长公共子序列(不一定连续)的长度
package com.AdvancedDataStructure.DP;
/**
* @Created with IntelliJ IDEA
* @Description: 求两个序列的最长公共子序列(不一定连续)的最大长度
* 这种判断是否相等的题与前面不一样.当其当前指针相等时,有自己子规模
* 当不相等时,又有不同的子规模.因此需要分别处理.
* 其动态规划方程为:
* dp[i][j]=dp[i-1][j-1]+1 (str1[i]=str2[j])
* dp[i][j]=Math.max(dp[i-1][j]+dp[i][j-1])+1 (str1[i]!=str2[j])
* @Package: com.AdvancedDataStructure.DP
* @author: FLy-Fly-Zhang
* @Date: 2019/7/20
* @Time: 20:27
*/
public class DPDemo4 {
static int nowNum=0;
static int MaxNum=0;
public static int func(int i, int j, String str1, String str2, int[][] dp){
if(i<0||j<0){
return 0;
}
//防止子规模重复
if(dp[i][j]!=0)
return dp[i][j];
if(str1.charAt(i)==str2.charAt(j)){
//两个子串同时递减
dp[i][j]= 1+func(i-1,j-1, str1, str2, dp);
}else{
//不相等时,要么str1指针移动,要么str2指针移动,
//找到两个子规模的最大值就好.
dp[i][j]=Math.max(func(i-1,j, str1, str2, dp),func(i,j-1, str1, str2, dp));
return dp[i][j];
}
public static void main(String[] args) {
String str1="helloworld";
String str2="helxtld";
int[][] dp=new int[str1.length()][str2.length()];
System.out.println(func(str1.length()-1,str2.length()-1,str1,str2,dp));
}
}
求两个序列的最长公共子序列(连续)的长度
package com.AdvancedDataStructure.DP;
/**
* @Created with IntelliJ IDEA
* @Description: 求两个序列的最长连续公共子序列(连续)的长度,并且打印出来
* 这种判断是否相等的题与前面不一样.当其当前指针相等时,有自己子规模
* 当不相等时,又有不同的子规模.因此需要分别处理.
* 其动态规划方程为:
* dp[i][j]=dp[i-1][j-1]+1 (str1[i-1]=str2[j-1])
* dp[i][j]=0 (str1[i-1]=str2[j-1])
* @Package: com.AdvancedDataStructure.DP
* @author: FLy-Fly-Zhang
* @Date: 2019/7/20
* @Time: 20:27
*/
public class DPDemo5 {
static int max;
static int idx;
static int num;
//递归
private static int func(String s1,String s2,int i,int j,int[][] dp){
//i/j实际上是dp的i/j其长度是比对应字符串长度大一个.
if(i<=0||j<=0)
return 0;
//避免子问题解重复
if(dp[i][j]!=0)
return dp[i][j];
if(s1.charAt(i-1)==s2.charAt(j-1)){
dp[i][j]=func(s1,s2,i-1,j-1,dp)+1;
if(max<dp[i][j]){
max=dp[i][j];
idx=i;
}
}else{
func(s1,s2,i,j-1,dp);
func(s1,s2,i-1,j,dp);
}
return dp[i][j];
}
//递归2
private static int func1(String s1,String s2,int i,int j,int[][] dp){
if(i<0||j<0)
return 0;
if(s1.charAt(i)==s2.charAt(j)){
dp[i][j]=func1(s1,s2,i-1,j-1,dp)+1;
if(max<dp[i][j]){
max=dp[i][j];
idx=i;
}
}else{
func1(s1,s2,i,j-1,dp);
func1(s1,s2,i-1,j,dp);
}
return dp[i][j];
}
//非递归
private static void func(String s1,String s2,int[][] dp){
int max=0;
int idx=0;
for (int i = 1; i <= s1.length(); i++) {
for (int j = 1; j <=s2.length(); j++) {
if(s1.charAt(i-1)==s2.charAt(j-1))
dp[i][j]=dp[i-1][j-1]+1;
if(max<dp[i][j]){
max=dp[i][j];
idx=i;
}
}
}
System.out.println(s1.substring(idx-max,idx));
}
public static void main(String[] args) {
String str1="helloworld";
String str2="helxtld";
//两个字符串的矩阵,大一列是为了解决第一个字符串不匹配的问题.
int[][] dp=new int[str1.length()+1][str2.length()+1];
func(str1,str2,str1.length(),str2.length(),dp);
System.out.println(max);
System.out.println(idx);
//substring包含头不包含尾
System.out.println(str1.substring(idx-max,idx));
System.out.println("递归二");
max=0;
idx=0;
func1(str1,str2,str1.length()-1,str2.length()-1,dp);
System.out.println(max);
System.out.println(idx);
//substring包含头不包含尾
System.out.println(str1.substring(idx-max+1,idx+1));
}
}