搜索算法【1】

搜索算法



前言

搜索算法是去搜索每一个可能,主要分以下几种算法

  1. 枚举法:暴力搜索
  2. 深度优先搜索
  3. 广度优先搜索
  4. 回溯

深度优先搜索

我们先思考这样一个问题:
假如有编号为1~3的3张扑克牌和编号为1~3的3个盒子,现在需要将3张牌分别放到3个盒子中去,且每个盒子只能放一张牌,一共有多少种不同的放法。

  • 当走到一个盒子面前的时候,到底要放那一张牌呢?在这里应该把所有的牌都尝试一遍。假设这里约定一个顺序,按牌面值从小到大依次尝试。在这样的假定下,当走到第一个盒子的时候,放入1号牌。
  • 放好之后,继续向后走,走到第二个盒子面前,此时还剩2张牌,牌面值最小的为2号牌,按照约定的规则,把2号牌放入第二个盒子。
  • 此时,来到第三个盒子面前,只剩一张牌,放入第三个盒子。此时手中的牌已经用完。
  • 继续向后走,走到了盒子的尽头,后面再也没有盒子,并且也没有可用的牌了,此时,一种放法已经完成了,但是这只是一种放法,这条路已经走到了尽头,还需要折返,重新回到上一个盒子。
  • 这里回到第三个盒子,把第三个盒子中的牌取出来,再去尝试能否再放其它的牌,这时候手里仍然只有一张3号牌,没有别的选择了,所以还需要继续向后回退,回到2号盒子面前。
  • 收回2号盒子中的2号牌,现在手里有两张牌,2,3,按照约定,再把3号牌放入2号盒子,放好之后,继续向后走,来到3号盒子。
  • 此时手里只有一张2号牌,把它放入3号盒子,继续向后走。
  • 此时这条路又一次走到了尽头,一个新的放法又产生了,继续向上折返,尝试其它可能,按照上述步骤,依次会产生所有结果。

代码如何实现这种过程呢?最主要的事情,向面前的盒子里放每一种牌,一个for循环搞定。这里还需考虑,现在手里有没有这张牌,用一个数组book标记手里是否有这张牌

for(int i = 1; i <= n; i++)
{
    if(book[i] == 0)  //第i号牌仍在手上
   {
        boxs[index] = i;
        book[i] = 1;  //现在第i号牌已经被用了
   }
}

面前的盒子处理完成之后,继续处理下一个盒子,下一个盒子的处理方法和当前一样。那么把上面的代码块封装一下,给它取一个名字,即为Dfs(Depth First Search)。

//index表示现在走到哪一个盒子面前
void Dfs(int index, int n, vector<int>& boxs, vector<int>& book)
{
    for(int i = 1; i <= n; i++)
 {
   if(book[i] == 0)  //第i号牌仍在手上
   {
       boxs[index] = i;
       book[i] = 1;  //现在第i号牌已经被用了
   }
 }
}

现在再去处理下一个盒子,直接调用Dfs(index + 1, boxs, book)即可。

void Dfs(int index, int n, vector<int>& boxs, vector<int>& book)
{
    for(int i = 1; i <= n; i++)
 {
   if(book[i] == 0)  //第i号牌仍在手上
   {
       boxs[index] = i;
       book[i] = 1;  //现在第i号牌已经被用了
            //处理下一个盒子
            Dfs(index + 1, n, boxs, book);
            //从下一个盒子回退到当前盒子,取出当前盒子的牌,
            //尝试放入其它牌。
            book[i] = 0;
   }
 }
}

现在考虑什么时候得到一种方法呢,走到尽头,也就是第n+1个盒子的时候,表明前面的盒子已经放好牌了,这时候直接打印每个盒子中的牌即可。已走到尽头,向上回退。

void Dfs(int index, int n, vector<int>& boxs, vector<int>& book)
{
    if(index == n + 1)
   {
        for(int i = 1; i <= n; i++)
            cout<<boxs[i]<<" ";
        cout<<endl;
        return; //向上回退
   }
    
    for(int i = 1; i <= n; i++)
 {
   if(book[i] == 0)  //第i号牌仍在手上
   {
       boxs[index] = i;
       book[i] = 1;  //现在第i号牌已经被用了
            //处理下一个盒子
             Dfs(index + 1, n, boxs, book);
            //从下一个盒子回退到当前盒子,取出当前盒子的牌,
            //尝试放入其它牌。
            book[i] = 0;
   }
 }
}

