由于我是动态规划界菜鸡中的菜鸡,所以这里列的都是很基础的示例
剑指 Offer 10- I:斐波那契数列
关于为什么需要动态规划,还要从经典的斐波那契数列说起(力扣力扣)
递归
这种题当然难不倒我,大一C语言就教了好吗?
class Solution {
/**
* @param Integer $n
* @return Integer
*/
function fib($n) {
if($n<=1){
return $n;
}
return $this->fib($n-1)+$this->fib($n-2);
}
}
在测试用例$n=39时,超出运行时间限制了...
本例中的递归解法差在哪了(只是本例,不是所有递归一概而论的)
如图:它有大量的重复计算,算f(n)前需要先算f(n-1)、f(n-2),再依次向下算...
map图
有一些改良的方法,就是在缓存或者增加数组来存储f(0)、f(1)...f(n-2)这些值,以避免重复计算,虽然会额外增加内存空间,但是思路还是不错的
class Solution {
public static $map = [];
/**
* @param Integer $n
* @return Integer
*/
function fib($n) {
if($n<=1){
self::$map[$n] = $n;
return $n;
}else{
$a = isset(self::$map[$n-1]) ? self::$map[$n-1] : $this->fib($n-1);
$b = isset(self::$map[$n-2]) ? self::$map[$n-2] : $this->fib($n-2);
//因为$n只与最邻近两个元素相关,所以可以删除前面的以减少内存占用开销,使得空间复杂度为常量O(1)
if(isset(self::$map[$n-3])){ unset(self::$map[$n-3]); }
self::$map[$n] = ($a+$b)%1000000007;
return self::$map[$n];
}
}
}
因为$map随着元素增加而线性递增,空间复杂度o(n)。
也可以按注释中类似方法删除不需要部分,空间复杂度o(1)。
动态规划
动态规划和递归有一点相似,但是动态规划往往要求总结和显式指出规律,在处理具有重叠子问题时相对递归的优势就非常明显。
f(n)只跟f(n-1)和f(n-2)有关,那么可以只记录更新f(n-1)、f(n-2)
class Solution {
/**
* @param Integer $n
* @return Integer
*/
function fib($n) {
if($n<=1){
return $n;
}
$a = 0;
$b = 1;
for($i=2;$i<=$n;$i++){
$sum = ($a+$b)%1000000007;
$a = $b;
$b = $sum;
}
return $sum;
}
}
GO:
package main
func main() {
f := fei(6)
println(f)
}
func fei(n int) int{
var a int = 0
var b int = 1
var sum int
if n<=1{
return n
}
for i :=2;i<=n;i++{
sum = a+b
a = b
b = sum
}
return sum
}
剑指 Offer 10- II. 青蛙跳台阶问题
与斐波那契数列几乎相同的还有青蛙跳台问题
递归
class Solution {
/**
* @param Integer $n
* @return Integer
*/
function numWays($n) {
if($n<=1){
return 1;
}
return $this->numWays($n-1)+$this->numWays($n-2);
}
}
气不气~
map
存一下过往数据
class Solution {
public static $map=[];
/**
* @param Integer $n
* @return Integer
*/
function numWays($n) {
if($n<=1){
self::$map[$n]=1;
return 1;
}else{
$a = isset(self::$map[$n-1]) ? self::$map[$n-1] : $this->numWays($n-1);
$b = isset(self::$map[$n-2]) ? self::$map[$n-2] : $this->numWays($n-2);
self::$map[$n]=($a + $b)%1000000007;
return self::$map[$n];
}
}
}
还有与其几乎完全一样的爬楼梯问题:力扣
动态规划
class Solution {
/**
* @param Integer $n
* @return Integer
*/
function numWays($n) {
if($n<=1){
return 1;
}
$a = $b = 1;
for($i=2;$i<=$n;$i++){
$sum = ($a+$b)%1000000007;
$a = $b;
$b = $sum;
}
return $sum;
}
}
时间空间复杂度都是常量级的O(1)
力扣198题:打家劫舍
当然这是动态规划非常基础的用法,还有非常经典的打家劫舍问题
网上分析很多,其实偷窃最大金额就在隔一间前的最大金额+最后一间的总金额与一间前的最大金额的对比结果。
动态规划1
非常经典的dp数组开辟额外存储空间,很好理解
class Solution {
/**
* @param Integer[] $nums
* @return Integer
*/
function rob($nums) {
if(count($nums)==1){
return $nums[0];
}elseif (count($nums)==2) {
return max($nums[0],$nums[1]);
}
$dp=[];
$dp[0] = $nums[0];
$dp[1] = max($nums[0],$nums[1]);
for($i=2;$i<count($nums);$i++){
$dp[$i] = max($dp[$i-2]+$nums[$i],$dp[$i-1]);
//确定不用后删除,内存占用变为常量级
unset($dp[$i-2]);
}
//因为最后执行的$i++,实际$i的值会=count($nums),需要-1以获取数组最后一个元素
return $dp[$i-1];
}
}
动态规划2
当然如果理解,其实这也是非常经典的辗转交换,不必开辟了空间再删除
class Solution {
/**
* @param Integer[] $nums
* @return Integer
*/
function rob($nums) {
if(count($nums)==1){
return $nums[0];
}elseif (count($nums)==2) {
return max($nums[0],$nums[1]);
}
$a = $nums[0];
$b = max($nums[0],$nums[1]);
for($i=2;$i<count($nums);$i++){
$c = max($a+$nums[$i],$b);
$a = $b;
$b = $c;
}
return $c;
}
}
不清楚abc具体为啥这样交换的话,参考上例中dp数组,就会发现它是一个平移
时间复杂度O(n),空间复杂度O(1)
种花问题
每块地种花的条件是:前后自己都没种过花,种了花的地需要标记为1,第一个和最后一个位置特殊情况单独考虑。
动态规划
class Solution {
/**
* @param Integer[] $flowerbed
* @param Integer $n
* @return Boolean
*/
function canPlaceFlowers($flowerbed, $n) {
if(count($flowerbed) == 1){
return intval($flowerbed[0] == 0) >= $n;
}else{
$nums = 0;
$start = 1;
if($flowerbed[0] == 0 && $flowerbed[1] == 0){
$nums ++;
$flowerbed[0] = 1;
}
$count = count($flowerbed);
for($i=$start;$i<$count-1;$i++){
if($flowerbed[$i-1] == 0 && $flowerbed[$i] == 0 && $flowerbed[$i+1] == 0){
$nums++;
$flowerbed[$i] = 1;
}
}
if($nums >= $n){
return true;
}
if($flowerbed[$count-2] == 0 && $flowerbed[$count-1] == 0){
$nums++;
}
}
return $nums >= $n;
}
}
最大子序和
动态规划
当前最大值$max和之前最大值有关,也和当前能累积到的最大“助力”$pre有关,当$nums[$i]加上自己能累积的“助力” > 之前最大值$max时,将取代之。
function maxSubArray($nums) {
$pre = 0;
$max = $nums[0];
for($i=0;$i<count($nums);$i++){
//$pre是与$nums[$i]连续后的最大值
$pre = max($pre+$nums[$i],$nums[$i]);
//当前最大值
$max = max($pre,$max);
}
return $max;
}
时间复杂度O(n),空间复杂度O(1)
使用最小花费爬楼梯
动态规划
所谓的爬一阶实际是到达相邻元素,爬两阶是到达隔一个的元素,当楼梯数大于等于3之后,可知f(n) = min(f(n-2)+cost[n-2],f(n-1)+cost[n-1]).动态规划的难点就在推导规律,可以先枚举再总结。