【人工智能】Java实现一致代价搜索(Uniform-cost Search,UCS)

1.原理

话说BFS(Breadth-first Search,宽度优先搜索)只在每步代价一样时才是最优的,如按照结点深度来搜索,每一层其代价都是1.但是如果每步代价不一样,BFS就不是最优了,这时就要构造一个行动代价函数,这就是一致代价搜索(Uniform-cost Search,后称UCS)。UCS与BFS有两点不一样:1)目标结点检测应用于被选择扩展时,原因是第一个生成的目标结点也许是次优的。2)如果在扩展时发现有更好的路径到达目标,那将替换掉它。其实个人觉得还有第三个不同,那就是UCS使用优先级队列,而BFS使用一般队列即可。

2.示例图

 

如上图,要搜索4=>6的路径,很明显4=>3=>2=>6是最短路径。(牢记优先级队列,小的排在队列前面)

  • 45:2,49:3,43:2,因4=>5路径短,故先探索结点5,故45出队,扩展结点5
  • 49:3,43:4 ,453:7 ,456:8 ,虽然找到了456路径,但456并未排在队首,49出队,扩展结点9
  • 43:4 ,493:5 ,453:7 ,456:8 ,此时43最短,将出队,扩展结点3
  • 493:5 ,432:6 ,439:6 ,453:7,456:8 ,435:9 ,493最短,出队,扩展结点3(虽然3已探索,但3不在当前路径上)
  • 432::6 ,439:6,4932:7 ,453:7 ,456:8 ,435:9 ,4935:10 ,此时432出队,扩展结点2
  • 439:6 ,453:7 ,4932:7, 4326:7 ,456:8,435:9 ,4935:10 ,4321:11 ,又找到目标路径4326,但此时有更短路径439,故439出队,扩展结点9(因9扩展后的结点为3,4,已经被探索过,且也在当前路径439上,故出队后没了)
  • 453:7 ,4326:7 ,4932:7 ,456:8 ,435:9 ,4935:10 ,4321:11,最短的453,出队,扩展3后的结点为2,4,5,9,去掉4,5,路径有4532:9、4539:9
  • 4326:7 ,4932:7,456:9 ,4532:9 ,4539:9 ,435:9 ,4935:10 ,4321:11 ,最短路径4326,出队,通过目标测试,找到路径4=>3=>2=>6,代价为7,UCS搜索结束,当然如果要再找出更多路径,可以设置一个计数器。

可以看出,为了找到最短路径,做了很多必要的搜索,因为刚开始就找到了路径456,但456不一定是最优的,故还需要扩展其他结点才能判断是否是最短的,最后找到4326路径,该路径在优先级队列里已经是最短的了,故肯定是最优的路径了(因为其他路径可能还没有结束搜索,即使结束搜索的路径也比这个大)。

3.数据结构

在上一篇文章中,我们用邻接表构造了图,这次我们使用邻接矩阵来构造该图。

1)图结构

public class Graph {
    final static int  MAXSIZE=1000;//最多1000个顶点
    String[] vertexes=new String[MAXSIZE];  //顶点数组
    int[][] edges=new int[MAXSIZE][MAXSIZE];//邻接矩阵
    int  verNum,edgeNum;//顶点个数、边条数
    public  Graph()
    {
        verNum=0;
        edgeNum=0;
    }
    public  Graph(String[] Vertexes,int[][] Edges,int VerNum,int EdgeNum)
    {
        vertexes=Vertexes;
        edges=Edges;
        verNum=VerNum;
        edgeNum=EdgeNum;
    }
}

上图的邻接矩阵见下面测试代码。

2)路径结构

路径结构比较简单:一个是路径、一个是代价

public class costPath {
    String  path;//路径,如3216
    int cost;//路径代价
    int estCost;//估计路径代价,启发函数或评估函数计算出的代价
    public  costPath(String Path)
    {
        path=Path; 
        cost=0; 
        estCost=0;
    }
    public  costPath(String Path,int Cost)
    {
        path=Path; 
        cost=Cost; 
        estCost=0;
    }
    public  costPath(String Path,int Cost,int EstCost)
    {
        path=Path; 
        cost=Cost; 
        estCost=EstCost;
    }
    public void  print()
    {
       System.out.print(path+",cost:"+cost+",estimate Cost:"+estCost+"||");
    }
}

4.输出图

    public  void print()
    {
        //输出顶点
        System.out.print("顶点:");
        for(int i=0;i<verNum;i++)
            System.out.print(vertexes[i]+" ");
       //输出每条边
        System.out.println();
        System.out.println("边:");
        for(int i=0;i<verNum;i++)
        {
            for(int j=0;j<verNum;j++)
                if(edges[i][j]>0) //表示有边,为0就没有了,不考虑负值边
                   System.out.print(vertexes[i]+"=>"+vertexes[j]+":"+edges[i][j]+" ");
             System.out.println();
        }
    }

5.查找顶点对应的编号

    public  int searchVertex(String vertex)
    {
        for(int i=0;i<verNum;i++)
            if(vertexes[i]==vertex) return i;
       return -1;
    }

