图论——代码篇

深搜和广搜都适合解决颜色类的问题,例如岛屿系列,其实都是 遍历+标记,所以使用哪种遍历都是可以的。

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

递推关系(两种情况取最小值):

  1. 节点i 到 节点j 的最短路径经过节点k:grid[i][j][k] = grid[i][k][k - 1] + grid[k][j][k - 1]
  2. 节点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;
    }
}
相关推荐
金枪不摆鳍2 小时前
hot100二分查找专题
数据结构·算法
YGGP2 小时前
【Golang】LeetCode 54. 螺旋矩阵
算法·leetcode·矩阵
十八岁讨厌编程2 小时前
【算法训练营 · 二刷总结篇】贪心算法、图论部分
算法·贪心算法·图论
没有医保李先生2 小时前
嵌入式面试八股文整理(持续更新)
算法
mit6.8242 小时前
ai五层结构
算法
F_D_Z2 小时前
最长连续序列的长度LongestConsecutive
算法·哈希表·最长连续序列
DeepModel2 小时前
【回归算法】广义线性模型(GLM)详解
人工智能·算法·回归
沪漂阿龙2 小时前
大模型采样策略终极指南:Top-k、Top-p与结构化输出最佳实践
人工智能·算法·机器学习
DeepModel2 小时前
【回归算法】局部加权回归(LWR)详解
人工智能·算法·回归