完整代码

//完整代码
#include <vector>
#include <iostream>
using namespace std;
void Dfs(int index, int n, vector<int>& boxs, vector<int>& book)
{
 	if (index == n + 1)
	{
 		for (int i = 1; i <= n; i++)
 			cout << boxs[i] << " ";
 		cout << endl;
 		return; //向上回退
 	}
 	for (int i = 1; i <= n; i++)
 	{
 		if (book[i] == 0)  //第i号牌仍在手上
 		{
 			boxs[index] = i;
 			book[i] = 1;  //现在第i号牌已经被用了
 			//处理下一个盒子
 			Dfs(index + 1, n, boxs, book);
 			//从下一个盒子回退到当前盒子,取出当前盒子的牌,
 			//尝试放入其它牌。
 			book[i] = 0;
 		}
 	}
}
int main()
{
 	int n;
 	vector<int> boxs;
 	vector<int> books;
 	cin >> n;
 	boxs.resize(n + 1, 0);
 	books.resize(n + 1, 0);
 	Dfs(1, n, boxs, books);
 	return 0;
}

从上面的代码可以看出,深度优先搜索的关键是解决"当下该如何做",下一步的做法和当下的做法是一样的。"当下如何做"一般是尝试每一种可能,用for循环遍历,对于每一种可能确定之后,继续走下一步,当前的剩余可能等到从下一步回退之后再处理。我们可以抽象出深度优先搜索的模型。

Dfs(当前这一步的处理逻辑)
{
 	1. 判断边界,是否已经一条道走到黑了:向上回退
 	2. 尝试当下的每一种可能
    3. 确定一种可能之后,继续下一步 Dfs(下一步)
}

练习1:员工的重要性

https://leetcode-cn.com/problems/employee-importance/
在这里插入图片描述
边界: 下属为空
每次先加第一个下属的重要性
按照相同的操作再去加下属的第一个下属的重要性

class Solution {
public:
    void dfs(unordered_map<int, Employee*>& info, int& sum, int id)
   {
        //这里无需判断边界,for循环就是一个边界,下属为空,直接结束
        sum += info[id]->importance;
        for(const auto& subid : info[id]->subordinates)
       {
            dfs(info, sum, subid);
       }
   }
    
    int getImportance(vector<Employee*> employees, int id) {
        if(employees.empty())
            return 0;
        unordered_map<int, Employee*> info;
        //把员工信息用map存储,方便后面的使用
        for(const auto& e : employees)
            info[e->id] = e;
        int sum = 0;
        dfs(info, sum, id);
        return sum;
   }
};
class Solution {
public:
    int DFS(unordered_map<int,Employee*>& info, int id)
   {
        int curImpo = info[id]->importance;
        for(const auto& sid : info[id]->subordinates)
       {
            curImpo += DFS(info, sid);
       }
       return curImpo;
   }
    int getImportance(vector<Employee*> employees, int id) {
        if(employees.empty())
            return 0;
        unordered_map<int,Employee*> info;
        for(const auto& e : employees)
       {
            info[e->id] = e;
       }
        return DFS(info, id);
   }
};

练习2:图像渲染

https://leetcode-cn.com/problems/flood-fill/
在这里插入图片描述
把和初始坐标开始,颜色值相同的点的颜色全部改为新的颜色
并且只要某个点颜色被更改,则继续以此点向周围渲染

比如题目的意思: 以位置(1,1)开始,向外渲染,只要渲染点的颜色值和(1,1)位置的颜色值相同,则继续向
外渲染
1 1 1
1 1 0
1 0 1

2 2 2
2 2 0
2 0 1

这里每一个符合要求的点都要向四个方向渲染
边界:位置是否越界
这里需要用的标记,避免重复修改,使时间复杂度不超过O(row * col)

