LeetCode算法练习——深度优先搜索 DFS(2)

我们继续LeetCode之旅.
做了一段时间的LeetCode,感觉还是不错的。算法很基础,没有特别难的(至少我看在做的),很适合考试,面试,就业之前的训练。对提升基本功很有帮助,我觉得如果有时间的话,每天都做上几道,日积月累,代码能力必然有提高。
这一篇继续进行DFS的练习,预计还会有十道题。

给个目录:
LeetCode109 有序链表转换二叉搜索树
LeetCode332 重新安排行程
LeetCode337 打家劫舍 III
LeetCode394 字符串解码
LeetCode417 太平洋大西洋水流问题
LeetCode473 火柴拼正方形
LeetCode494 目标和
LeetCode491 递增子序列
LeetCode515 在每个树行中找最大值
LeetCode513 找树左下角的值

LeetCode109 有序链表转换二叉搜索树

题目

给定一个单链表,其中的元素按升序排序,将其转换为高度平衡的二叉搜索树。

本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。

示例:

1
2
3
4
5
6
7
8
9
给定的有序链表: [-10, -3, 0, 5, 9],
一个可能的答案是:[0, -3, 9, -10, null, 5], 它可以表示下面这个高度平衡二叉搜索树:
0
/ \
-3 9
/ /
-10 5

C++代码

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
bool isValidBST(TreeNode* root) {
return dfs(root,LONG_MAX,LONG_MIN);
}
bool dfs(TreeNode* root, long max,long min){
if(!root) return true;
if(root->val<=min || root->val>=max) return false;
else return (dfs(root->left,root->val,min) && dfs(root->right,max,root->val));
}
};

体会

这个题利用了二叉检索树自身的性质,左边节点小于根节点,右边节点大于根节点。初始化时带入系统最大值和最小值,在递归过程中换成它们自己的节点值,用long代替int就是为了包括int的边界条件。
如果这棵树遍历到了叶节点,则返回true。如果在遍历的过程中出现了当前节点大于等于父节点(左子树)或小于等于父节点(右子树)则返回false。对结果做&&运算。返回最终的结果。

LeetCode332 重新安排行程

题目

给定一个机票的字符串二维数组 [from, to],子数组中的两个成员分别表示飞机出发和降落的机场地点,对该行程进行重新规划排序。所有这些机票都属于一个从JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 出发。

说明:

如果存在多种有效的行程,你可以按字符自然排序返回最小的行程组合。例如,行程 [“JFK”, “LGA”] 与 [“JFK”, “LGB”] 相比就更小,排序更靠前
所有的机场都用三个大写字母表示(机场代码)。
假定所有机票至少存在一种合理的行程

示例 1:
tickets = [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]]
返回["JFK", "MUC", "LHR", "SFO", "SJC"]

示例 2:
tickets = [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
返回["JFK","ATL","JFK","SFO","ATL","SFO"]
另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"]。但是它自然排序更大更靠后。

C++代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
unordered_map<string,multiset<string>> hash; //用来存储起点与终点的信息
vector<string> res; //最终的路径结果
vector<string> findItinerary(vector<pair<string, string>> tickets) {
if(tickets.size()==0) return {}; //如果输入为空 返回空
for (int i=0;i<tickets.size();i++){
hash[tickets[i].first].insert(tickets[i].second); //将ticket转化为hash
}
dfs("JFK"); //dfs
reverse(res.begin(), res.end()); //因为最终得到的结果是一个反序列的 我们要返回来
return res;
}
void dfs(string from){
while(hash[from].size()>0){ //如果当前起点还有终点没有去过的话
string tem = *hash[from].begin(); //记录下当前起点去往的第一个终点
hash[from].erase(hash[from].begin()); //将第一个终点从hash中删除
dfs(tem); //继续dfs
}
res.push_back(from); //将当前的出发点加入res
}
};

体会

