基本概念
主要思想:将大规模的问题转换成小规模的问题,并且缓存中间结果。
打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。
输入输出样例
样例1
输入:[1,2,3,1]
输出:4
解释:偷窃1号房屋(金额=1),然后偷窃3号房屋(金额=3)。偷窃到的最高金额为1+3=4。
样例2
输入:[2,7,9,3,1]
输出:12
解释:偷窃1号房屋(金额=2),偷窃3号房屋(金额=9),接着偷窃5号房屋(金额=1)。偷窃到的最高金额为2+9+1=12。
题解
对于[2,7,9,3,1]
,从最后一个数向前推。其递归状态树为:
递归三部曲:
- 选择递归函数的参数
- 终止条件
- 递归方向
对应到该题目中,递归三部曲为:
- 递归函数的参数:随着递归状态而发生改变的变量,即
int rec(index)
。 - 终止条件:
if index==0: return nums[0]
或者if index==1: return max(nums[0],nums[1])
。 - 递归方向:
int select=rec(index+2)+nums[index]
和int uselect=rec(index+1)+nums[index]
,最终return max(select,unselect)
。
代码实现
public class Dynamic1 {
public static int solution(int[] money){
int res = getMoney(money, money.length-1);
return res;
}
public static int getMoney(int[] money,int index){
if (index==1) {
return Math.max(money[0], money[1]);
}else if(index==0){
return money[0];
}
int result = Math.max(getMoney(money, index-2)+money[index], getMoney(money, index-1));
return result;
}
public static void main(String[] args) {
int[] money = {2,7,9,3,1};
int result = solution(money);
System.out.println(result);
}
}
这种方法可以正确实现,但是属于暴力解法,会出现超时!如在下图中存在着两个相同的子树,也就意味着它会进行重复的递归操作进行运算,为了避免这个问题,我们可以使用一个记忆数组进行优化处理。
代码实现
import java.util.Arrays;
public class Dynamic1 {
static int[] cache; // 记忆数组,用于存储当前索引记录的最大值
public static int solution(int[] money){
cache = new int[money.length];
Arrays.fill(cache,-1);
int res = getMoney(money, money.length-1);
return res;
}
public static int getMoney(int[] money,int index){
if (index==1) {
return Math.max(money[0], money[1]);
}else if(index==0){
return money[0];
}
if (cache[index]!=-1) {
return cache[index];
}
int result = Math.max(getMoney(money, index-2)+money[index], getMoney(money, index-1));
cache[index] = result;
return result;
}
public static void main(String[] args) {
int[] money = {2,7,9,3,1};
int result = solution(money);
System.out.println(result);
}
}
使用动态规划进行优化
基本上所有的动态规划的题目都可以由递归式子进行转换。
如果一道题目,我们可以通过递归方法进行实现,但是存在大量的重复计算,那么我们就可以考虑使用动态规划来解决问题。
动态规划一般不需要递归操作。
动态规划的核心思想就是将大规模的问题转化为多个小规模的问题进行求解。
递归式子(状态转移方程式):相当于就是递归方向。
如:
f
(
i
n
d
e
x
)
=
m
a
x
(
f
(
i
n
d
e
x
−
1
)
,
f
(
i
n
d
e
x
−
2
)
+
n
u
m
s
[
i
]
)
f(index) = max(f(index-1),f(index-2)+nums[i])
f(index)=max(f(index−1),f(index−2)+nums[i])
这就是一个状态转移方程式。
几个核心点:
-
basecase(初始化)
-
状态转移方程式
-
缓存中间结果
-
顺序问题
对应到该题目中:
- basecase:
index=0:nums[0]
,index=1:max(nums[0],nums[1])
- 状态转移方程式:
f(index) = max(f(index-1),f(index-2)+nums[i])
- 缓存中间结果:
int[] dp = new int[nums.length]
- 顺序问题:index从
0->n-1
代码实现
public class Dynamic1 {
public static int solution(int[] money){
if (money.length==0) return 0;
if (money.length==1) return money[0];
int[] dp = new int[money.length];
dp[0] = money[0];
dp[1] = Math.max(money[0], money[1]);
for (int i = 2; i < dp.length; i++) {
dp[i] = Math.max(dp[i-1], dp[i-2]+money[i]); // 状态转移方程式
}
return dp[money.length-1];
}
public static void main(String[] args) {
int[] money = {2,7,9,3,1};
int result = solution(money);
System.out.println(result);
}
}
可以对空间进行优化,只需要存储刚刚计算过的两个数字即可。
public class Dynamic1 {
public static int solution(int[] money){
if (money.length==0) return 0;
if (money.length==1) return money[0];
int pre1 = money[0];
int pre2 = Math.max(money[0], money[1]);
int result = pre2;
for (int i = 2; i < money.length; i++) {
result = Math.max(pre2, pre1+money[i]);
pre1 = pre2;
pre2 = result;
}
return result;
}
public static void main(String[] args) {
int[] money = {2,7,9,3,1};
int result = solution(money);
System.out.println(result);
}
}