经典算法之动态规划(java详解)

本文还在更新

概述理论

1+1+1=3
1+1+1+1=? 4! 就是3+1呀,所以很快就算出来了
so 通过记住一些东西来节省时间-》动态规划

每个阶段只有一个状态->递推;
每个阶段的最优状态都是由上一个阶段的最优状态得到的->贪心;
每个阶段的最优状态是由之前所有阶段的状态的组合得到的->搜索;
每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到,而不管之前这个状态是如何得到的->动态规划。

最后一句话涵盖了Dp的两个性质,最优子结构,无后效性
状态很好理解,1+1+1=3就是一个状态,然后根据这个状态得到状态4,和一般递归很像,其实区分点就在这个阶段是不是有多个状态,就比如经典的01背包问题,有选和不选两个状态这两个状态最后选哪一个为这个阶段的状态是由前几个阶段的状态通过状态转移方程得出的,而贪心是前一个状态得出。
ok
现在从经典例题中理解,由浅入深总结做题方法

题目实战

斐波那锲数列
//1,斐波那锲数列(下一项等于前两项之和)
	public int feibonaqie(int n) {
		if(n==0) {
			return 1;
		}
		else if(n==1) {
			return 2;
		}
		else {
			int[] result=new int[n];//用来存储每一次的状态
			result[0]=1;
			result[1]=2;
			for(int i=2;i<n;i++) {
				result[i]=result[i-1]+result[i-2];
			}
			return result[n];
		}
		
	}

虽然我们这里是由上一个状态得到下一个状态并用一位数组存贮这些状态值,不过这个还体现不出来一个阶段很多状态,因为只用递归也能做

B站教程例1(工作时间组合求赚钱最多)

在这里插入图片描述
(红色数字是赚的钱)这里的阶段不就是从(黑色数字)1到8这8个选择阶段嘛,每个阶段都有选和不选这两种状态,这两种状态都有可能是最优状态,所以就需要想状态转移方程进行求解,其实就是比较两个哪个大,这两种状态的值是根据前面的某些状态得到的,不一定是上一个状态也可能是上两个,这就是和贪心的区别,贪心是只由上一个状态就能得到!
思路:首先根据时间知道选了谁就不能选谁,然后阶段最优状态的得出就是比较选和不选的哪个值大,不选的值就是前一个的值,选的值就是前一个prev的值加本身的值

public static int maxMoney(int[][] time,int[] money) {
		//先找出时间上合理的最近的那个前置节点保存到数组中
		int[] prev=new int[money.length];
		prev[0]=money.length;//代表前面没有前置的节点
		for(int i=1;i< money.length;i++) {
			prev[i]=money.length;//初始化为0
			for(int j=i-1;j>=0;j--) {
				if(time[j][1]<=time[i][0]) {
					prev[i]=j;//时间不相交
					break;
				}
			}
		}
		//然后从一开始计算每一个阶段的最优状态并保存到数组中
		int[] max=new int[money.length+1];
		max[0]=money[0];
		max[money.length]=0;
		for(int i=1;i< money.length;i++) {
			//判断选和不选那个值大
			//先判断有无前置
			if(max[i-1]>(money[i]+max[prev[i]])) {//如果不选大于选
				max[i]=max[i-1];
			}
			else {
				max[i]=money[i]+max[prev[i]];
			}
			
		}
		
		return max[money.length-1];
	}
	
B站教程例2(不相邻的数字组合值最大)

在这里插入图片描述
和上题很像,可以从左往右算出每一个阶段的最优状态值保存到数组

B站教程例3(数字组合相加为要求值,求这个组合)

在这里插入图片描述
在这里插入图片描述
这个题还是说的选和不选,不过值得注意的是当时用递归式,他的出口的选择
那么用Dp的话
在这里插入图片描述

