【人工智能】Java实现宽度优先搜索(Breadth-First Search,BFS)任意两点之间的所有路径

1.原理

宽度优先搜索(Breadth-First Search)类似于层次搜索,搜索深度逐渐加深。

如上图所示,如果要搜索1=>6的路径,会经过下面的顺序

1

12,18

123,126(找到),186(找到),187

1234,1235,1876(找到)

12345,12356(找到)

123456(找到)

2.Java编码

1)数据结构

A 一个顶点的邻接点的表示

如上图中的顶点6可以表示为:6,{2,5,7,8}

如果加上边的权重可以表示为:6,{2,5,7,8},{3,3,1,2}

import java.util.Arrays;
import java.util.List;

public class Adjacent {
    int vertex; //顶点
    List<Integer> child; //顶点的所有邻接顶点
    List<Integer> weight; //上面边对应的权重
    public  Adjacent(int v,Integer[] c,Integer[] w)
    {
        vertex=v;
        child= Arrays.asList(c);
        weight=Arrays.asList(w);
    }
}

B 图的表示

顶点集合,如上图{1,2,3,4,5,6,7,8}

所有边集合,如上图可表示为:

1,{2,8}

2,{1,3,6}

3,{2,4,5}

......

8,{1,6,7}

当然,这样表示对于无向图有冗余的,但对有向图的表示还是可以。

public class Graph {
    List<String> vertexes=new ArrayList<>(); //顶点集合
    List<Adjacent>  edge=new ArrayList<>(); //所有邻接边的集合
    public  Graph(){}
    public  Graph(String[] v,Adjacent[] adj)
    {
        vertexes= Arrays.asList(v);
        edge=Arrays.asList(adj);
    }
}

2).简单输出图

  public void print()
    {
        for(int i=0;i<vertexes.size();i++)
            System.out.print(vertexes.get(i)+" "); //输出顶点
        System.out.println();
        for(int i=0;i<edge.size();i++)
        {
            Adjacent  adj=edge.get(i);
            for(int j=0;j<adj.child.size();j++)
                //输出边,输出边权值
                System.out.println(vertexes.get(adj.vertex)+"-"+vertexes.get(adj.child.get(j))+":"+ adj.weight.get(j)); 
        }
    }

3)查找顶点的编号

   public int searchVertex(String v)
    {
        for(int i=0;i<vertexes.size();i++)
            if(vertexes.get(i)==v) return i;
         return -1;
    }

4)判断路径字符串数组是否存在某个结点

    public  boolean  isInStringArray(String[] s,int v)
    {
        for(int i=0;i<s.length;i++)
            if(Integer.parseInt(s[i])==v) return  true;
        return  false;
    }

5)宽度优先搜索代码

思路:frontier表示路径队列,采用字符串表示路径,如路径012,表示到达顶点2需经过编号为0,1的顶点。explored表示已探索的顶点集合,在该集合中的顶点不再扩展。而allPath表示找到的所有路径的集合。代码中的while循环负责遍历所有顶点,而其中的for循环找到当前遍历的顶点的所有邻接顶点,如果找到的顶点不在explored集合中或者不在当前路径中,分两种情况讨论:一是找到的顶点正好是目标顶点,则将当前路径与该顶点组合成新路径并添加到allPath(表示找到的路径)集合中;二是如果找到的顶点不在当前路径上,则将当前路径连同找到的这个结点组合成新路径将其加入到frontier 队列中。

public ArrayList<String>  bfs(int src,int dst)
    {
        if(src<0 || dst<0 || src>=vertexes.size()
             || dst>=vertexes.size()) return null;
        //路径队列
        Queue<String> frontier=new LinkedList<>();
        //已探索顶点集合
        ArrayList<Integer>  explored=new ArrayList<>();
        //所有路径集合
        ArrayList<String> allPath=new ArrayList<>();
        frontier.offer(String.valueOf(src));
        while(!frontier.isEmpty())
        {   //将队首路径出队
            String  p=frontier.remove();
            String[] pathSplit=p.split("-");
            //获取路径的最后一个结点
            int ver=Integer.parseInt(pathSplit[pathSplit.length-1]);
            //表示已探索过该结点
            explored.add(ver);
            for(int i=0;i<edge.size();i++)
            {
                Adjacent  adj=edge.get(i);
                if(adj.vertex==ver)
                {
                    //获取顶点ver的所有邻接结点
                    for(int j=0;j<adj.child.size();j++)
                    {
                        int  otherVer=adj.child.get(j);
                        //组合成新路径
                        String p1 = p+"-"+otherVer;
                        //判断是否在当前路径上(否则会循环不止)
                        boolean isInCurrentPath=!isInStringArray(pathSplit,i);
                        //不在已探索集合,或不在当前路径上
                        if (explored.indexOf(otherVer) == -1 || !isInCurrentPath)
                        {
                            //找到路径
                            if(otherVer==dst)  allPath.add(p1);
                            //不在当前路径上才能入队
                            else if(!isInCurrentPath) frontier.offer(p1);
                        }
                    }
                }
            }
        }
        return allPath;
    }

