使用邻接表 表示图

之前一直使用邻接矩阵和deaultdict()来表示图,从来没有使用过邻接表,最近刷题突然碰到了,感觉挺有意思,虽然说的是使用链表记录某一个节点所有的边,但是并没有使用链表,而是使用若干个数组来表示的链表,理解起来比较复杂,这里做一点总结。

B站关于邻接表的介绍视频
CSDN关于使用数组表示邻接表博客

acwing上使用邻接表表示图的题目:
4742 . 电

使用邻接表表示图本身并不难理解,就是将一个节点的所有边都以链表的形式串起来,然后将这个节点指向这个链表的头节点,难以理解的是为啥可以使用若干个数组表示邻接表,换句话说,为啥可以使用若干个数组表示许多个链表?这就是关键的地方。

以数组a = [2,-1,3,1]为例,a[i]中的i表示的是节点i,a[i]对应的值j表示节点i的下一个节点是节点j,那么数组a就表示这样一个链表:
0->2->3->1,其中a[1]=-1表示节点1没有下一个节点,在使用数组表示邻接表时,这里的节点其实是边的编号。

除此之外还有一个关键的地方就是如何找到这个链表的头节点,因为只有知道了头节点才可以顺着头节点找下去,所以还需要一个数组来记录每一个图中节点的头节点。到这里已经差不多够了,但是有一个关键的点就是这里只保存了边的编号以及这个边的下一条边(这个链表存储的是某个节点的所有边,所以这个”下一条“的意思并不是真的图中的下一条,仅仅是某个节点的所有的边组成的链表的下一条),没有保存这个边的另一头是哪个节点,所以虽然知道了某个节点的所有边的编号但是并不知道这个节点通过这个边可以去哪里,所以还需要一个数组来存储编号为i的边的终点是哪个节点,这样才能将整个图构建出来。

另外,有的图的边是有权重的,所以可能还会在多一个数组w来存储编号为i的边的权重。

至此,我们一共需要4个数组来记录一个图,分别是:
h[i]: 表示节点i对应的所有边组成的链表的头节点
e[j]: 表示第j条边的终点是节点几
ne[j]: 表示第j条边的下一条边编号是几,这个就是那个关键的链表,他的头节点id存储在h中
w[j]: 表示第j条边的权重,没有权重的图没有这个数组

下面以c++为例,创建一个使用数组表示临界表来记录图,并给出添加边的函数和遍历某一个节点的函数

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 200010, M = N * 2;

int n;
int m;
int h[N], e[M], ne[M], idx;
int w[N]; // 这里的w[i]表示第i个节点的值


// 关键的添加一条从a指向b的边
void add(int a,int b){
    
    
    e[idx]=b;ne[idx]=h[a];h[a]=idx++;
}

int main(){
    
    
    n=10;// 表示10个节点
    m = 9; // 表示边的个数
    // 初始化h为-1,注意并不是全部初始化,而是只初始n+1个,因为节点的编号可能从1开始
    memset(h,-1,(n+1)*4); 
    scanf("%d",&n); // 读取节点个数
    scanf("%d",&m); // 读取边个数
    printf("一共有%d个节点, %d条边\n",n,m);
    for(int i=1;i<=n;i++) scanf("%d",&w[i]);// 读取第i个节点的值,从第一个开始,没有第0个
    idx=0; // 第一条边的序号是0
    for(int i=0;i<m;i++){
    
    
        int a,b;
        scanf("%d%d",&a,&b);
        // 这里是无向图,因此要添加两次
        add(a,b);
        add(b,a);
    }
    // 到这里图就已经构造完成了,接下来是记录一个节点1的邻居的个数
    int num=0;
    for(int i=1;~i;i=ne[i]){
    
    
        num+=1;
    }
    printf("节点%d一共有%d个邻居\n",1,num);

    return 0;
}

下面是最上面提到的acwing上面的题目,有一个地方需要注意的是,如果一个图有N个节点,有N-1条边,那他就是一颗树,一个树意味着当从一个节点经过往下走之后,不可能回到这个节点上面的节点,就像一个树,所以这一题才可以使用dfs搜索,因为每一个节点dfs之后的节点不可能交叉。

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 200010, M = N * 2;

int n;
int h[N], e[M], ne[M], idx;
// 这一题w存储的不是边的权重,而是点i的值,这一题特殊之处在于节点的编号与值无关,因为有
// 可能有两个值一样的节点,所有将节点的值根据索引单独给出,因此需要一个数组w单独存取节点的值
int w[N]; 
int f[N];

// 添加变得重要函数,只要知道这三个数组分别是什么意思就好理解
// 其中idx是边的编号
void add(int a, int b) {
    
    
    e[idx] = b; ne[idx] = h[a]; h[a] = idx++;
}

// 说是dp但我感觉更像是dfs,而且是记忆化dfs
int dp(int u) {
    
    
    // f[u]是记忆化用的,减少计算量
    if (f[u] != -1) {
    
    
        return f[u];
    }
    int res = 1;
    // 这里的循环体有点奇怪,用的是~i,因为for循环中间的循环条件只要不是0或者FALSE,循环
    // 就会进行下去,对-1取反结果是0,因此当遍历到”链表“的最后一个时值是-1,退出循环
    for (int i = h[u]; ~i; i = ne[i]) {
    
    
        int j = e[i];
        if (w[u] > w[j]) {
    
    
            res += dp(j);
        }
    }
    f[u] = res;
    return res;
}

int main(){
    
    
    int T;
    scanf("%d",&T);
    for (int cases = 1; cases <= T; cases++) {
    
    
        scanf("%d", &n);
        // 读取这一个case的所有节点的值
        for (int j = 1; j <= n; j++) {
    
    
            scanf("%d", &w[j]);
        }
        memset(h,-1,(n+1)*sizeof(int));
        idx = 0;// 第一条边的编号是0
        // 注意是n-1行数据,不是n行
        for (int i = 0; i < n-1; i++) {
    
    
            int a, b;
            scanf("%d%d", &a, &b);
            add(a, b); // 添加一条a指向b的边
            add(b, a); // 添加一条b指向a的边
        }
        memset(f, -1, (n + 1) * 4);
        int res = 0;
        // 节点的编号是从一开始的,所以上面的memset中使用的是n+1
        for (int i = 1; i <= n; i++) {
    
    
            res = max(res, dp(i));
        }
        printf("Case #%d: %d\n",cases,res);
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_41926099/article/details/131405842