图的专题(入门及最小生成树MST)

如何表示图这个数据结构

大家学习图算法的时候常常会面临这个问题,书和视频中介绍的各种算法,Kruskal算法 Dijkstra算法,Prim算法以及BFS DFS都仅仅介绍了图是算法,但是图这个数据结构对你仍是黑箱的,所以看不懂这些算法,下面我剥茧抽丝的讲解图的表示和其他关于图的算法。

图的节点类型

不论是无向连通图还是有向连通图,不论是带权还是不带权。都可以用这样的节点类型表示
它包含自身值、入度、出度、邻接点集、邻接边集合。

import java.util.ArrayList;
//值,入度出度,点集边集
public class Node {
    public int value;
    public int in;
    public int out;
    public ArrayList<Node> nexts;
    public ArrayList<Edge> edges;

    public Node(int value) {
        this.value = value;
        in = 0;
        out = 0;
        nexts = new ArrayList<Node>();
        edges = new ArrayList<Edge>();
    }
}

图的单向连通,双向连通。是通过边来描述的,双向连通图就是通过两条边表示,单向通过一条边表示。带权的给权值赋值,非带权的赋相同值

//权重 连接哪两个点
public class Edge {
    public int weight;
    public Node from;
    public Node to;

    public Edge(int weight, Node from, Node to) {
        this.weight = weight;
        this.from = from;
        this.to = to;
    }
}

图包含点集和边集

import java.util.HashMap;
import java.util.HashSet;

public class Graph {
    public HashMap<Integer,Node> nodes;
    public HashSet<Edge> edges;

    public Graph() {
        nodes = new HashMap<Integer,Node>();
        edges = new HashSet<Edge>();
    }
}

以上就是图结构的全部细节。但我们竞赛,学习,面试中遇到的图往往不是这样一种数据结构,而是类似于这样
4//表示边的个数
1 2 2//点1与点2 权值为 2
2 6 1
7 5 1
1 7 5
以这种数据如何生成一张图呢

public class GraphGenerator {

    public static Graph createGraph(Integer[][] matrix) {
        Graph graph = new Graph();
        for (int i = 0; i < matrix.length; i++) {
            Integer from = matrix[i][0];
            Integer to = matrix[i][1];
            Integer weight = matrix[i][2];
            if (!graph.nodes.containsKey(from)) {
                graph.nodes.put(from, new Node(from));
            }
            if (!graph.nodes.containsKey(to)) {
                graph.nodes.put(to, new Node(to));
            }
            Node fromNode = graph.nodes.get(from);
            Node toNode = graph.nodes.get(to);
            Edge newEdge = new Edge(weight, fromNode, toNode);
            fromNode.nexts.add(toNode);
            fromNode.out++;
            toNode.in++;
            fromNode.edges.add(newEdge);
            graph.edges.add(newEdge);
        }
        return graph;
    }

}

之后,就可以使用图算法了。

最小生成树MST

最小生成树问题是图论中最经典的问题之一,在生活中也有广泛的应用,算基站最少需要多长的电缆等等。
有这样一个定理
在要求解的连通图中,任意选择一些点属于集合A,剩余的点属于集合B,必定存在一颗最小生成树包含两个顶点分别属于集合A和集合B的边(即连通两个集合的边)中权值最小的边。
意思就是每个点都能连接到

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1233

还是畅通工程 Time Limit: 4000/2000 MS (Java/Others) Memory Limit:
65536/32768 K (Java/Others) Total Submission(s): 53217 Accepted
Submission(s): 24160

Problem Description

某省调查乡村交通状况,得到的统计表中列出了任意两村庄间的距离。省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可),并要求铺设的公路总长度为最小。请计算最小的公路总长度。

Input

测试输入包含若干测试用例。每个测试用例的第1行给出村庄数目N ( < 100
);随后的N(N-1)/2行对应村庄间的距离,每行给出一对正整数,分别是两个村庄的编号,以及此两村庄间的距离。为简单起见,村庄从1到N编号。
当N为0时,输入结束,该用例不被处理。

Output

对每个测试用例,在1行里输出最小的公路总长度。

Sample Input
3
1 2 1
1 3 2
2 3 4
4
1 2 1
1 3 4
1 4 1
2 3 3
2 4 2
3 4 5
0
Sample Output

