算法刷题记录 八十二【图论理论基础及深度优先搜索算法】

前言

本篇开始图论章节。第1篇。
记录 八十二【图论理论基础及深度优先搜索算法】


一、图论理论基础

图论理论基础 参考链接
在这里插入图片描述


二、深度优先搜索理论基础

深度优先搜索理论基础 参考链接

2.1 知识点框架

在这里插入图片描述

2.2 例题【98. 所有可达路径】

2.2.1题目阅读

给定一个有 n 个节点的有向无环图节点编号从 1 到 n。请编写一个函数,找出并返回所有从节点 1 到节点 n 的路径。每条路径应以节点编号的列表形式表示。

输入描述

第一行包含两个整数 N,M,表示图中拥有 N 个节点,M 条边;
后续 M 行,每行包含两个整数 s 和 t,表示图中的 s 节点与 t节点中有一条路径。

输出描述

输出所有的可达路径,路径中所有节点之间空格隔开,每条路径独占一行,存在多条路径,路径输出的顺序可任意。如果不存在任何一条路径,则输出 -1。

注意输出的序列中,最后一个节点后面没有空格! 例如正确的答案是 1 3 5,而不是 1 3 5 , 5后面没有空格!

输入示例

5 5
1 3
3 5
1 2
2 4
4 5

输出示例

1 3 5
1 2 4 5

用例解释:
在这里插入图片描述

有五个节点,其中的从 1 到达 5 的路径有两个,分别是 1 -> 3 -> 5 和 1 -> 2 -> 4 -> 5。

因为拥有多条路径,所以输出结果为:

1 3 5
1 2 4 5

1 2 4 5
1 3 5
都算正确。

数据范围:

图中不存在自环
图中不存在平行边
1 <= N <= 100
1 <= M <= 500

2.2.2 尝试实现【思路】

  1. 首先实现图的构造。图的表示有邻接矩阵和邻接表两种方式。以邻接表构造。邻接表:数组+链表。而且链表需要构造节点,所以先构造一个node结构体,代表链表的节点:

    struct node{
         int index;//节点编号
         node* next;
         node(int val) : index(val),next(nullptr){};
         node(int val,node* nextnode) : index(val),next(nextnode){};
    };
    
  2. 实现输入获取。根据输入描述,先获取节点个数N和边的个数M。然后根据N构造邻接表数组:vector < node*> adjList; 形成的邻接表如下图:数组下标和节点编号是错位的。
    在这里插入图片描述

输入处理
	int nodenum,edgenum;
    cin>>nodenum>>edgenum;//获取N,M。
    
    //定义邻接表
    vector<node*>  adjList;
    for(int i = 0;i < nodenum;i++){
        node* root = new node(i+1);//首先把每个链表的第一个节点创建
        adjList.push_back(root);
    }
    
    int from,to;//获取一下的M行
    while(edgenum--){
        cin>>from>>to;
        node* nextnode = new node(to);//构造后面的链表
        node* cur = adjList[from-1];
        while(cur->next) cur = cur->next;//找到链表的最后,把新节点加入。
        cur->next = nextnode;//输入
    }
  1. 深度优先搜索dfs。根据2.1理论,解决方法是递归+回溯,搜索路径。递归三步:
  • 确定函数的返回值:定义vector< vector< int>> result和vector< int> path两个全局变量存放路径。所以返回值是void;
  • 确定函数的参数:
    • 传入邻接表:vector<node*>& G。引用形式。
    • 传入节点个数:int n。用来判断终止条件,path走到n结束,搜集结果。
    • 传入当前搜索节点:node* index。一个指针类型。
  • 确定终止条件:当path.back()最后一个元素是n,应该搜集结果,放入result。
  • 确定逻辑:
    • 第一次调用dfs之前,先把节点1放入path中;传入的index是adjList[0]->next;
    • 使得cur = index;
    • while判断(cur)不为空,把cur的编号放到path中。//处理节点
    • 递到下一层:传入的index是adj[cur->index -1] ->next;//减1是因为错位。指向处理节点的链表。
    • 回溯:path弹出该节点。
    • 再让cur = cur->next;移到链表的下一个选项
      在这里插入图片描述