public static boolean jioacheng3(int[] arr,int num) {
		//定义用来存储状态的二维数组,行代表arr中的数字,列代表0到num个状态
		//arr.length个阶段,每个阶段有num个状态,下一个阶段要用到上一个阶段的状态但是不一定是最优状态(这里是和贪心的区别)
		boolean[][] subset=new boolean[arr.length][num+1];
		for(int i=0;i<arr.length;i++) {
			for(int j=0;j<num+1;j++) {
				subset[i][j]=false;
			}
		}
		//第一列初始化,因为每一列都可以不选等于0
		for(int j=0;j<arr.length;j++) {
			subset[j][0]=true;
		}
		subset[0][arr[0]]=true;
		
		//初始化完毕,开始一个一个给数组赋值吧
		for(int i=1;i<arr.length;i++) {
			for(int j=1;j<num+1;j++) {
				if(arr[i]>j) {
					subset[i][j]=subset[i-1][j];
				}
				else {
					subset[i][j]=(subset[i-1][j-arr[i]]||subset[i-1][j]);
				}
			}
		}
		return subset[arr.length-1][num];
	}

蓝桥杯算法训练 K好数

题目:如果一个自然数N的K进制表示中任意的相邻的两位都不是相邻的数字,那么我们就说这个数是K好数。求L位K进制数中K好数的数目。例如K = 4,L = 2的时候,所有K好数为11、13、20、22、30、31、33 共7个。由于这个数目很大,请你输出它对1000000007取模后的值。

思路分析:借上题做一个二维数组dp[][]存储每一阶段每选择一个状态的数目,行是1~L位数,列是1-k-1每一位上可能的取值,这样dp[L][k-1]不就是总数,dp[2][2]的含义就是第二位数字是2的个数,不能相邻所以第一位不能是dp[1][1],因此dp[2][2]=dp[1][0]+dp[1][2],OK

	public static int Khaoshu(int K,int L) {
		int[][] resultK=new int[L][K];
		//初始化(行是位数,列是可能的数)
		resultK[0][0]=0;
		for(int i=1;i<K;i++) {
			resultK[0][i]=1;
		}
		for(int i=1;i<L;i++) {
			for(int j=0;j<K;j++) {
				for (int j2 = 0; j2 < K; j2++) {
					if(j2 != j-1 && j2 != j+1){
						resultK[i][j] += resultK[i-1][j2];
						resultK[i][j] %=1000000007; 
					}
				}

			}
		}
		//最后一行加起来就L位数字的总个数,第n行就第n位
		int sum = 0;
		for (int i = 1; i < K; i++) {
			sum+= resultK[L-1][i];
			sum %= 1000000007;
		}
		return resultK[L][K-1];
	}
	

我又从蓝桥杯里找到一个类似的题,再来练练手

蓝桥杯历届试题 格子刷油漆

题目:http://lx.lanqiao.cn/problem.page?gpid=T38
思路:
题解:https://blog.csdn.net/Crystal_viv/article/details/79450285

首先,对这些墙刷油漆主要分为两大类:

1.从最边缘的四个格子出发,然后遍历完所有格子;

2:从中间的某个格子出发,先遍历完一边的格子,回到这个格子所对的格子,然后遍历另一边的格子。

所以,我们不妨用两个数组来表示这两种情况:

1.a[i]数组表示从最边缘的四个格子中某个出发,遍历完长度为i,个数为2i个格子的所有种类数;

2.b[i]数组表示从除了最边缘的四个格子外的某个中间的格子出发,遍历完一边回到所对的格子;

然后,我们来分别分析a[i]和b[i]两种不同的情况:

1.a[i]第一种情况:先走这个格子(以左上角的格子为例)所对的下面的格子,然后从下面这个格子的位置出发,有两种走法,分别到第二列的两个格子,所以第一种情况有:

