《算法》-图[无向图]

2020-09-09  本文已影响0人  算法手记

四种重要的图模型:

无向图

定义:由一组顶点和一组能够将两个顶点相连的边组成。

特殊:自环(一条连接一个顶点和其自身的边);平行边(连接同一对顶点的两条边)

在这里插入图片描述
数学家将含有平行边的图称为多重图;将没有平行边或自环的图称为简单图。现实当中,两点就可以指代一条边。

术语表

在这里插入图片描述

树的定义非常通用,稍作改动就可以变成用来描述程序行为的(函数调用层次)模型和数据结构(二叉查找树、2-3树等)。

当且仅当一幅含有V个节点的图G满足下列5个条件之一时,它就是一棵树:

图的密度是指已经连接的顶点对占所有可能被连接的的顶点对的比例。一般来说,如果一幅图中不同的边的数量只占顶点总数V的一小部分,那么就认为这幅图是稀疏的,否则是稠密的。

二分图是一种能够将所有节点分为两部分的图,其中图的每条边所连接的两个顶点都分别属于不同的部分。二分图会出现在许多场景中。

在这里插入图片描述

表示无向图的数据类型

图的基本操作的API:


在这里插入图片描述

两个构造,得到顶点数V( )和边数E( ),增加一条边addEdge( int v, int w )。本节所有算法都基于adj( )方法所抽象的基本操作。第二个构造函数接受的输入由2E+2个整数组成:首先是V, 然后是E, 在然后是 E 对 0到V-1之间的整数,每个整数对都表示一条边。


在这里插入图片描述

图的几种表示方法

要面对的下一个图处理问题就是用哪种数据结构来表示并实现这份API,这包含两个要求:

要求比较模糊,但是仍然能帮我们在三种图的表示方法中进行选择。

邻接表的数据结构

非稠密图的标准表示称为邻接表的数据结构,它将每个顶点的所有相邻顶点都保存在该顶点对于的元素所指向的一张链表中。使用这个数组就是为了快速访问给定顶点的邻接顶点列表

使用Bag抽象数据类型(也可用Java中的<LinkedList>)来实现这个链表,这样就可以在常数时间内添加新的边或遍历任意顶点的所有相邻顶点。


在这里插入图片描述
/**
 * 无向图
 */
public class Graph {
    private int vertexCount;            // 顶点数
    private int edgeCount;                // 边数
    private LinkedList<Integer>[] adj;    // 邻接表数组
    public Graph(int v){
        this.adj = new LinkedList[v];
        for(int i = 0; i<v; i++) adj[i] = new LinkedList<>();// 初始化邻接表数组
        this.vertexCount = v;
    }
    public Graph(In in) {
        this(in.readInt());
        int e = in.readInt();//得到边数
        // 读取每条边,进行图的初始化操作
        for(int i = 0; i<e;i++){
            int v = in.readInt();     // 起点
            int w = in.readInt();     // 终点
            addEdge(v, w);
        }
    }
    /*** 增加一条边*/
    public void addEdge(int start, int end) {
        adj[start].add(end);
        adj[end].add(start);
        this.edgeCount++;
    }
    public int getEdgeCount() { return edgeCount; }
    public int getVertexCount() { return vertexCount; }
    /** 返回顶点v的邻接表*/
    public LinkedList<Integer> adj(int v){return adj[v];}
    /** 把图转化成标准字符串形式*/
    public String toString(){
        String NEWLINE = System.getProperty("line.separator");
        StringBuilder sb = new StringBuilder();
        sb.append("vertex count: ").append(getVertexCount())
                .append(" edge count: ").append(getEdgeCount())
                .append(Config.NEWLINE);
        for (int v = 0; v < getVertexCount();v++){
            LinkedList<Integer> list = adj(v);
            sb.append(v).append(":\t").append("[");
            for (int i=0; i < list.size();i++){
                sb.append(list.get(i)).append(",");
            }
            sb.deleteCharAt(sb.length() - 1);
            sb.append("]").append(NEWLINE);
        }
        return sb.toString();
    }
    public static void main(String[] args) {
        String dir = Graph.class.getPackage().getName().replace(".", "/");
        String path = Graph.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath();
        In in = new In(new File(path));
        Graph g = new Graph(in);
        System.out.println(g.toString());
    }
}
在这里插入图片描述
/**
 * 图的基本常用操作工具类
 */
