動態規劃比較經典的題目
給定一支股票一段日期內的的每日價格price
題目1: 求這段時間內買賣一次獲得的最大收益
思路:即找到一組i和j,使得 price[i] - price[j] 最大,且i<j
狀態轉移方程式爲: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];
}