前言
本篇开始图论章节。第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 尝试实现【思路】
-
首先实现图的构造。图的表示有邻接矩阵和邻接表两种方式。以邻接表构造。邻接表:数组+链表。而且链表需要构造节点,所以先构造一个node结构体,代表链表的节点:
cppstruct node{ int index;//节点编号 node* next; node(int val) : index(val),next(nullptr){}; node(int val,node* nextnode) : index(val),next(nextnode){}; };
-
实现输入获取 。根据输入描述,先获取节点个数N和边的个数M。然后根据N构造邻接表数组:vector < node*> adjList; 形成的邻接表如下图:数组下标和节点编号是错位的。
cpp
输入处理
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;//输入
}
- 深度优先搜索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;移到链表的下一个选项 。
cpp
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;
}
- 主函数处理输出:如果result是空,输出-1;不为空,输出path.最后一个元素单独输出,不加空格。
cpp
输出处理
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;
}
}
- 细节理解:为什么主函数调用dfs时,只需要传入adjList[0] ->next即可。从此处作为图搜索的开端。有没有可能有多个搜索开端呢?
- 提示中:没有环,说明终止条件没有处理闭环的情况;
- 提示说没有平行边,说明只有从节点1连接的节点开始搜索即可。不存在另一个开端。如下图:
代码实现【邻接表,定义链表】
把思路合并到一起。因为链表定义节点实现,所以递归中的遍历需要用while判断不为空,cur = cur->next;
cpp
#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 参考学习
- 图的存储------邻接矩阵。节点个数是n,申请n*n的矩阵。为了使下标和编号对应,所以申请(n+1) * (n+1).
cpp
vector<vector<int>> graph(n+1,vector<int> (n+1,0));
输入m个边,输入处理:
while(m--){
cin>>s>>t;
graph[s][t] = 1;//代表从s指向t。
}
- 图的存储------邻接表。使下标和编号对应,定义数组大小是n+1。链表不在定义节点实现,直接用容器list。
cpp
// 节点编号从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做链表操作,方便使用。不用手动处理链表。
- 参考给出了邻接矩阵和邻接表表示的代码实现
在邻接表实现中,for循环使用range-for形式遍历,如果是常规的for循环,用iterator遍历,改成如下:
cpp
#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实现
思路
- 题目说有向无环图,而且不存在自环,所以终止条件没有环的处理。如果图中有环,但是终止条件没有遇到环的处理,会陷入死循环。
- 提示中没有说存不存在平行边:比如示例二改一下,少一条0->1的边。也没有关系,因为题目求的是从0到n-1,所以从0这个入口开始即可,如果有其余入口,但不是从0开始,所以不算。
- 从0开始,深度搜索 直到遇到节点n-1后,回溯改变方向,继续深度搜索。
代码实现
cpp
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 收获
- 从题目的输入graph来看,这也是邻接表的另一种表达方式。所以,邻接表表示图,有两种方法:
cpp
第一种:
vector<list<int>> graph;
第二种:
vector<vector<int>> graph;
总结
(欢迎指正,转载标明出处)