public class GraphUtils {
    /** 计算顶点v的度数*/
    public static int degree(Graph graph, int v){return graph.adj(v).size();}
    /** 计算图中最大的度*/
    public static int maxDegree(Graph graph){
        int max = 0;
        for(int i = 0;i<graph.getVertexCount();i++){
            int currentDegree = degree(graph, i);
            max = currentDegree > max ? currentDegree : max;
        }
        return max;
    }
    /** 计算图的平均度数*/
    public static int avgDegree(Graph g){ return 2 * g.getEdgeCount() / g.getVertexCount(); }
    /** 计算自环的个数*/
    public static int numberOfSelfLoops(Graph g){
        int count = 0;
        for(int v = 0; v < g.getVertexCount(); v++)
            for(int w: g.adj(v))
                if(v == w)    count++;
        return count / 2; // 每条边计算了两次
    }
    public static void main(String[] args) {
        String dir = GraphUtils.class.getPackage().getName().replace(".", "/");
        String path = GraphUtils.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath();
        In in = new In(new File(path));
        Graph g = new Graph(in);
        for (int i = 0; i < g.getVertexCount(); i++) {
            System.out.println(i+" degree : "+GraphUtils.degree(g, i));    
        }
        System.out.println("the max degree is : " + GraphUtils.maxDegree(g));
        System.out.println(g.toString());
        System.out.println("avg degree: "+GraphUtils.avgDegree(g));
        System.out.println("count of self loop: "+GraphUtils.numberOfSelfLoops(g));
    }
}
0 degree : 4
1 degree : 1
2 degree : 1
3 degree : 2
4 degree : 3
5 degree : 3
6 degree : 2
7 degree : 1
8 degree : 1
9 degree : 3
10 degree : 1
11 degree : 2
12 degree : 2
the max degree is : 4
vertex count: 13 edge count: 13
0:    [5,1,2,6]
1:    [0]
2:    [0]
3:    [4,5]
4:    [3,6,5]
5:    [0,4,3]
6:    [4,0]
7:    [8]
8:    [7]
9:    [12,10,11]
10:    [9]
11:    [12,9]
12:    [9,11]

avg degree: 2
count of self loop: 0

图的处理算法的设计模式

将图的表示和实现分离开。为每个任务创建一个相应的类,用例可以创建相应的对象来完成任务。


在这里插入图片描述

深度优先搜索

探索迷宫方法:tremaux搜索:

绳子可保证总能找到一条出路,标记则能保证不会两次经过同一条通道或同一个路口。


在这里插入图片描述

看Java代码实现:

/**
 * 图的深度优先搜索算法
 */
public class DepthFirstSearch {
    private int count;
    private boolean[] marked; // 数组存储每个顶点是否被遍历过
    /**
     * 从顶点s开始对g进行深搜
     * @param g
     * @param s
     */
    public DepthFirstSearch(Graph g, int s) {
        marked = new boolean[g.getVertexCount()];
        dfs(g, s);
    }
    /** 深搜*/
    private void dfs(Graph g, int s) {
        marked[s] = true;                    // 1.标记顶点s
        count++;                            // 2.count数加一
        LinkedList<Integer> list = g.adj(s);// 3.获取s的邻接表
        for(int w: list)                    // 4.对邻接表进行遍历
            if(!isMarked(w))    dfs(g,w);    // 5.如果遍历到的顶点没有被标记过,对该顶点继续递归深搜
    }
    /** 顶点w是否和起点s相连通*/
    public boolean isMarked(int w){return marked[w];}
    
    /** 与起点s连通的顶点数量*/
    public int count(){return count;}
    
