有权图的设计
增加一个边的类,邻接表和邻接矩阵中保存的都是类的对象
public class Edge<Weight extends Number & Comparable> implements Comparable<Edge>{
private int 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.a = e.a;
this.b = e.b;
this.weight = e.weight;
}
public int v() {
return a;
}
public int w() {
return b;
}
public Weight wt() {
return weight;
}
public int other(int x) {
if (x != a && x != b) {
return -1;
}
return x == a ? b : a;
}
@Override
public int compareTo(Edge that) {
return 0;
}
@Override
public String toString() {
return "" + a + "-" + ": " + weight;
}
}
加权图的接口
public interface WeightedGraph<Weight extends Number & Comparable> {
public int getNodesCount();
public int getEdgesCount();
public void addEdge(Edge<Weight> e);
public boolean hasEdge(int v, int w);
public void show();
public Iterable<Edge<Weight>> adj(int v);
}
邻接表
import java.util.List;
import java.util.ArrayList;
public class SparseWeightedGraph<Weight extends Number & Comparable> implements WeightedGraph {
private int n;
private int m;
private boolean directed;
private List<Edge<Weight>>[] g;
public SparseWeightedGraph(int n, boolean directed) {
if (n < 0) {
return;
}
this.n = n;
this.directed = directed;
this.g = (ArrayList<Edge<Weight>>)new ArrayList[n];
for (int i = 0; i < n; i++) {
g[i] = new ArrayList<Edge<Weight>>();
}
}
@Override
public int getNodesCount() {
return n;
}
@Override
public int getEdgesCount() {
return m;
}
@Override
public boolean hasEdge(int v, int w) {
if (!isLegal(v) || !isLegal(w)) {
return false;
}
for (Edge e : g.adj(v)) {
if (e.other(v) == w) {
return true;
}
}
return false;
}
@Override
public void addEdge(Edge e) {
if (e == null) {
return;
}
int v = e.v();
int w = e.w();
if (!isLegal(v) || !isLegal(w)) {
return;
}
g[v].add(new Edge(e));
if (v != w && !directed) {
g[w].add(new Edge(w, v, e.wt()));
}
m++;
}
@Override
public Iterable<Edge<Weight>> adj(int v) {
List<Edge<Weight>> res = new ArrayList<>();
if (!isLegal(v)) {
return res;
}
return g[v];
}
@Override
public void show() {
for( int i = 0 ; i < n ; i ++ ){
System.out.print("vertex " + i + ":\t");
for( int j = 0 ; j < g[i].size() ; j ++ ){
Edge e = g[i].get(j);
System.out.print( "( to:" + e.other(i) + ",wt:" + e.wt() + ")\t");
}
System.out.println();
}
}
private boolean isLegal(int i) {
return i >= 0 && i < n;
}
}
在此处任性地增加一段知识点
| Summary of Queue methods |
Method | Throws exception | Returns special value |
---|---|---|
Insert | add(e) | offer(e) |
Remove | remove() | poll() |
Examine | element() | peek() |
最小生成树
针对带权无向图 针对连通图
找v-1条边,连接v个节点,使总权值最小
切分定理
把图中的节点分为两部分,成为一个切分(Cut).
如果一个边的两个端点,属于切分(Cut)不同的两边,这个边称为横切边。
切分定理:
给定任意切分,横切边中权值最小的边必然属于最小生成树。
Lazy Prim算法
Lazy prim的时间复杂度O(ElogE),E为边数
需要维护一个最小堆
import java.lang.reflect.Array;
public class MinHeap<T extends Comparable> {
private T[] data;
private int count;
private int capacity;
private Class<T> type;
public MinHeap(int capacity, Class<T> type) {
this.capacity = capacity;
this.count = 0;
this.type = type;
data = (T[]) Array.newInstance(type, capacity + 1);
}
public int size() {
return count;
}
public boolean isEmpty() {
return count == 0;
}
public void insert(T item) {
if (count + 1 >= capacity && capacity * 2 + 1 < Integer.MAX_VALUE) {
this.capapcity *= 2;
T[] newData = (T[]) Array.newInstance(this.type, this.capacity + 1);
System.arraycopy(data, 0, newData, 0, count + 1);
data = newData;
}
data[++count] = item;
shiftUp(count);
}
private void shiftUp(int v) {
while (v > 1 && data[v].compareTo(data[v / 2]) < 0) {
swap(data, v, v / 2);
v /= 2;
}
}
public T extractMin() {
if (count <= 0) {
return null;
}
T res = data[1];
swap(data, 1, count--);
shiftDown(1);
return res;
}
private void shiftDown(int k) {
while (k * 2 <= count) {
int j = k * 2;
if (j + 1 < count && data[j + 1].compareTo(data[j]) < 0) {
j++;
}
if (data[k].compareTo(data[j]) <= 0) {
break;
}
swap(data, k, j);
k = j;
}
}
private void swap(T[] data, int i, int j) {
T t = data[i];
data[i] = data[j];
data[j] = t;
}
}
lazy prim实现
public class LazyPrimMST<Weight extends Number & Comparable> {
private MinHeap<Edge<Weight>> minHeap;
private List<Edge<Weight>> mst;
private Number mstWeight;
private Graph g;
private boolean[] marked;
public LazyPrimMST(Graph g) {
this.g = g;
int n = g.getNodesCount();
int m = g.getEdgesCount();
minHeap = new MinHeap<>(Edge.class, m);
marked = new boolean[n];
visit(0);
while (!minHeap.isEmpty()) {
Edge<Weight> e = minHeap.extractMin();
//这条边已经不是横切边了
if (marked[e.v()] == markded[e.w()]) {
continue;
}
//不然的话,这条横切边应该在最小生成树中,将其加入list
mst.add(e);
if (marked[e.v()]) {
visit(e.w());
} else {
visit(e.v());
}
}
mstWeight = mst.get(0).wt();
for (int i = 1; i < mst.size(); i++) {
mstWeight += mst.get(i).wt();
}
}
//处于还未处于的结点,发现横切边,将横切边存入最小堆中
private void visit(int k) {
if (marked[k]) {
return;
}
marked[k] = true;
for (Edge<Weight> e : g.adj(v)) {
//如果这是一条横切边
if (!marked[e.other(v)]) {
minHeap.insert(e);
}
}
}
}
Kruskal算法
需要采用并查集的算法,用来判断是否存在环。
回顾并查集
并查集的归并过程,是将元素放入一个集合中,集合用一个数组id表示
先回顾quickUnion版本
public class QuickUnion {
private int[] id;//元素分组
private int count;//有count个元素
public QuickUnion(int count) {
this.count = count;
this.id = new int[count];
//初始化时,每个元素一个分组
for (int i = 0; i < count; i++) {
id[i] = i;
}
}
public void union(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (qRoot == pRoot) {
return;
}
//任意使一个结点指向另一个的父结点即可
id[p] = qRoot;
}
//每个元素的分组是按根节点分组
public int find(int p) {
if (p < 0 || p >= count) {
return -1;
}
//根结点自己指向自己
while (p != id[p]) {
p = id[p];
}
return p;
}
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
}
优化的方向是使树的高度尽可能合理地低,每一步可以使结点数量少的移到数量多的根上,降低树的高度。但是可能出现的问题是,结点数多不见得树就高。因此,有基于rank的优化,其中rank[i]表示以i为根的集合所表示的树的层数
public class RankUF {
private int[] parent;//用这样一个数组来表示元素所属的集合,表示元素指向的根结点
private int count;
private int[] rank;//rank[i]表示以i为根的集合所表示的树的层数
public RankUF(int count) {
this.count = count;
this.parent = new int[count];
this.rank = new int[count];
for (int i = 0; i< count; i++) {
parent[i] = i;
rank[i] = i;
}
}
public int find(int p) {
if (p < 0 || p >= count) {
return -1;
}
// 不断去查询自己的父亲节点, 直到到达根节点
// 根节点的特点: parent[p] == p
while (p != parent[p]) {
p = parent[p];
}
return p;
}
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
/**
* 关键在union的过程
* 根据两个元素所在树的元素个数不同判断合并方向
* 将元素个数少的集合合并到元素个数多的集合上
*/
public void union(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的值
rank[qRoot] += 1;
}
}
}
最后的优化为路径压缩
此时有两种方法,第一种是每隔一层,压缩一层,修改find方法即可,如下
public int find(int p) {
if (p < 0 || p >= count) {
return -1;
}
while (p != parent[p]) {
parent[p] = parent[parent[p]];//增加这一行即可
p = parent[p];
}
return p;
}
第二种方法是压缩到底,这里效率不见得最高,因为用到了递归,会有一定的损失,所以上述方法已经很好了。正则压缩到底的优化
public int find(int p) {
if (p < 0 || p >= count) {
return -1;
}
if (p != parent[p]) {
parent[p] = find(parent[p]);
}
return parent[p];
}
kruskal算法思想
每次找最短边,只要不构成环,那么这条边就是最小生成树中的边
import java.util.List;
import java.util.ArrayList;
public class KruskalMST<Weight extends Number & Comparable> {
private List<Edge<Weight>> mst;
private Number mstWeight;
public KruskalMST(WeightedGraph g) {
int n = g.getNodesCount();
int m = g.getEdgesCount();
MinHeap<Edge> minHeap = new MinHeap<>(m, Edge.class);
for (int i = 0; i < n; i++) {
for (Object o : g.adj(i)) {
Edge<Weight> e = (Edge<Weight>) o;
//防止重复放入边
if (e.v() <= e.w()) {
minHeap.insert(e);
}
}
}
RankUF uf = new RankUF(n);
while (!minHeap.isEmpty() && mst.size() < n - 1) {
Edge<Weight> e = minHeap.extractMin();
if (uf.isConnected(e.v(), e.w())) {
continue;
}
mst.add(e);
uf.union(e.v(), e.w());
}
mstWeight = mst.get(0).wt();
for (int i = 1; i < mst.size(); i++) {
mstWeight = mstWeight.doubleValue() + mst.get(i).wt().doubleValue();
}
}
public List<Edge<Weight>> getMSTEdges() {
return mst;
}
public Number getWeight() {
return mstWeight;
}
}
总结:此时有泛型擦除问题
public interface Interface1<Weight extends Number & Comparable> {
public Iterable<Edge<Weight>> adj();
}
public class Test {
public Test(Interface1 t) {
for (Edge<Weight> e : t.adj()) {
//此时编译会报错,因为Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。
//此时在Test眼中,是看不到Interface1的返回值中的泛型的,只能看到Iterable,在编写java代码时,要时刻提醒自己,“这不是个泛型,这只是个Object”
}
}
}
有两种解决方法
第一种,即代码中,用Object接收,然后强制类型转换
第二种,泛型在当前类中,因此编译时可以识别到
public class Test {
public Test(Interface2 t) {
for (Edge<Weight> e : t.adj()) {
//此时不会报错
}
}
}
interface Interface2 {
public Iterable<Edge<Weight>> adj();
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。