众所周知,图的存储有三种,分别是:
1.邻接矩阵
2.邻接表
3.链式向前星
邻接矩阵
邻接矩阵就是一个矩阵,比如用一个二维数组 a a a 来存储, a a a[ i i i][ j j j]表示节点 i i i 与节点 j j j 之间有没有边,如果有,权值是多少,比如下面这个有向图(没有权值):
那么它就可以用邻接矩阵表示(0表示没有边,1表示有边):
1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|
1 | 0 | 1 | 0 | 0 | 0 |
2 | 0 | 0 | 1 | 0 | 0 |
3 | 0 | 0 | 0 | 0 | 0 |
4 | 0 | 0 | 0 | 0 | 1 |
5 | 1 | 0 | 1 | 0 | 0 |
它的缺点很明显,就是耗内存,存储 n n n 个节点的图空间复杂度是 O ( n 2 ) O(n^2) O(n2) ,优点也很明显,可以用 O ( 1 ) O(1) O(1) 的复杂度来找到指定两个节点之间的边,对于边比较多的还是比较划算的,在节点少的时候是个非常好的选择
邻接表
每一个节点存储与它相连的节点,比较适合排序每一个点,而且节省空间
vector<int>d[NR];//d[i]表示与i相连的点
链式向前星
铺垫
各位都用过链表吧,但是为什么却在OI里直接应用的题目那么少呢?无非就是动态分配内存时间复杂度太高了,所以本蒟蒻想出了一个办法来优化链表,那就是提前分配一个很大的数组的内存,然后这个数组的下标就是我们自己定义的地址,而不是指针,不好解释,代码:
//一般的链表
struct node
{
int val;//节点信息
node *next;//下一个节点
node *last;//上一个节点
};
node *head, *tail;
//改进复杂度后的算法
const int NR=1e6+10;//提前分配的内存大小
struct data
{//存储每个节点的数据
//...
};
data a[NR];//提前分配内存
int c;//记录现在前c块内存分配过了
int nxt[NR];//每个节点的下一个节点的索引
int lst[NR];//每个节点的上一个节点的索引
int head, tail;
这样也可以实现链表的效果,比如在 x x x 节点之后插入一个 d a t dat dat 数据,可以写成:
a[++c]=dat;//模拟分配内存
nxt[c]=nxt[x];
lst[c]=x;
lst[nxt[x]]=c;
nxt[x]=c;
删除节点 x x x:
nxt[lst[x]]=nxt[x];
时间复杂度都很不错,于是我们就有了这种链表
言归正传,链式向前星呢其实跟这种链表的原理差不多,它是用一个链表来存储以节点 x x x 为起点的边,那么对于有 n n n 个节点的边有 n n n 个链表,对于以节点 x x x 为起点的边的链表头我们表示为 h e a d head head[ x x x] , d a t a data data 用来存储每一条边,所以代码长这样:
struct edge//data改名了
{
int to, w;
//终点的节点编号,权值
//注意这里不需要记录从哪里来,因为每个链表中起点是确定的
}e[MR];//MR边数,作用同上文
int head[NR], c, nxt[MR];//NR是节点数,c的作用同上文
void add(int u, int v, int w)
{//起点,终点,权值
c++;//分配内存
e[c].to=u;
e[c].w=w;
//边的信息
nxt[c]=head[u];//在链表头前面添加元素
head[u]=c;//更新链表头
}
这就是链式向前星,不过其实我更愿意把 n x t nxt nxt 数组换一个写法:
struct edge
{
int to, w, nxt;
}e[MR];
int c;
int head[NR];
void add(int u, int v, int w)
{
c++;
e[c].to=u;
e[c].w=w;
e[c].nxt=head[u];
head[u]=c;
}