6)按顶点解析输出路径

   public  void  parsePath(ArrayList<String> path)
    {
        for(int i=0;i<path.size();i++)
        {
            String[] realPath=path.get(i).split("-");
            for(int j=0;j<realPath.length;j++)
            {
                if(j==0) System.out.print(vertexes[Integer.parseInt(realPath[j])]);
                else System.out.print("=>"+vertexes[Integer.parseInt(realPath[j])]);
            }
            System.out.println();
        }
    }

3.测试编码

import java.util.*;

public class Main {

    public static void main(String[] args) {
        //顶点集合
        String[] city={"c1","c2","c3","c4","c5","c6","c7","c8"};
        //每个顶点的邻接顶点集合
        Adjacent[] adj=new Adjacent[8];
        Integer[][] a={{1,7},{0,2,5},{1,3,4},{2,4},{2,3,5},{1,4,6,7},{5,7},{0,5,6}};
        Integer[][] b={{5,2},{5,2,3},{2,4,5},{4,2},{5,2,3},{3,3,1,2},{1,3},{2,2,3}};
        for(int i=0;i<a.length;i++)
            adj[i]=new Adjacent(i,a[i],b[i]);
        //创建图
        Graph g=new Graph(city,adj);
        //搜索c1到c6的所有路径,并输出
        g.parsePath(g.bfs(g.searchVertex("c4"),g.searchVertex("c6")));

    }

输出结果为:

c4=>c5=>c6
c4=>c3=>c2=>c6
c4=>c3=>c5=>c6
c4=>c5=>c3=>c2=>c6
c4=>c3=>c2=>c1=>c8=>c6
c4=>c3=>c2=>c1=>c8=>c7=>c6
c4=>c5=>c3=>c2=>c1=>c8=>c6
c4=>c5=>c3=>c2=>c1=>c8=>c7=>c6

可以看到,找到的路径按路径顶点个数排序,这因为是宽度优先搜索,从源结点一层一层去搜索目标结点。

4.分析

BFS时间复杂度和空间复杂度皆为:O(b^d), b表示每一层的结点为b个,d表示总的搜索深度。当b=10,d=16时,这个数据将非常大,空间复杂度将达到10EB级别。BFS算法在每一步扩展时,若行动代价是一样的,则是最优的,如本例中并没有考虑权值,而只是简单考虑其深度;否则就不是最优的,这可以使用一致代价搜索算法(Uniform-Cost Search,UCS)使得每步都是最优的。BFS使用一般队列即可,而UCS使用优先级队列来处理后续结点。

5.计算路径权重

路径权重的计算比较简单,在此略过

6.DFS及其他

DFS(Deep-First Search)总是沿着当前结点往最深处探索,很快会推进到其最深层。编程时,一般使用栈(Stack,先进后出,LIFO)来存储后继邻接顶点。显然DFS不是最优的。请看DFS的输出结果:

c4=>c5=>c6

c4=>c5=>c3=>c2=>c6

c4=>c5=>c3=>c2=>c1=>c8=>c6

c4=>c5=>c3=>c2=>c1=>c8=>c7=>c6

c4=>c3=>c5=>c6

c4=>c3=>c2=>c6

c4=>c3=>c2=>c1=>c8=>c6

c4=>c3=>c2=>c1=>c8=>c7=>c6

显然DFS的输出有些乱,但也是有规律的,就是一个方向的路径输出完后,再输出其他路径,这是因为DFS使用栈的原因,DFS总是先朝一个方向不断前进、搜索。

DFS的时间复杂度为:O(b^d),这与BFS差不多,但其空间复杂度降低到O(bd),这在空间复杂度上是非常有优势的。DFS的改进算法:深度受限搜索DLS——根据情况限制搜索深度,迭代加深的深度优先搜索IDS——不断增加深度限制,直到找到目标。还有一种称为叫迭代加长搜索ILS,按路径代价不断叠加受限。双向搜索是从两个方向进行搜索,一个从开始点搜索,另一个从目标点搜索,若两者相遇,则找到解,显然双向搜索不是最优解,但其时间、空间复杂度大大降低:O(b^(d/2))。但存在结点的Predecessors(一个结点的双亲集)定义问题,这个定义其实也不难。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值