算法入门——动态规划

动态规划常常适用于有重叠子问题和最优子结构性质的问题,并且记录所有子问题的结果

动态规划有自底向上(递推)和自顶向下(记忆化递归)两种解决问题的方式

「动态规划」 - 学习计划

无后效性特点:一旦一个子问题的求解得到结果,以后的计算过程就不会修改它,求解问题的过程形成了一张有向无环图

动态规划只解决每个子问题一次,具有天然剪枝的功能,从而减少计算量

一、爬楼梯

递归式:f(n) = f(n - 1) + f(n - 2) , f(1) = 1, f(2) = 2, f(0) = 0

结束条件以及递归过程都可由递归式得出

同高度阶梯被计算多次的递归解法

class Solution {
    public int climbStairs(int n) {
        if(n > 1) {
            return climbStairs(n -1) + climbStairs(n - 2);
        }
        return 1;
    }
}

记忆递归(将计算过的高度阶梯保存到记忆数组)

class Solution {
    //记忆递归
    public int climbStairs(int n) {
        if(n == 1) {//防止数组下标越界的特殊处理
            return 1;
        }
        int[] memory = new int[n + 1];
        memory[1] = 1;
        memory[2] = 2;
        return climbCount(memory, n);
    }

    private int climbCount(int[] memory, int n) {
        if(memory[n] != 0) {
            return memory[n];
        }
        memory[n] = climbCount(memory, n -1) + climbCount(memory, n - 2);
        return memory[n];
    }

}

这个视频讲得非常好 : 爬楼梯

动态规划解法

动态规划的转移方程:f(x)=f(x−1)+f(x−2)

边界条件:f(0)=1,f(1) = 1。枚举x,得出计算结果说明边界可用

class Solution {
    public int climbStairs(int n) {
        if (n == 1)
            return 1;
        if (n == 2)
            return 2;
        int[] climb = new int[n + 1];
        climb[1] = 1;
        climb[2] = 2;
        for (int i = 3; i <= n; i++) {
            climb[i] = climb[i - 1] + climb[i - 2];
        }
        return climb[n];
    }
}
  • 优化:使用滚动数组
class Solution {
    public int climbStairs(int n) {
        int p = 0, q = 0, r = 1;
        for (int i = 1; i <= n; ++i) {
            p = q; 
            q = r; 
            r = p + q;
        }
        return r;
    }
}

可以用动态规划的问题都能用递归

  • 从子问题入手,解决原问题,分两种做法:自顶向下自底向上

  • 前者对应递归,借助函数调用自己,是程序解决问题的方式,它不会记忆解

  • 后者对应动态规划,利用迭代将子问题的解存在数组里,从数组0位开始顺序往后计算

  • 对比:

    • 递归的缺点在于包含重复的子问题(没有加记忆化的情况下)
    • 动态规划的效率更高

DP局限性

  • DP 相比于 递归,有时候不太好理解,或者边界情况比较难确定

  • 必须是一步步邻接的,连续地计算

  • 加入了记忆化的递归,就灵活很多,它在递归基础上稍作修改,往往更好理解,也少了局限性,不好用DP时一定能用它

  • 比如有时候要求出达到某个结果的路径,递归(DFS)回溯出路径,显然更有优势

二、打家劫舍

是否可以使用动态规划:可以将问题分为更小的问题(子问题)

