日练三题,冰冻三尺非一日之寒。
今日题目:1、出现一次的数II;2、房子小偷III;3、生成括号。
今日摘录:有些事,只能一个人做。有些关,只能一个人过。有些路啊,只能一个人走。–龙应台《目送》
137. Single Number II | Difficulty: Medium
Given an array of integers, every element appears three times except for one. Find that single one.
Note:
Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?
题意:一个数组中所有数字都出现3次唯独一个数只出现一次,找到它。
相似题目:Single Number I | Single Number III
思路:
1、首先感觉这道题目有trick,但是一时间找不到的话不妨先用蠢一点的办法试试,因此先写了一个python版本。
代码:
python:
class Solution(object):
def singleNumber(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
num_set = set(nums)
for key in num_set:
if nums.count(key)==1:
return key
break
else:
pass
结果:Time Limit Exceeded
失败的原因是测试样例中有一个20002个数字的测试样例,这个逐个访问的方法行不通。
2、扫了一眼b哥hzh的代码,用加法代替count,稍微用一个移位把时间从81ms减少到58ms
python:
class Solution(object):
def singleNumber(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
num_set = set(nums)
return (sum(num_set)*3-sum(nums))>>1
结果:58ms
3、为了方便讨论,我们将这道题描述为在应该出现k次的数字中查找只出现了p次的那一个数字。
这道题是k=3,p=1,如果情况再复杂一点,这个方法就可能不再适用。这个时候不妨去discuss看看votes较高的那些回答。
https://discuss.leetcode.com/topic/2031/challenge-me-thx的代码。
C++
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ones = 0, twos = 0;
for(int i = 0; i < nums.size(); i++)
{
ones = (ones ^ nums[i]) & ~twos;
twos = (twos ^ nums[i]) & ~ones;
}
return ones;
}
};
结果:12ms
刚看到这一段代码,感觉不太好理解。
ones = (ones ^ nums[i]) & ~twos;
twos = (twos ^ nums[i]) & ~ones;
这两行究竟是什么意思?为什么会可以再这里解决问题?
首先,从状态的角度来思考这个问题,我们需要的目的是使得每次一个数出现3次和0次效果等价,这就需要形成一个环,00->X0->0X->00(起点),那么,就需要两个比特位来表示这个状态信息,如果只有一个数字出现1次,其他都是3次,即k=3,p=1,最后的状态一定是X0,其中的X就是我们要找的那个数。
那么现在就来实现状态转换方程:
对于第一位而言:
如果第二位是0,那么第一位就发生状态变化;
如果第二位是X就置0。
这等价于ones = (ones ^ nums[i]) & ~twos;
对于第二位而言:
当第一位是X就置0;
第一位是0就发生状态变化
第一位是X就保持状态不变。
这等价于 twos = (twos ^ nums[i]) & ~ones;
两个位置都是根据另外一个位置来决定自己的状态变化与否的。
总结起来就是,每次从第一位开始计算,如果另外一位是0,就发生状态变化,如果不是0,就保持不变。
扩展:那么,如果我稍微改下题目,改成k=5,p=1,如何去做呢?
本想顺着推一下。发现太蠢推不出来,先留在这里。
4、把上述写成一种更好理解的形式
状态方程为:00->X0->0X->00(起点)
分别记为 0、 1、 2、 3状态
假设输入数组为[2,3,2,2],很显然,这等价于[2,2,2,3]
那么我们的方程目的就是使得3个2输入进来之后ones和twos都是0,然后最后将3计入到ones中从而得到我们希望求解的值。
怎么理解这个方程呢?
首先对twos进行操作,twos与(ones&i)的值有关,奇数状态时(1、3状态),ones为0,(ones&i)为0,twos就保持不变,偶数状态时(2状态),ones为i,(ones&i)为i,twos就改变状态。
总而言之就是x1不为0就变,x1为0就不变
ones的值每次都会变化,从0->i->0->i。
而twos的值就是奇不变偶变,从 0->0->i->i。
这个时候经过三次变化从(0,0)变成了(i,i),并没有如我们所愿形成一个环路。这个时候下一个变量的作用就出现了。
显然我们可以看出来只要ones和twos中有一个为0,这个变量就不起作用,只有当两个都不为0的时候,将ones和twos进行一下状态的变化。
mask = ~(x1 & x2) 当x1或者x2至少有1个0,mask值为-1,再将x1&mask;x2&mask就不会产生变化。
当x1或者x2中没有0的时候,mask = ~i;x1&~i==x2&~i==i&~i==0。也就是置0操作。
至此,就能写出代码:
class Solution {
public:
//x1代替ones,x2代替twos以便于书写
int singleNumber(vector<int>& nums) {
int x1 = 0;
int x2 = 0;
int mask = 0;
for (int i : nums) {
x2 ^= x1 & i;
x1 ^= i;
mask = ~(x1 & x2);
x2 &= mask;
x1 &= mask;
}
return x1;
}
};
结果:15ms
还是考虑拓展情况k=5,p=3;
还是一样思路,效仿之前x2需要x1有值才变,
首先对x3进行操作,x3需要x1和x2都不为0才变,x2只需要x1不为0才变,x1无需条件,每次都变
可以得出
0->1->0->1->0->1
0->0->1->1->0->0
0->0->0->0->1->1
这次的mask变量需要一些变化,应该是101状态的时候才将mask置0,mask置0也就是x1、x2、x3置0.
因此
mask = ~(x1 & ~x2 & x3);
容易写出代码
class Solution {
public:
//x1代替ones,x2代替twos以便于书写
int singleNumber(vector<int>& nums) {
int x1 = 0;
int x2 = 0;
int x3 = 0;
int mask = 0;
for (int i : nums) {
x3 ^= x2 & x1 & i;
x2 ^= x1 & i;
x1 ^= i;
mask = ~(x1 & ~x2 & x3);
x3 &= mask;
x2 &= mask;
x1 &= mask;
}
return x1;
}
};
5、累加数组中所有数字的二进制位,然后遍历一次每个二进制位,如果某些为不能被k整除(k是数组中多数出现的数),那么就将这些位置进行一次累加,就可以得到我们要找的数字。
C++
class Solution {
public:
int singleNumber(vector<int>& nums) {
int bits[32];
int i, j;
// 累加数组中所有数字的二进制位
memset(bits, 0, 32 * sizeof(int));
for (i = 0; i <nums.size(); i++)
for (j = 0; j < 32; j++)
bits[j] += ((nums[i] >> j) & 1);
// 如果某位上的结果不能被整除,则肯定目标数字在这一位上为
int result = 0;
for (j = 0; j < 32; j++)
if (bits[j] % 3 != 0)
result += (1 << j);
return result;
}
};
结果:20ms
337. House Robber III | Difficulty: Medium
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.
Example 1:
3
/ \
2 3
\ \
3 1
Maximum amount of money the thief can rob = 3 + 3 + 1 = 7.
Example 2:
3
/ \
4 5
/ \ \
1 3 1
Maximum amount of money the thief can rob = 4 + 5 = 9.
题意:House Robber的一个延伸,这里的小偷偷的房子不再是一个数组,而是一个二叉树的形式。
思路:
1、最大值可能的情况是
①、选择根节点情况下根节点的左节点的子节点中能取到的最大值(不包括根节点的左节点)和根节点的右节点能取到的最大值(不包括根节点的右节点)之和
②、不选择根节点的情况下递归调用,使得根节点的左节点为根节点和根节点的右节点为根节点的情况下取得的最大值。
这两者中的更大者将是题中所求的最大解。
/**
* 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;
int val=0;
if(root->left!=NULL)
val+=rob(root->left->left)+rob(root->left->right);
if(root->right!=NULL)
val+=rob(root->right->left)+rob(root->right->right);
return max(val+root->val,rob(root->left)+rob(root->right));
}
};
结果:Time Limit Exceeded
显然,这样递归去寻求解遇到比较复杂的树时间复杂度太高。
2、是否可以考虑用一个辅助的哈希表来进行一些中间状态的存储呢?这样可以减少一些递归调用开销。
/**
* 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) {
unordered_map<TreeNode*,int> maps;
return robSub(root,maps);
}
int robSub(TreeNode* root, unordered_map<TreeNode*,int>maps)
{
if(root==NULL) return 0;
if(maps.find(root)!=maps.end()) return maps[root];
int val=0;
if(root->left!=NULL)
val+=robSub(root->left->left,maps)+robSub(root->left->right,maps);
if(root->right!=NULL)
val+=robSub(root->right->left,maps)+robSub(root->right->right,maps);
val = max(val+root->val,robSub(root->left,maps)+robSub(root->right,maps));
maps.insert(make_pair(root,val));
return val;
}
};
结果:Time Limit Exceeded
依然超时
3、那么,还有什么办法呢?
感觉从一个栗子来看会不会好一点?
3
/ \
4 5
/ \ \
1 3 1
就题目给的栗子来看。
从根节点开始考虑,可以用一个左数组存左边可能的两种情况,右数组存右边可能的两种情况。
这两种情况又是分别对应着哪两种情况呢?首先看左边,左子树的根节点是4,
①、选择以1节点为根能取得的最大值和以3节点为根的最大值;
②、这个时候可以选择4节点和(1、3各自节点中的最大值)。
将①记为res[0],将②记做res[1]。
那么,对于左子树的根节点4而言,又可以用两个数组来表示左边节点的信息和右边节点的信息。
左边节点信息就是递归再去调用左子节点1,右边节点信息就是递归再去调用右子节点3。
对于叶子节点而言,已经没有左右子节点,left=[0,0],right=[0,0]
res=[0,叶节点的值] 。
如何直观理解left、right、result?
left[0]左边与根节点有间隔的取值
left[1]左边与根节点无间隔的取值
right[0]右边与根节点有间隔的取值
right[1]右边与根节点无间隔的取值
res[0]:不取根节点的情况下,左边能取到的最大值和右边能取到的最大值之和。
res[1]:取根节点的情况下,那么left与right就不能取无间隔的取值,也就是不能取left[1]和right[1],那么res[1]就代表取了根节点的情况下,剩下左右两边的无间隔取法值与根节点之和。
平凡情况下的叶子节点res=[0,叶节点值]的原因是以叶子节点作为根节点,res[0]表示不能取根节点,所以一定是0,res[1]是取根节点情况,剩下也没有节点可以继续选取了,所以就是叶节点的值。
/**
* 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) {
vector<int> res = robSub(root);
return max(res[0],res[1]);
}
vector<int>robSub(TreeNode* root)
{
if(root==NULL) return vector<int>(2,0);
int val=0;
vector<int> left = robSub(root->left);
vector<int> right = robSub(root->right);
vector<int> result(2,0);
result[0] = max(left[0],left[1])+max(right[0],right[1]);
result[1] = root->val+left[0]+right[0];
return result;
}
};
结果:20ms
22. Generate Parentheses | Difficulty: Medium
Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.
For example, given n = 3, a solution set is:
[
“((()))”,
“(()())”,
“(())()”,
“()(())”,
“()()()”
]
题意:给定n对括号,求所有正确生成的括号组合。
思路:
1、枚举法,以n为3为例,一共有3对括号,因为第一个一定是左括号,最后一个一定是右括号,所以变化部分就在于中间4个,枚举2的4次方种可能性,一共16种可能,对每种可能性判断合法性。这种方法在n小的时候适用,当n=100的时候,有2的198次方种可能性,指数级增长速度。
判断合法的代码主要是通过截止当前括号为止左边括号比右边多的数目,如果小于0肯定不对,如果大于n肯定也是不对的。
代码略。
2、递归思想,首先用n表示还剩下左括号的数量,m表示还应该放置右括号以匹配已有左括号的数量。开始状态为(n,0),每次判断,右括号优先级比左括号高,也就是说,只要m>0,就先放右括号,如果不放右括号的时候再判断放左括号,条件为n>0。
以n为3举例,
首先生成(3,0)->(2,1)->(2,0)->(1,1)->(1,0)->(0,1)->(0,0)生成()()()
然后回溯到(1,1)状态,因为(1,1)除了能先放右括号还能放左括号。生成(3,0)->(2,1)->(2,0)->(1,1)->(0,2)->(0,1)->(0,0)生成()(())
然后回溯到(2,1)状态,因为(2,1)除了能先放右括号还能放左括号。生成(3,0)->(2,1)->(1,2)->(1,1)->(1,0)->(0,1)->(0,0)生成(())()
然后回溯到(1,1)状态,因为(1,1)除了能先放右括号还能放左括号。生成(3,0)->(2,1)->(1,2)->(1,1)->(0,2)->(0,1)->(0,0)生成(()())
然后回溯到(1,2)状态,因为(1,2)除了能先放右括号还能放左括号。生成(3,0)->(2,1)->(1,2)->(0,3)->(0,2)->(0,1)->(0,0)生成((()))
大概思路就是这样,代码其实很简单
class Solution {
public:
vector<string> generateParenthesis(int n) {
vector<string> res;
addingpar(res, "", n, 0);
return res;
}
void addingpar(vector<string> &v, string str, int n, int m){
if(n==0 && m==0) {
v.push_back(str);
return;
}
if(m > 0){ addingpar(v, str+")", n, m-1); }
if(n > 0){ addingpar(v, str+"(", n-1, m+1); }
}
};
结果:0ms