关于链式前向星及其简单运用

首先来一段网上关于前向星的描述:

前向星是一种特殊的边集数组,我们把边集数组中的每一条边按照起点从小到大排序,如果起点相同就按照终点从小到大排序,

并记录下以某个点为起点的所有边在数组中的起始位置和存储长度,那么前向星就构造好了.

用len[i]来记录所有以i为起点的边在数组中的存储长度.

用head[i]记录以i为边集在数组中的第一个存储位置.

再来一段另外的一个介绍

图的存储一般有两种:邻接矩阵、邻接表(邻接表包括一种东西叫前向星)。

若图是稀疏图,边很少,开二维数组a[][]很浪费;

若点很多(如10000个点)a[10000][10000]又会爆.只能 前向星做.

前向星的效率不是很高,优化后为链式前向星,直接介绍链式前向星。

来一张图片用来介绍一下前向星:

我们输入边的顺序为:

1 2

2 3

3 4

1 3

4 1

1 5

4 5

那么排完序后就得到:

编号:     1      2      3      4      5      6      7

起点u:    1      1      1      2      3      4      4

终点v:    2      3      5      3      4      1      5

但是利用前向星会有排序操作,如果用快排时间至少为O(nlog(n))

如果用链式前向星,就可以避免排序.

那么我们先来建立一个结构体

struct NODE{
	int w; // 权值
	int to; // 该条边的末尾的点
	int next; //next表示与第i条边同起点的下一条边的储存位置,其实就是下一个边的起点的下标
}edge[MAXN];

 再来看一段比较重要的代码,具体看注释 

void Add(int u, int v, int w) {  //起点u, 终点v, 权值w 
	//cnt为边的计数,从1开始计 
	edge[cnt].next = head[u];
	edge[cnt].w = w;
	edge[cnt].to = v;
	head[u] = cnt++;    //第一条边为当前边 ,其实head在这里面可以理解为链表中的头指针的类似的作用
} 

下面来看一段网上摘抄的代码来简单理解链式前向星,结构体设置的有些不一样,不过不影响阅读;

​
#include<bits/stdc++.h>
using namespace std;
#define MAXN 100501
struct NODE{
	int w;
	int e;
	int next; //next[i]表示与第i条边同起点的上一条边的储存位置
}edge[MAXN];
int cnt;
int head[MAXN]; 
void add(int u,int v,int w){
	edge[cnt].w=w;
	edge[cnt].e=v;    //edge[i]表示第i条边的终点 
	edge[cnt].next=head[u]; //head[i]表示以i为起点的最后一条边的储存位置 
	head[u]=cnt++;
}
int main(){
	memset(head,0,sizeof(head));
	cnt=1;
	int n;
	cin>>n;
	int a,b,c;
	while(n--){
		cin>>a>>b>>c;
		add(a,b,c);
	}
	int start;
	cin>>start;
	for(int i=head[start];i!=0;i=edge[i].next)
	   cout<<start<<"->"<<edge[i].e<<" "<<edge[i].w<<endl;
	return 0;
}

​

结果如图:

我们来对该图的输入输出做一个分析

注意cnt的初值我们初始化为1                  cnt
edge[1].next = head[1] = 0, head[1] = 1,  2 ;
edge[2].next = head[2] = 0, head[2] = 2,  3 ; 
edge[3].next = head[3] = 0, head[3] = 3,  4 ; 
edge[4].next = head[1] = 1, head[1] = 4,  5 ; 
edge[5].next = head[4] = 0, head[4] = 5,  6 ; 
edge[6].next = head[1] = 4, head[1] = 6,  7 ;
我们可以看出cnt的值一直在变,其实可以理解为数组的下标,然后head值得就是指向该边的上一条边的下标,这个还得靠自己结合代码悟,我也一时无法解释清楚

在这里我们可以更加深入理解head数组的作用,说实话我也是看了这个才明白他的真正的作用,关于他的遍历是倒着来的,一直到初始值,初始值根据需要设置为-1或者0

接下来我们来看一个题目,运用到了并查集与链式前向星,根据这个题目我们来更熟悉一下他的运用:

题目描述

很久以前,在一个遥远的星系,一个黑暗的帝国靠着它的超级武器统治着整个星系。

某一天,凭着一个偶然的机遇,一支反抗军摧毁了帝国的超级武器,并攻下了星系中几乎所有的星球。这些星球通过特殊的以太隧道互相直接或间接地连接。

