动态规划比较经典的题目
给定一支股票一段日期内的的每日价格price
题目1: 求这段时间内买卖一次获得的最大收益
思路:即找到一组i和j,使得 price[i] - price[j] 最大,且i
状态转移方程式为:profit = max(profit, price[i] - minPrice);
而minPrice = min(minPrice, price[i]);
function solution1($price) {
if(!$price)
return 0;
$profit = 0;
$min = $price[0];
for ($i = 1; $i < count($price); $i++) {
$min = min($min, $price[$i]);
$profit = max($profit, $price[$i] - $min);
}
return $profit;
}
题目2: 求这段时间内无限次买卖可获得的最大收益
思路一、动态规划法:对某一时间点i,有两种状态,持有或不持有,那么同理在i-1天也有这两种状态,用hold表示在i天持有的状态下的最大收益,free表示在i天不持有的最大收益,状态转移方程式就可以写为
hold[i] = max(hold[i - 1], free[i - 1] - price[i]);//持有状态有两种情况:前一天就持有的收益和前一天空仓当天买入的收益做比较
free[i] = max(free[i - 1], hold[i - 1] + price[i]);//不持有有两种情况:前一天就不持有的收益和前一天持有当天卖出的收益做比较
状态初始条件为:
hold[0] = -price[0];
free[0] = 0;
状态转移方程式可以优化空间效率,因为当前收益总是单纯依赖前一天的收益,因此可写为:
hold = max(hold, free - price[i]);
free = max(free, hold + price[i]);
function solution2($price) {
if(!$price)
return 0;
$hold = -$price[0];
$free = 0;
for ($i = 1; $i < count($price); $i++) {
$pre_hold = $hold;
$hold = max($hold, $free - $price[$i]);
$free = max($free, $pre_hold + $price[$i]);
}
return $free;
}
思路二、贪心法:
只要后一天比前一天贵,就卖出,比较容易理解
题目3: 带冷却时间的购买,即卖出后必须等待一天,才能再次购买
思路一:
这里有两种解题思路,先看比较简单的一种,与题目2基本一致,只是第i天的持有状态要从第i-2天转移来而不是i-1天,状态转移公式改为:
hold[i] = max(hold[i - 1], free[i - 2] - price[i]);
free[i] = max(free[i - 1], hold[i - 1] + price[i]);
状态初始条件要改为:
hold[0] = -price[0];
hold[1] = max(hold[0], -price[1]);
free[0] = 0;
free[1] = max(free[0], hold[0]+price[1]);
做一下空间效率优化
hold = max(hold, pre_free - price[i]);//pre_free不是前一天,而是前两天,在状态转移方程式等号右边中的free本身就是前一天的状态
free = max(free, hold + price[i]);
状态初始条件:
hold = max(-price[0], -price[1]);
pre_free = 0;
free = max(pre_free, hold + price[1]);
function solution4($price) {
if(!$price)
return 0;
$hold = max(-$price[0], -$price[1]);
$pre_free = 0;
$free = max($pre_free, $hold+$price[1]);
for ($i = 2; $i < count($price); $i++) {
$pre_hold = $hold;
$hold = max($hold, $pre_free - $price[$i]);
$pre_free = $free;
$free = max($free, $pre_hold + $price[$i]);
}
return $free;
}
思路二:
第二种思路引入了rest这种状态,即冷却日期,好在rest的前一状态必为持有状态hold,状态机如图
因此状态转移方程式即为(这里直接写空间优化后的):
free = max(free, rest);//stay at s0, or rest from s2
hold = max(hold, free - price[i]); //stay at s1, or buy from s0
rest = hold + price[i]; // only one way from s1
状态初始条件为:
hold = -price[0];
free = 0;
rest = 0;
function solution3($price) {
if(!$price)
return 0;
$free = 0;
$hold = -$price[0];
$rest = 0;
for ($i = 1; $i < count($price); $i++) {
$pre_hold = $hold;
$pre_free = $free;
$free = max($free, $rest);
$hold = max($hold, $pre_free - $price[$i]);
$rest = $pre_hold + $price[$i];
}
return max($free, $rest);
}
题目4: 求这段时间内最多交易两次的最大收益
思路:由于题目限制在买入之前必须未持有,因此不会产生交叉交易的情况,我们可以找到一个中间点,分成两段分别计算最大收益,转化为两个与题目1一样的子问题。
假设总共有n天preProfit[i]表示从0到i天的最大收益,postProfit[i] 表示从i天到n天的最大收益。有状态转移方程式:
preProfit[i] = max(preProfit[i-1], price[i] - minPrice);
postProfit[i] = max(postProfit[i+1], maxPrice - price[i]);
最后再同时遍历两数组,max(preProfit[i]+postProfit[i]);即为所求
function solution5($price) {
if(!$price)
return 0;
$pre_profit = array(0);
$post_profit = array();
$post_profit[count($price) - 1] = 0;
$min = $price[0];
for ($i = 1; $i < count($price); $i++) {
$min = min($min, $price[$i]);
$pre_profit[$i] = max($pre_profit[$i - 1], $price[$i] - $min);
}
$max = $price[count($price) - 1];
for ($i = count($price) - 2; $i >= 0; $i--){
$max = max($max, $price[$i]);
$post_profit[$i] = max($post_profit[$i + 1], $max - $price[$i]);
}
$max_profit = 0;
for ($i = 0; $i < count($price); $i++) {
$max_profit = max($max_profit, $pre_profit[$i] + $post_profit[$i]);
}
return $max_profit;
}
题目5: 求这段时间内最多交易k次的最大收益
思路一:参照Code Ganker 的思路,维护两个变量,原文为local和global,local[i][j]表示第i天一定有交易发生,globlal即为第i天最多进行k次交易的最大收益,其实我觉得用mustSell描述local更容易理解些,状态转移方程式为:
mustSell[i][j] = max(global[i – 1][j – 1]+max(0, profit) , mustSell[i – 1][j] +profit);
//profit = prices[i] – prices[i – 1];
global[i][j] = max(global[i – 1][j], mustSell[i][j]);
mustSell 的值在 前一天的最大收益global[i – 1][j – 1]+max(0, 当天卖出的收益) 和 前一天必须卖出+当天的收益(注意mustSell[i – 1][j]这里并不是j-1,即表示合并前一天的买卖为一次)中取大值
global很好理解,就是第i天卖与不卖之间取最大值
function solution6($price, $k){
if(!$price)
return 0;
if($k > count($price)/2)
return solution2($price);
$mustSell = array();
$global = array();
for ($i = 1; $i < count($price); $i++) {
$profit = $price[$i] - $price[$i - 1];
for ($j = 1; $j <= $k; $j++) {
$mustSell[$i][$j] = max($global[$i - 1][$j - 1]+max(0, $profit), $mustSell[$i - 1][$j] + $profit);
$global[$i][$j] = max($global[$i - 1][$j], $mustSell[$i][$j]);
}
}
return $global[count($price) - 1][$k];
}
当k > I/2时,问题退化为题目2
思路二、滚动扫描:
我们并不需要知道每个时间点买卖收益的全部信息,只需要知道每个时间点的最大收益信息就可以了。我们用free[k]表示在第k次交易卖出的最大收益,hold[k]表示在第k次交易买入的最大交易,状态转移方程式:
free[k] = max(free[k], hold[k] + price[k]);
hold[k] = max(hold[k], free[k-1] - price[k]);
function solution7($price, $k) {
if(!$price)
return 0;
if($k > count($price)/2)
return solution2($price);
$free = array(0);
for ($i = 0; $i <= $k; $i++) {
$hold[$i] = -INF;
}
for ($i = 0; $i < count($price); $i++) {
for ($j = 1; $j <= $k; $j++) {
$free[$j] = max($free[$j], $hold[$j] + $price[$i]);
$hold[$j] = max($hold[$j], $free[$j-1] - $price[$i]);
}
}
return $free[$k];
}