题目看起来并没有什么难度。DFS+回溯,但我觉得有坑。。。
第一次写,写的这个代码,所有样例测试完全正确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Solution {
private:
vector<string> res;
public:
vector<string> findItinerary(vector<pair<string, string>> tickets) {
vector<string> path;
vector<int> mark;
for(int i=0;i<tickets.size();i++)
mark.push_back(0);
path.push_back("JFK");
dfs(tickets,mark,path,"JFK");
tickets.clear();
path.clear();
mark.clear();
return res;
}
void dfs(vector<pair<string,string>> tickets, vector<int> mark, vector<string> path, string from){
int flag = true;
for(int i=0;i<tickets.size();i++){
if(mark[i]==0) flag=false;
}
if(flag == true){
res = path;
return;
}
for(int i=0;i<tickets.size();i++){
if(tickets[i].first==from && mark[i]==0){
path.push_back(tickets[i].second);
mark[i] = 1;
dfs(tickets,mark,path,tickets[i].second);
mark[i]=0;
path.pop_back();
}
}
return;
}
};

可是,每次提交都会报一个错误,本地调试也没有问题。就很迷。
换了思路重新写。
各位有兴趣麻烦帮我检查一下代码。

另外,此题少给出了一个条件。就是默认输出最长的路径,路径长度相同时,再以字典序列输出。

LeetCode337 打家劫舍 III

题目

小偷又发现一个新的可行窃的地点。 这个地区只有一个入口,称为“根”。 除了根部之外,每栋房子有且只有一个父房子。 一番侦察之后,聪明的小偷意识到“这个地方的所有房屋形成了一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

在不触动警报的情况下,计算小偷一晚能盗取的最高金额。

示例 1:

1
2
3
4
5
3
/ \
2 3
\ \
3 1

能盗取的最高金额 = 3 + 3 + 1 = 7.

示例 2:

1
2
3
4
5
3
/ \
4 5
/ \ \
1 3 1

能盗取的最高金额 = 4 + 5 = 9.

C++代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
//偷 对第一个根节点 采取偷与不偷的方式 最后求最大值
int rob(TreeNode* root) {
if(!root) return 0;
return std::max(robRoot(root),notRobRoot(root));
}
//偷根节点的情况 就不能偷两个子节点
int robRoot(TreeNode *root){
if(!root) return 0;
return notRobRoot(root->left) +notRobRoot(root->right)+root->val;
}
//不偷根节点的情况 可以偷两个子节点 但不一定就要偷
int notRobRoot(TreeNode *root){
if(!root) return 0;
return rob(root->left) + rob(root->right);
}
};

体会

这个题属于思路很清晰就很简单的题。
首先我们来分析题目,题目中说如果两个节点连在一起,那么偷东西就会被发现。所有只要我们偷东西的节点不连在一起就没有问题。连在一起的情况只有根节点与子节点。
对于一个根节点分为偷与不偷两种情况,如果偷的话,那么他的子节点就不能偷;如果不偷的话,可以偷他的子节点,也可以不偷,根据偷到的金额去做判断。对于每个根节点,我们要把这两周情况都考虑上,代码如上。

LeetCode394 字符串解码

题目

给定一个经过编码的字符串,返回它解码后的字符串。

编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。

你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。

此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。

示例:

1
2
3
s = "3[a]2[bc]", 返回 "aaabcbc".
s = "3[a2[c]]", 返回 "accaccacc".
s = "2[abc]3[cd]ef", 返回 "abcabccdcdcdef".

C++代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public:
string decodeString(string s) {
dfs(s);
return s;
}
void dfs(string &s){
int l=0,r=0; //方括号的左起点 右起点
string num_s=""; //存储数字的部分
int num_len =0; //数字的长度
int num; //数字本身
//遍历找方括号
for(int i=0;i<s.length();i++){
if(s[i]=='[') { //如果是左括号
l = i; //记录位置
num = atoi(num_s.data()); //将之前存储的num转换为数字
num_len=num_s.size(); //得到数字的长度 用于后面裁切字符串
num_s.clear(); //清空 方式影响后面内容
}
else if(s[i]==']') {
r = i; //记录右括号位置
break; //结束循环
}
else if(s[i]<='9' && s[i]>='0') num_s+=s[i]; //记录每一位的数字
}
if(l>=r) return ; //结束条件
string newstr=""; //预备重复的部分字符串
for(int i=0;i<num;i++)
newstr+=s.substr(l+1,r-l-1); //重复字符串
s = s.substr(0,l-num_len)+newstr+s.substr(r+1,s.length()-r-1); //制作新的字符串
dfs(s);//递归调用
}
};