    public static void main(String[] args) {
        String dir = DepthFirstSearch.class.getPackage().getName().replace(".", "/");
        String path = DepthFirstSearch.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath();
        In in = new In(new File(path));
        Graph g = new Graph(in);
        int start = 0;
        DepthFirstSearch search = new DepthFirstSearch(g, start);
        System.out.print("start vertex: "+ start+". ");
        StringBuilder sb = new StringBuilder();
        for(int i = 0; i< g.getVertexCount(); i++)
            if(search.isMarked(i)) sb.append(" "+ i);
        System.out.println("Connected " + sb.toString());
        // 如果和s连通的顶点数量和图的顶点数量相同,说明是连通图
        if(search.count() == g.getVertexCount())    System.out.println("g is a connected graph.");
        else System.out.println("g is not a connected graph.");
    }
}
start vertex: 0. Connected  0 1 2 3 4 5 6
g is not a connected graph.
在这里插入图片描述

“两个给定顶点是否连通?”等价于“两个给定的顶点之间是否存在一条路径”,也叫路径检测问题。

union-find算法的数据结构并不能解决找出这样一条路径问题,DFS是已经学习过的方法中第一个能够解决该问题的算法

能解决的另一问题:单点路径----给定一幅图和一个起点s,“从S到给定的顶点V是否存在一条路径,如果有,找出”

寻找路径

[图片上传失败...(image-afd0cd-1599664548149)]
构造函数接受一个起点S作为参数,计算S到与S连通的每个顶点之间的路径。在为S创建了Paths对象后,用例可以调用pathTo()实例方法来遍历从S到任意和S连通的顶点的路径上的所有顶点。以后会实现只查找具有某些属性的路径。

在这里插入图片描述
/**
 * 深搜寻找路径问题
 */
public class DepthFirstPaths {
    private boolean[] marked;        
    private int[] edgeTo;        // 路径
    private int start;            // 起点
    public DepthFirstPaths(Graph g, int s){
        marked = new boolean[g.getVertexCount()];
        edgeTo = new int[g.getVertexCount()];
        this.start = s;
        dfs(g, s);
    }
    private void dfs(Graph g, int s) {
        marked[s] = true;
        for(int w: g.adj(s)){
            if(!marked[w]){
                // 如果w没有被标记过,把路径数组中的w处置为s,意思:从s到达了w。此处记录了每一次深搜的路径节点
                edgeTo[w] = s; 
                dfs(g, w);
            }
        }
    }
    /** 从起点s到顶点v是否存在通路*/
    public boolean hasPathTo(int v){return marked[v];}
    public Stack<Integer> pathTo(int v){
        if(!hasPathTo(v))    return null;
        Stack<Integer> stack = new Stack<>();
        for(int x = v; x!=start; x=edgeTo[x]) // 从终点开始,倒着找起点,依次push入栈
            stack.push(x);
        stack.push(start);// for循环到起点处终止,所以在循环结束后要把起点入栈,至此 一条完整的路径依次入栈
        return stack;
    }
    public static void main(String[] args) {
        String dir = DepthFirstPaths.class.getPackage().getName().replace(".", "/");
        String path = DepthFirstPaths.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath();
        In in = new In(new File(path));
        Graph g = new Graph(in);
        int start = 0;
        DepthFirstPaths pathSearch = new DepthFirstPaths(g, start);
        StringBuilder sb = new StringBuilder();
        for(int i = 0; i<g.getVertexCount(); i++){
            if(i == start) continue;
            if(!pathSearch.hasPathTo(i)){
                System.out.println(start+" to "+ i +" : not connected.");
                continue;
            }
            sb.setLength(0);
            sb.append(start).append(" to ").append(i).append(": ");
            Stack<Integer> p = pathSearch.pathTo(i);
            while(!p.isEmpty()) sb.append(p.pop()).append("->");
            sb.deleteCharAt(sb.length()-1);
            sb.deleteCharAt(sb.length()-1);
            System.out.println(sb.toString());
        }
    }
}
0 to 1: 0->1
0 to 2: 0->2
0 to 3: 0->5->4->3
0 to 4: 0->5->4
0 to 5: 0->5
0 to 6: 0->5->4->6
0 to 7 : not connected.
0 to 8 : not connected.
0 to 9 : not connected.
0 to 10 : not connected.
0 to 11 : not connected.
0 to 12 : not connected.

广度优先搜索BFS

深搜得到的路径不仅取决于图的结构,还取决于图的表示和递归调用的性质。我们自然对最短路径感兴趣:

单点最短路径。给定一幅图和一个起点S,从S到给定顶点V是否存在一条路径?如果有,请找出其中最短的那条(所含边数最少)。

实现:

算法4.2实现了BFS。使用队列保存所有已经被标记过但其邻接表还未被检查过的顶点。先将起点加入队列,然后重复下面步骤直到队列为空:

算法4.2中的方法不是递归的,不像递归中隐式使用的栈,而是显式地使用了一个队列。


在这里插入图片描述
/**
 * 广搜找到最短路径
 *         对于从s可达的任意顶点v,广搜都能找到一条从s到v的最短路径
 *         (没有其他从s到v的路径所含边比这条路径更少)
 * 广搜所需时间在最坏情况下和(v + e)成正比。
 */
public class BreadthFirstPaths {
    private boolean[] marked;
    private int[] edgeTo;
    private int start;
    public BreadthFirstPaths(Graph g, int s){
        this.start = s;
        marked = new boolean[g.getVertexCount()];
        edgeTo = new int[g.getVertexCount()];
        bfs(g, s);
    }
    private void bfs(Graph g, int s) {
        Queue<Integer> queue = new Queue<>();    
        marked[s] = true;     // 标记起点
        queue.enqueue(s);    // 起点入队
        while(!queue.isEmpty()){
            int head = queue.dequeue();    // 从队列中取出队首
            LinkedList<Integer> list = g.adj(head);    // 得到队首的邻接表
            for(int w: list){     //遍历邻接表
                if(!marked[w]){    // 若当前节点没有被标记过
                    edgeTo[w] = head;    // 1.存入路径
                    marked[w] = true;    // 2.进行标记
                    queue.enqueue(w);    // 3.节点入队
                }
            }
        }
    }
    /** 从起点s到顶点v是否存在通路*/
    public boolean hasPathTo(int v){return marked[v];}
    /** 返回从起点s到顶点v的一条最短路径*/
    public Stack<Integer> pathTo(int v){
        if(!hasPathTo(v))    return null; // 若不存在到v的路径,返回Null
        Stack<Integer> path = new Stack<>();
        for(int x = v; x!=start; x=edgeTo[x])
            path.push(x);
        path.push(start);
        return path;
    }
    public static void main(String[] args) {
        String dir = BreadthFirstPaths.class.getPackage().getName().replace(".", "/");
        String path = BreadthFirstPaths.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath();
        In in = new In(new File(path));
        Graph g = new Graph(in);
        int start = 5;
        BreadthFirstPaths bfPath = new BreadthFirstPaths(g, start);
        for(int i = 0; i<g.getVertexCount();i++){
            if(i == start) continue;
            if(!bfPath.hasPathTo(i)){
                System.out.println(start + " to "+ i + " : not connected.");
                continue;
            }
            StringBuilder sb = new StringBuilder();
            sb.append(start + " to "+ i + " : ");
            Stack<Integer> p = bfPath.pathTo(i);
            while(!p.isEmpty()){
                sb.append(p.pop() + "->");
            }
            sb.deleteCharAt(sb.length() - 1);
            sb.deleteCharAt(sb.length() - 1);
            System.out.println(sb.toString());
        }
    }
}
5 to 0 : 5->0
5 to 1 : 5->0->1
5 to 2 : 5->0->2
5 to 3 : 5->3
5 to 4 : 5->4
5 to 6 : 5->0->6
5 to 7 : not connected.
5 to 8 : not connected.
5 to 9 : not connected.
5 to 10 : not connected.
5 to 11 : not connected.
5 to 12 : not connected.

对于这个例子,edgeTo[]数组在第二步之后就已经完成了。和深搜一样,一点所有顶点都已经被标记,余下的计算工作就只是在检查连接到各个已被标记的顶点的边而已。

命题:对于从S可达到的任意顶点V, 广搜都能找到一条从S到V的最短路径(没有其他从S到V得路径所含的边比这条路径更少)

续: 广搜所需的时间在最坏情况下和V+E成正比

DFS和BFS都会先将起点存入数据结构中,然后重复以下步骤知道数据结构被清空:

不同之处在于从数据结构中获取下一个顶点的规则:广搜是最早加入加粗样式的顶点;深搜是最晚加入的顶点。这种差异得到了处理图的两种完全不同的视角,无论哪种,所有与起点连通的顶点和边都会被检查到

连通分量

深搜下一个直接应用就是找出一幅图的所有连通分量。API:

[图片上传失败...(image-be1cfc-1599664548149)]
CC的实现使用了marked[ ]数组来寻找一个顶点作为每个连通分量中深度优先搜索的起点。递归的深搜第一次调用的参数是顶点0,会标记所有与0连通的顶点。然后构造函数中的for循环会查找每个没有被标记的顶点并递归调用dfs来标记和它相邻的所有顶点。另外,它还使用了一个以顶点作为索引的数组id[ ],将同一个连通分量中的顶点和连通分量的标识符关联起来。这个数组使得connected( )方法的实现变得十分简单。

/**
 * 强连通分量
 */
public class CC {
    private boolean[] marked;
    private int[] id;
    private int count;
    public CC(Graph g){
        marked = new boolean[g.getVertexCount()];
        id = new int[g.getVertexCount()];
        for(int s = 0; s < g.getVertexCount(); s++){
            if(!marked[s]){
                dfs(g,s);
                count++;
            }
        }
    }
    private void dfs(Graph g, int v) {
        marked[v] = true;
        id[v] = count;
        for(int w: g.adj(v))
            if(!marked[w])
                dfs(g,w);
    }
    /** v和w连通吗*/
    public boolean connected(int v, int w)    { return id[v] == id[w]; }
    /** v所在的连通分量的标识符*/
    public int id(int v)    { return id[v]; }
    /** 连通分量数*/
    public int count()        {return count;}
    public static void main(String[] args) {
        String dir = CC.class.getPackage().getName().replace(".", "/");
        String path = CC.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath();
        In in = new In(new File(path));
        Graph g = new Graph(in);
        CC cc = new CC(g);
        int m = cc.count();
        System.out.println("number of components: "+ m);
        LinkedList<Integer>[] components = new LinkedList[m];
        for(int i =0;i<m;i++)
            components[i] = new LinkedList<>();
        for(int v = 0; v< g.getVertexCount(); v++)
            components[cc.id(v)].add(v);
        for(int i=0;i<m;i++){
            for(int v: components[i])
                System.out.print(v + " ");
            System.out.println();
        }
    }
}
number of components: 3
0 1 2 3 4 5 6 
7 8 
9 10 11 12 

其实现基于一个由顶点索引的数组id[ ].若V属于第i个连通分量,则id[v]的值为i。构造函数会找出一个未被标记的顶点并调用递归函数dfs( )来标记并区分出所有和它连通的顶点,如此重复直到所有的顶点都被标记并区分。

命题C:深搜的预处理使用的时间和空间与V+E成正比且可以在常数时间内处理关于图的连通性查询。

下面对两个问题进行解答:

检测环解题


/**
 * 给定的图是无环图吗
 * 检测自环:假设没有自环,没有平行边
 */
public class Cycle {
    private boolean[] marked;
    private boolean hasCycle;
    public Cycle(Graph g){
        marked = new boolean[g.getVertexCount()];
        for(int i = 0;i<g.getVertexCount();i++)
            if(!marked[i])    dfs(g, i, i);
    }
    private void dfs(Graph g, int v, int u) {
        marked[v] = true;
        for(int w: g.adj(v))
            if(!marked[w])    dfs(g, w, v); // 若w没被标记过,那么从w继续递归深搜,把w的父节点作为第二参数
            else if(w != u) hasCycle = true; // 若w被标记过,那么若无环,w必然和父节点相同,否则就是有环
    }
    /** 是否含有环*/
    public boolean hasCycle(){return hasCycle;}
    public static void main(String[] args) {
        String dir = Cycle.class.getPackage().getName().replace(".", "/");
        String pathCycle = Cycle.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath();
        String pathNoCycle = Cycle.class.getClassLoader().getResource(dir+"/cycle_test.txt").getPath();
        In in = new In(new File(pathCycle));
        Graph g = new Graph(in);
        Cycle c = new Cycle(g);
        System.out.println(c.hasCycle());
        In in2 = new In(new File(pathNoCycle));
        Graph g2 = new Graph(in2);
        Cycle c2 = new Cycle(g2);
        System.out.println(c2.hasCycle());
    }
}
true
false