6.解析路径并输出对应代价

    public  void  parsePath(ArrayList<costPath> path)
    {
        for(int i=0;i<path.size();i++)
        {
            costPath subPath=path.get(i);
            String[]  realPath=subPath.path.split("-");
            for(int j=0;j<subPath.path.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(",Cost:"+subPath.cost);//输出代价
        }
    }

7.判断结点是否在当前路径字符数组

    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;
    }

8.一致代价搜索UCS

思路:用allPath存储找到的路径(如果需要返回多个路径的话),用explored存储已探索结点,用priFrontier存储路径队列(注意UCS需要使用优先级队列)。由于使用了路径类(路径、代价),在路径类优先级队列的比较中需要重写比较器。先测试是否是目标结点,如不是再扩展该结点。

public ArrayList<costPath>  ucs(int src,int dst) {
        if (src < 0 && dst < 0 && src >= verNum && dst >= verNum) return null;
        //allPath存储路径及代价
        ArrayList<costPath> allPath = new ArrayList<>();
        if (src == dst) {
            allPath.add(new costPath(String.valueOf(src), 0));
            return allPath;
        }
        //重写优先级队列的比较函数
        PriorityQueue<costPath> priFrontier = new PriorityQueue<>(
            new Comparator<costPath>() {
                 public int compare(costPath cp1, costPath cp2) {
                     return cp1.cost - cp2.cost;
                  }
        });
        //explored存储已探索过的结点
        ArrayList<Integer> explored = new ArrayList<>();
        //源结点入队,设置代价为0
        priFrontier.offer(new costPath(String.valueOf(src), 0));
        while (!priFrontier.isEmpty()) {
            //代价最小的路径出队(优先级队列嘛)
            costPath cp = priFrontier.remove();
            //p为路径
            String p = cp.path;
            //获取路径最后一个结点
            String[] pathSplit=p.split("-");//拆分路径
            int v = Integer.parseInt(pathSplit[pathSplit.length-1]);
            if (v == dst) {  //目标测试,发现目标
                allPath.add(cp);//将路径及代价添加到allPath
                //return  allPath;//如果只要最优路径直接返回退出即可
            }
            else {
                //将该结点设置为已探索
                explored.add(v);
                //下面for循环找到结点v的后继结点并做相应处理:入队、替换
                for (int i = 0; i < verNum; i++) {
                    if (edges[v][i] > 0) { //有边
                        //组合当前路径及后继结点,备用
                        String p1 = p+"-"+i;
                        //后继结点是否在当前路径上,如果是,则会形成环,将丢弃
                        boolean isInCurrentPath = isInStringArray(pathSplit,i);
                        //判断该后继结点是否未探索、或者是否不在当前路径上
                        //未探索过的肯定需要入队
                        //另外,虽然已探索过,但并不在当前路径上的结点也需入队,
                        //这样有更多备选的路径,不至于错过短的路径
                        if (explored.indexOf(i) == -1 || !isInCurrentPath)
                            //将其入队
                            priFrontier.offer(new costPath(p1, cp.cost + edges[v][i]));
                        }
                    }
                }
            }
        }
        return allPath;
    }

9.测试及结果

public class Main {

    public static void main(String[] args)
    {
        //顶点表
        String[] vers = {"c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9"};
        int[][]  edgs={     //邻接矩阵
                {0,5,0,0,0,0,0,2,0},
                {5,0,2,0,0,1,0,0,0},
                {0,2,0,4,5,0,0,0,2},
                {0,0,4,0,2,0,0,0,3},
                {0,0,5,2,0,6,0,0,0},
                {0,1,0,0,6,0,1,2,0},
                {0,0,0,0,0,1,0,3,0},
                {2,0,0,0,0,2,3,0,0},
                {0,0,2,3,0,0,0,0,0}
        };
        Graph  g=new Graph(vers,edgs,9,13);
        System.out.println("Uniform-cost Search:");
        g.parsePath1(g.ucs(g.searchVertex("c4"),g.searchVertex("c6")));
    }
}

运行结果如下:(如果ucs方法里找到目标后直接return allPath,将只会返回第一条路径,也就是最短的或最优的路径)

Uniform-cost Search:
c4=>c3=>c2=>c6,Cost:7
c4=>c5=>c6,Cost:8
c4=>c9=>c3=>c2=>c6,Cost:8
c4=>c3=>c2=>c1=>c8=>c6,Cost:15
c4=>c9=>c3=>c2=>c1=>c8=>c6,Cost:16
c4=>c3=>c2=>c1=>c8=>c7=>c6,Cost:17
c4=>c9=>c3=>c2=>c1=>c8=>c7=>c6,Cost:18

从结果可以看出,路径代价依次增加,如果代价一样,则路径深度浅的排在前面。因此,ucs是最优的

10.分析

UCS以路径代价而不是深度来扩展结点,路径代价最小的优先扩展,BFS只是UCS的一个特例,UCS是最优的,只要不存在0代价结点(我的理解,如自环),每步代价都大于一个很小的数e,那么UCS也是完备的。

假设最优路径的代价为C*,设每步代价不少于e,则最坏情况的时空复杂度为O(b^{1+|C*/e|}),其中||表示向下取整,显然比BFS要大很多,因为要探索更多小的路径。

  • 7
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值