体会

算是一道完全的模拟题。
我用的思路比较简单,每次都只处理最里面的一个[],将其中的内容重复。逐次递归调用,最后解析完所有的字符串。注意s = s.substr(0,l-num_len)+newstr+s.substr(r+1,s.length()-r-1);各个字符串的起始位置和裁切长度。

LeetCode417 太平洋大西洋水流问题

题目

给定一个 m x n 的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。

规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。

请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。

提示:
输出坐标的顺序不重要
m 和 n 都小于150

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
给定下面的 5x5 矩阵:
太平洋 ~ ~ ~ ~ ~
~ 1 2 2 3 (5) *
~ 3 2 3 (4) (4) *
~ 2 4 (5) 3 1 *
~ (6) (7) 1 4 5 *
~ (5) 1 1 2 4 *
* * * * * 大西洋
返回:
[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (上图中带括号的单元).

C++代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Solution {
public:
vector<pair<int, int>> pacificAtlantic(vector<vector<int>>& matrix) {
if (matrix.empty() || matrix[0].empty()) return {};
int m = matrix.size(); //矩阵行数
int n = matrix[0].size(); //矩阵列数
vector<pair<int,int>> res; //最终结果
vector<vector<bool>> pacific(m, vector<bool>(n, false)); //太平洋
vector<vector<bool>> atlantic(m, vector<bool>(n, false)); //大西洋
//遍历第一行,最底下一行
for(int i=0;i<m;i++){
dfs(matrix,pacific,i,0,matrix[i][0]);
dfs(matrix,atlantic,i,n-1,matrix[i][n-1]);
}
//遍历第一列,最右边一列
for(int i=0;i<n;i++){
dfs(matrix,pacific,0,i,matrix[0][i]);
dfs(matrix,atlantic,m-1,i,matrix[m-1][i]);
}
//将pacific与atlantic重合的部分 作为最终结果
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(atlantic[i][j] && pacific[i][j])
res.push_back(make_pair(i,j));
}
}
return res;
}
void dfs(vector<vector<int>>& matrix,vector<vector<bool>> &visited,int x,int y,int val){
//越界 被访问 或者 当前值小于上一个值的都直接返回
if (x<0 || x>matrix.size()-1 || y<0 || y>matrix[0].size()-1 || visited[x][y] || matrix[x][y] < val) return;
//将该坐标设置为访问过
visited[x][y] = true;
dfs(matrix,visited,x+1,y,matrix[x][y]);
dfs(matrix,visited,x-1,y,matrix[x][y]);
dfs(matrix,visited,x,y+1,matrix[x][y]);
dfs(matrix,visited,x,y-1,matrix[x][y]);
}
};

体会

这个题第一眼看到觉得是一个很简单DFS搜索题。第一个想法肯定是对每一个点进行dfs,看他是否能够走到太平洋或者大西洋。显然这样,时间复杂度会很高。我们可以对其优化,每一次得到一条路径后,将路径上的所有点都记录下来,这样可以减少很多次dfs的过程,但优化效果不明显。
这个题,我们采用倒推法的思路。从大西洋、太平洋向内搜索,每次找比当前节点高的或者相同的节点,将这些节点设置为True。最终将大西洋中为True的,太平洋中也为True的节点作为最终的答案。

LeetCode473 火柴拼正方形

题目

还记得童话《卖火柴的小女孩》吗?现在,你知道小女孩有多少根火柴,请找出一种能使用所有火柴拼成一个正方形的方法。不能折断火柴,可以把火柴连接起来,并且每根火柴都要用到。

