Leetcode上有三道连续的题目,名字叫做Horse Robber,下面我们一一描述。
一、Horse Robber(id:198)
原文描述:
You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security system connected and it will automatically contact the police if two adjacent houses were broken into on the same night.
Given a list of non-negative integers representing the amount of money of each house, determine the maximum amount of money you can rob tonight without alerting the police.
这道题目简单的意思就是从一个正整数数列里面尽可能挑选互不相邻的数字,使它们的和最大。题目的输入是一个整数数字,输出是最大的和。比如说【2,1,1,2】最大的和是4 ,【5,1,1,1,5】最大的和为11。
笔者看到这个题目最直接的想法是分别求奇数标号和偶数标号元素的和,这种想法显然是错的,举的第一个例子就是反例。有时候为了特别大的两个数字能同时添加需要放弃隔一个取一个这种数字数量上最优的选法。这个问题最直观的解法是枚举取法,在决策树的每个内部节点最多只能去掉数列中的2个元素,树的深度大约是n的一半,时间复杂度是指数级别的。
实际上上述的做法会有大量的重复判断和计算。用迭代的思想考虑问题,如果我们知道一个数列能得到的最大和是多少,在它后面再增加一个数字,这个新数列能得到的最大值是能容易知道的,因为我们只需要判断要不要在选取时加入最后一个数所以我们就能从记忆化搜索的角度得到迭代公式。假设数列前k项得到的最大和是f(k),那么:
f(k)=max(f(k-1)+f(k-2)+ak),另外f(0)=0,f(1)=a1
用这个公式对数列进行一次遍历,就能找到整个数列能得到的最大和。另外,我们看到每一步式子里只涉及最后两个f的值,可以把解的数组压缩成奇偶两个取值,这个算法的时间复杂度是O(n),空间上几乎完全是原地作业。
源代码如下:
class Solution {
public:
int rob(vector& nums) {
int even=0;
int odd=0;
int n=nums.size();
for (int i=0;i
二、Horse Robber (id:213)
原文描述:
After robbing those houses on that street, the thief has found himself a new place for his thievery so that he will not get too much attention.
This time, all houses at this place are arranged in a circle. That means the first house is the neighbor of the last one. Meanwhile,
the security system for these houses remain the same as for those in the previous street.
Given a list of non-negative integers representing the amount of money of each house, determine the maximum amount of money you can
rob tonight without alerting the police.
这个题目和上一个几乎一样,求一个数列中互不相邻数字的最大和,唯一的不同是这次的数列是环形的,首项和末项被视为相邻的数,我们不能再直接套用上面的算法,因为不能确定f(1)的取值。
笔者首先想到的方法是对数列中每一个元素进行一次尝试,将其挑选后其左右两个元素都不用再考虑,然后剩下的数列是头尾不相邻的数列,可直接使用上面的算法解决,之后再汇总每次尝试的最大值得到答案。这种算法时间复杂度是O(n^2).
然而,细心的读者不难发现这种方法会有大量的重复的判断,每一次循环在数列相同的位置都在做差不多的判断。笔者希望能将上一例中较为聪明的算法经过一定转化在这个问题中转化,但忘记了除了转化算法适应问题之外,还有转化问题适应算法的做法。首尾相邻的数列和首位不相邻的数列,在这个问题上除了头和尾不能同时选择外没有其他任何的区别。只要去掉首项或者末项,数列就变成和上例一样的数列。反正首项和末项我们最多只能取一个,我们只要分别对去掉首项和去掉末项的数列应用上面的算法,再比较得到的两个最大值,就能得到问题的解了。实际操作过程中发现这个做法不适合只有一个元素的数列,我们要加入一次小小的判断。
源代码如下:
class Solution {
public:
int rob(vector& nums) {
int even=0;
int odd=0;
int x=0,y=0;
int n=nums.size();
for (int i=0;i
原文描述:
The thief has found himself a new place for his thievery again. There is only one entrance to this area, called the "root." Besides the root, each house has one and only one parent house. After a tour, the smart thief realized that "all houses in this place forms a binary tree". It will automatically contact the police if two directly-linked houses were broken into on the same night.
Determine the maximum amount of money the thief can rob tonight without alerting the police.
这次的问题又稍有不同,我们要从一棵二叉树中选取尽量多不邻接的节点,使得它们的和最大。和前面两次一样,因为选择元素的限制仅仅是相邻,我们希望通过迭代解决问题。因为作和过程中父节点和子节点没什么区别,我们既可以从下往上求解,也可以从上往下求解。限于输入结构体的限制,我们选择前者。设二叉树根节点为r,记它能得到的最大和是f(r),那么:
f(r)=max(ar+f(r->left->left)+f(r->left->right)+f(r->right->left)+f(r->right->right),f(r->left)+f(r->right));
简单来说要在两课子树的最大和以及4颗子树的子树加上根节点的值这两个值之间选择。由于 递推式涉及子节点的子节点,这个算法实现过程中要避免指针指空比较的麻烦,代码中出现了较多的分支,源代码如下:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
#include
#include
class Solution {
public:
int rob(TreeNode* root) {
if (root==NULL) return 0;
if (root->left==NULL&&root->right==NULL){
return root->val;
}
if (root->right==NULL){
return max(rob(root->left),root->val+rob(root->left->left)+rob(root->left->right));
}
if (root->left==NULL){
return max(rob(root->right),root->val+rob(root->right->left)+rob(root->right->right));
}
return max(rob(root->left)+rob(root->right),root->val+rob(root->left->left)+rob(root->left->right)+rob(root->right->left)+rob(root->right->right));
}
};
根据递推式简单估计我们得到时间复杂度满足:
O(n)=2O(n/2)+4O(n/4)
实际运行124个样例需要1301ms,在计算f(r->left)时就调用了f(r->left->left)等,所以这些重复的调用通过把第一次算出来的值存起来是可以去掉的,我们在结构体中增加一个数据域,在计算前把输入深拷贝到新的对象中,再进行计算,这样我们实际上每个节点只用进行一次计算,时间复杂度是O(n),运行124个OJ上的样例只用13ms
源程序如下(新类用TreeNode的派生类会更合适,但还是要写深拷贝函数)
/**
* 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) {
node* root2=copy(root);
return getRob(root2);
}
private:
struct node{
int val;
int rob; //rob=-1表示没有访问过这个节点
node* left;
node* right;
node(int x){
val=x;
left=NULL;
right=NULL;
rob=-1;
}
node(int x,node* l,node* r){
val=x;
left=l;
right=r;
rob=-1;
}
};
node* copy (TreeNode* b){
if (b==NULL){
return NULL;
}
return new node(b->val,copy(b->left),copy(b->right));
}
int getRob(node*& root){
if (root==NULL) return 0;
if (root->rob==-1){
if (root->left==NULL&&root->right==NULL){
root->rob=root->val;
}
else
if (root->right==NULL){
root->rob= max(getRob(root->left),root->val+getRob(root->left->left)+getRob(root->left->right));
}
else
if (root->left==NULL){
root->rob= max(getRob(root->right),root->val+getRob(root->right->left)+getRob(root->right->right));
}
else
root->rob=max(getRob(root->left)+getRob(root->right),root->val+getRob(root->left->left)+getRob(root->left->right)+getRob(root->right->left)+getRob(root->right->right));
}
return root->rob;
}
};
四、结语
要成为一名出色的盗马贼不是一件简单的事情。在第一次盗马过程中我们学会了通过记忆化搜索把问题简化成只用决定最后一步,在第二次偷窃中我们懂得稍稍改变输入让其适应我们已有的算法,在最后一次尝试中我们学会了把搜索过程中访问的值进行存储减少分治过程中运算量的消耗。