版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jt102605/article/details/84657292
一. 有权图
- 边上的权值不一定是数值,而可以是各种类型
- 有权图的邻接矩阵和邻接表
- 抽象出一个Edge类,存放边的信息
//Edge类, 表示边的信息
public class Edge<Weight extends Number & Comparable> implements Comparable<Edge> {
private int a, b; //边的两个节点, 表示边 a->b
private Weight weight; //边的权重,不一定是数字类型
public Edge(int a,int b, Weight weight){
this.a = a;
this.b = b;
this.weight = weight;
}
public Edge(Edge<Weight> e){
this(e.a, e.b, e.weight);
}
//返回第一个顶点
public int v(){
return this.a;
}
//返回第二个顶点
public int w(){
return this.b;
}
//返回权重
public Weight weight(){
return this.weight;
}
//给定一个顶点返回另一个顶点
public int otherV(int v){
assert v == a || v == b;
return v == a? b:a;
}
//输出边的信息
public String toString(){
return ""+a+"-"+b+":"+weight;
}
//边之间的比较
@Override
public int compareTo(Edge other){
if(weight.compareTo(other.weight())>0){
return 1;
}else if(weight.compareTo(other.weight())<0){
return -1;
}else{
return 0;
}
}
}
//有权稠密图 邻接矩阵表示
public class DenseGraph<Weight extends Number & Comparable> implements WeightGraph<Weight> {
private int n; //节点数
private int m; //边数
private boolean isDirected; //是否为有向图
private Edge<Weight>[][] g; //图的具体数据
public DenseGraph(int n, boolean isDirected){
assert n>=0;
this.n = n;
this.m = 0;
this.isDirected = isDirected;
// g初始化为n*n的矩阵, 每一个g[i][j]均为null, 表示没有任和边
this.g = new Edge[n][n];
for(int i=0; i<n; i++){
for(int j=0; j<n; j++){
g[i][j] = null;
}
}
}
@Override
public int V(){
return this.n;
}
@Override
public int E(){
return this.m;
}
//检查边 v——>w 是否存在
@Override
public boolean hasEdge(int v, int w){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
return g[v][w] != null;
}
@Override
public void addEdge(Edge<Weight> e){
assert e.v()>=0 && e.v()<n;
assert e.w()>=0 && e.w()<n;
//去除重复边
//if(e.compareTo(g[e.v()][e.w()]) == 0){
// return;
//}
//去重平行边
if(hasEdge(e.v(), e.w())){
return;
}
g[e.v()][e.w()] = e;
if(e.v()!=e.w() && !isDirected){
g[e.w()][e.v()] = e;
}
m++;
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
@Override
public Iterable<Edge<Weight>> adjIterator(int v){
assert v >= 0 && v < n;
Vector<Edge<Weight>> vector = new Vector<>();
for(int i=0; i<n; i++){
if(g[v][i]!=null){
vector.add(g[v][i]);
}
}
return vector;
}
@Override
public void show(){
for(int i=0; i<n; i++){
for(int j=0; j<n; j++){
if(g[i][j]!=null){
System.out.printf("%.2f\t",g[i][j].weight());
}else{
System.out.print("null\t");
}
}
System.out.println();
}
}
}
//有权稀疏图 邻接表的表示
public class SpraseGraph<Weight extends Number & Comparable> implements WeightGraph<Weight> {
private int n; //节点数
private int m; //边数
private boolean isDirected; //是否为有向图
private Vector<Edge<Weight>>[] g; //图的具体数据
public SpraseGraph(int n, boolean isDirected){
assert n>=0;
this.n = n;
this.m = 0;
this.isDirected = isDirected;
this.g = (Vector<Edge<Weight>>[]) new Vector[n];
//初始化数组中的每个Vetor
for(int i=0; i<n; i++){
g[i] = new Vector<Edge<Weight>>();
}
}
@Override
public int V(){
return this.n;
}
@Override
public int E(){
return this.m;
}
//检查边 v——>w 是否存在
@Override
public boolean hasEdge(int v, int w){
assert v>=0 && v<n;
assert w>=0 && w<n;
for(int i=0; i<g[v].size(); i++){
if(g[v].elementAt(i).w() == w){
return true;
}
}
return false;
}
@Override
public void addEdge(Edge<Weight> e){
assert e.v()>=0 && e.w()<n;
assert e.w()>=0 && e.w()<n;
//去除平行边
// 注意, 由于在邻接表的情况, 查找是否有重边需要遍历整个链表
// 我们的程序允许重边的出现
//if(hasEdge(e.v(), e.w())){
// return ;
//}
g[e.v()].add(e);
if(e.v() != e.w() && !isDirected){
g[e.w()].add(new Edge(e.w(), e.v(), e.weight()));
}
m++;
}
@Override
public Iterable<Edge<Weight>> adjIterator(int v){
assert v >= 0 && v < n;
return g[v];
}
@Override
public void show(){
for(int i = 0; i<n; i++){
System.out.print("Vertex "+i+" : ");
for(int j=0; j<g[i].size(); j++){
System.out.print("(to: "+g[i].elementAt(j).w()+", weight: "+g[i].elementAt(j).weight()+")\t");
}
System.out.println();
}
}
}
二. 有权图的最小生成树问题
1.最小生成树
- 生成树:一个连通图中,能连通所有顶点而又不产生回路的任何子图都是它的生成树(n个顶点+n-1条边)
- 最小生成树:所有生成树中,各边的权重和最小的生成树
- 最小生成树针对的是:带权无向图,连通图
- 求最小生成树就是找v-1条边连接v个顶点,使得总权值最小
2.切分定理(Cut Property)
- 把图中的节点分为两个部分,成为一个切分(Cut)
- 如果一个边的两个端点,属于切分(Cut)不同的两边,这个边称为横切边(Crossing Edge)
- 切分定理:给定任意切分,横切边中权值最小的边必然属于最小生成树
- 若图中有多个相等的"横切边",则该图的最小生成树不唯一
3. lazy-prim算法求最小生成树
- 缺点:所有的边都要进入最小堆
- 时间复杂度为:O(logE)
//lazy-prim算法带权无向图最小生成树
public class LazyPrimMST<Weight extends Number & Comparable> {
private WeightGraph<Weight> graph; //用来生成最小生成树的图
private boolean[] isVisited; //标记节点是否被访问
private Vector<Edge<Weight>> mst; //最小生成树中的边
private Number mstWeight; //最小生成树的权重和
private PriorityQueue<Edge<Weight>> pq; //算法的辅助数据结构,用来选出权重最小的边
public LazyPrimMST(WeightGraph<Weight> graph){
this.graph = graph;
this.isVisited = new boolean[graph.V()];
this.mst = new Vector<>();
this.mstWeight = 0;
this.pq = new PriorityQueue<>(graph.E(), new Comparator<Edge<Weight>>() {
@Override
public int compare(Edge<Weight> o1, Edge<Weight> o2) {
return o1.compareTo(o2);
}
});
}
//辅助方法,访问节点,并挑选该节点未被访问过的邻边加入优先队列
private void visit(int v){
//判断传入的节点v是否被访问过
assert !isVisited[v];
//访问节点v
isVisited[v] = true;
for(Edge<Weight> e: graph.adjIterator(v)){
if(!isVisited[e.otherV(v)]){
pq.add(e);
}
}
}
//最小生成树实现 lazy-prim算法
public void mst(){
//初始化,先访问节点0
visit(0);
//lazy-prim
while(!pq.isEmpty()){
//从优先队列中取得当前队列中权重最小的边
Edge<Weight> e = pq.remove();
//若边的两个端点均被访问过,则抛弃这条边,在从优先队列中取得下一个权重最小的边
if(isVisited[e.v()] == isVisited[e.w()]){
continue;
}
//若边的另一个端点没有被访问过,则该边为最小生成树中的一条边
mst.add(e);
//顺着这条边接着访问边中那个为被访问过的节点
if(!isVisited[e.v()]){
visit(e.v());
}else{
visit(e.w());
}
}
//计算最小生成树中的权重和
for(int i=0; i<mst.size(); i++){
mstWeight = mstWeight.doubleValue() + mst.elementAt(i).weight().doubleValue();
}
}
// 返回最小生成树的所有边
public Vector<Edge<Weight>> mstEdges(){
return this.mst;
}
// 返回最小生成树的权值
public Number minWeight(){
return this.mstWeight;
}
}
4. prim算法
- prim算法是贪婪算法的一个典型例子,有点类似于dijkstra算法。
- 算法思想:
- 横切边:若一条边中有且只有一个节点被访问过(被标记过),则该边为一条横切边
- 根据切分定理,将节点切分已加入最小生成树的部分和未加入最小生成树的部分,从树节点(已标记的节点)出发寻找最短横切边,添加该最短横切边直到所有结点都加入到最小生成树。
- 从任意一个点开始选择,找出这个点连接的所有的边,然后找出最短的,选中这条边加入到生成树中,枚举每一个树顶点到每一个非树顶点的所有的边,然后找最短的边加入到生成树,一直加边n-1次, 直到所有的顶点都被加入到生成树中。
- 辅助数据结构:利用一个最小索引堆,开辟V个空间,索引为节点,索引下存储的值是当前节点到树区的权重(距离)
- 时间复杂度:O(logV)
//索引堆(构造时,可根据参数选择最大或者最小), 底层基于数组实现
public class IndexMinHeap<E extends Comparable> {
private boolean isMin; //是否为最小索引堆
private E[] items; //存放具体数据
private int[] indexes; //索引堆的底层数组,数组内存储的是具体数据的索引,保持堆的排序
private int[] reverse; //索引堆中的反向索引 reverse[indexes[i]]=i;
private int size; //堆中数据的个数
private int capacity; //堆的容量
public IndexMinHeap(int capacity, boolean isMin){
this.isMin = isMin;
this.capacity = capacity;
this.size = 0;
this.indexes = new int[capacity];
this.items = (E[])new Comparable[capacity];
this.reverse = new int[capacity];
for(int i=0; i<capacity; i++){
reverse[i] = -1;
}
}
// 返回索引堆中的元素个数
public int size(){
return this.size;
}
// 返回一个布尔值, 表示索引堆中是否为空
public boolean isEmpty(){
return this.size == 0;
}
// 向索引堆中插入一个新的元素, 新元素的索引为i, 元素为item
public void add(int i, E item){
assert i>=0 && i<capacity;
assert size<capacity;
assert !contain(i);
items[i] = item;
indexes[size] = i;
reverse[indexes[size]] = size;
shiftUp(size);
size++;
}
// 从索引堆中取出堆顶元素, 同时维护索引堆
public E remove(){
assert size>0;
E ret = items[indexes[0]];
indexes[0] = indexes[size-1];
reverse[indexes[0]] = 0;
size--;
reverse[indexes[size]] = -1;
shiftDown(0);
return ret;
}
// 从索引堆中取出堆顶元素的索引
public int removeIndex(){
assert size>0;
int ret = indexes[0];
indexes[0] = indexes[size-1];
reverse[indexes[0]] = 0;
size--;
reverse[indexes[size]] = -1;
shiftDown(0);
return ret;
}
// 获取索引堆中的堆顶元素
public E get(){
assert size>0;
return items[indexes[0]];
}
// 获取索引堆中的堆顶元素的索引
public int getIndex(){
assert size>0;
return indexes[0];
}
// 看索引i所在的位置是否存在元素
public boolean contain(int i){
assert i>=0 && i<capacity;
return reverse[i]!=-1;
//return items[i]!=null;
}
// 获取索引堆中索引为i的元素
public E get(int i){
assert reverse[i] != -1;
return items[i];
}
// 将索引堆中索引为i的元素修改为newItem
public void set(int i, E newItem){
assert contain(i);
items[i] = newItem;
shiftUp(reverse[i]);
shiftDown(reverse[i]);
}
//********************
//* 最小索引堆核心辅助函数
//********************
// 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
private void shiftUp(int i){
E e = items[indexes[i]];
int ret = indexes[i];
int j;
//选择最小索引堆或者是最大索引堆
if(isMin){
for(j=i; e.compareTo(items[indexes[(j-1)/2]])<0 && j>0; j=(j-1)/2){
indexes[j] = indexes[(j-1)/2];
reverse[indexes[j]] = j;
}
}else{
for(j=i; e.compareTo(items[indexes[(j-1)/2]])>0 && j>0; j=(j-1)/2){
indexes[j] = indexes[(j-1)/2];
reverse[indexes[j]] = j;
}
}
indexes[j] = ret;
reverse[indexes[j]] = j;
}
// 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
private void shiftDown(int i){
while (2*i+1<size){
int j = 2*i+1;
if(isMin){
if(j+1<size && items[indexes[j+1]].compareTo(items[indexes[j]])<0){
j = j+1;
}
if(items[indexes[i]].compareTo(items[indexes[j]])<0){
break;
}
}else{
if(j+1<size && items[indexes[j+1]].compareTo(items[indexes[j]])>0){
j = j+1;
}
if(items[indexes[i]].compareTo(items[indexes[j]])>0){
break;
}
}
int temp = indexes[i];
indexes[i] = indexes[j];
reverse[indexes[i]] = i;
indexes[j] = temp;
reverse[indexes[j]] = j;
i = j;
}
}
}
//prim算法 求带权无向图的最小生成树
public class PrimMST<Weight extends Number & Comparable> {
private WeightGraph<Weight> graph; //待求最小生成树的图
private Vector<Edge<Weight>> mst; //最小生成树的所有边
private Number mstWeight; //最小生成树的权重和
private boolean[] isVisited; //辅助数据结构,记录节点是否被访问过
private Edge<Weight>[] edgeTo; //辅助数据结构,记录“横切边”, edgeTo[w] 表示已标记区域中某个节点到未标记节点w的边
private IndexMinHeap<Weight> imheap; //辅助数据结构,最小索引堆,索引表示节点,对应的数据表示该节点到已标记区域的权重
public PrimMST(WeightGraph graph){
this.graph = graph;
assert( graph.E() >= 1 );
this.imheap = new IndexMinHeap<>(graph.V(), true);
this.isVisited = new boolean[graph.V()];
this.mst = new Vector<Edge<Weight>>();
this.mstWeight = 0;
this.edgeTo = (Edge<Weight>[]) new Edge[graph.V()];
for(int i=0; i<graph.V(); i++){
edgeTo[i] = null;
isVisited[i] = false;
}
}
//访问节点v
private void visit(int v){
assert !isVisited[v];
isVisited[v] = true;
for(Edge<Weight> e: graph.adjIterator(v)){
if(!isVisited[e.otherV(v)]){
//判断边 v——otherV(v) 是否被标记为"横切边"
//若未标记过,则进行标记,并加入最小堆
//若标记过,则判断之前标记的“横切边”与当前由节点v发出的“横切边”v——otherV(v)那个更短,更新最小堆
if(edgeTo[e.otherV(v)] == null){
edgeTo[e.otherV(v)] = e;
imheap.add(e.otherV(v), e.weight());
}else if(edgeTo[e.otherV(v)].weight().compareTo(e.weight())>0){
//更新到节点e.otherV(v)的“横切边”
edgeTo[e.otherV(v)] = e;
imheap.set(e.otherV(v), e.weight());
}
}
}
}
//prim算法, 生成最小生成树
public void prim(){
//prim
visit(0);
while (!imheap.isEmpty()){
// 使用最小索引堆找出已经访问的边中权值最小的边
// 最小索引堆中,索引表示节点,索引对应的值表示该节点到已标记区域的权重(距离)
int v = imheap.removeIndex();
//assert( edgeTo[v] != null );
mst.add(edgeTo[v]);
visit(v);
}
// 计算最小生成树的权值
for (int i=0; i<mst.size(); i++){
mstWeight = mstWeight.doubleValue()+mst.elementAt(i).weight().doubleValue();
}
}
//返回最小生成树的所有边
public Vector<Edge<Weight>> mstEdges(){
return this.mst;
}
//返回最小生成树的权重和
public Number mstWeight(){
return this.mstWeight;
}
}
5. Kruslal算法
- 算法思想:将图中所右边按照权重排序,从小到大依次试着将边加入到节点之间,若加入后不构成环,则该条边为最小生成树中的一条边,直至加构V-1条边;对于环的判断可以使用并查集实现
//并查集
public class UnionFind {
private int[] parent;
private int capacity;
private int[] rank;
public UnionFind(int capacity){
this.capacity = capacity;
this.parent = new int[capacity];
this.rank = new int[capacity];
for(int i=0; i<capacity; i++){
parent[i] = i;
rank[i] = 1;
}
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p){
while (p!=parent[p]){
parent[p]=parent[parent[p]];
p = parent[p];
}
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected(int p, int q){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot){
return;
}
// 根据两个元素所在树的元素个数不同判断合并方向
// 将元素个数少的集合合并到元素个数多的集合上
if (rank[pRoot]<rank[qRoot]){
parent[pRoot] = qRoot;
} else if (rank[pRoot] > rank[qRoot]){
parent[qRoot] = pRoot;
}else{
parent[pRoot] = qRoot;
rank[qRoot]++;
}
}
}
//kruskal算法
public class KruskalMST<Weight extends Number & Comparable> {
private WeightGraph<Weight> graph;
private Vector<Edge<Weight>> mst;
private Number mstWeight;
public KruskalMST(WeightGraph graph){
this.graph = graph;
this.mst = new Vector<>();
this.mstWeight = 0;
}
public void kruskal(){
//利用优先队列对图中各条边按照权重从小到大排序
PriorityQueue<Edge<Weight>> pq = new PriorityQueue<>(graph.E());
for(int i=0; i<graph.V(); i++){
for(Edge<Weight> e: graph.adjIterator(i)){
//防止无向图中,同一条边两次入队
if(e.v()<=e.w()){
pq.add(e);
}
}
}
//利用并查集检查加入一条边之前,该边的两个节点是否已经连接
UnionFind uf = new UnionFind(graph.V());
while(!pq.isEmpty() && mst.size()<graph.V()-1){
Edge<Weight> e = pq.remove();
if(uf.isConnected(e.v(), e.w())){
continue;
}
mst.add(e);
uf.unionElements(e.v(), e.w());
}
//计算最小生成树权重
for(int i=0; i<mst.size(); i++){
mstWeight = mstWeight.doubleValue() + mst.elementAt(i).weight().doubleValue();
}
}
// 返回最小生成树的所有边
public Vector<Edge<Weight>> mstEdges(){
return mst;
}
// 返回最小生成树的权值
public Number mstWeight(){
return mstWeight;
}
}
6. 三种算法的时间复杂度
7. Vyssotsky算法
- 将边逐渐添加到生成树中,一旦形成环,删除环中权值最大的边