66.一维动态规划

动态规划

动态规划:用空间代替重复计算,包含一整套原理和技巧的总和,课程会用非常大的篇幅来全盘介绍

知道怎么算的算法 vs 知道怎么试的算法

有些递归在展开计算时,总是重复调用同一个子问题的解,这种重复调用的递归变成动态规划很有收益
如果每次展开都是不同的解,或者重复调用的现象很少,那么没有改动态规划的必要
下节课会举例,哪些递归没有必要改动态规划的必要

任何动态规划问题都一定对应着一个有重复调用行为的递归
所以任何动态规划的题目都一定可以从递归入手,逐渐实现动态规划的方法
题目1到题目4,都从递归入手,逐渐改出动态规划的实现

尝试策略 就是 转移方程,完全一回事!
推荐从尝试入手,因为代码好写,并且一旦发现尝试错误,重新想别的递归代价轻!

题目一:斐波那契数

问题描述:

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n ,请计算 F(n) 。

方案一:暴力递归

算法思想:

普通暴力递归的思想

代码如下:

	public static int fib1(int n) {
   
		return f1(n);
	}

	public static int f1(int i) {
   
		if (i == 0) {
   
			return 0;
		}
		if (i == 1) {
   
			return 1;
		}
		return f1(i - 1) + f1(i - 2);
	}

方案一:记忆化搜索(傻缓存法)

算法思想:

我们发现,暴力递归的时候时间复杂度是O(2^n),因为会计算大量重复的工作,就比如说算f(7),
它就去跑f(6)和f(5),但是f(5)可能在别的地方已经计算过了,我们每一次遇见这样就得算一次会大大影响我们的效率。
所以如果我们第一次算出来f(5)就把它缓存起来,下一次需要用的时候直接再缓存表里面查它就行。这样时间复杂度就变成了O(n)

这是一种从顶到底的动态规划

代码如下:

	public static int fib2(int n) {
   
		int[] dp = new int[n + 1];
		Arrays.fill(dp, -1);
		return f2(n, dp);
	}

	public static int f2(int i, int[] dp) {
   
		if (i == 0) {
   
			return 0;
		}
		if (i == 1) {
   
			return 1;
		}
		if (dp[i] != -1) {
   
			return dp[i];
		}
		int ans = f2(i - 1, dp) + f2(i - 2, dp);
		dp[i] = ans;
		return ans;
	}

方案三:动态规划

算法思想:

如果我们能知道他的规律,一次性把f(1),f(2)....f(n)都算出来,取出f(n)就行。
这样时间复杂度是O(n)
问题的关键是找一个式子算出来

这是一种从底到顶的动态规划

代码如下:

	public static int fib3(int n) {
   
		if (n == 0) {
   
			return 0;
		}
		if (n == 1) {
   
			return 1;
		}
		int[] dp = new int[n + 1];
		dp[1] = 1;
		for (int i = 2; i <= n; i++) {
   
			dp[i] = dp[i - 1] + dp[i - 2];
		}
		return dp[n];
	}

使用变量来优化数组:

	public static int fib4(int n) {
   
		if (n == 0) {
   
			return 0;
		}
		if (n == 1) {
   
			return 1;
		}
		int lastLast = 0, last = 1;
		for (int i = 2, cur; i <= n; i++) {
   
			cur = lastLast + last;
			lastLast = last;
			last = cur;
		}
		return last;
	}

题目二:最低票价

问题描述:

在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行。在接下来的一年里,你要旅行的日子将以一个名为 days 的数组给出。每一项是一个从 1 到 365 的整数。

火车票有 三种不同的销售方式 :

一张 为期一天 的通行证售价为 costs[0] 美元;
一张 为期七天 的通行证售价为 costs[1] 美元;
一张 为期三十天 的通行证售价为 costs[2] 美元。
通行证允许数天无限制的旅行。 例如,如果我们在第 2 天获得一张 为期 7 天 的通行证,那么我们可以连着旅行 7 天:第 2 天、第 3 天、第 4 天、第 5 天、第 6 天、第 7 天和第 8 天。

返回 你想要完成在给定的列表 days 中列出的每一天的旅行所需要的最低消费 。

在这里插入图片描述

方案一:暴力递归

算法思想:

1.使用递归,递归函数作用:days[i..... 最少花费是多少 (从dayi往后的最少花费是多少)
2.递归条件:days数组越界(后面已无旅行),返回0
3.递归每一步干的事情:来到当前days[i],算出当前day[i]花费的三种尝试(1,7,30)+后面的day的最小花费,计算出最小花费。

代码如下:

	// 无论提交什么方法都带着这个数组      0  1  2
	public static int[] durations = {
    1, 7, 30 };

	// 暴力尝试
	public static int mincostTickets1(int[] days, int[] costs) {
   
		return f1(days, costs, 0);
	}

	// days[i..... 最少花费是多少 
	public static int f1(int[] days, int[] costs, int i) {
   
		if (i == days.length) {
   
			// 后续已经无旅行了
			return 0;
		}
		// i下标 : 第days[i]天,有一场旅行
		// i.... 最少花费是多少 
		int ans = Integer.MAX_VALUE;
		for (int k = 0, j = i; k < 3; k++) {
   
			// k是方案编号 : 0 1 2
			//while是计算当前选择1,7,30对应下一次j(去哪一天)该跳到哪
			while (j < days.length && days[i] + durations[k] > days[j]) {
   
				// 因为方案2持续的天数最多,30天
				// 所以while循环最多执行30次
				// 枚举行为可以认为是O(1)
				j++;
			}
			ans = Math.min(ans, costs[k] + f1(days, costs, j));
		}
		return ans;
	}

方案二:记忆化搜索(傻缓存法)

算法思想:

增加缓存dp,每次要求这层的最小花费先查缓存

代码如下:

	// 无论提交什么方法都带着这个数组      0  1  2
	public static int[] durations = {
    1, 7, 30 };
		// 暴力尝试改记忆化搜索
	// 从顶到底的动态规划
	public static int mincostTickets2(int[] days, int[] costs) {
   
		int[] dp = new int[days.length];
		for (int i = 0; i < days.length; i++) {
   
			dp[i] = Integer.MAX_VALUE;
		}
		return f2(days, costs, 0, dp);
	}

	public static int f2(int[] days, int[] costs, int i, int[] dp) {
   
		if (i 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值