图的存储
食用指南:
对该算法程序编写以及踩坑点很熟悉的同学可以直接跳转到代码模板查看完整代码
只有基础算法的题目会有关于该算法的原理,实现步骤,代码注意点,代码模板,代码误区的讲解
非基础算法的题目侧重题目分析,代码实现,以及必要的代码理解误区
题目描述:
- 题目中常见的图:
- 描述整体画面:
如棋盘/迷宫:标记为0的可以走,1的不可以 - 描述点和点的关系:
如城市之间是否连通,标记为1则连通,0则不连通
又如城市之间的距离,标记为常数则连通,无穷则不连通
题目分析:
- 对第一类类似于画的图,我们直接采用二维数组
- 对第二类描述点间关系的图,我们需要考虑两种存储方式:邻接表和邻接矩阵
- 下面分别讲述邻接表和邻接矩阵
算法模板:
算法原理:
邻接矩阵:
1. 存储形式:
-
二维数组:
若图中含有n个点,每个点和包括自身的n个点的关系采用一个长为n的一维数组存储
n个点共计采用n行一维数组,组成一个n行n列的二维数组
有时由于无0号点,所以我们从二维数组第一行 & 第一列开始存储
2. 优点:
- 记录全面:
由于每个点都在数组中有自己的一行
且该行记录了这个点和自己,以及和其余n-1个点的关系
所以n*n个元素将所有点和所有其他的关系全部记录了下来 - 查找方便:
想要查询所有与第x个点建立连接的点
只需要遍历arr[x][j]行或者arr[i][x]列
3. 缺点:
-
只能记录两点之间一种关系
如两个城市i j之间含有三条路径,分别长x yz
但是arr[i][j]只能记录x y z中的一者所以当两点关系不只一个时,要经过选择,将最合适题意的关系记录到邻接矩阵中
-
遍历耗时:
就算第x点和其余点均不连接,但是还是要遍历arr[x][i],每次都耗时O(n)
4. 适用情况:
- 稠密图,即点和点连接的边数非常多的图
- 边数m最多等于点数n的平方,即每两个点之间必有边
5. 初始化:
- 初始所有边都不连接,则边长arr[x][y]置为0 或 无穷
邻接表:
1. 存储形式:
- n条静态链表,类似于拉链法哈希
- 头节点数组h[] & 静态链表的距离值数组&next数组:
- 每个h[i]作为链表头拉出一条单链表,仅仅存储和自己连通的点
- h[i]本身就是最近插入的一个节点
由于采用的是头插法,所以靠后遍历到的插入节点作为新的链表头,连接旧的链表头 - 具体静态单链表和类似静态二维数组的内容请看上面模板算法中的往期链接
2. 优点:
- 高效,节省空间
每个h[i]开头的链表内只有连通的节点 - 动态:
所有节点都依靠于idx创建
使用idx创建节点后,与h[i]连接即可 - 记录同一点多对关系:
由于该链表是动态的,所以链表中可以用多个节点 存储i 到 j的多条路
同理,此时不进行选择最优路径,进行选择功能的代码会增加工作量
3. 缺点:
- 代码多了点
需要头节点数组h[i]
需要边长数组val[i]
需要next[]将节点相连
需要idx统一索引
需要初始化头节点数组
需要插入函数 - 代码还能再多点:
单纯的val[N] & next[N]仅仅记录了边长和下一个节点索引
未记录通过该边到达的点
需要增加p[idx]记录第idx个节点对应的终点是图中哪个节点
4. 适用情况:
- 稀疏图,本身边数小于等于节点个数
- 这样一个h[i]拉出来的链表长度较小
- 若每个h[i]都拉出长度为n-1的链表,还不如直接邻接矩阵arr[N][N];
5. 初始化:
- 若单纯记录是否连通:
h[x]都初始化为不存在的节点序号,如-1 或 无穷 - 还需记录连通边长:
h[x]还是初始化为不存在的节点序号,如-1 或 无穷
树图关系 与 有向无向图关系:
-
树图关系:
-
树是一种特殊的图,无环连通图
-
图上任意两点之间可以存在0条或多条通路,树上任意两点之间有且只有一条通路
-
树上任意加一条边成为了图
-
-
有向无向图关系:
-
无向图是一种特殊的有向图
-
无向图两点相连的线段:i <-> j
等价于有向图的两点之间两条不同向线段:i->j && j -> i
所以只需要考虑有向图的存储
-
代码实现:
- 邻接矩阵:
const int N = 110;
bool h[N][N];
//假设n个点m条边
int m, n;
int main(){
cin >>n >>m;
for(int i=0; i<m; i++){
int x, y;
cin >>x >>y;
h[x][y] = 1;
}
return 0;
}
- 邻接表:
需要存储边长时,需要开一个终点数组p[]和边长数组val[]
const int N = 110;
//起点数组
int h[N];
//单链表部分
int p[N], val[N], next[N],idx;
//通过x可以到达y
void insert(int x, int y, int k){
p[idx] = y;
val[idx] = k;
next[idx] = h[x];
h[x] = idx++;
}
int main(){
//链表以-1结尾
memset(h,-1,sizeof(h));
for(int i=0; i<m; i++){
int x, y, k; //x y之间有边,边长k
cin >>x >>y >>k;
insert(x, y, k);
}
}
- 邻接表:
仅仅需要表示连接时,不需要边长数组,val[]此时表示边终点
const int N = 110;
//起点数组
int h[N];
//单链表部分
int val[N], next[N],idx;
//通过x可以到达y
void insert(int x, int y){
val[idx] = y;
next[idx] = h[x];
h[x] = idx++;
}
int main(){
//链表以-1结尾
memset(h,-1,sizeof(h));
for(int i=0; i<m; i++){
int x, y; //x y之间有边
cin >>x >>y;
insert(x, y);
}
}
- 邻接表:vector<vector<int> >版
注意版本旧一点的编译器需要最后的两个>>之间空格
//除了用静态链表,还可以用本身就是动态的vector<int>存储
vector<vector<int> >h;
vector<int> t;
h.push_back(t); //占位,从1计数,0号链表不作插入
//第一条链表的可达点
t.push_back(2);
t.push_back(6);
h.push_back(t);
t.clear();
//第二条链表的可达点
t.push_back(3);
t.push_back(7);
h.push_back(t);
t.clear();
代码误区:
1. 稠密图和稀疏图的定义?
- 稠密图指边数m 约等于节点数n2
- 稀疏图指边数m 小于等于节点数n
- 稠密图最好使用邻接矩阵
- 稀疏图最好使用邻接表
2. 邻接表的存储变量有哪些?
- idx统一节点索引
- h[i]存储连接到i的节点所形成链表的头节点
- 节点的val[idx]存储边长
- 节点的p[idx]存储图中的某一节点序号
- next[idx]存储idx节点在链表中的下一节点索引
3. 邻接矩阵和邻接表对"连通的"表述:
- 若只需记录 x y连通,不需记录边长
邻接矩阵:arr[x][y] = 1;
邻接表:val[idx] = y; next[idx] = h[x]; h[x] = idx++; - 若需要记录 x y 之间边长k:
邻接矩阵:arr[x][y] = k;
邻接表:p[idx] = y; val[idx] = k; next[idx] = h[x]; h[x] = idx++;
本篇感想:
- 图论第一篇就是图的存储,本篇讲完了所有图的存储形式:
类似于画者直接二维数组
稠密关系图使用邻接矩阵
稀疏关系图使用邻接表 - 本篇其实也可以不写,题中多用自然就会了,目前看不懂不要紧
- 看完本篇博客,恭喜已登 《筑基境-初期》
距离登仙境不远了,加油