2a[i-1]种;a[i]第二种情况:(举例)先从左上角的格子走到第二列某个格子,然后从第二列的格子出发,遍历完右面所有的格子,再回到第二列格子所对的格子,最后到第一列未遍历的格子,所以这种情况就是我们定义的b[i];a[i]第三种情况:就是遍历完一二列的所有格子,从第三列的格子出发,进行遍历。由于遍历完一二列的所有格子有四种情况,所以第三种情况为:4a[i-2];

所以,a[i]=2a[i-1]+b[i]+4a[i-2];

2.b[i]的情况:b[i]比较简单,只有两种情况,从同一行的格子出发,回到他所对的格子;或者从他对角的格子出发,回到他所对的格子。

所以,b[i]=2*b[i-1];

接下来,就是算总和:

1.四个角:定义sun=4*a[n];

2.设从第i列开始,则前面有i-1列,后面有n-i列。

第i列有两个格子,这两个格子所表示的情况数是相同的,所以只需要讨论一个。

然后,我们以第i列的上面的格子为例,先遍历他的左边,然后遍历右边。(回到他所对的格子,遍历右边时,又分为两种情况。从第i+1列上面的格子开始和从下面的格子开始)

即b[i]2a[n-i],化简得:2*b[i-1]2a[n-i];

同理得:先遍历右边,再遍历左边为:2*b[n-i]2a[i-1];

然后又因为a[1]=1;a[2]=6;b[1]=1; ————————————————
原文链接:https://blog.csdn.net/qq_22891105/article/details/51050565

理清过程挺难的,但理清之后就是两个简单的一位数组存储

public static long geziyouqi(int n){
		long a[]=new long[n+1];
		long b[]=new long[n+1];
		long sum;
		b[1]=1;
		//这里的吧b[]是从中间出发只走一边的格子回到起点那一列,要和题目中从中间出发遍历所有的格子做一下对比
		for(int i=2;i<=n;i++){
			b[i]=2*b[i-1]%1000000007;
		}
		a[1]=1;a[2]=6;
		for(int i=3;i<=n;i++){
			a[i]=(2*a[i-1]+b[i]+4*a[i-2])%1000000007;
		}
		sum=(4*a[n])%1000000007;
		for(int i=2;i<n;i++){
			sum+=((8*b[i-1]*a[n-i])%1000000007+(8*b[n-i]*a[i-1])%1000000007)%1000000007;//必须每个项都取余,防止有大于这个数的情况
			sum%=1000000007;
		}
		return sum;
		
	}

数塔问题

写一个程序来计算从最高点开始在底部任意处结束的路径经过数字的和的最大。 每一步可以走到左下方的点也可以到达右下方的点。
状态转移方程如下 dp[i][j]=Math.max(dp[i-1][j-1], dp[i-1][j]) + n[i][j]

public static int minNumberInRotateArray(int n[][]) {
		int max = 0;
		int dp[][] = new int[n.length][n.length];
		dp[0][0] = n[0][0];
		for(int i=1;i<n.length;i++){
			for(int j=0;j<=i;j++){
				if(j==0){
					//如果是第一列,直接跟他上面数字相加
					dp[i][j] = dp[i-1][j] + n[i][j];
				}else{
					//如果不是第一列,比较他上面跟上面左面数字谁大,谁大就跟谁相加,放到这个位置
					dp[i][j] = Math.max(dp[i-1][j-1], dp[i-1][j]) + n[i][j];
				}
				max = Math.max(dp[i][j], max);
			}
		}
		return max;
	}
01背包

有一个背包,背包容量是M=150。有7个物品,物品可以分割成任意大小。要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。
物品 A B C D E F G
重量 35 30 60 50 40 10 25
价值 10 40 30 50 35 40 30

思路:

public static int beibao_01(int c, int[] weight, int[] value) {
    	//还是二维数组,行是我的物品,列是0~容量
    	int[][] result=new int[weight.length][c];
    	//初始化这个二维数组,全为0
    	for(int i=0;i<weight.length;i++) {
    		for(int j=1;j<=c;c++) {
    			//先判断当前物品是不是小于当前背包容量
    			if(weight[i]<=j) {
    				//判断是否要装这个物品
    				//result[i-1][j]是装上一个物品后的价值,result[i-1][j-weight[i]]+value[i]是装这个物品
    				result[i][j]=Math.max(result[i-1][j], result[i-1][j-weight[i]]+value[i]);
    			}
    			else {
    				result[i][j]=result[i-1][j];
				}
    		}
    	}
    	
    	
    	return 0;
    }
	
蓝桥杯最大的算式

题目很简单,给出N个数字,不改变它们的相对位置,在中间加入K个乘号和N-K-1个加号,(括号随便加)使最终结果尽量大。因为乘号和加号一共就是N-1个了,所以恰好每两个相邻数字之间都有一个符号。例如:
  N=5,K=2,5个数字分别为1、2、3、4、5,可以加成:
  12(3+4+5)=24
  1*(2+3)(4+5)=45
解析:dp[4][1]的含义就是前4位数(0,1,2,3)中间有一称号的最大值,此处为9,是经过这样一个循环得到的
1
(2+3)=5
(1+2)*3=9

import java.util.Scanner;
 
public class Main1 {
 
    public static void main(String[] args) {
        Scanner cin = new Scanner(System.in);
        int n, k;
        n = cin.nextInt();
        k = cin.nextInt();
        long[][] dp = new long[n+1][n+1];
        long[] sum = new long[n+1];
        long t;
        //初始化dp数组
        for(int i = 0; i <= n; i++) {
            for(int j = 0; j <= k; j++) {
                dp[i][j] = 0;
            }
        }
        //得到累加和
        sum[0] = 0;
        for(int i = 1; i <= n; i++) {
            t = cin.nextLong();
            sum[i] = sum[i-1] + t;
            dp[i][0] = sum[i];
        }
        //计算:
        for(int i = 2; i <= n; i++) {     //数的个数
            for(int j = 1; j <= k; j++) { //乘号的个数
                for(int tt = 1; tt < i; tt++) { //遍历最后一个乘号的位置
                    dp[i][j] = Math.max(dp[i][j], dp[tt][j-1] * (sum[i] - sum[tt]));
                }
            }
        }
        System.out.println(dp[n][k]);
        cin.close();
    }
}

递归到动规的一般转化方法
递归函数有n个参数,就定义一个n维的数组,数组的下标是递归函数参数的取值范围,数组元素的值是递归函数的返回值,这样就可以从边界值开始, 逐步填充数组,相当于计算递归函数值的逆过程。

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java排序算法是用于对一系列数据进行排列顺序的一种算法。在Java中,常见的排序算法包括冒泡排序、选择排序、插入排序、快速排序、归并排序等。 冒泡排序是一种比较简单的排序算法,它通过对相邻的元素进行比较和交换来实现排序。该算法的时间复杂度为O(n^2),属于比较低效的排序算法。选择排序是一种简单直观的排序算法,它通过选择最小的元素并放置在已排序的部分来实现排序。该算法的时间复杂度也是O(n^2)。 插入排序是一种比较高效的排序算法,它通过将未排序的元素插入到已排序的部分来实现排序。该算法的时间复杂度也是O(n^2)。快速排序是一种递归的排序算法,它通过选取一个基准值来对数组进行分区,并对每个分区进行排序来实现最终的排序。该算法的时间复杂度为O(nlogn),是比较高效的排序算法之一。 归并排序是一种分治的排序算法,它将数组分成两个子数组,并对每个子数组进行排序,最后将两个子数组合并成一个有序数组。该算法的时间复杂度也是O(nlogn)。在实际应用中,我们通常会选择合适的排序算法来应对不同的排序需求,比如对于小规模数据可以选择简单的排序算法,对于大规模数据可以选择高效的排序算法。总之,了解Java排序算法的原理和性能表现对于编程人员来说是非常重要的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值