3
5

Hint Hint Huge input, scanf is recommended.
Source

浙大计算机研究生复试上机考试-2006年

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Scanner;

public class 还是畅通工程 {


    static int pre[] = new int[101];
    public static void main(String[] args) {
        Scanner input=new Scanner(System.in);
        int n=input.nextInt();
        ArrayList<Edge> list=new ArrayList<Edge>();
        Edge e;
        for (int i = 0; i < n*(n-1)/2; i++) {
            e=new Edge(input.nextInt(),input.nextInt(),input.nextInt());
            list.add(e);
        }
        list.sort(new Comparator<Edge>() {//按权值增值排列所有的边
            @Override
            public int compare(Edge e1, Edge e2) {
                // TODO Auto-generated method stub
                return e1.weight-e2.weight;
            }           
        });
        //init unionfind
        for (int i = 0; i < pre.length; i++) {
            pre[i]=i;//自己是自己的主元
        }
        int ans=0;//初始化权值累加变量
        for (int i = 0; i < list.size(); i++) {
            int fx=find(list.get(i).from);
            int fy=find(list.get(i).to);
            if (fx!=fy) {
                pre[fx]=fy;//y入赘到x上
                ans+=list.get(i).weight;
                //如果最后要那些选取的边集可以之前创建一个集合,在这里加入边
            }
        }


        //System.out.println(ans);//基础版本在这里输出答案就可以
        //总结就是边权值升序排列,检查每一个边的两个点在一个集合吗,如果合并,累加权值就是答案

        //扩展部分,题目数据限制了必然有MST,如果给的数据无法生成MST就输出“Worring Input!”
        //对所有节点判断,是否属于同一个连通分支
        for (int i = 0; i < list.size(); i++) {
            int fx=find(list.get(i).from);
            int fy=find(list.get(i).to);
            if (fx!=fy) {
                System.out.println("Worring Input!");
                break;
            }
            if (i==list.size()-1) {
                System.out.println(ans);
            }
        }

    }
    public static int find(int x) {
        int r=x;
        while (pre[r]!=r) {
            r=pre[r];
        }
        return r;
    }

}
//上面代码的节点类型适应各种形式,根据题目可以适当调整变的更加简洁
class Edge {
    public int from;
    public int to;
    public int weight;

    public Edge(int from, int to, int weight) {
        this.from = from;
        this.to = to;
        this.weight = weight;
    }
}

用完整的节点,边,图类型也是可以完成同样的操作,之前的简化版易于理解,现在可以理解完全版的代码(适用于各种情况)


import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.PriorityQueue;
import java.util.Set;

//undirected graph only
public class Code_05_Kruskal {

    // Union-Find Set
    public static class UnionFind {
        private HashMap<Node, Node> fatherMap;//集合关系
        private HashMap<Node, Integer> rankMap;//连通分支的元素数量

        public UnionFind() {
            fatherMap = new HashMap<Node, Node>();
            rankMap = new HashMap<Node, Integer>();
        }

        private Node findFather(Node n) {
            Node father = fatherMap.get(n);
            if (father != n) {
                father = findFather(father);
            }
            fatherMap.put(n, father);
            return father;
        }

        public void makeSets(Collection<Node> nodes) {
            fatherMap.clear();
            rankMap.clear();
            for (Node node : nodes) {
                fatherMap.put(node, node);
                rankMap.put(node, 1);
            }
        }

        public boolean isSameSet(Node a, Node b) {
            return findFather(a) == findFather(b);
        }

        public void union(Node a, Node b) {
            if (a == null || b == null) {
                return;
            }
            Node aFather = findFather(a);
            Node bFather = findFather(b);
            if (aFather != bFather) {
                int aFrank = rankMap.get(aFather);
                int bFrank = rankMap.get(bFather);
                if (aFrank <= bFrank) {
                    fatherMap.put(aFather, bFather);
                    rankMap.put(bFather, aFrank + bFrank);
                } else {
                    fatherMap.put(bFather, aFather);
                    rankMap.put(aFather, aFrank + bFrank);
                }
            }
        }
    }

    public static class EdgeComparator implements Comparator<Edge> {

        @Override
        public int compare(Edge o1, Edge o2) {
            return o1.weight - o2.weight;
        }

    }

