算法想被我拿下~摆烂er的学习头脑战~

之前会,但只会一点点,所以详细是很详细,废话也是真的多

动态规划DP

动态规划算法是通过拆分问题(动态规划的核心),定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。

参考文章

经典中的经典算法:动态规划(详细解释,从入门到实践,逐步讲解)

以下是个人理解部分,如有不当/错误,还请斧正

适用问题: 【求最优解】

【动态规划理论】:一篇文章带你彻底搞懂最优子结构、无后效性和重复子问题

  1. 问题具有最优子结构
  2. 无后效性
  3. 重复子问题
递归与递推:【个人理解】
  • 递推:n=f(n-1),使用前项获得后项;先计算第一项(一般已知),按照递推公式获得目的项。【只进行了一轮计算:首项->目的项】
  • 递归:n=f(n+1),使用后项获得前项;首先根据目的项,推算出对应的(最佳)最后一项(一般已知),再根据最后一项倒推出目的项。【表面上进行了两轮计算:目的项->尾项(即边界值)->目的项】
递归函数的辅助数组:【降低时间复杂度的方法】

数组元素记录子问题的解,即该子问题的返回值;通过记录这些返回值,避免重复计算某些子问题(重复子问题),从而降低时间复杂度

1. 维数=递归函数的参数个数
2. 数组长度=递归函数参数的取值范围
3. 数组元素的值=递归函数的返回值(应有一个初始化的值,递归过程中使用该位置的返回值代替初始值)

实战(其实是练习)【力扣 53. 最大子数组和

思考过程:【仅供我看,你看反而还有可能被误导,扰乱思路】

*** 暴力法。用每个元素依次作为子数组中的首元素,遍历原数组并计算子数组的和;若子数组和增加,则该元素为所求子数组的元素,继续遍历;若子数组和减少,则该元素不应该在子数组中,记录加入本次元素之前的子数组,该元素作为首元素的子数组结束本次遍历。
【很明显时空复杂度都很高,而且会出错(比如某次遍历,到某个元素时和减小,到下个元素时和增大且大于减小之前的和);没有验证这个思路】

*** 使用图作为辅助。以原数组的元素为基础结点,将相邻元素的和生成一个结点(上层相邻结点的右/左子节点为同一结点,这就导致重复计算);再将生成的相邻结点作为基础,再生成结点;最后只剩一个结点;搜索所有结点(生成的结点和原元素的结点)中的最大值。
【有明显的错误,计算的子数组和中,除首尾,中间元素均被多次累计;并且时空开销均很大】

*** 使用二叉树作为辅助。结点的生成类似于上面的方法,不同的是每个结点生成上层新结点的时候,只被使用了1次。
【这样做,错误变成了子数组的元素个数只能为偶数(个数为偶数的说法没有考虑原数组元素个数为奇数的情况,因为已经产生错误了,不需要再多解释了 虽然已经解释完了 )】

  1. 分析问题/划分子问题
    1)若数组仅含一个元素,则该元素为最大子数组和;
    2)若数组含有多个元素,则使用一个一维数组maxSum[nums.length-1]保存以对应下标结束的(最大)子数组和;
    【想不出来,看了讨论区的思路;以下是我的思考:之所以不用一个二维数组保存开始位和结束位,是因为二维数组得到的递归式与暴力法相似(只是把暴力法的for循环改为递归调用,本质没什么差别);仅保存结束位,是因为maxSum[i]的值为以该位结束的和最大的子数组和,虽然没有记录是从哪一位开始的,但是题中所要求的和已经求得。】
  2. 状态之间的关系/递归式
 maxSum[i]=max{maxSum[i-1]+nums[i],nums[i]};
 /*
 以某位(下标为i)结束的、和最大的子数组的和 
 = 前一位(下标为i-1)的值(以这位结束的最大子数组和)和某位(下标为i的原数组值)的和,
 某位(下标为i的原数组值)的和,
 二者中的较大值
 */
  1. 初始状态/边界条件
  maxSum[0]=nums[0];
  //0位结束的(最大)子数组和即为nums[0]的值
  maxSum[1]=max{maxSum[0]+nums[1],nums[1]};
  //1位结束的最大子数组和的值由以下二者中的较大值决定:
  //0位(即前一位的最大子数组和)和1位的和
  //1位本身

是的,到这里我还很清晰;但也就到此为止了😓