[图片上传失败...(image-6cd882-1599664548149)]

双色问题解题

/**
 * 双色问题:能够用两种颜色将图的所有顶点着色,使得任意一条边上的两个端点的颜色都不同吗?
 * 等价于:判断是否是二分图的问题
 */
public class TwoColor {
    private boolean[] marked;
    private boolean[] color;
    private boolean isColorable;
    public TwoColor(Graph g){
        isColorable = true;
        marked = new boolean[g.getVertexCount()];
        color = new boolean[g.getVertexCount()];
        for(int i = 0; i<g.getVertexCount(); i++)//遍历所有顶点
            if(!marked[i])    dfs(g, i);//没有mark就进行深搜
    }
    private void dfs(Graph g, int v) {
        marked[v] = true;        // 标记
        for(int w: g.adj(v))    // 对邻接表进行遍历
            if(!marked[w]){        // 如果没有被标记
                color[w] = !color[v];    // 当前w节点颜色置为和父节点不同的颜色
                dfs(g, w);                // 对当前节点继续深搜
            }else if(color[w] == color[v]){    // 如果已经被标记,看是否颜色和父节点相同
                isColorable = false;         // 若相同则不是二分图
            }
    }
    /** 是否是二分图*/
    public boolean isBipartite(){return isColorable;}
    public static void main(String[] args) {
        String dir = TwoColor.class.getPackage().getName().replace(".", "/");
        String path = TwoColor.class.getClassLoader().getResource(dir+"/color_test.txt").getPath();
        String path2 = TwoColor.class.getClassLoader().getResource(dir+"/color_test2.txt").getPath();
        In in = new In(new File(path));
        Graph g = new Graph(in);
        TwoColor t = new TwoColor(g);
        System.out.println(t.isBipartite());
        
        In in2 = new In(new File(path2));
        Graph g2 = new Graph(in2);
        TwoColor t2 = new TwoColor(g2);
        System.out.println(t2.isBipartite());
    }
}
true
false
在这里插入图片描述

符号图

典型应用中,图都是通过文件或者网页定义的,使用的是字符串而非整数来表示和指代顶点。为了适应这样的应用,定义拥有以下性质的输入格式:

例子:


在这里插入图片描述
在这里插入图片描述

定义了一个构造来读取并构造图,用name( )方法和index( )方法将输入流中的顶点名和图算法使用的顶点索引对应起来。

测试用例

例子:飞机场routes.txt--输入机场代码查找从该机场起飞到达的城市,但这些信息并不是直接从文件中能得到的。

例子:电影movies.txt--输入一部电影名字得到演员列表。这不过是在照搬文件中对应的行数据,

​ 但输入演员名字 查看其出演的电影列表,相当于反向索引。

​ 尽管数据库的构造是为了将电影名连接到演员,二分图模型同时也意味着将演员连接到电影名。

​ 二分图的性质自动完成了反向索引。这将成为处理更复杂的和图有关的问题的基础。

符号图的实现

SymbolGraph用到了3种数据结构:

SymbolGraph会遍历两遍数据结构来构造以上数据结构,主要是因为构造Graph对象需要顶点总数V。在典型的实际应用中,在定义图的文件中指明V和E可能会不方便,从而有了SymbolGraph,这样就可以方便地在routes.txt或者movies.txt中添加或者删除条目而不用但系需要维护边或者顶点的总数。

在这里插入图片描述

/**
 * 符号图
 */
