一、贪心算法(容易被考察)
贪心最好的方法就是举例子
1.分糖果(455)
优先从需求因子小的对象进行满足
先将两个数组进行从小到大排序,然后依次比较,注意直接使用变量child同时记录满足孩子数以及遍历数组。
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(),g.end());
sort(s.begin(),s.end());
int child = 0;
int cookies = 0;
while(child<g.size()&&cookies<s.size()){
if(g[child]<=s[cookies]){
child++;
}
cookies++;
}
return child;
}
2.摇摆序列(376)
贪心体现在:比如数组[2,3,1,10,12,15,10,14],第4个到第6个数都处于up阶段,没有摇摆,故选择去掉10和12取最大的15,才最容易和第7个数组成摇摆序列。利用状态机表现为在第四个数时max_length++了,但一直到第6个数max_length都保持不变,等价于将10,12舍去。
采用状态机的思路
注意每个case之后利用break跳出switch.
int wiggleMaxLength(vector<int>& nums) {
if(nums.size()<2)return nums.size();//少于两个数的序列也属于摇摆
static const int Begin = 0;//静态存储的只读变量
static const int Up = 1;
static const int Down = 2;
int State = 0;
int max_length = 1;
for(int i=1;i<nums.size();i++){
cout<<max_length<<endl;
switch(State){
case Begin:
if(nums[i-1]>nums[i]){
State = Down;
max_length++;
}
if(nums[i-1]<nums[i]){
State = Up;
max_length++;
}
break;
case Down:
if(nums[i-1]<nums[i]){
State=Up;
max_length++;
}
break;
case Up:
if(nums[i-1]>nums[i]){
State=Down;
max_length++;
}
break;
}
}
return max_length;
}
3.移除k个数字(402)
位数为n的整数,去除k个数,使剩下的数最小。
利用vector模拟栈,(1)字符转为整数number依次入栈vc(条件:vc不为空且number不为0)(2)vc不为空且k不为0,比较删除队尾(队尾大于number则弹出)(3)最后若k不为0,则继续删出队尾(4)串联结果组成字符串
string removeKdigits(string num, int k) {
string res="";
vector<int>vc;
for(int i=0;i<num.size();i++){
int number = num[i] - '0';
while(vc.size()!=0&&k>0&&vc[vc.size()-1]>number){
vc.pop_back();
k--;
}
if(vc.size()!=0||number!=0){
vc.emplace_back(number);
}
}
while(vc.size()!=0&&k>0){
vc.pop_back();
k--;
}
for(int i=0;i<vc.size();i++){
res.append(1,vc[i]+'0');
}
if(res.size()==0){
res+="0";
}
return res;
}
4.跳跃游戏(55)
贪心思想在于:利用jump遍历nums,每一步都将最远能到达的索引存储到max_index中,jump每次循环最多能跳max_index步。
bool canJump(vector<int>& nums) {
vector<int> index;//存储当前位置能到达的最大索引位置
int max_index = nums[0];
int jump = 0;
for(int i=0;i<nums.size();i++){
index.emplace_back(i+nums[i]);//注意不要憨批的直接用赋值来初始化vector:index[i]=i+nums[i]
}
while(jump<nums.size()&&jump<=max_index){
if(max_index<index[jump]){
max_index=index[jump];
}
jump++;
}
if(jump==index.size()){
return true;
}
return false;
}
5.用最少数量的箭射爆气球(452)
每一个气球最少都需要一支箭来引爆,所以贪心在于每只箭要尽可能多的引爆气球。
vector>
可以采用双重括号访问元素,vc[0][0]static bool compare(const vector<int>& point1, const vector<int>& point2){
return point1[0] < point2[0];
}
int findMinArrowShots(vector<vector<int>>& points) {
if(points.size() == 0)return 0;
sort(points.begin(),points.end(),compare);
int shoot_begin = points[0][0];
int shoot_end = points[0][1];
int num = 1;
//cout<
for(int i=1;i<points.size();i++){
if(shoot_end>=points[i][0]){//两种情况都要进行这一步
shoot_begin = points[i][0];
if(shoot_end>points[i][1]){//第二种情况才需要
shoot_end = points[i][1];
}
}
else{//当前区间与新区间没有交集
num++;
shoot_begin = points[i][0];
shoot_end = points[i][1];
}
}
return num;
}
6.模拟行走机器人(874)
使用状态机模拟四个方向状态,并注意使用预先实验变量nx,ny查找是否下一步为障碍物。并注意使用set来查找障碍物比较省事。
int robotSim(vector<int>& commands, vector<vector<int>>& obstacles) {
std::set<vector<int>> obstacles_set;
for(auto it : obstacles){//使用set存储障碍物并查找
obstacles_set.insert(it);
}
int len = obstacles.size();
static const int N = 0;
static const int E = 1;
static const int W = 2;
static const int S = 3;
int res = 0;
int diretion = 0;
int x = 0;
int y = 0;
for(int item : commands){
switch(diretion){
case N:
if(item == -1){//右转
diretion = E;
}
else if(item == -2){//左转
diretion = W;
}
else{//行走
int count = item;
while(count>0){ //每走一步都要判断下一步是否遇到障碍
int ny = y + 1;//实验下一步是否为障碍物
int nx = x;
if(obstacles_set.find({nx,ny})!=obstacles_set.end()){
break;
}
y++;
count--;
res = max(res, x*x + y*y);
}
}
break;
case E:
if(item == -1){
diretion = S;
}
else if(item == -2){
diretion = N;
}
else{
int count = item;
while(count>0){
int ny = y;
int nx = x + 1;
if(obstacles_set.find({nx,ny})!=obstacles_set.end()){
break;
}
x++;
count--;
res = max(res, x*x + y*y);
}
}
break;
case W:
if(item == -1){
diretion = N;
}
else if(item == -2){
diretion = S;
}
else{
int count = item;
while(count>0){
int ny = y;
int nx = x - 1;
if(obstacles_set.find({nx,ny})!=obstacles_set.end()){
break;
}
x--;
count--;
res = max(res, x*x + y*y);
}
}
break;
case S:
if(item == -1){
diretion = W;
}
else if(item == -2){
diretion = E;
}
else{
int count = item;
while(count>0){
int ny = y - 1;
int nx = x;
if(obstacles_set.find({nx,ny})!=obstacles_set.end()){
break;
}
y--;
count--;
res = max(res, x*x + y*y);
}
}
break;
}
}
return res;
}
二、回溯与递归
对于排列组合问题,多会用到回溯算法
1.子集(78)
2.括号生成(22)
vector<string> res;
string str = "";
vector<string> generateParenthesis(int n) {
solver(n, n);
return res;
}
void solver(int left, int right){
if(left==0&&right==0){
res.emplace_back(str);
return;
}
if(left > right) return;
if(left>0){
str += "(";
solver(left - 1, right);
str.pop_back();
}
if(right>0){
str += ")";
solver(left, right-1);
str.pop_back();
}
}
3.全排列(46)
和二叉树路径遍历相似,使用path记录下路径的元素。
终止条件:path中的元素数量等于nums数 path.size()==nums.size()
回溯时候判断有没有使用过元素:used数组记录对应位置的元素有没有被使用过,使用过就跳过
vector<int> path;
vector<vector<int>> res;
void solver(vector<int>& nums, vector<int>& used){
if(path.size()==nums.size()){
res.emplace_back(path);
return;
}
for(int i=0;i<nums.size();i++){
if(used[i]==1) continue; //使用过就跳过
path.emplace_back(nums[i]);
used[i] = 1; //注意修改
solver(nums, used);
path.pop_back();
used[i] = 0; //注意修改
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<int> used(nums.size(),0);
solver(nums,used);
return res;
}
4.全排列II(47)
与全排列I不同之处在于原始数组中存在重复数字,所以在进行回溯递归的时候需要判断元素和前一个是否是重复的
vector<int> path;
vector<vector<int>> res;
void solver(vector<int>& nums, vector<int>& used){
if(path.size()==nums.size()){
res.emplace_back(path);
return;
}
for(int i=0;i<nums.size();i++){
if(used[i]==1||(i>0&&nums[i]==nums[i-1]&&used[i-1]==1)){ //与前一个元素相同,且前一个元素用过被标记为1
continue;
}
path.emplace_back(nums[i]);
used[i] = 1;
solver(nums, used);
path.pop_back();
used[i] = 0;
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<int>used(nums.size(),0);
sort(nums.begin(),nums.end());
solver(nums,used);
return res;
}
5.岛屿数量(200)
int numIslands(vector<vector<char>>& grid) {
int m = grid.size();
int n = grid[0].size();
int count = 0;
vector<vector<int>> num(m,vector<int>(n,0));
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(grid[i][j]=='1'){
count++;
dfs(grid, i, j, m, n);
}
}
}
return count;
}
void dfs(vector<vector<char>>& grid, int i, int j, int m, int n){
grid[i][j] = '0'; //每次都把当前遍历的属于同一个岛屿的部分标记为0,防止重复
if(i-1>=0&&grid[i-1][j]=='1') dfs(grid, i-1, j, m, n); //向上遍历
if(i+1<m&&grid[i+1][j]=='1') dfs(grid, i+1, j, m, n); //向下
if(j-1>=0&&grid[i][j-1]=='1') dfs(grid, i, j-1, m, n); //向左
if(j+1<n&&grid[i][j+1]=='1') dfs(grid, i, j+1, m, n); //向右
}
一条路走到黑
回退一步
另寻他路
for 循环
递归
for循环的作用在于另寻他路: 你可以用for循环可以实现一个路径选择器的功能,该路径选择器可以逐个选择当前节点下的所有可能往下走下去的分支路径。 例如: 现在你走到了节点a,a就像个十字路口,你从上面来到达了a,可以继续向下走。若此时向下走的路有i条,那么你肯定要逐个的把这i条都试一遍才行。而for的作用就是可以让你逐个把所有向下的i个路径既不重复,也不缺失的都试一遍
递归可以实现一条路走到黑和回退一步: 一条路走到黑: 递归意味着继续向着for给出的路径向下走一步。 如果我们把递归放在for循环内部,那么for每一次的循环,都在给出一个路径之后,进入递归,也就继续向下走了。直到递归出口(走无可走)为止。 那么这就是一条路走到黑的实现方法。 递归从递归出口出来之后,就会实现回退一步。
因此for循环和递归配合可以实现回朔: 当递归从递归出口出来之后。上一层的for循环就会继续执行了。而for循环的继续执行就会给出当前节点下的下一条可行路径。而后递归调用,就顺着这条从未走过的路径又向下走一步。这就是回朔
说了这么多,回朔法的通常模板是什么呢? 递归和for又是如何配合的呢?