void dfs(vector<node*>& G,int n,node* index){
    if(!path.empty() && path.back() == n){
        result.push_back(path);
        return;
    }
    
    node* cur = index;
    while(cur) {
        path.push_back(cur->index);
        dfs(G,n,G[cur->index-1]->next);
        path.pop_back();
        cur = cur->next;
    }
    return;
}
  1. 主函数处理输出:如果result是空,输出-1;不为空,输出path.最后一个元素单独输出,不加空格。
输出处理
if(result.empty()){
       cout<<-1<<endl; 
    }else{
        for(int i = 0;i < result.size();i++){
            for(int j = 0;j < result[i].size()-1;j++){
                cout<<result[i][j]<<' ';
            }
            cout<<result[i].back()<<endl;
        }
    }
  1. 细节理解:为什么主函数调用dfs时,只需要传入adjList[0] ->next即可。从此处作为图搜索的开端。有没有可能有多个搜索开端呢?
  • 提示中:没有环,说明终止条件没有处理闭环的情况;
  • 提示说没有平行边,说明只有从节点1连接的节点开始搜索即可。不存在另一个开端。如下图:
    在这里插入图片描述

代码实现【邻接表,定义链表】

把思路合并到一起。因为链表定义节点实现,所以递归中的遍历需要用while判断不为空,cur = cur->next;

#include <iostream>
#include <vector> 
using namespace std;

vector<int> path;
vector<vector<int>> result;
struct node{
     int index;//节点编号
     node* next;
     node(int val) : index(val),next(nullptr){};
     node(int val,node* nextnode) : index(val),next(nextnode){};
};

void dfs(vector<node*>& G,int n,node* index){
    if(!path.empty() && path.back() == n){
        result.push_back(path);
        return;
    }
    
    node* cur = index;
    while(cur) {
        path.push_back(cur->index);
        dfs(G,n,G[cur->index-1]->next);
        path.pop_back();
        cur = cur->next;
    }
    return;
}

int main(){
    int nodenum,edgenum;
    cin>>nodenum>>edgenum;
    
    //定义邻接表
    vector<node*>  adjList;
    for(int i = 0;i < nodenum;i++){
        node* root = new node(i+1);
        adjList.push_back(root);
    }
    
    
    int from,to;
    while(edgenum--){
        cin>>from>>to;
        node* nextnode = new node(to);
        node* cur = adjList[from-1];
        while(cur->next) cur = cur->next;
        cur->next = nextnode;//输入
    }
    path.clear();
    result.clear();
    
    path.push_back(1);//先放入1.
    dfs(adjList,nodenum,adjList[0]->next);
    
    
    if(result.empty()){
       cout<<-1<<endl; 
    }else{
        for(int i = 0;i < result.size();i++){
            for(int j = 0;j < result[i].size()-1;j++){
                cout<<result[i][j]<<' ';
            }
            cout<<result[i].back()<<endl;
        }
    }
    
    return 0;
}

2.3 参考学习

深度优先搜索 实现参考学习链接

  1. 图的存储——邻接矩阵。节点个数是n,申请n*n的矩阵。为了使下标和编号对应,所以申请(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;//代表从s指向t。
}
  1. 图的存储——邻接表。使下标和编号对应,定义数组大小是n+1。链表不在定义节点实现,直接用容器list
// 节点编号从1到n,所以申请 n+1 这么大的数组
vector<list<int>> graph(n + 1); // 邻接表,list为C++里的链表

输入处理:
while(m--){
	cin>>s>>t;
	graph[s].push_back(t);
}