输入为小女孩拥有火柴的数目,每根火柴用其长度表示。输出即为是否能用所有的火柴拼成正方形。

示例 1:

1
2
3
4
输入: [1,1,2,2,2]
输出: true
解释: 能拼成一个边长为2的正方形,每边两根火柴。

示例 2:

1
2
3
4
输入: [3,3,3,3,4]
输出: false
解释: 不能用所有火柴拼成一个正方形。

注意:

给定的火柴长度和在 0 到 10^9之间。
火柴数组的长度不超过15。

C++代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
bool makesquare(vector<int>& nums) {
if(nums.size()<4 || nums.size()==0) return false; //如果数组的大小不满足4 就直接返回0
int sum = accumulate(nums.begin(),nums.end(),0); //对整个数组进行求和
int target = sum /4 ; //计算每一条边需要的长度
vector<int> sums(4,0); //新建一个长度为4的数组,用来记录每一个边当前的长度
sort(nums.rbegin(),nums.rend()); //对数组进行排序,先计算大的,这样可以优化不少效率
bool res = dfs(nums,sums,0,target); // dfs
return res;
}
bool dfs(vector<int>& nums,vector<int> sums, int pos, int target){
if(pos>nums.size()-1){ //如果pos>nums.size()-1证明数组已经被遍历完,判断当前所有的边是否都满足要求
return sums[0]==target && sums[1]==target && sums[2]==target && sums[3]==target;
}
//对每一条边进行尝试
for(int i=0;i<4;i++){
if(sums[i]+nums[pos]>target) continue; //如果i边 放入一个数字后,超过target则不放入,进入下一条边
sums[i]+=nums[pos]; //i边 长度增加
if(dfs(nums,sums,pos+1,target)) return true; //pos+1 继续dfs
sums[i]-=nums[pos]; //回溯
}
return false;
}
};

体会

这个题是一个比较标准DFS+回溯题。这问题可以理解为,一个数组如何分为四个相同的数。我们创建一个数组,长度为四,用来表明四条边当前的长度,每次遍历时将当前的长度与待放入长度相加后target进行比较。大于,则不放入,小于,则放入,不断dfs,每次pos+1。当pos超出范围时,证明所有的数字都被尝试过,返回当前的状态。

LeetCode494 目标和

题目

给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
输入: nums: [1, 1, 1, 1, 1], S: 3
输出: 5
解释:
-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3
一共有5种方法让最终目标和为3。

注意:

数组的长度不会超过20,并且数组中的值全为正数。
初始的数组的和不会超过1000。
保证返回的最终结果为32位整数。

C++代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
int solution =0; //方案数
int findTargetSumWays(vector<int>& nums, int S) {
int sum = 0;
dfs(nums,S,0,sum);
return solution;
}
void dfs(vector<int>& nums,int S,int pos,int sum){
//S表示最后结果,pos表示当前位置,sum表示目前总和
if(pos>nums.size()-1) {
if(sum == S) solution+=1; //如果当前sum==S 方案数+1
return ;
}
sum += nums[pos]; //先尝试+
dfs(nums,S,pos+1,sum); //dfs
sum -= nums[pos]; //回溯
sum -= nums[pos]; //尝试-
dfs(nums,S,pos+1,sum); //dfs
sum += nums[pos]; //回溯
return ;
}
};

体会

一道非常清晰的DFS+回溯题,我觉得都可以做标准模版了。S表示最后结果,pos表示当前位置,sum表示目前总和,solution表示方案数。dfs中先尝试+,回溯后,尝试-。当sum == S时,方案数+1。最终返回方案数。

LeetCode491 递增子序列

题目

给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。

示例:

1
2
输入: [4, 6, 7, 7]
输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]

说明:

给定数组的长度不会超过15。
数组中的整数范围是 [-100,100]。
给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。

