深搜和广搜都适合解决颜色类的问题,例如岛屿系列,其实都是 遍历+标记,所以使用哪种遍历都是可以的。
01 所有可达路径--dfs
有一个输出的技巧,不要在输出时,多加一个空格
cpp
#include <iostream>
#include <vector>
using namespace std;
vector<vector<int>> result;
vector<int> path;
void dfs (const vector<vector<int>>& graph, int x, int n){
//终止条件,当前正在遍历的x为n,到达终点了。则打印输出
if(x == n){//找到了一一条符合条件的路径
result.push_back(path);
return;
}
//单层递归逻辑,遍历当前节点x,指向的所有节点i
for(int i = 1; i <= n; i++){
if(graph[x][i] == 1){//遍历所有的节点,如果x到它有路径,则输出
path.push_back(i);//处理,遍历到的节点加入到路径中
dfs(graph, i, n);//递归,此时递归的起始节点为i,终点节点为
path.pop_back();//回溯
}
}
}
int main(){
int n, m, s, t;
cin >> n >> m;
//初始化邻接矩阵,有n个节点,节点编号从1开始,为节点标号和下标对齐,申请n+1维数组
vector<vector<int>> graph(n+1, vector<int>(n + 1, 0));
//输入m个边
while(m--){
cin >> s >> t;
graph[s][t] = 1;//使用邻接矩阵,1表示节点s指向t
}
//graph构建好之后,开始遍历,这两行就是背住
path.push_back(1);//无论什么路径一定是从1节点出发
dfs(graph, 1, n);//开始遍历
//输出结果
if(result.size() == 0) cout << -1 <<endl; //result.size为0,则无可达路径
for(const vector<int> &pa : result){//循环的是result中存的每种路径
for(int i = 0; i < pa.size() - 1; i++){ //循环的是pa中存的每个节点,打印到倒数第二个
cout << pa[i] << " ";
}
cout << pa[pa.size() - 1] << endl;
}
}



邻接表的输出写法
cpp
#include <list>
void dfs (const vector<list<int>>& graph, int x, int n)
for (int i : graph[x]){ // 找到 x指向的节点
path.push_back(i); // 遍历到的节点加入到路径中来
dfs(graph, i, n); // 进入下一层递归
path.pop_back(); // 回溯,撤销本节点
}
// 节点编号从1到n,所以申请 n+1 这么大的数组
vector<list<int>> graph(n + 1); // 邻接表
while (m--) {
cin >> s >> t;
// 使用邻接表 ,表示 s -> t 是相连的
graph[s].push_back(t);
}

02 孤岛计数--dfs
和回溯有点类似
有一个从当前节点,去访问右,下,上,左的数组
cpp
#include <iostream>
#include <vector>
using namespace std;
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1};
void dfs(const vector<vector<int>> &grid, vector<vector<bool>> &visited, int x, int y) {
//终止条件:访问过的节点(visited[x][y] == true) 或者 遇到海水,就返回。或者将
if (visited[x][y] || grid[x][y] == 0) return;
visited[x][y] = true; //标记访问过
//单层递归逻辑,确定其上下左右是否grid == 1,是的话意味着可以连城大岛屿
for (int i = 0; i < 4; i++) {//遍历上下左右节点的方法要记住
int nextx = x + dir[i][0];
int nexty = y + dir[i][1];
if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue;//越界就跳过
dfs(grid, visited, nextx, nexty);//没越界就递归
}
}
int main() {
int n, m;
cin >> n >> m;
vector<vector<int>> grid(n, vector<int>(m, 0));
//循环将数组信息输入grid
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> grid[i][j];
}
}
vector<vector<bool>> visited(n, vector<bool>(m, false));
int result = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (!visited[i][j] && grid[i][j] == 1) {
result++;//遇到没访问过的陆地
dfs(grid, visited, i, j);//将与其链接的陆地都标记上true
}
}
}
cout << result << endl;
}





