452. 用最少数量的箭引爆气球
-
思路:
我们就根据上图这个例子来分析一下:
- 气球1和气球2是重叠的,需要一个箭,然后我们分析气球3
- 当气球1和气球2重叠的时候可以看到,气球1和气球2的空间应该合并成[2,6]然后再和气球3作比较
- 从上面特点可以看出,我们只需要关注气球的最右边界,找到右边界值最小的,成为新的右边界,然后再和气球3比
- 所以每次右边界改变就行
以上就是这套题的全部思路,一般这种题第一肯定是先排序再说其他。
-
代码
static bool compare(vector<int>& v1, vector<int>& v2){ return v1[0] < v2[0] || (v1[0] == v2[0] && v1[1] > v2[1]); } int findMinArrowShots(vector<vector<int>>& points) { int n = points.size(); sort(points.begin(), points.end(), compare); for(int i = 0; i < n; i++){ cout<<points[i][0]<<","<<points[i][1]<<" "<<endl; } int count = 1; for(int i = 0; i < n - 1 ; i++){ if(points[i][1] < points[i + 1][0]){ count++; }else{ points[i + 1][1] = min(points[i][1], points[i+1][1]); } } return count; }
435. 无重叠区间
-
思路
很简单,计算能连续的区间有多少个,取差就行
注意是移除最小区间数量,也就意味着我们尽可能少的移动区间,所以可以想一下,当你的
intervals[i][1]
约小,你能减少的区间就越小,当你的intervals[i][1]
越大,表明你要移除更多的区间才行。 -
代码
bool static cmp(vector<int>& v1, vector<int>& v2){ //return v1[0] < v2[0] || (v1[0] == v2[0] && v1[1] < v2[1]); return v1[1] < v2[1]; } int eraseOverlapIntervals(vector<vector<int>>& intervals) { int n = intervals.size(); //sort(intervals.begin(), intervals.end(), cmp); sort(intervals.begin(), intervals.end(), cmp); int end = INT_MIN; int count = 0; for(int i = 0; i < n ; i++){ cout<<intervals[i][0]<<" "<<intervals[i][1]<<endl; } for(int i = 0; i < n ; i++){ if(end <= intervals[i][0]){ end = intervals[i][1]; cout<<"end:"<<end<<endl; count++; } } cout<<"count:"<<count<<endl; return n - count; }
763. 划分字母区间
-
思路
在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。
-
代码
vector<int> partitionLabels(string S) { int len = S.size(); vector<int> distance(26); /*求每个字母*/ for(int i = 0; i < len; i++){ distance[S[i] - 'a'] = i; } int start = 0; int end = 0; vector<int> res; for(int i = 0; i < len; i++){ end = max(end, distance[S[i] - 'a']); if(i == end){ int tmp = end - start + 1; start = end + 1; res.push_back(tmp); } } return res; }
56. 合并区间
-
思路
这个题比之前那个射气球好点在于重叠不用考虑几个区间都重叠部分,只要有重叠就可以合并
要特别注意这个用例:[[2,3],[4,5],[6,7],[8,9],[1,10]]
-
代码
static bool cmp(vector<int>& v1, vector<int>& v2){ return v1[0] < v2[0] || (v1[0] == v2[0]) && (v1[1] < v2[1]); } vector<vector<int>> merge(vector<vector<int>>& intervals) { int len = intervals.size(); if(len <= 1){ return intervals; } vector<vector<int>> tmp1; sort(intervals.begin(), intervals.end(), cmp); int i = 0; /*每个元素是长度为2的int数组*/ vector<int> tmp2(2); /*首先初始化里面的数组,让其为第一个索引的数组值*/ tmp2[0] = intervals[0][0]; tmp2[1] = intervals[0][1]; /*用for也行*/ while(i < len - 1){ /*在这里的判断条件不是intervals[i][1] >= intervals[i+1][0]*/ /*因为这种类型都要保存右边最大的那个索引,参考射气球的那一道题*/ /*因此我认为记录最大索引值,是这种题的精髓*/ if(tmp2[1] >= intervals[i+1][0]){ tmp2[1] = max(intervals[i+1][1], tmp2[1]); }else{ tmp1.push_back(tmp2); tmp2[0] = intervals[i+1][0]; tmp2[1] = max(intervals[i+1][1],tmp2[1]); } i++; } tmp1.push_back(tmp2); return tmp1; }
53. 最大子序和
-
思路
这道题之所以困扰我这么久并不是因为状态状态转移方程,这个其实很好理解
而是max即返回连续最大和 与 dp之间的关系
我之前一直以为要返回dp数组某个值,导致动态规划出现问题,但是其实这道题的max和dp是平行的,max是一个独立的变量,
比如
-2 1 -3
这个例子,当i=3时dp的值行该是max(dp[i-1]+nums[i-1], nums[i-1])
注意这个求得可不是某个区间的最大值,而某个区间的最大值在max中存储,因此还需要一步max=max(dp[i], max)
,这道题最关键的就是这一步。 -
代码
int maxSubArray(vector<int>& nums) { int n = nums.size(); if(n <= 1){ return nums[0]; } vector<int> dp(n+1, -10000); int max_num = nums[0]; dp[1] = nums[0]; for(int i = 2; i < n+1; i++){ dp[i] = max(dp[i-1] + nums[i-1], nums[i-1]); max_num = max(max_num, dp[i]); } return max_num; }
134. 加油站
-
这题第一反应肯定是每个索引作为一个起点走一遍,这种方法也不能说出,但可能面试官不认,所以你得弄点新活儿。
思路:①一定能跑完全程,表明总加油数大于等于油耗数。②计算每个站的为起始点的剩余油量,会发现一个规律就是从站i开始到站j邮箱里面的油就是站i到站j的剩余油量相加,即cost[i]+…+cost[j]。③如果前面邮箱是负的,那么后面肯定是正的,因为要跑完全程啊,就得那么多,肯定要大于等于0才行。
因此,如果区间i-j的剩余油量和为负数,那么就从j+1开始算起。
-
代码
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) { int n = gas.size(); int rest_sum = 0; int rest_total = 0; int start = 0; for(int i = 0; i < n; i++){ rest_sum += gas[i]-cost[i]; //这个rest_total就是说计算所有剩余邮箱的和是否小于0的,放在里面不用再循环一下了,一次循环就解决了 rest_total += gas[i] - cost[i]; if(rest_sum < 0){ rest_sum = 0; start = i + 1; } } //只有剩余邮箱和小于0才会饶不了一圈 if(rest_total < 0){ return -1; } return start; }
DFS(回溯算法)(必须背熟)
需要经常的刷
对于回溯算法的总结和概括,有两个说的特别好。
接下来我准备总结一下他俩的精华
-
回溯算法的定义
回溯算法又叫做回溯搜索算法,主要是一种搜索方式,主要用在决策树类型的问题上。因此如果用回溯算法,就要遍历所有可能性,然后会返回所有可能,针对这些可能我们再做打算。因此可以看到,回溯是和递归相辅相成的,回溯可以说是递归的副产物。
-
回溯的效率
回溯算法的效率不高。
因为回溯会访问每一个节点,根据这个节点在访问下一个节点,因此本质上就是穷举。穷举本身能解决很多问题,有时候很多问题不得不用穷举来做做。
-
回溯算法解决的问题
-
组合问题:N个数里面按一定规则找出k个数的集合
-
排列问题:N个数按一定规则全排列,有几种排列方式
-
切割问题:一个字符串按一定规则有几种切割方式
-
子集问题:一个N个数的集合里有多少符合条件的子集
-
棋盘问题:N皇后,解数独等等
注意,排列和组合是不同的概念。排列是要注意顺序,而组合不用。
-
-
对回溯法的理解
回溯法解决的问题都可以抽象为树形结构
算法从宽度和深度两个层面递进,集合的大小就是树的宽度,递归的深度就是树的深度。
-
模板
回溯就是决策树的遍历过程,在这个过程中需要思考三个问题:
- 路径。已经做出的选择
- 选择列表。当前可以做的选择
- 结束条件。到达决策树底层,直接return
函数起名为back_tracing
返回值一般为void
参数的选取不固定,要具体问题具体分析
下图表现得最为直观:
代码模板如下:
void backtracking(参数) { if (终止条件) { 存放结果; return; } for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点(做选择); backtracking(路径,选择列表); // 递归 回溯,撤销处理结果 } }
排列问题
46. 全排列
vector<vector<int>> res;
vector<int> temp;
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> use_check(nums.size(), false);
back_tracing(nums, use_check);
return res;
}
void back_tracing(vector<int>& nums, vector<bool>& use_check){
if(temp.size() == nums.size()){
res.push_back(temp);
return;
}
for(int i = 0; i < nums.size(); i++ ){
//列表中的这个元素用过了,直接跳过
if(use_check[i] == true){
continue;
}
//列表中的这个元素还没有用
use_check[i] = true;
temp.push_back(nums[i]);
back_tracing(nums, use_check);
temp.pop_back();
use_check[i] = false;
}
}
-
主要的问题
-
对于vector来说我们不需要new,因为vectorr的空间是自动增长的,它自己管理的,也是自己释放,不需要new
-
这道题对节点问题的处理主要在于判断是否是用过的节点。针对这中情况我们设一个vector,长度和给的列表长度一样长,里面初始化为false。如果某个元素用过了我们就用true表示。
当递归到决策树的最后一个节点后,会回溯,撤销处理结果,因此这个时候我们需要重新吧所有的全部初始化为false,重新使用。放入路径中的元素也全部拿出来,这里用的是vector c++11 的pop_back()方法。
-
不用去想具体实现步骤,就是只关注那个功能区写具体代码就行。
-
47. 全排列 II
这道题和前面那道题的区别是这道题不能重复,因为给的序列可能会重复
vector<vector<int>> res;
vector<int> temp;
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<bool> user_check_vec(nums.size(), false);
sort(nums.begin(), nums.end());
back_tracing(nums, user_check_vec);
return res;
}
void back_tracing(vector<int>& nums, vector<bool> vec){
if(temp.size() == nums.size()){
res.push_back(temp);
return;
}
for(int i = 0; i < nums.size(); i++){
if(i > 0 && nums[i-1] == nums[i] && vec[i -1] == false){
continue;
}
if(vec[i] == true){
continue;
}
vec[i] = true;
temp.push_back(nums[i]);
back_tracing(nums,vec);
//回溯
temp.pop_back();
vec[i] = false;
}
}
-
注意的问题
最开始我是这样想的,为了让第一个加入进去的元素不重复,我用一个map保存不重复的元素,用一个我设置为true。反正这样子不行,不去想了,有点难。
然后就是作者的思路:
if(i > 0 && nums[i-1] == nums[i] && vec[i-1] == false){ continue; }
这句话保证了不重复,是不是非常精妙!
nums[i-1] == nums[i]
这句话保证了这次选的元素和上次选的一样,但是可能是同一层也有可能是同一个枝。我们要吧同一层的去掉,因此还要加上vec[i-1] == false
。这句话保证了在递归算法中,与当前元素相同的前一个元素是没有用过的!!!切记,递归函数上面的代码不要想着回溯
!当然做这道题之前一定要先排序,先排序!!!
组合问题
重复的话就是排列问题,i不用指定。后面的基本都是i=start
77. 组合
下面这幅图就是本题的思路

vector<vector<int>> res;
vector<int> temp;
vector<vector<int>> combine(int n, int k) {
back_tracing(n ,k, 1);
return res;
}
void back_tracing(int n, int k, int start){
if(temp.size() == k){
res.push_back(temp);
return;
}
for(int i = start; i <= n; i++){
temp.push_back(i);
back_tracing(n, k, i+1);
temp.pop_back();
}
}
重点,第二次写还是遇到了相同的问题
这是一道基本的组合问题,最最基本。这道题出问题的点在于不能出现像[2,2]或者[3,2]这样的组合
对于[2,2]这样的,我们必须要保证同一个树枝下,上一个要小下一个才行,
对于[3,2]这样的,我们必须保证同一层,前一个要小于后一个才行
不然的话写这道题的时候最开始会出现[[1,1],[1,2],[1,3],[1,4],[2,2],[2,3],[2,4],[3,2],[3,3],[3,4],[4,2],[4,3],[4,4]]
这样的答案,苦死很久没发现为什么
其实问题出在for循环中 back_tracing(n, k, i+1);
这段代码
对于同一树枝下的递归来说,我们考虑for时候,主要放在循环变量i上,而不是start上,因为start是控制层的递进。而i是控制递归的递进!!
第二次写补充: for 选择 in 选择列表
其实由于是组合问题,所以每次选择列表都要少一个数,因为不能重复!所以选择列表必须要i+1
在组合问题中,需要设置一个start变量。因为在递归层中,start决定从哪个索引开始遍历
每次从集合中选取元素,可选择的范围都在进行收缩,调整可选择的范围靠的就是start