与其说java,不如说几乎所有算法竞赛中链式前向星是必不可少的技巧。其用途就是使用最少的空间来存储图,同时加快遍历速度。比如下面这组数据
第一行为三个正整数 n, m, s。
第二行起 m 行,每行三个非负整数 ui, vi, wi,表示从 ui 到 vi 有一条权值为 wi 的有向边。
4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
这里先讲一下邻接表和邻接矩阵,若直击链式前向星请看最后面部分。
一. 邻接矩阵
邻接矩阵是最简单的存储图的方式,直接开一个二维数组 int[][] g = new int[?][?] 进行图的存储。g[i][j] 表示结点 i 和结点 j 之间存在路径的情况,无权图中,0表示不存在路径,1表示存在路径。有权图中0表示不存在路径,0以外的数表示该路径的权值。
邻接矩阵的优点很明显,就是简便。问题就是占用空间较大,遍历耗时长。打个比方,假如1≤n≤10000; 那么开出来的数组为 int[][] g = new int[10001][10001],大小为10000*10000,占用了很大的空间。遍历图的时候,由于存储了很多无用的信息,会导致遍历的时候浪费了很多时间。
public class Main {
static int n,m;
static int[][] g = new int[10001][10001];
static Scanner sc = new Scanner(System.in);
public static void main(String[] args) throws IOException {
n=sc.nextInt();
m=sc.nextInt();
//有向图
for(int i=1;i<=m;i++){
int u=sc.nextInt();
int v=sc.nextInt();
g[u][v]=1;
}
//无向图
for(int i=1;i<=m;i++){
int u=sc.nextInt();
int v=sc.nextInt();
g[u][v]=1;
g[v][u]=1;
}
//有向 有权 图
for(int i=1;i<=m;i++){
int u=sc.nextInt();
int v=sc.nextInt();
int w=sc.nextInt();
g[u][v]=w;
}
//无向 无权 图
for(int i=1;i<=m;i++){
int u=sc.nextInt();
int v=sc.nextInt();
int w=sc.nextInt();
g[u][v]=w;
g[v][u]=w;
}
}
}
二. 邻接表
不像c++那样可以使用vector,在java中vector几乎很少使用,至少目前来说我还没见过用vector的。一个很好的解决方式是用ArrayList。邻接表原理很简单,每一个1到n的结点都是一个单独的头结点,各种形成链表。每一个结点所连通的结点,都记录在该节点所形成的链表里面。也就是说,我们只记录可以去到的结点,那些无法直接到达的结点不记录,相当于只记录邻接矩阵中 g[i][j] == 1 的结点(i,j),g[i][j] == 0 的话,这个结点就不记录。在算法竞赛中我们就没有必要使用链表了,直接使用ArrayList效率一样的。
邻接表占用空间明显比邻接矩阵少,而且只存储了有效信息,遍历速度也快很多。
无权图:
public class Main {
static int n,m;
static List<List<Integer>> g = new ArrayList<>();
static Scanner sc = new Scanner(System.in);
public static void main(String[] args) {
n=sc.nextInt();
m=sc.nextInt();
for(int i=0;i<=n;i++) g.add(new ArrayList<>());
//无权 有向图
for(int i=1;i<=m;i++){
int u=sc.nextInt();
int v=sc.nextInt();
g.get(u).add(v);
}
/*
//无权 无向图
for(int i=1;i<=m;i++){
int u=sc.nextInt();
int v=sc.nextInt();
g.get(u).add(v);
g.get(v).add(u);
}
*/
}
}
有权图麻烦一些,因为要记录权值,我们需要定义一个class,用来记录 可以到达的点 和 这一条边的权值 ,其实就是记录边的信息。
class node{
int v;
int w;
public node(int v,int w){
this.v=v;
this.w=w;
}
}
public class Main {
static int n,m;
static List<List<node>> g = new ArrayList<>();
static Scanner sc = new Scanner(System.in);
public static void main(String[] args) {
n=sc.nextInt();
m=sc.nextInt();
for(int i=0;i<=n;i++) g.add(new ArrayList<>());
//有权 有向图
for(int i=1;i<=m;i++){
int u=sc.nextInt();
int v=sc.nextInt();
int w=sc.nextInt();
g.get(u).add(new node(v,w));
}
/*
//有权 无向图
for(int i=1;i<=m;i++){
int u=sc.nextInt();
int v=sc.nextInt();
int w=sc.nextInt();
g.get(u).add(new node(v,w));
g.get(v).add(new node(u,w));
}
*/
}
}
三. 链式前向星
链式前向星可以说是最好的存储图的方式了,虽然比前面两种复杂不少,但是空间使用 和 遍历效率都是极限的存在。很多难一点的图论算法题几乎都可以使用它,甚至有些不用它的话会运行超时。而对于天生就比c++慢很多的java而言,链式前向星无疑是救星!链式前向星和邻接表非常相似,c++里面可以使用结构体来构造前向星,但是java与之对应的class太臃肿,每一个class在堆中都要new出来,非常消耗性能。数据稍微大一点的题目,java直接鸡鸡。这时候最原始的数组表示法就来了!!!
下面是 有权有向图 的存储(其他形式的图的存储都可以从这里推出来,不多bb):
public class Main { static int N = 10001; static int M = 10001; static int n,m,cnt; static int[] vv = new int[M]; //存储点u可以直接到达的点,相当于存储边 static int[] ww = new int[M]; //存储边的权值 static int[] to = new int[M]; //用于连接同一个点u可以直接到达的点 static int[] he = new int[N]; //用于存储遍历的第一个点的信息 static Scanner sc = new Scanner(System.in); public static void main(String[] args) { n=sc.nextInt(); m=sc.nextInt(); for(int i=1;i<=m;i++){ int u=sc.nextInt(); int v=sc.nextInt(); int w=sc.nextInt(); addEdge(u,v,w); } } public static void addEdge(int u,int v,int w) { vv[++cnt] = v; ww[cnt]=w; to[cnt] = he[u]; he[u] = cnt; } }
那么链式前向星究竟是如何存储图的呢,强烈建议把上面的数据自己模拟一下。只可意会不可言传,模拟一遍数据之后读者就恍然大悟了!
第一行为三个正整数 n, m, s。
第二行起 m 行,每行三个非负整数 ui, vi, wi,表示从 ui 到 vi 有一条权值为 wi 的有向边。
4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
那么,我们又该如何遍历图呢?
public class Main {
static int N = 10001;
static int M = 10001;
static int n,m,cnt;
static int[] vv = new int[M];
static int[] ww = new int[M];
static int[] to = new int[M];
static int[] he = new int[N];
static Scanner sc = new Scanner(System.in);
public static void main(String[] args) {
n=sc.nextInt();
m=sc.nextInt();
for(int i=1;i<=m;i++){
int u=sc.nextInt();
int v=sc.nextInt();
int w=sc.nextInt();
addEdge(u,v,w);
}
for(int u=1;u<=n;u++){
//u是当前结点
for(int i=he[u];i>0;i=to[i]){
//int i=he[u];i>0;i=to[i] ,这一段的意思是遍历u结点所有直接相连通的结点
int v=vv[i]; //v就是u结点直接连通的结点
int w=ww[i]; //w是u->v这条边的权值
}
}
}
public static void addEdge(int u,int v,int w) {
vv[++cnt] = v;
ww[cnt]=w;
to[cnt] = he[u];
he[u] = cnt;
}
}
不理解的小伙伴再模拟一遍数据吧!!!
over!!!