    public static Set<Edge> kruskalMST(Graph graph) {
        UnionFind unionFind = new UnionFind();
        unionFind.makeSets(graph.nodes.values());
        PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());
        for (Edge edge : graph.edges) {
            priorityQueue.add(edge);
        }
        Set<Edge> result = new HashSet<>();
        while (!priorityQueue.isEmpty()) {
            Edge edge = priorityQueue.poll();
            if (!unionFind.isSameSet(edge.from, edge.to)) {
                result.add(edge);
                unionFind.union(edge.from, edge.to);
            }
        }
        return result;
    }
}

之前考察Kruskal的题目表述过于直白,这里还有一道题目含蓄的题

Freckles

题目1144:Freckles 时间限制:1 秒内存限制:32 兆特殊判题:否提交:882解决:452 题目描述:
In an episode of the Dick Van Dyke show, little Richie connects the freckles on his Dad’s back to form a picture of the Liberty Bell.
Alas, one of the freckles turns out to be a scar, so his Ripley’s
engagement falls through.
Consider Dick’s back to be a plane with freckles at various (x,y) locations. Your job is to tell Richie how to connect the dots so as to
minimize the amount of ink used. Richie connects the dots by drawing
straight lines between pairs, possibly lifting the pen between lines.
When Richie is done there must be a sequence of connected lines from
any freckle to any other freckle.
输入:
The first line contains 0 < n <= 100, the number of freckles on Dick’s back. For each freckle, a line follows; each following line
contains two real numbers indicating the (x,y) coordinates of the
输出:
Your program prints a single real number to two decimal places: the minimum total length of ink lines that can connect all the
freckles. 样例输入: 3
1.0 1.0
2.0 2.0
2.0 4.0 样例输出:
3.41

2009年北京大学计算机研究生机试真题

题目大意为平面上有若干个点,我们需要一组线段来将这些点连接起来使任意两个点都能够通过一系列的线段相连,给出所有点的坐标,求一种连接方式使所有线段的长度和最小,输出长度和。将点抽象为节点,线段抽象为带权边,,类似几何最优解的问题就被转化为图论的最小生成树问题。

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Scanner;
public class Freckles {
    static int pre[] = new int[1001];

    public static void main(String[] args) {
        Scanner input=new Scanner(System.in);
        int n=input.nextInt();
        ArrayList<Point> list_p=new ArrayList<Point>();//点集
        Point point;
        for (int i = 0; i < n; i++) {
            point=new Point();
            point.x=input.nextDouble();
            point.y=input.nextDouble();
            list_p.add(point);
        }
        ArrayList<Edge2> list_e=new ArrayList<Edge2>();//边集
        Edge2 edge;
        for (int i = 1; i <= n; i++) {//连接两点的线段抽象成边
            for (int j = i+1; j <= n; j++) {
                edge=new Edge2(i, j, list_p.get(i-1).getDistance(list_p.get(j-1)));
                list_e.add(edge);
            }
        }
        //现在就变成了一个MST问题
        list_e.sort(new Comparator<Edge2>() {//按权值增值排列所有的边
            @Override
            public int compare(Edge2 e1, Edge2 e2) {
                // TODO Auto-generated method stub
                return e1.weight<e2.weight?-1:1;
            }           
        });
        for (int i = 0; i < pre.length; i++) {
            pre[i]=i;
        }
        double ans=0;
        for (int i = 0; i < list_e.size(); i++) {
            int fx=find(list_e.get(i).from);
            int fy=find(list_e.get(i).to);
            if (fx!=fy) {
                pre[fx]=fy;//y入赘到x上
                ans+=list_e.get(i).weight;
            }
        }
        System.out.printf("%.2f\n",ans);

    }
    public static int find(int x){
        int r=x;
        while (r!=pre[r]) {
            r=pre[r];
        }
        return r;
    }

}
class Point{
    double x;
    double y;
    double cost;
    double getDistance(Point p){
        return Math.sqrt((x-p.x)*(x-p.x)+(y-p.y)*(y-p.y));          
    }
}
class Edge2{
    public int from;
    public int to;
    public double weight;

    public Edge2(int from, int to, double weight) {
        this.from = from;
        this.to = to;
        this.weight = weight;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值