C++代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
public:
set<vector<int>> res; //使用set 实现去除重复的功能
vector<vector<int>> findSubsequences(vector<int>& nums) {
vector<vector<int>> ans; //最终结果
vector<int> path; //每一次遍历的路径
//对数组中的每一个元素都要dfs
for(int i=0;i<nums.size();i++){
dfs(nums,path,i);
}
//将set中的数据转换到vector
set<vector<int>>::iterator it; //迭代器
for(it=res.begin();it!=res.end();it++){
ans.push_back(*it);
}
return ans;
}
void dfs(vector<int> nums,vector<int> path,int pos){
//如果path的大小为0,则证明这是第一个数据,直接放入
if(path.size()==0) path.push_back(nums[pos]);
//向后遍历,逐个添加
for(int i=1;i<nums.size();i++) {
//如果待添加的值小于path的最后一个值 不添加,如果 当前的位置大于数组长度 证明 nums已经遍历完 此时continue其实就是return了
if(nums[pos+i]<path.back() || (pos+i>nums.size()-1)) continue;
path.push_back(nums[pos+i]);//path放入新数据
res.insert(path); //将path添加进set
dfs(nums, path, pos + i); //对新的数据再进行dfs
path.pop_back(); //回溯
}
}
};

体会

求一个序列中的所有递增子序列。首先区分一下子序列与子串,子序列中的数字不需要是连续的,子串的数字是连续的。因为数据中不能有重复,我们使用set来达到这一效果。dfs内部,path表示当前的路径,pos表示当前的位置。思路很清晰,我们从当前数据开始,对其后面的每一个数据做插入操作后,继续dfs,知道完成整个序列。注意回溯。

LeetCode515 在每个树行中找最大值

题目

您需要在二叉树的每一行中找到最大的值。

示例:

1
2
3
4
5
6
7
8
9
输入:
1
/ \
3 2
/ \ \
5 3 9
输出: [1, 3, 9]

C++代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
map<int,vector<int>> hash; //key为树的深度 vector存放该层的数据
vector<int> largestValues(TreeNode* root) {
dfs(root,0);
vector<int> res;
for(auto key : hash){
res.push_back(*max_element(key.second.begin(),key.second.end())); //将每一行的最大值存在res中
}
return res;
}
void dfs(TreeNode* root,int depth){
if(!root) return;
hash[depth].push_back(root->val); //将当前节点存在hash中对应层的数组中
if(root->left) dfs(root->left,depth+1); //遍历左孩子
if(root->right) dfs(root->right,depth+1); //遍历右孩子
}
};

体会

思路很清晰,map的key表示第几层,value表示对应的节点。我们使用DFS的方法遍历整个树。遍历的过程中,记录下每一个节点的深度,将每一层的节点都存在map中对应数组中。最后将每一层最大的数据存储来即可。

LeetCode513 找树左下角的值

题目

给定一个二叉树,在树的最后一行找到最左边的值。

示例 1:

1
2
3
4
5
6
7
8
输入:
2
/ \
1 3
输出:
1

示例 2:

1
2
3
4
5
6
7
8
9
10
11
12
输入:
1
/ \
2 3
/ / \
4 5 6
/
7
输出:
7

C++代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
map<int,vector<int>> hash;
int max_depth = INT_MIN; //初始化最大深度为一个极小值
int findBottomLeftValue(TreeNode* root) {
dfs(root,0); //从根节点开始dfs
return hash[max_depth][0]; //将最底下一层最左边的元素返回出来
}
void dfs(TreeNode* root,int depth){
if(!root) { // 如果是叶节点下面的节点 对最大深度作出修改
if(depth-1>max_depth) max_depth=depth-1;
return;
}
hash[depth].push_back(root->val); //将对应层的节点依次放入
dfs(root->left,depth+1); //dfs左子树
dfs(root->right,depth+1); //dfs右子树
}
};

体会

这个题跟上一个题的思路完全一致,没有任何区别。唯一的不同就在于我们这一次不需要将一行中的最大值求出来,我们只需要返回最左边的那个值就可以了。