无向图:即图的边是没有方向属性的。
单点路径
最短单点路径
连通分量
环检测
图的二分性
DFS可以用来解决单点路径,是否有环,连通分量,图的二分性等问题;BFS可以用来解决最短单点路径问题。
单点路径
给定无向图,判断图中某一个顶点到其他顶点的连通路径问题,如果存在单点路径,则输出该路径。
给定以下无向图以及起始点0,求出0到其余各顶点的单点路径
对于给定图进行DFS深度优先搜索,利用marked数组来标识某个顶点是否访问过,利用edgeTo数组来保存最后一次到达该顶点的顶点,即edgeTo[w]=v表示由顶点v可以直接到达顶点w。要求起始点s到顶点x的单点路径,则从edgeTo[x]开始,依次往回寻找x=edgeTo[x]直到x==s,将该路径上的顶点值利用栈来保存,最后依次弹出栈中元素便可得到由s到x的单点路径。
代码如下:
//无向图 深度优先搜索 单点路径
public class DepthFirstPaths {
private boolean[] marked;
//edgeTo[w]=v的含义是由v可以直接到达w
private int[] edgeTo;
private final int s;
public DepthFirstPaths(Graph g, int s){
this.marked = new boolean[g.V()];
this.edgeTo = new int[g.V()];
this.s = s;
dfs(g, s);
}
private void dfs(Graph g, int v) {
marked[v] = true;
for(int w : g.adj(v)){
if(!marked[w]){
//向v下一个指向w
//edgeTo[x]得到谁指向x
edgeTo[w] = v;
dfs(g, w);
}
}
}
//起点s到节点v是否存在单点路径
public boolean hasPathTo(int v){
return marked[v];
}
//返回s-->v路径
public Iterable<Integer> pathTo(int v){
if(!hasPathTo(v)) return null;
//利用栈倒序存储
Stack<Integer> s = new Stack<>();
//由最终节点向起始节点寻找 edgeTo[x]表示谁指向x
for(int x=v; x!=this.s; x = edgeTo[x]){
s.push(x);
}
s.push(this.s);
return s;
}
}
上图邻接表数组表示如下:
0: 2--->1--->5
1: 0--->2
2: 0--->1--->3--->4
3: 5--->4--->2
4: 3--->2
5: 3--->0
进行一次DFS搜索得到marked,edgeTo数组
x edgeTo[x]
0 0
1 2
2 0
3 2
4 3
5 3
[true, true, true, true, true, true]
edgeTo数组保存的信息是在无向图中,以起点s为根,到达各连通节点的一棵树。
最终起点0到其余顶点的单点路径如下图所示:
0:1 0-->2-->1
0:2 0-->2
0:3 0-->2-->3
0:4 0-->2-->3-->4
0:5 0-->2-->3-->5
DFS可以解决单点路径,判断两个点的连通性问题,但是得到的单点路径并不一定是最短的。
最短单点路径
给定无向图以及一个起始点s,求出起始点到其余各顶点的最短单点路径。
对于图进行BFS广度优先搜索,利用marked数组来记录访问过的顶点,利用edgeTo[v]数组来保存最后一次来访问v的顶点。
代码如下:
//广度优先搜索 无向图 单点最短路径
public class BreadthFirstPaths {
private boolean[] marked;
private int[] edgeTo;
private final int s;
public BreadthFirstPaths(Graph g, int s){
marked = new boolean[g.V()];
edgeTo = new int[g.V()];
this.s = s;
bfs(g, s);
}
private void bfs(Graph g, int s) {
Queue<Integer> q = new LinkedList<>();
marked[s] = true;
q.add(s);
while(!q.isEmpty()){
int v = q.poll();
for(int w : g.adj(v)){
if(!marked[w]){
edgeTo[w] = v;
marked[w] = true;
q.add(w);
}
}
}
}
//起点s到节点v是否存在单点路径
public boolean hasPathTo(int v){
return marked[v];
}
//返回s-->v路径
public Iterable<Integer> pathTo(int v){
if(!hasPathTo(v)) return null;
//利用栈倒序存储
Stack<Integer> s = new Stack<>();
//由最终节点向起始节点寻找 edgeTo[x]表示谁指向x
for(int x=v; x!=this.s; x = edgeTo[x]){
s.push(x);
}
s.push(this.s);
return s;
}
public List<Integer> pathTo2(int v){
if(!hasPathTo(v)) return null;
//利用栈倒序存储
Stack<Integer> s = new Stack<>();
//由最终节点向起始节点寻找 edgeTo[x]表示谁指向x
for(int x=v; x!=this.s; x = edgeTo[x]){
s.push(x);
}
s.push(this.s);
List<Integer> l = new ArrayList<>();
while(!s.isEmpty()){
l.add(s.pop());
}
return l;
}
public String pathTo3(int v){
if(!hasPathTo(v)) return null;
List<Integer> l = pathTo2(v);
StringBuilder sb = new StringBuilder();
sb.append(s + ":" + v + " ");
for(int i=0; i<l.size(); i++){
sb.append(l.get(i));
if(i!=l.size()-1){
sb.append("-->");
}
}
return sb.toString();
}
}
最终结果显示如下:
0:1 0-->1
0:2 0-->2
0:3 0-->2-->3
0:4 0-->2-->4
0:5 0-->5
由于BFS首先访问距离顶点最近的所有顶点,BFS中使用队列来保存访问过的节点,距离起始点越近的顶点越早进入队列,越早离开队列,一旦离开队列,说明该顶点已经访问,标记该节点已被访问,之后不会再次访问,因此edgeTo数组中此时保存的路径是最短路径。
连通分量
给定无向图,找出图中的所有连通分量。
利用DFS进行深度优先搜索,marked数组保存访问过的节点,count用来计数连通分量总数,id数组用来保存每个顶点的连通分量序号;
对于图的顶点数组进行遍历,对于每一个顶点,进行DFS搜索可以获取与该顶点相连的所有顶点,将这些顶点利用id数组来统一标识;然后寻找下一个没有被标记的顶点,说明该顶点时另外一个连通分量,count++,继续进行DFS。
//无向图 深度优先搜索 所有连通分量
public class CC {
private boolean[] marked;
//将每个节点所在连通分量的编号记录下来
private int[] id;
//连通分量总数
private int count;
public CC(Graph g){
marked = new boolean[g.V()];
id = new int[g.V()];
for(int s=0; s<g.V(); s++){
if(!marked[s]){
dfs(g, s);
count++;
}
}
}
private void dfs(Graph g, int s) {
marked[s] = true;
id[s] = count;
for(int v : g.adj(s)){
if(!marked[v]){
dfs(g, v);
}
}
}
public boolean connected(int v, int w){
return id[v] == id[w];
}
public int count(){
return count;
}
public int id(int v){
return id[v];
}
}
得到最终连通分量结果如下:
环检测
检测给定的图是否含有环。
对于每次dfs调用,从起始点s出发进行DFS深度优先搜索,对于即将访问某一个节点v时,记录v的上一个节点u,如果访问到v已经被标记过,并且v!=u,则说明有环;如果v==u,此时v与u直接相连,并不能构成环。
如图:
邻接表数组为:
0: 2--->1--->5
1: 0--->2
2: 0--->1--->3--->4
3: 5--->4--->2
4: 3--->2
5: 3--->0
对于每一个未被标记的点s来进行dfs深度优先搜索,如果s的邻接表第一个节点未被标记,则继续dfs,如果已被标记,则判断该第一个节点是否为s,如果为s说明属于直接回退,例如从0到1,在从1到0,此时不属于有环情况;如果该节点已被标记,并且不等于s,则说明有环。
//无向图 深度优先搜索 是否有环
public class Cycle {
private boolean[] marked;
private boolean hasCycle = false;
public Cycle(Graph g){
marked = new boolean[g.V()];
//遍历每一个节点
for(int s=0; s<g.V(); s++){
//如果该节点没有被标记过,则进行深度优先搜索
if(!marked[s]){
dfs(g, s, s);
}
}
}
public boolean hasCycle(){
return hasCycle;
}
private void dfs(Graph g, int s, int u) {
marked[s] = true;
for(int v : g.adj(s)){
System.out.println(s + " " + u + " " + v + " " + marked[v]);
//搜索节点s的所有临接节点v 如果v没有标记过,继续dfs
if(!marked[v]){
dfs(g, v, s);
}
//u保存访问v的上一个节点
//如果已经标记过,如果此时v==u 说明v直接相连于u,即u-->v 此时并没有环
else if(v != u){
hasCycle = true;
System.out.println("end");
return;
}
}
}
}
图的二分性
给定无向图,判断图是否具有二分性,即将图的每一个顶点置为红色或者黑色,要求同一条边的两个顶点颜色不一样。
进行DFS遍历搜索时,由某一个顶点进而访问下一个顶点,判断下一个待访问的顶点是否访问过,如果没有访问过,则将下一个顶点与本顶点颜色设为相反,如果访问过,并且颜色一致,则说明图不能二分。
//无向图 深度优先搜索 是否具有二分性
public class TwoColor {
private boolean[] marked;
private boolean[] color;
private boolean isTwoColorable = true;
public TwoColor(Graph g){
marked = new boolean[g.V()];
color = new boolean[g.V()];
//遍历每一个节点
for(int s=0; s<g.V(); s++){
//如果该节点没有被标记过,则进行深度优先搜索
if(!marked[s]){
dfs(g, s);
}
}
}
public boolean isBipartite(){
return isTwoColorable;
}
private void dfs(Graph g, int s) {
marked[s] = true;
for(int v : g.adj(s)){
//此时访问s的临接顶点v
if(!marked[v]){
//将v颜色设置为与s相反
color[v] = !color[s];
dfs(g, v);
}
//如果已经标记过,并且颜色相同,则说明图不具有二分性
else if(color[v]==color[s]){
isTwoColorable = false;
return;
}
}
}
}