目录
198.打家劫舍
绘制如下递归树,如考虑偷取[0 ... n-1]范围的所有房子,可以考虑偷取0,1,到n-1的房子。
如偷取0,则下一步则考虑偷取[2...n-1]的所有房子,因为1与0相邻,所以不考虑偷取
此递归树中,可以明显的看出有重叠子问题
状态转移方程:f(x)表示偷取[x ... n-1]范围里的最多的房子
/******** 记忆化搜索 ********/
class Solution {
private $nums,$len; //初始化数组和长度
private $memo = []; //初始化记忆数组
/**
* @param Integer[] $nums
* @return Integer
*/
function rob($nums) {
$this->nums = $nums; //赋值成员变量
$this->len = count($nums);
return $this->tryRob(0); //开始递归
}
//考虑抢劫 nums[index...$len]这个范围内的所有房子
private function tryRob($index){
if($index >= $this->len) return 0;
if(isset($this->memo[$index])) //重叠子问题,如果已经找过了,直接返回即可
return $this->memo[$index]; //memo[i]表示考虑抢劫nums[i...n]所能获得的最大收益
$res = 0;
for($i = $index;$i<$this->len;++$i){
$res = max($res,$this->nums[$i] + $this->tryRob($i + 2)); //考虑当前所获的收益和盗取后面两个所获得的最大收益取较大值
}
$this->memo[$index] = $res;
return $res;
}
}
/******** 动态规划 ********/
class Solution {
/**
* @param Integer[] $nums
* @return Integer
*/
function rob($nums) {
$len = count($nums);
if($len == 0) return 0; //初始化判断,当数组为空时,无法抢劫
$dp = []; //dp[i]表示考虑抢劫nums[i...n]所能获得的最大收益
$dp[$len-1] = $nums[$len-1]; //初始化盗取最后一个节点的值
for($i = $len - 2;$i >= 0;--$i){ //从后往前,dp记录从每一个房间开始抢劫的最大金额数
$dp[$i] = 0; //初始化新的盗取节点
for($j = $i;$j<$len;++$j){ //考虑当前所获的收益和盗取后面两个所获得的最大收益取较大值
$dp[$i] = max($dp[$i], $nums[$j] + $dp[$j+2]);
}
}
return $dp[0]; //开始递归
}
}
213.打家劫舍II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [2,3,2] 输出: 3 解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入: [1,2,3,1] 输出: 4 解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。
与198区别,这是一个环形的小区,说明第一个房屋和最后一个房屋不能同时盗取,因此需要考虑两种情况
(1)考虑偷取 [ 0 , n - 2 ] 的房屋 (2)考虑偷取 [ 1, n - 1 ] 的房屋
取大者即为答案
class Solution {
/**
* @param Integer[] $nums
* @return Integer
*/
function rob($nums) {
$len = count($nums);
if($len == 0) return 0; //初始化判断,当数组为空时,无法抢劫
if($len == 1) return $nums[0]; //当数组长度为 1时,只有一种盗取可能
$dp = []; //dp[i]表示考虑抢劫nums[i...n]所能获得的最大收益
//先考虑偷取[0...n-2]的房子
$dp[$len - 2] = $nums[$len - 2]; //初始化盗取 n-2 节点的值
for($i = $len - 3;$i >= 0;--$i){ //从后往前,dp记录从每一个房间开始抢劫的最大金额数
$dp[$i] = 0; //初始化新的盗取节点
for($j = $i;$j<=$len - 2;++$j){//考虑当前所获的收益和盗取后面两个所获得的最大收益取较大值
$dp[$i] = max($dp[$i], $nums[$j] + $dp[$j+2]);
}
}
$max = $dp[0]; //保存偷取[0...n-2]的房子的最大收益
$dp = [];
//考虑偷取[1...n-1]的房子
$dp[$len - 1] = $nums[$len - 1]; //初始化盗取 n-1 节点的值
for($i = $len - 2;$i >= 1;--$i){
$dp[$i] = 0; //初始化新的盗取节点
for($j = $i;$j<=$len - 1;++$j){
$dp[$i] = max($dp[$i], $nums[$j] + $dp[$j+2]);
}
}
return max($max, $dp[1]); //两者取大值即可
}
}
337.打家劫舍III
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
示例 1:
输入: [3,2,3,null,3,null,1] 3 / \ 2 3 \ \ 3 1 输出: 7 解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
示例 2:
输入: [3,4,5,1,3,null,1] 3 / \ 4 5 / \ \ 1 3 1 输出: 9 解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.
递归分析两种情况:
(1)盗取当前节点,则不盗取当前节点的左右节点
(2)不盗取当前节点,则从盗取当前节点的左右节点中选择较大值
/**************** 递归:超出输出限制 ****************/
class Solution {
/**
* @param TreeNode $root
* @return Integer
*/
function rob($root) {
if($root == null) return 0;
//计算盗取左孩子的左右孩子的最大值
if($root->left) $left = $this->rob($root->left->left) + $this->rob($root->left->right);
//计算盗取右孩子的左右孩子的最大值
if($root->right) $right = $this->rob($root->right->left) + $this->rob($root->right->right);
//计算盗取 包含该节点 和 不盗取该节点 的两种情况的最大值
return max($root->val+$left+$right,$this->rob($root->left)+$this->rob($root->right));
}
}
/**************** 记忆化搜索 ****************/
class Solution {
/**
* @param TreeNode $root
* @return Integer
*/
function rob($root) {
$res = $this->findMax($root);
return max($res);
}
private function findMax($root){
$res = [0,0]; //用$res 来标记抢和不抢root的所获得的最大值
if($root == null) return $res; //递归结束标志
$left = $this->findMax($root->left); //盗取左孩子
$right = $this->findMax($root->right); //盗取右孩子
$res[0] = $root->val + $left[1] + $right[1];//盗取该节点时,则不盗取该盗取该节点的左右孩子
$res[1] = max($left) + max($right); //不盗取该节点时,则无所谓盗不盗取该节点的左右孩子,大就行
return $res;
}
}
309. 最佳买卖股票时机含冷冻期
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
- 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
- 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例:
输入: [1,2,3,0,2] 输出: 3 解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
分析:参考【LeetCode】309. Best Time to Buy and Sell Stock with Cooldown解题思路
状态转移,首先要明白只有三种状态,买入buy,卖出sell,冷冻期rest
我们这里把状态分成了三个,根据每个状态的指向
每个状态的表示如下
S0:sell后的rest 、 rest后的rest
S1:rest后的buy 、 buy后的rest
S2:rest后的sell 、 buy后的sell
可以得出下面的状态转移方程 : 分别代表三种状态下的最大利润值
S0[i] = max(S0[i-1], S2[i-1]) :取S0(继续休息)和S2(卖出后的休息)的最大利润值值
S1[i] = max(S1[i-1], S0[i-1] - price[i]) :buy的最大利润值为max(rest期间价格不变,buy后的rest则需要付费该股票)
S2[i] = S1[i-1] + price[i] : S1 状态的最大利润加上卖出的金额
class Solution {
/**
* @param Integer[] $prices
* @return Integer
*/
function maxProfit($prices) {
$len = count($prices);
if($len <= 1) return 0; //初始化判断,长度为<=1,无法进行卖出,则无收益
$s0 = 0; //rest 冷冻期状态:初始化利润为0
$s1 = -$prices[0]; //buy 买入状态 :初始化为第一个股票价格的负值
$s2 = 0; //sell 卖出状态 :初始化为0,因为还没有买卖
for($i = 1;$i<$len;++$i){
$pre0 = $s0; //用pre代表[i-1]的状态
$pre1 = $s1;
$pre2 = $s2;
$s0 = max($pre0,$pre2); //取S0(继续休息)和S2(卖出后的休息)的最大利润值值
$s1 = max($pre0 - $prices[$i],$pre1);//buy的最大利润值为max(rest期间价格不变,buy后的rest则需要付费该股票)
$s2 = $pre1 + $prices[$i]; //S1 状态的最大利润加上卖出的金额
}
return max($s0,$s2); //最大的利润最后出现在sell和rest之间,不可能出现在buy,即s1不考虑
}
}
动态规划的难点在于要清楚有哪些状态,已经每个状态是如何转移的,把大问题化为一个又一个的小问题