LeetCode_ House Robber(Ⅰ)、(Ⅱ)、(Ⅲ)

来源:互联网 发布:影像混搭软件 编辑:程序博客网 时间:2024/05/17 06:23

本次做的三道题都是动态规划(dynamic programming)的,属于入门级别,通过三个题,层层递进,加深我对于这些动态规划的题的思考。第一题是直线型的动态规划,第二题是环形的动态规划,第三题是树状动态规划。

House Robber


起初看到这题的时候会简单的以为把奇数位的数字之和与偶数位的数字之和比较后取较大的即可,但其实随意拿个例子演算一下就可以知道其中的错误了,比如2、1、1、5,明显2+5是大于2+1与1+5的。因此为了到更好的效果,在取数字的时候,我们不再相隔一个,也可以相隔两个。

而由于我们不知道现在的取数对将来的取数会有什么影响,因此也不能采用贪心算法。但是我们可以把大问题化简成小问题,比如 抢n家最多能抢多少 与 抢n-1家最多能抢多少 这两个问题是同一个性质的,但显然n-1会比n来得简单一些(规模变小),同样的n-2,n-3....依然是同一性质的问题,而当抢3家,2家甚至1家时,答案就显而易见了。这样就很容易想到可以利用动态规划的思想来解决这个问题了。

我们先看个简单的例子:

3,4,5,1,3,5

我们用d(i)标记抢前i家最多能抢到多少

i=1:显然d(1)=3 

i=2:因为4>3,所以d(2)=4

i=3:因为3+5>4,所以d(3)=8

i=4:因为d(3)>d(2)+1,所以d(4)=8

i=5:因为d(3)+3>d(4),所以d(5)=11

i=6:因为d(4)+5>d(5),所以d(6)=13

使用动态规划,首先我们需要找到动态规划里两个很重要的概念:状态与状态转移方程。状态的寻找从问题的简化就能够很容易得到,就是上面的d(i)。因此我们知道d(0)=0,d(1)=v(1),其中v(1)表示第1家的价值。而抢2家最多能抢到多少要看抢1家多还是抢0家加上抢第2家的多,即d(2)=max{d(0)+v(2),d(1)},同样的抢3家最多能抢到多少要看抢2家多还是抢1家加上抢第3家的多,即d(3)=max{d(1)+v(3),d(2)}......因此我们很容易归纳出:对于i>=2d(i)=max{d(i-2)+v(i),d(i-1)},这就是状态转移方程。

有了状态以及状态转移方程后我们就能够很容易编程实现了,但是编程时需要注意家的序号与家的数量是相差一的,因为序号从0开始算起。

class Solution {public:    int d[100000];    int rob(vector<int>& nums) {        int n = nums.size();    if(n == 0) return 0;    if(n == 1) return nums[0];    d[0] = 0;        d[1] = nums[0];    for(int i = 1; i < n; i++){    if(d[i] >= d[i-1]+nums[i]){    d[i+1] = d[i];    }else    d[i+1] = d[i-1] + nums[i];    }    return d[n];    }};
当然可以不用开辟数组来存储d(i),可以用递归调用的方式获得,不过这样会重复计算很多次相同的值,往往造成很大的时间浪费。


House Robber II


这道题就是把上道题中首尾变成了邻居,抽象来看就是把直线首尾相连变成了圆圈。因此很容易想到把圆圈还原为直线后就变成了上一题一样的问题了。

考虑从某两个相邻节点i、j(i<j)断开连接,那么只有以下三条直线存在了:

1.舍弃节点 i、取节点 j,直线则为 j 到 i-1。

2.舍弃节点 j、取节点 i,直线则为 j+1到 i。

3.舍弃节点 i 与节点 j,直线则为 j+1到 i-1。

最后比较这三条直线得到的最大值,就是在此处断开连接所能达到的最大值了。

其实分析可知,不管在哪处断开,其效果是一样的,因此我们没必要去尝试每一处断开,直接取首尾断开就可以了。

同时从直线的说明上可以知道,实际上第三条直线就是第二条直线去掉 i 的部分。

class Solution {public:    int d[1000000];    int rob1(vector<int>& nums) {    memset(d,0,sizeof(d));    int n = nums.size();    if(n == 0) return 0;    if(n == 1) return nums[0];    d[0]=0;    d[1]=nums[0];    for(int i = 1; i < n; i++){    <span style="white-space:pre"></span>if(d[i] >= d[i-1]+nums[i])    d[i+1] = d[i];    else    d[i+1] = d[i-1]+nums[i];    }    return d[n];    }    int rob(vector<int>& nums) {        int n = nums.size();    if(n == 0) return 0;    if(n == 1) return nums[0];    int temp = nums[n-1];    nums.pop_back();//只舍弃末尾,直线1    int max1 = rob1(nums);    nums.push_back(temp);//还原    nums.erase(nums.begin());//只舍弃开头,直线2    int max2 = rob1(nums);    return max(max1,max(max2,d[n-2]));//d[n-2]为直线2的一部分,即直线3    }};


House Robber III


这同样是第一题的一种拓展,而这种树状结构,很容易就想到约束条件就是抢了某个节点,那么它的左右儿子节点都不能抢,而它第一代孙子节点则可以。那么如果把它的儿子节点统一看成一个节点,孙子节点统一看成一个节点,那么是不是又变成了第一题的问题,同样的直线问题。比如上面的第二个例题,把4与5看成一个节点9,把1,3,1看成一个节点5,那么问题就变成了3,9,5,即与第一题一样。

同时 一整颗树最多能抢多少 与 它的左子树或者右子树最多能抢多少 是同一性质的问题,因此这题的状态就是抢一棵树 R(root) 最多能抢多少,而状态转移方程就是上面分析的:R(root) = max{R(root->left)+R(root->right), R(root->left->left)+R(root->left->right)+R(root->right->left)+R(root->right->left)+V(root)}。

但是我们知道我们并不是很方便的能够标记每一颗子树的节点之间的先后关系,因此像前两题把所有状态记下来的做法并不适用。于是采用递归调用的方式实现。编程时需要注意的是子节点为NULL的情况(不为空才可能有孙子节点)。

/** * Definition for a binary tree node. * struct TreeNode { *     int val; *     TreeNode *left; *     TreeNode *right; *     TreeNode(int x) : val(x), left(NULL), right(NULL) {} * }; */class Solution {public:    int rob(TreeNode* root) {      if(root == NULL) return 0;    if(root->left == NULL && root->right == NULL){    return root->val;    }    int lc = 0, lr = 0;    if(root->left != NULL) lc = rob(root->left->left)+rob(root->left->right);    if(root->right != NULL) lr = rob(root->right->left)+rob(root->right->right);        int val1 = root->val + lc + lr;//父节点+孙节点    int val2 = rob(root->left) + rob(root->right);//子节点        return max(val1,val2);      }};


这三题的核心思想都是动态规划,从前遇到这种题都比较束手无策,现在至少知道可以从化简问题入手,寻找动态规划的状态与状态转移方程,也是一种进步吧。

当然动态规划还是一门比较高深莫测的学问,这几题感觉还比较基础,以后还需要尝试一下比较难的问题。





0 0