但好景不长,很快帝国又重新造出了他的超级武器。凭借这超级武器的力量,帝国开始有计划地摧毁反抗军占领的星球。由于星球的不断被摧毁,两个星球之间的通讯通道也开始不可靠起来。

现在,反抗军首领交给你一个任务:给出原来两个星球之间的以太隧道连通情况以及帝国打击的星球顺序,以尽量快的速度求出每一次打击之后反抗军占据的星球的连通块的个数。(如果两个星球可以通过现存的以太通道直接或间接地连通,则这两个星球在同一个连通块中)。

输入输出格式

输入格式:

输入文件第一行包含两个整数,NN (1 < = N < = 2M1<=N<=2M) 和 MM (1 < = M < = 200,0001<=M<=200,000),分别表示星球的数目和以太隧道的数目。星球用 00 ~ N-1N−1 的整数编号。

接下来的 MM 行,每行包括两个整数 XX, YY,其中( 0 < = X <> Y0<=X<>Y 表示星球 xx 和星球 yy 之间有 “以太” 隧道,可以直接通讯。

接下来的一行为一个整数 kk ,表示将遭受攻击的星球的数目。

接下来的 kk 行,每行有一个整数,按照顺序列出了帝国军的攻击目标。这 kk 个数互不相同,且都在 00 到 n-1n−1 的范围内。

输出格式:

第一行是开始时星球的连通块个数。接下来的 KK 行,每行一个整数,表示经过该次打击后现存星球的连通块个数。

输入输出样例

输入样例#1: 复制

8 13
0 1
1 6
6 5
5 0
0 6
1 2
2 3
3 4
4 5
7 1
7 2
7 6
3 6
5
1
6
3
5
7

输出样例#1: 复制

1
1
1
2
3
3

附送一段题解代码:

// luogu-judger-enable-o2
#include<iostream>
#define IOS ios::sync_with_stdio(false)
using namespace std;
const int maxn = 4e5 + 4;
int father[maxn], head[maxn], store[maxn], ans[maxn];
int cnt = 0;
bool vis[maxn];
struct Node{
    int from;
    int to;
    int next;
}edge[maxn];
//和一般的前向星还是有一些小小的差别
void insert(int u, int v) {
    edge[cnt].from = u;
    edge[cnt].to = v;
    edge[cnt].next = head[u];
    head[u] = cnt++;
}

int findFather(int x) {
    while (x != father[x]) x = father[x];
    return x;
}

int Union(int a, int b) {
    int faU = findFather(a);
    int faV = findFather(b);
    if (faU != faV) {
        father[faU] = faV;
        return 1;
    }
    return 0;
}
//和一般的思路不一样,我们假设所有的星球都是毁灭的,然后每次修复好一个星球,并且将结果存储到数组里,最后输出即可,这样可以有个更好的思路
int main() {
    IOS;//可省略
    int n, m, a, b, k, x;
    cin >> n >> m;
    fill(head, head + maxn, -1);
    for (int i = 0; i < n; i++) {
        father[i] = i;
    }
    for (int i = 0; i < m; i++) {
        cin >> a >> b;
        insert(a, b);//无向图
        insert(b, a);
    }
    cin >> k;
    int total = n - k;//假设全部都是毁灭状态
    for (int i = 0; i < k; i++){
        cin >> x;
        store[i] = x;
        vis[x] = true;
    }
    for (int i = 0; i < 2 * m; i++) {//2*m的原因是因为有2*m条边,因为是无向边,所以乘以2
        if (vis[edge[i].from] == false && vis[edge[i].to] == false) {
            if(Union(edge[i].from, edge[i].to))
                total--;
        }
    }
    ans[k] = total;
    for (int i = k - 1; i >= 0; i--) {
        int u = store[i];
        total++;
        vis[u] = false;
        for (int j = head[u]; j != -1; j = edge[j].next) {
            if (vis[edge[j].to] == false && Union(edge[j].from, edge[j].to)) {
                total--;
            }
        }
        ans[i] = total;
    }
    for (int i = 0; i <= k; i++) {
        cout << ans[i] << endl;
    }
    return 0;
}

关于链式前向星还可以看看其他博客,不过很多题目都运用到了这个技巧,例如spfa使用链式前向星之类的,之后再刷题目的过程中会慢慢熟悉的

猜你喜欢

转载自blog.csdn.net/LanQiLi/article/details/84867521