03 广度优先搜索
和树的层序遍历有点类似
适合于解决两个点之间的最短路径问题
cpp
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1};
void bfs(const vector<vector<int>> &grid, vector<vector<bool>> &visited, int x, int y) {
queue<pair<int, int>> que;
que.push({x, y});
visited[x][y] = true;
while(!que.empty()){
pair<int, int> cur = que.front();//查看队首元素,并拷贝给cur;
que.pop();//用完就弹出
int curx = cur.first;
int cury = cur.second;//当前节点坐标
for (int i = 0; i < 4; i++) {//遍历上下左右节点的方法要记住
int nextx = curx + dir[i][0];
int nexty = cury + dir[i][1];//获取周边四个方向的坐标
if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue;//越界就跳过
if(!visited[nextx][nexty] && grid[nextx][nexty] == 1){//没越界且没有被访问过
que.push({nextx, nexty});//队列添加该节点为下一轮要遍历的节点
visited[nextx][nexty] = true;//只要加入队列就立即标记,避免重复访问
}
}
}
}
int main() {
int n, m;
cin >> n >> m;
vector<vector<int>> grid(n, vector<int>(m, 0));
//循环将数组信息输入grid
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> grid[i][j];
}
}
vector<vector<bool>> visited(n, vector<bool>(m, false));
int result = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (!visited[i][j] && grid[i][j] == 1) {
result++;//遇到没访问过的陆地
bfs(grid, visited, i, j);//将与其链接的陆地都标记上true
}
}
}
cout << result << endl;
}



04 并查集
- 寻找根节点
- 将两个节点添加在同一个集合中(集合特别多)
- 判断两个节点是不是在一个集合里
路径压缩,将u的根节点赋值给u的father,效果是直接让u连到根节点上
图的各点是双向图连接,判断一个顶点到另一个顶点有没有有效路径,其实就是看着两个顶点是否在同一个集合中。有边连在一起,就是一个集合。
cpp
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
int n;
vector<int> father = vector<int>(n + 1, 0);
//并查集初始化
void init(){
for(int i = 0; i < n; i++) father[i] = i;//这个就代表这个就是根节点,默认指向自己
}
//并查集里寻根的过程(在find部分可以压缩路径)
int find(int u){
if(u == father[u]) return u;//根节点就是自己,则直接返回
else return find(father[u]);//不是自己,就找u父节点的根节点
//else return father[u] = find(father[u]);让最后找到的根节点为u的父节点
}
//判断u和v是否找到同一个根
bool isSame(int u, int v){
u = find(u);
v = find(v);
return u == v;//一样返回1,不一样返回0
}
//将v->u这条边加入并查集
void join(int u, int v){
u = find(u);//寻找u的根
v = find(v);//寻找v的根
if(u == v) return;//根相同,说明在一个集合,不用两个节点相连直接返回
father[v] = u;//箭头指的是父节点
}
int main(){
int m , s, t, source, destination;
init();//初始化
while(m--){
cin >> s >> t;
join(s, t);//将有边相连的加入并查集
}
cin >> source >> destination;
if(isSame(source, destination)) cout << 1 << endl;
else cout << 0 << endl;
}
05 拓扑排序(广搜版)
解决的问题:1)要先完成某些问题,后面的才能完成。2)给一个有向图,将有向图转换为线性的排序。3)判断有向无环图的常用方法
找入度为0的节点;删除节点;答案不唯一
为什么用unordered_map
在图中删除某个节点,同时删除它为出度的边
cpp
#include <iostream>
#include <vector>
#include <queue>
#include <unordered_map>
using namespace std;
//疑问,为啥要想到用unordered_map-------
int main(){
int m, n, s, t;
cin >> n >> m;
vector<int> inDegree(n + 1, 0);//记录每个文件的入度
vector<int> result;//记录结果
unordered_map<int, vector<int>> umap;//记录文件依赖关系
while(m--){
cin >> s >> t;
inDegree[t]++;//t的入度加一
umap[s].push_back(t);//记录s指向哪些文件
}
//度为0的节点不止一个,都放入队列
queue<int> que;
for(int i = 0; i < n; i++){
if(inDegree[i] == 0) que.push(i);
}
//开始从队列里遍历入度为0的节点,将其放入结果集
while(que.size()){
int cur = que.front();//当前选中的节点
que.pop();//从队列中弹出
result.push_back(cur);
//放入结果集之后,要把该节点从图中移除---其实就是把连接的节点入度减掉
vector<int> files = umap[cur];//获取该文件指向的文件,即files中存的就是从cur节点指出去的节点
if(files.size()){
for(int i = 0; i < files.size(); i++){
inDegree[files[i]] --;//代表删除了cur节点的指向,同意为删除节点
if(inDegree[files[i]] == 0) que.push(files[i]);
}
}
}
if(result.size() == n){//能成功处理,输出的个数和节点数目相同
for(int i = 0; i < n - 1; i++) cout << result[i] << " ";
cout << result[n - 1];
}
else cout << -1 << endl;//不能成功处理,说明有环,或者不连通,所以数目不同
}