//关键在于每次递归都产生全新的maxSum(长度与递归调用函数的数组参数相等)
//递归时并不能将这些maxSum作为一个整体
	public static int maxSubArray(int[] nums) {
		int[] maxSum = new int[nums.length];// 辅助数组
		// arrayPrint(maxSum);
		int sum = 0;// 记录辅助数组中的最大值
		int i = nums.length - 1;
		while (i >= 0) {
			if (i == 0) {
				maxSum[0] = nums[0];
				System.out.println("maxSum[0]=" + maxSum[0]);
			} else {
				// 创建一个数组,其元素为nums的前i个(含),即nums2的长度为i;(调用)递归时作为“原”数组
				int[] nums2 = Arrays.copyOf(nums, i);
				System.out.print("nums2:");	arrayPrint(nums2);//打印数组,检查点
				maxSum[i - 1] = maxSubArray(nums2);
				maxSum[i] = Math.max(maxSum[i - 1] + nums[i], nums[i]);
				System.out.print("maxSum:");arrayPrint(maxSum);//检查点
			}
			sum = maxSum[i] > sum ? maxSum[i] : sum;
			return maxSum[i];//最后到这里就返回值了(最后的返回值)
		}
		for (int ele : maxSum) 
			sum = ele > sum ? ele : sum;
		return sum;
	}
//产生了这样的结果
nums2:-2  1  -3  4  -1  2  1  -5  
nums2:-2  1  -3  4  -1  2  1  
nums2:-2  1  -3  4  -1  2  
nums2:-2  1  -3  4  -1  
nums2:-2  1  -3  4  
nums2:-2  1  -3  
nums2:-2  1  
nums2:-2  
maxSum[0]=-2
maxSum:-2  1  
maxSum:0  1  -2  
maxSum:0  0  -2  4  
maxSum:0  0  0  4  3  
maxSum:0  0  0  0  3  5  
maxSum:0  0  0  0  0  5  6  
maxSum:0  0  0  0  0  0  6  1  
maxSum:0  0  0  0  0  0  0  1  -3  
//最后的结果为-3
//很明显就是递归创建了多个不同长度maxSum[],最后在else中就返回了
//修改一下也可以执行到最后,返回sum,但此时返回的sum为最后一个maxSum[]中的最大值
//呵,递归调用产生这个问题有多少次了,怎么还没记住?
真·看了答案【为什么别人写的就可以那么简洁?】

先说结论:(于我来说)如果写很长还是没有办法解决问题,那就删掉重写吧🤣那什么山而已,没什么好留恋的😅

出现问题的原因是太拘泥于递归,导致写出的代码一定经历了两轮(即由目的项到已知项再到目的项)

以下代码从已知项开始计算

//重新定义问题:以第k项结尾的子数组的最大和
	public static int maxSubArray(int[] nums) {
		int maxSum = nums[0];// 用于记录(已经遍历到的)最大子数组和
		int preMax = 0;// 维持动态规划状态转移式(式子一)
		//换成我能理解的话就是:
		//记录本次遍历到的元素(为结尾)的子数组的最大和
		for (int ele : nums) {
			preMax = Math.max(preMax + ele, ele);// 式子一;
			//即本次遍历的元素ele对应的最大和是二者中的较大值:
			//以 前一个元素为结尾的 子数组的 最大和+本轮元素本身的和or本轮元素自身
			maxSum = Math.max(maxSum, preMax);
		}
		return maxSum;
	}
	//100%通过
实战2【力扣 118. 杨辉三角(不自己写出来是没有意义的)】
  1. 划分子问题
    按行解决即可
    【不会描述】
  2. 状态关系式
    【这个比较容易想到】
nums[r][c]=nums[r-1][c-1]+nums[r-1][c];
//按矩阵存储,除边界外,每个元素为该元素左上和正上方的元素之和
//每行也有对称位
  1. 边界条件
nums[r][1]=1;//每行第一个元素为1
nums[r][r]=1;//每行最后一个元素为1

【核心代码】

	public static List<List<Integer>> generate(int numRows) {
		List<List<Integer>> nums = new ArrayList<List<Integer>>();
		for (int i = 0; i < numRows; i++) {
			ArrayList<Integer> nums0 = new ArrayList<Integer>();// 将会创建numRows行
			nums0.ensureCapacity(i + 1);// 指定容量大小,下标为i的行有i+1个元素
			nums.add(nums0);// 将新创建的List插入

			if (i == 0) {
				nums0.add(1);// 第一行的元素
			} else {
				nums0.add(1);// 每行第一个元素为1
				for (int j = 1; j < i; j++) {
					nums0.add(j, nums.get(i - 1).get(j - 1) + nums.get(i - 1).get(j));//
					// nums0.add(i - 1 - j, nums0.get(j));// 对称位
				}
				nums0.add(1);// 每行最后一个元素为1
			}
		}
		return nums;
	}
	//实际没用上对称位