#include <vector>
#include <iostream>
using namespace std;
//四个方向的位置更新:顺时针更新
int nextPosition[4][2] = { { 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 } };
class Solution {
public:
 	void dfs(vector<vector<int>>& image, int row, int col, vector<vector<int>>& book, int sr, int sc, int oldColor, int newColor)
 	{
 		//处理当前逻辑,修改颜色,并且标记已经修改过了
 		image[sr][sc] = newColor;
 		book[sr][sc] = 1;
 		//遍历每一种可能,四个方向
 		for (int k = 0; k < 4; ++k)
 		{
 			int newSr = sr + nextPosition[k][0];
 			int newSc = sc + nextPosition[k][1];
 			//判断新位置是否越界
 			if (newSr >= row || newSr < 0 || newSc >= col || newSc < 0)
 				continue;
 			//如果颜色符合要求,并且之前也没有渲染过,则继续渲染
 			if (image[newSr][newSc] == oldColor && book[newSr][newSc] == 0)
 			{
 				dfs(image, row, col, book, newSr, newSc, oldColor, newColor);
 			}
 		}
 	}
 	vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int newColor) {
 		if (image.empty())
 			return image;
 		int row = image.size();
 		int col = image[0].size();
 		//建立标记
 		vector<vector<int>> book;
 		book.resize(row);
 		for (int i = 0; i < row; ++i)
 		{
 			book[i].resize(col, 0);
 		}
 		//获取旧的颜色
 		int oldColor = image[sr][sc];
 		dfs(image, row, col, book, sr, sc, oldColor, newColor);
 		return image;
 	}
};

广度优先搜索

现在来思考这样一个问题, 迷宫问题:
假设有一个迷宫,里面有障碍物,迷宫用二维矩阵表示,标记为0的地方表示可以通过,标记为1的地方表示障碍物,不能通过。现在给一个迷宫出口,让你判断是否可以从入口进来之后,走出迷宫,每次可以向任意方向走。

  • 假设是一个10*10的迷宫,入口在(1,1)的位置,出口在(8,10)的位置,通过(1,1)一步可以走到的位置有两个(1,2),(2,1)
  • 但是这两个点并不是出口,需要继续通过这两个位置进一步搜索,假设现在在(1,2),下一次一步可以到达的新的位置为(1,3),(2,2)。而通过(2,1)可以一步到达的新的位置为(2,2),(3,1),但是这里(2,2)是重复的,所以每一个点在走的过程中需要标记是否已经走过了。
  • 两步之后,还没没有走到出口,这时候需要通过新加入的点再去探索下一步能走到哪些新的点上,重复这个过程,直到走到出口为止。

代码解析这个过程,最关键的步骤用当前位置带出新的位置,新的位置可以存放在一个vector或者队列中。位置需要用坐标表示,这里封装出一个node。