写出子问题的递推关系:最难的一步

  • 递归式:f(n) = MAX(f(n - 1) , f(n - 2) + nums[n - 1])
    • n:被偷的第n号房子(从1开始),最大为数组的长度(也是最后一个房
    • f(n - 1):偷前一个房子,当前n号房子不偷
    • f(n - 2) + nums[n - 1]:偷前面的前面的房子,还有当前n号房子
    • 取两者最大值
    • 递归结束:偷n小于等于0的房子
  • 普通递归(超时)
class Solution {
    public int rob(int[] nums) {
        return robMax(nums, nums.length);
    }

    private int robMax(int[] nums, int n) {
        if(n <= 0) {
            return 0;
        }
        return Math.max(robMax(nums, n - 1), robMax(nums, n - 2) + nums[n - 1]);
    }

}
  • 记忆递归
class Solution {
    //记忆递归
    public int rob(int[] nums) {
        int[] memory = new int[nums.length];
        for(int i = 0; i < nums.length; i++) {
            memory[i] = -1;
        }
        return robMax(nums, nums.length, memory);
    }
    
    private int robMax(int[] nums, int n, int[] memory) {
        if(n <= 0) {
            return 0;
        }
        if(memory[n - 1] == -1) {
            memory[n - 1] = Math.max(robMax(nums, n - 1, memory), robMax(nums, n - 2, memory) + nums[n - 1]);
        }
        return memory[n - 1];
    }

}

动态规划解法

动态规划的转移方程:f(x)=MAX(f(x − 1), f(x − 2) + nums[x - 1])

计算偷 x 个 房子 的最大值(x是从1开始算,数组下标从0)

边界条件:f(0)=1,f(1) = nums[0]。枚举x,得出计算结果说明边界可用

class Solution {
    public int rob(int[] nums) {
        int length = nums.length;
        int[] ans = new int[length + 1];//多一位,存放初始条件 f(0) = 0、f(1) = nums[0];最后一位是答案
        int i = 1;//nums数组的下标
        ans[i] = nums[0];//题目给出条件:nums至少一个元素
        for(; i < length; i++) {
            ans[i + 1] = Math.max(ans[i], ans[i - 1] + nums[i]);//计算 偷(本次循环有)i + 1个房子的最大值
        }
        return ans[i];
    }
}
  • 优化:使用滚动数组

    class Solution {
        public int rob(int[] nums) {
            int p = 0;
            int q = nums[0];
            int ans = q;
            for(int i = 1; i < nums.length; i++) {
                ans = Math.max(q, p + nums[i]);
                p = q;
                q = ans;
            }
            return ans;
        }
    }
    

图解动态规划的解题四步骤(C++/Java/Python) - 打家劫舍

三、三角形最小路径和

递归式总是难倒人:

若定义 f(i, j)f(i,j) 为 (i, j)(i,j) 点到底边的最小路径和,则易知递归求解式为:

f(i, j) = min(f(i + 1, j), f(i + 1, j + 1)) + triangle[i] [j]

记忆递归写法

为什么使用Integer的二维数组:初始每个元素全为空,如果是没有计算过的,那么判断条件就是空值,再将结果放入,将不为空

使用int数组:初始化为0,那么计算中可能得到的是0,将其放入,0这个值不利于判断是否已经计算

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        Integer[][] memory = new Integer[triangle.size()][triangle.get(triangle.size() - 1).size()];
        return minTotal(triangle, 0, 0, memory);
    }

    private int minTotal(List<List<Integer>> triangle, int i, int j, Integer[][] memory) {
        if(i >= triangle.size()) {
            return 0;
        }
        if(memory[i][j] == null) {
            memory[i][j] = triangle.get(i).get(j) + Math.min(minTotal(triangle, i + 1, j, memory), minTotal(triangle, i + 1, j + 1, memory));
        }
        return memory[i][j];
    }
}

递归 + 记忆化 + DP

从底向上递推,ans记录每一行的每个元素向下走的最小路径和

无论是走哪一条路都可以回到ans[0] [0]:即使链表第一行的第一个元素

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        //ans作为dp数组
        int n = triangle.size();
        int[][] ans = new int[n + 1][n + 1];
        int i = n - 1;
        while(i >= 0) {
            for(int j = 0; j <= i; j++) {
                ans[i][j] = Math.min(ans[i + 1][j], ans[i + 1][j + 1]) + triangle.get(i).get(j);
            }
            i--;
        }
        return ans[0][0];
    }
}

优化:

手动模拟一遍上述算法,会发现ans存在累加现象:第n层的值会加到第 n - 1层

那么只用使用 O(n) 的额外空间(n 为三角形的总行数)就能储存各行加起来的结果

ans = new int[triangle.size()]

其中,ans[0] :把下面的每一行都取了最小值添加

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        //ans作为dp数组
        int n = triangle.size();
        int[] ans = new int[n + 1];
        int i = n - 1;
        while(i >= 0) {
            for(int j = 0; j <= i; j++) {
                ans[j] = Math.min(ans[j], ans[j + 1]) + triangle.get(i).get(j);
            }
            i--;
        }
        return ans[0];
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容