实战3【力扣 746. 使用最小花费爬楼梯
  1. 划分子问题
    PS:楼梯顶部并非最后一阶楼梯,而是最后一阶之后的一阶,所以minCost.length=cost.length+1
    子问题:到达每阶楼梯的最小花费【听君一席话如听一席话】
  2. 状态关系式
minCost[i]=min{minCost[i-1]+cost[i-1],minCost[i-2]+cost[i-2]}
//到达每阶台阶最小的花费为到达前一/二阶台阶最小的花费+本阶台阶的费用
  1. 边界
minCost[0]=0;
minCost[1]=0;
//最开始选择第0阶和第1阶楼梯不需要花费
实战4【力扣121. 买卖股票的最佳时机

剑指 Offer 63. 股票的最大利润的差别仅在数组长度不同
剑指为0 <= 数组长度 <= 10^5,多个0

//解法一
//遍历prices,每遍历一个元素就计算与该元素之前的所有元素的差值
//两层循环,思路大致没有错,但是超时
	public static int maxProfit(int[] prices) {
		int len = prices.length;// 至少为1
		int[] maxP = new int[len];// 辅助数组,保存对应下标的卖股日能获得的最大收益
		if (len == 1)
			return 0;
		else {
			for (int i = 1; i < len; i++) {//遍历元素
				for (int j = i - 1; j >= 0; j--) {//计算差值
					maxP[i] = Math.max(maxP[i], prices[i] - prices[j]);
				}
			}
		}
		int max = -1;// 此处可以赋任意值(<=0即可),因为收益最小为0
		for (int ele : maxP) {
			max = max > ele ? max : ele;
			//System.out.println(ele);
		}
		return max;
	}
//解法二
//执行用时:4 ms, 在所有 Java 提交中击败了24.54%的用户
//内存消耗:54.1 MB, 在所有 Java 提交中击败了69.77%的用户
//性能不好,但起码过了🤣
//题目的本质是找到某元素的所有前驱元素中最小的一个,此时差值最大,返回值为所有差值的最大值
//受解法一的启发,辅助数组中不保存差值,而是保存以该元素结尾的子数组中的最小值
//对应下标的prices-辅助元素的值即为以该天为卖股日所获得的利润,边遍历prices边计算
//使用一个整数记录目前的最大利润(而不是计算完辅助数组才遍历计算最大利润)
//PS:前驱的用法不准确,想要表达的意思是下标比某元素小的所有元素
	public static int maxProfit(int[] prices) {
		int[] preMin = new int[prices.length];// 辅助数组,保存对应下标元素为结尾的最小元素
		preMin[0] = prices[0];
		int maxP = 0;// 最大收益
		for (int i = 1; i < prices.length; i++) {
			preMin[i] = Math.min(preMin[i - 1], prices[i]);// 该位的最小值为:前一位中的最小值、本位值,二者中的较小值
			maxP = maxP > prices[i] - preMin[i] ? maxP : prices[i] - preMin[i];
		}
		return maxP;
	}
//解法三
//与解法二的差别仅在:不使用辅助数组,而仅使用一个整数保存前驱元素的最小值
//执行用时:2 ms, 在所有 Java 提交中击败了62.96%的用户(时间型嫩给提升还算挺多
//因为辅助数组中保存的所有最小值并非都有用处,真正有用的只有前一个	
	public static int maxProfit(int[] prices) {
		int preMin = prices[0];// 某位元素的前驱最小值
		int maxP = 0;// 总的最大收益
		int len = prices.length;
		for (int i = 1; i < len; i++) {
			//某元素对应的最小值为:该位的值,已保存的前驱最小值,二者中的较小值
			preMin = Math.min(prices[i], preMin);
			//顺便就可以计算目前的最大收益
			maxP = maxP > prices[i] - preMin ? maxP : prices[i] - preMin;
		}
		return maxP;

递归

参考文章

什么是递归,通过这篇文章,让你彻底搞懂递归

实战【力扣 汉诺塔问题
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值