struct node
{
 	int x;
 	int y;
};
//queue实现
bool Bfs(vector<vector<int>> graph, int startx, int starty, int destx, int desty)
{
 	//迷宫的大小
 	int m = graph.size();
 	int n = graph[0].size();
 	//存储迷宫中的位置
 	queue<node> q;
 	//标记迷宫中的位置是否被走过
 	vector<vector<int>> book;
 	book.resize(m);
 	for (size_t i = 0; i < m; i++)
 		book[i].resize(n, 0);
 	q.push(node(startx, starty));
 	//标记已经走过
 	book[startx][starty] = 1;
 	//四个行走的方向,上下左右
 	int next[4][2] = { { -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 } };
 	//标记是否可以出去
 	bool flag = false;
 	while (!q.empty())
 	{
 		//当前位置带出所有新的位置, 可以向上下左右走
 		for (size_t i = 0; i < 4; ++i)
 		{
 			//计算新的位置
 			int nx = q.front()._x + next[i][0];
 			int ny = q.front()._y + next[i][1];
 			//新的位置越界,继续下一个
 			if (nx >= m || nx < 0 || ny >= n || ny < 0)
 			{
 				continue;
 			}
 			//如果新的位置无障碍并且之前也没走过,保存新的位置
 			if (graph[nx][ny] == 0 && book[nx][ny] == 0)
 			{
 				q.push(node(nx, ny));
 				//标记已被走过
 				book[nx][ny] = 1;
 			}
 			//如果新的位置为目标位置,则结束查找
 			if (nx == destx && ny == desty)
 			{
 				flag = true;
 				break;
 			}
 		}
 		if (flag)
 			break;
 		//否则,用新的位置继续向后走
 		q.pop();
 	}
 	return flag;
}
//vector实现
bool Bfs(vector<vector<int>> graph, int startx, int starty,int destx, int desty)
{
    //迷宫的大小
    int m = graph.size();
    int n = graph[0].size();
    
    //存储迷宫中的位置
    vector<node> queue;
    queue.resize(m*n);
    
    
    //标记迷宫中的位置是否被走过
    vector<vector<int>> book;
    book.resize(m);
    for(size_t i = 0; i < m; i++)
        book[i].resize(n, 0);
    
    int head = 0;
    int tail = 1;
    queue[head].x = startx;
    queue[head].y = starty;
    //标记已经走过
    book[startx][starty] = 1;
    //四个行走的方向,上下左右
    int next[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    //标记是否可以出去
    bool flag = false;
    
    while(head < tail)
   {
        //当前位置带出所有新的位置, 可以向上下左右走
        for(size_t i = 0; i < 4; ++i)
       {
            //计算新的位置
            int nx = queue[head].x + next[i][0];
            int ny = queue[head].y + next[i][1];
            //新的位置越界,继续下一个
            if(nx >= m || nx < 0 || ny >= n || ny < 0)
            {
                continue;
           }
            //如果新的位置无障碍并且之前也没走过,保存新的位置
            if(graph[nx][ny] == 0 && book[nx][ny] == 0)
           {
                queue[tail].x = nx;
                queue[tail].y = ny;
                //标记已被走过
                book[nx][ny] = 1;
                ++tail;
           }
            //如果新的位置为目标位置,则结束查找
            if(nx == destx && ny == desty)
           {
                flag = true;
                break;
           }
       }
        if(flag)
            break;
        //否则,用新的位置继续向后走
        ++head;
   }
    return flag;
}
int main()
{
 	int sx, sy, ex, ey;
 	vector<vector<int>> graph;
 	int m, n;
 	cout << "请输入迷宫的大小: 行,列" << endl;
 	cin >> m >> n;
 	graph.resize(m);
 	for (size_t i = 0; i < m; ++i)
 	{
 		graph[i].resize(n);
 	}
 	cout << "请输入迷宫的元素" << endl;
 	for (size_t i = 0; i < m; ++i)
 	{
 		for (size_t j = 0; j < n; ++j)
 		{
 			cin >> graph[i][j];
 		}
 	}
 	while (1)
 	{
 		cout << "请输入迷宫入口和出口" << endl;
 		cin >> sx >> sy >> ex >> ey;
 		cout << "是否可以走出迷宫: " << Bfs(graph, sx, sy, ex, ey) << endl;
 	}
 	return 0;
}

广度优先搜索模型

Bfs()
{
    1. 建立起始步骤,队列初始化
    2. 遍历队列中的每一种可能,whlie(队列不为空)
   {
        通过队头元素带出下一步的所有可能,并且依次入队
       {
            判断当前情况是否达成目标:按照目标要求处理逻辑
       }
        继续遍历队列中的剩余情况
        
   }
}

练习1:N叉树的层序遍历

https://leetcode-cn.com/problems/n-ary-tree-level-order-traversal/
在这里插入图片描述

class Solution {
public:
 	vector<vector<int>> levelOrder(Node* root) {
 		vector<vector<int>> treeVec;
 		if (root == nullptr) 
 			return treeVec;
 		//临时存放每一层的元素
 		vector<int> newFloor;
 		queue<Node*> q;
 		q.push(root);
 		while (q.size()){
 			//获取当前层元素个数,即整个队列元素
 			int size = q.size();
 			//存放新层元素之前先清空
 			newFloor.clear();
 			while (size--){
 				auto node = q.front();
 				q.pop();
 				newFloor.push_back(node->val);
 				//孩子入队
 				for (auto& child : node->children){
 					if (child) 
 					q.push(child);
 				}
 			}
 			//新层有元素,则放入vector
 			if (!newFloor.empty()) 
 				treeVec.push_back(newFloor);
	 	}
 		return treeVec;
 	}
};

练习2:腐烂的橘子

https://leetcode-cn.com/problems/rotting-oranges/
在这里插入图片描述
本题可以先找到所有的腐烂橘子,入队,用第一批带出新一批腐烂的橘子
每以匹橘子都会在一分钟之内腐烂,所以此题可以转化为求BFS执行的大循环的次数
这里的step的更新需要有一个标记,只有新的腐烂的橘子加入,step才能自加
最后BFS执行完之后,说明所有可以被腐烂的都完成了,再去遍历grid,如何还有值为1的,说明没有办法完全腐烂,返回-1,如果没有,则返回step

class Solution {
public:
 	int orangesRotting(vector<vector<int>>& grid) {
        //用pair存放位置
 		queue<pair<int, int>> q;
 		int row = grid.size();
 		int col = grid[0].size();
 		//已经腐烂的位置入队
 		for (int i = 0; i < row; ++i)
 		{
 			for (int j = 0; j < col; ++j)
 			{
 				if (grid[i][j] == 2)
 				q.push(make_pair(i, j));
 			}
 		}
        //可以蔓延的方向
 		static int nextP[4][2] = { { 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 } };
 		int step = 0;
 		while (!q.empty())
 		{
 			int n = q.size();
            int flag = 0;
            //用当前这一批已经腐烂的橘子带出下一批要腐烂的橘子
            //故要遍历队列中的所有位置
 			while (n--)
 			{
 				auto Curpos = q.front();
 				q.pop();
                //当前位置向四个方向蔓延
 				for (int i = 0; i < 4; ++i)
 				{
 					int nx = Curpos.first + nextP[i][0];
 					int ny = Curpos.second + nextP[i][1];
 					//如果位置越界或者是空格,或者已经是腐烂的位置,则跳过
                    if (nx >= row || nx < 0
 						|| ny >= col || ny < 0
 						|| grid[nx][ny] != 1)
                        continue;
 					//标记有新的被腐烂
                    flag = 1;
 					grid[nx][ny] = 2;
 					q.push(make_pair(nx, ny));
 				}
 			}
            //如果有新的腐烂,才++
            if(flag)
    		++step;
 		}
        //判断是否还有无法腐烂的
 		for (int i = 0; i < row; ++i)
 		{
 			for (int j = 0; j < col; ++j)
 			{
 				if (grid[i][j] == 1)
 				return -1;
 			}
 		}
 		return step;
 	}
};
 

回溯

回溯是一种通过穷举所有可能情况来找到所有解的算法。如果一个候选解最后被发现并不是可行解,回溯算法会舍弃它,并在前面的一些步骤做出一些修改,并重新尝试找到可行解。回溯算法一般会结合在搜索算法中。

练习1:电话号码的字母组合

https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/
在这里插入图片描述
此题DFS+回溯

static string mapString[] = {"", "", "abc", "def", "ghi", "jkl", "mno",
                            "pqrs", "tuv", "wxyz"}; 
class Solution {
public:
    void backTrace(string& digits, vector<string>& ret, string curStr, int curDepth)
   {
        //边界,找到一种组合,放入数组中,结束此路径,向上回溯
        if(curDepth == digits.size())
       {
            if(!curStr.empty())
           {
                ret.push_back(curStr);
                }
            return;
       }
        //找到当前字符在string映射表中的位置
        int curMapIndex = digits[curDepth] - '0';
        string curMap = mapString[curMapIndex];
        //遍历每一种可能的组合
        for(auto& ch : curMap)
       {
            backTrace(digits, ret, curStr + ch, curDepth + 1);
       }
   }
    
    vector<string> letterCombinations(string digits) {
        vector<string> ret;
        backTrace(digits, ret, "", 0);
        return ret;
   }
};

练习2:组合总和

https://leetcode-cn.com/problems/combination-sum/
在这里插入图片描述
此题相加的元素可以重复,所以去下一个元素的位置可以从当前位置开始, DFS + 回溯
为了保证组合不重复(顺序不同,元素相同,也算重复),不再从当前位置向前看

  1. 从第一个元素开始相加
  2. 让局部和继续累加候选的剩余值
  3. 局部和等于目标值,保存组合,向上回退,寻找其它组合
class Solution {
public:
    void dfs(vector<int>& candidates, vector<vector<int>>& solutions,
             vector<int>& solution, int curSum, 
             int prevPosition, int target)
   {
        //边界,如果大于等于目标,则结束
        if(curSum >= target)
       {
            //等于目标,找到一个组合
            if(curSum == target)
                solutions.push_back(solution);
            return;
       }
        //可以从上一个位置开始,因为元素可以重复
        for(int i = prevPosition; i < candidates.size(); ++i)
       {
       		//单个值已经大于目标,直接跳过
            if(candidates[i] > target)
                continue;
            solution.push_back(candidates[i]);
            dfs(candidates, solutions, solution, curSum + candidates[i], i, target);
            //回溯,向上回退
            solution.pop_back();
       }
   }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<vector<int>> solutions;
        vector<int> solution;
        if(candidates.empty())
            return solutions;
        int curSum = 0;
        dfs(candidates, solutions, solution, curSum, 0, target);
        return solutions;
   }
};
  • 12
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李憨憨_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值