05 dijkstra(朴素版)
解决的问题:求有向有权图的最短路径,选择一条花费时间最少的路线,权值不能为负数
dijkstra朴素版
选距离源点最近且未访问的节点;标记该点访问过;更新所有未访问节点到源点的路数
数组的定义:记录所有未访问过的节点到源点的距离
dijkstra 算法可以同时求起点到所有节点的最短路径,存在距离的数组中
cpp
#include <iostream>
#include <vector>
#include <climits>//为了定义一些有用的常量
using namespace std;
int main(){
int n, m, p1, p2, val;
cin >> n >> m;
vector<vector<int>> grid(n + 1, vector<int>(n + 1, INT_MAX));
for(int i = 0; i < m; i++){
cin >> p1 >> p2 >> val;
grid[p1][p2] = val;
}
int start = 1;
int end = n;
//存储从源点到每个节点的最短距离
vector<int> minDist(n + 1, INT_MAX);
//记录顶点是否被访问
vector<bool> visited(n + 1, false);
minDist[start] = 0;//起始点到自身的距离为0
for(int i = 1; i <= n; i++){//遍历所有节点
int minVal = INT_MAX;
int cur = 1;
//1.选距离源点最近且未访问过的节点
for(int v = 1; v <= n; v++){
if(!visited[v] && minDist[v] < minVal){
minVal = minDist[v];
cur = v;
}
}
visited[cur] = true;//2.标记该节点已被访问
//3.更新非访问节点到源点的距离(更新minDist数组)
for(int v = 1; v <= n; v++){
if(!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]){
minDist[v] = minDist[cur] + grid[cur][v];
}
}
}
if(minDist[end] == INT_MAX) cout << -1 << endl;//不能到达终点
else cout << minDist[end] << endl;//到达终点最短路径
}

06 Belman_ford(队列优化版)
解决边的权值有负数的单源最短路径,无负权回路
07 Floyd(动规---三层循环版)
解决的问题:多源最短路径,每个点到别的点的最短路径,对边的权值没有正负要求
k一定要放在最外层,背的时候背二维的,想原理的时候用三维
floyd算法的时间复杂度相对较高,适合 稠密图且源点较多的情况
如果 源点少,其实可以 多次dijsktra 求源点到终点。
动规五部曲:
dp数组定义:grid[i][j][k] = m,表示 节点i 到 节点j 以[1...k] 集合中的一个节点为中间节点的最短距离为m。
递推关系(两种情况取最小值):
- 节点i 到 节点j 的最短路径经过节点k:grid[i][j][k] = grid[i][k][k - 1] + grid[k][j][k - 1]
- 节点i 到 节点j 的最短路径不经过节点k:grid[i][j][k] = grid[i][j][k - 1]
cpp
#include <iostream>
#include <vector>
using namespace std;
int main(){
int n, m, p1, p2, val;
cin >> n >> m;
//为啥边最长的距离事10000,这里就要初始化为10005
vector<vector<int>> grid(n + 1, vector<int>(n + 1, 10005));
//从三维考虑,i和j平面被初始化
for(int i = 0; i < m; i++){
cin >> p1 >> p2 >> val;
grid[p1][p2] = val;
grid[p2][p1] = val;//这里是双向图
}
//开始floyd,k必须放在最外层,说了节点从1-n
for(int k = 1; k <= n; k++){
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j]);
}
}
}
//最后一个n是指,允许在全国前n个大城市里热议中转
int z, start, end;
cin >> z;
while(z--){
cin >> start >> end;
if(grid[start][end] == 10005) cout << -1 << endl;//代表了没有最小的路
else cout << grid[start][end] << endl;
}
}