public class SymbolGraph {
    private HashMap<String, Integer> map;     // key:顶点名  value:索引
    private String[] keys;                    // 反向索引,保存每个顶点索引对应的顶点名
    private Graph g;                        // 使用索引来引用图中的顶点
    public SymbolGraph(String path, String sp){
        map = new HashMap<>();
        BufferedReader reader;
        String line;
        try {
            reader = new BufferedReader(new FileReader(new File(path)));
            while((line = reader.readLine()) != null){//第一遍,构造索引
                String [] vertexs = line.split(sp);
                for(String s : vertexs)
                    if(!map.containsKey(s))    map.put(s, map.size());
            }
            reader.close();
            keys = new String[map.size()]; 
            for(String name: map.keySet()){    // 遍历map的key,构造顶点名的反向索引
                keys[map.get(name)] = name; 
            }
            g = new Graph(map.size());
            line = "";
            reader = new BufferedReader(new FileReader(new File(path)));
            while((line = reader.readLine()) != null){ // 第二遍,构造图,将每一行的顶点和该行其他点相连
                String[] strs = line.split(sp);
                int start = map.get(strs[0]);//获取起点
                for(int i = 1; i< strs.length; i++)
                    g.addEdge(start, map.get(strs[i]));
            }
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /** key是一个顶点吗*/
    public boolean  contains(String key){return map.containsKey(key);}
    /** key的索引*/
    public int index(String key){return map.get(key);}
    /** 索引v的顶点名*/
    public String name(int v){return keys[v];}
    /** 隐藏的Graph对象*/
    public Graph graph(){return g;}
    public static void main(String[] args) {
        String dir = Cycle.class.getPackage().getName().replace(".", "/");
        String path = Cycle.class.getClassLoader().getResource(dir+"/routes.txt").getPath();
        SymbolGraph sg = new SymbolGraph(path, " ");
        Graph g = sg.graph();
        HashMap<String, Integer> map = sg.map;
        for(Entry<String, Integer> s: map.entrySet()){
            System.out.println(s.getKey() + "-" +s.getValue());
        }
        System.out.println(g.toString());
        String start = "JFK";
        if(!sg.contains(start)){
            System.out.println("起点"+start + " 不在数据库.");
            return;
        }
        int s = sg.index(start);
        BreadthFirstPaths bfs = new BreadthFirstPaths(g, s);
        String end = "LAS";
        if(!sg.contains(end)){
            System.out.println("终点"+end + " 不在数据库.");
        }else{
            int t = sg.index(end);
            if(!bfs.hasPathTo(t)){
                System.out.println(start +" 和 " + end + " 没有路径相同.");
                return;
            }
            Stack<Integer> stack = bfs.pathTo(t);
            StringBuilder sb = new StringBuilder();
            while(!stack.isEmpty()){
                sb.append(sg.name(stack.pop())).append(" ");
            }
            System.out.println("起点"+start+"到终点"+end+"的路径为:");
            System.out.println(sb.toString());
        }
    }
}
LAS-9
LAX-8
DFW-5
ORD-2
JFK-0
HOU-4
ATL-7
DEN-3
PHX-6
MCO-1
vertex count: 10 edge count: 18
0:    [1,7,2]
1:    [0,7,4]
2:    [3,4,5,6,0,7]
3:    [2,6,9]
4:    [2,7,5,1]
5:    [6,2,4]
6:    [5,2,3,8,9]
7:    [0,4,2,1]
8:    [6,9]
9:    [3,8,6]

起点JFK到终点LAS的路径为:
JFK ORD DEN LAS 

同样可以把电影-演员作为例子输入:

这个Graph实现允许用例用字符串代替数字索引来表示图中的顶点。

它维护了

为了构造这些数据结构,代码会将图的定义处理两遍(定义的每一行都包含一个顶点以及它的相邻顶点列表,用分隔符sp隔开)

间隔的度数

图处理的一个经典问题就是,找到一个社交网络之中两个人间隔的度数。

演员K演过很多电影,为图中每个演员附一个K数:

可以看到K数必须为最短电影链的长度,因此不用计算机,很难知道。

用例DegreesOfSeparation所示,BreadthFirstPaths才是我们所需要的程序,通过最短路径来找出movies.txt中任意演员的K数。

总结

几个基本概念:

上表总结了本节所有图算法的实现。适合作为图处理的入门学习。随后学习复杂类型图以及更加困难的问题时,会用到这些代码的变种。

考虑了边的方向以及权重之后,同样地问题会变得困难得多,但同样地算法仍然凑效并将成为解决更复杂问题的起点。

参考:
算法第四版4.1-无向图详解

上一篇下一篇

猜你喜欢

热点阅读