**解释list**
1. list做容器,在插入insert或者删除erase元素时时间复杂度是O(1),双向链表。vector在insert或erase时,时间复杂度是O(N)2. 所以没有[]访问运算符。只能iterator遍历。
3. list和forward_list区别,forward_list是单向链表。
4. 直接用list做链表操作,方便使用。不用手动处理链表。
  1. 参考给出了邻接矩阵和邻接表表示的代码实现
    在邻接表实现中,for循环使用range-for形式遍历,如果是常规的for循环,用iterator遍历,改成如下:
#include <iostream>
#include <vector>
#include <list>
using namespace std;

vector<vector<int>> result;
vector<int> path;

void dfs(const vector<list<int>>& graph,int n,int i){
    if(i == n){
        result.push_back(path);
        return;
    }

    for(auto it = graph[i].begin();it != graph[i].end();++it){
        path.push_back(*it);
        dfs(graph,n,*it);
        path.pop_back();
    }
    return;
}

int main(){
    int n,m;
    cin>>n>>m;
    int s,t;

    vector<list<int>> graph(n+1);//邻接表
    while(m--){
        cin>>s>>t;
        graph[s].push_back(t);
    }

    path.push_back(1);
    dfs(graph,n,1);

    if(result.empty()){
        cout<<-1<<endl;
    }else{
        for(int i = 0;i < result.size();i++){
            for(int j = 0;j < result[i].size()-1;j++){
                cout<<result[i][j]<<' ';
            }
            cout<<result[i].back()<<endl;
        }
    }
    return 0;
}

三、力扣题目【797. 所有可能的路径】

3.1 题目阅读

给你一个有 n 个节点的 有向无环图(DAG),请你找出所有从节点 0 到节点 n-1 的路径并输出(不要求按特定顺序)

graph[i] 是一个从节点 i 可以访问的所有节点的列表(即从节点 i 到节点 graph[i][j]存在一条有向边)。

示例 1:
在这里插入图片描述

输入:graph = [[1,2],[3],[3],[]]
输出:[[0,1,3],[0,2,3]]
解释:有两条路径 0 -> 1 -> 3 和 0 -> 2 -> 3

示例 2:
在这里插入图片描述

输入:graph = [[4,3,1],[3,2,4],[3],[4],[]]
输出:[[0,4],[0,3,4],[0,1,3,4],[0,1,2,3,4],[0,1,4]]

提示:

n == graph.length
2 <= n <= 15
0 <= graph[i][j] < n
graph[i][j] != i(即不存在自环)
graph[i] 中的所有元素 互不相同
保证输入为 有向无环图(DAG)

3.2实现

思路

  1. 题目说有向无环图,而且不存在自环,所以终止条件没有环的处理。如果图中有环,但是终止条件没有遇到环的处理,会陷入死循环。
  2. 提示中没有说存不存在平行边:比如示例二改一下,少一条0->1的边。也没有关系,因为题目求的是从0到n-1,所以从0这个入口开始即可,如果有其余入口,但不是从0开始,所以不算。
  3. 从0开始,深度搜索直到遇到节点n-1后,回溯改变方向,继续深度搜索。

代码实现

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    void dfs(const vector<vector<int>>& graph,int n,int cur){
        if(cur == n){
            result.push_back(path);
            return;
        }

        for(int i =0;i < graph[cur].size();i++){
            path.push_back(graph[cur][i]);
            dfs(graph,n,graph[cur][i]);
            path.pop_back();
        }
        return;
    }
    vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
        result.clear();
        path.clear();

        path.push_back(0);
        dfs(graph,graph.size()-1,0);
        return result;
    }
};

3.3 收获

  1. 从题目的输入graph来看,这也是邻接表的另一种表达方式。所以,邻接表表示图,有两种方法
第一种:
vector<list<int>> graph;
第二种:
vector<vector<int>> graph;

总结

在这里插入图片描述
(欢迎指正,转载标明出处)

  • 23
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值