• 常用10种算法(二)


    一、普里姆算法

      源码:普里姆算法

    1,介绍

      普里姆算法是图结构中寻找最小生成树的一种算法。所谓生成树,即为连通图的极小连通子图,其包含了图中的n个顶点,和n-1条边,这n个顶点和n-1条边所构成的树即为生成树。当边上带有权值时,使生成树中的总权值最小的生成树称为最小代价生成树,简称最小生成树。最小生成树不唯一,且需要满足一下准则:

    • 只能使用图中的边构造最小生成树
    • 具有n个顶点和n-1条边
    • 每个顶点仅能连接一次,即不能构成回路

    2,案例

      看一个应用场景和问题:

    • 有胜利乡有7个村庄(A, B, C, D, E, F, G) ,现在需要修路把7个村庄连通
    • 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里
    • 问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?

          

    3,思路

         

    4,实现O(n3)

      采用三层for循环,第一层控制生成的边的条数,第二层表示已经被访问的顶点,第三层为未被访问的顶点。获取满足这三者的最小权重值

    //最终会生成vertexNum-1条边,故而只需要遍历vertexNum-1次。当然也可以多遍历因为后面会有最小值的控制
    for (int k = 1; k < graph.vertexNum; k++) {
        min = 10000;
        //表示被访问的结点
        for (int i = 0; i < graph.vertexNum; i++) {
            //表示未被访问的结点
            for (int j = 0; j < graph.vertexNum; j++) {
                //如果当前为i结点被访问,j结点为未被访问,当前路径权重小于最小权重
                if (visited[i]==1 && visited[j]==0 && graph.weight[i][j] < min ) {
                    //此时记录最小权重,记录i结点和j结点
                    min = graph.weight[i][j];
                    x = i;
                    y = j;
                }
            }
        }
    
        //如果当前结点全部被访问,则min==10000,否则就有边未被访问。
        if (min < 10000) {
            //记录当前y为被访问,并输出此时访问的边
            visited[y] = 1;
            System.out.println(getVertex(x) +" -> " +getVertex(y) +"  "+ min);
        }
    }

    5,优化实现O(n2)

    int minIndex:   记录每轮查找的最小值对应索引
    int[] indexArr:记录每次访问的结点对应的索引
    int[] visited:  记录访问状态(如果为0表示已访问)和距离。当查找到最小值对应的索引minIndex后,如果以minIndex为原点到达各顶点的距离 小于 原始visited的值,就更新visited当前索引值
    /**
     * prim算法(优化)
     *
     * @param data 顶点
     * @param weight 邻接矩阵
     * @param start 开始顶点
     */
    public static void prim(char[] data,int[][] weight,int start) {
        int length = data.length;
        //记录各最小节点的索引
        int[] indexArr = new int[length];
        //如果当前值为0 表示已经被访问, 其他情况表示未访问
        int[] visited = new int[length];
    
        //获取start这一行与其他节点的距离
        for (int i = 0; i < length; i++) {
            visited[i] = weight[start][i];
        }
        //标记start行为已访问
        visited[start] = 0;
        //记录第一个结点
        int index = 0;
        indexArr[index++] = start;
    
    
        int min;
        int minIndex ;
        //记录权重
        int sum = 0;
        for (int i = 1; i <length; i++) {
            //记录每轮最小值
            min = 10000;
            //记录每轮的最小索引
            minIndex = -1;
            //获取visited中最小值(非零)
            for (int j = 0; j < length; j++) {
                if (visited[j] != 0 && visited[j] < min  ) {
                    min = visited[j];
                    minIndex = j;
                }
            }
    
            //证明这一轮没有最小索引,循环结束
            if (minIndex == -1) {
                break;
            }
            //记录(最小的索引)
            indexArr[index++] = minIndex;
            sum += min;
    
            //更新visited中的数据
            visited[minIndex] = 0;
            for (int j = 0; j < length; j++) {
                //如果当前visited数组中j的值  > 邻接矩阵中从minIndex到j的值  更新visited
                if (visited[j] != 0 && visited[j] > weight[minIndex][j]) {
                    visited[j] = weight[minIndex][j];
                }
            }
        }
    
        System.out.println("总的权重为: " + sum);
        for (int i = 0; i < length; i++) {
            System.out.printf("%s	",data[indexArr[i]]);
        }
        System.out.println();
    
    }

    二、克鲁斯卡尔算法

      源码:克鲁斯卡尔算法

    1,介绍

    • 克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法。
    • 基本思想:按照权值从小到大的顺序选择 n-1 条边,并保证这 n-1 条边不构成回路
    • 具体做法:首先构造一个只含 n 个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止

    2,案例

      看一个应用场景和问题:

    • 某城市新增7个站点(A, B, C, D, E, F, G) ,现在需要修路把7个站点连通
    • 各个站点的距离用边线表示(权) ,比如 A – B 距离 12公里
    • 问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?

          

    3,思路

    • 对所有的边按照权值大小进行排序
    • 添加权值最小并且不构成回路的边

      如果判定新添加的边是否与原来的边构成回路?新添加边的两个顶点的终点不相同,下面展示如果求指定索引的终点:

    /**
     * 查询当前索引对应的终点的索引
     *  如果当前索引对应的终点为0,则直接返回自己,
     *  如果不为0记录向后找当前的终点  循环得到其终点
     *  例如: E的终点为F D的终点为E(此时会通过E找到F) 那么就是D和E的终点均为F
     * @param end   数组就是记录了各个顶点对应的终点是哪个,ends 数组是在遍历过程中,逐步形成
     * @param i     表示传入的顶点对应的下标
     * @return  返回的就是 下标为i的这个顶点对应的终点的下标
     */
    public int getEnd(int[] end,int i) {
        while (end[i] != 0) {
            i = end[i];
        }
        return i;
    }

    4,代码实现

      遍历图的右半边,将其构建成一个存储所有边的数组。对边进行排序(升序),遍历每条边获取其start和end的终点是否相同,不同就加入结果集,并更新终点数组

    /**
     * 克鲁斯卡尔算法
     */
    public void kruskal() {
        System.out.println("排序前:"+Arrays.toString(edges));
        sort(edges);
        System.out.println("排序后:"+Arrays.toString(edges));
        //最终的结果肯定是length-1条
        Edge[] res = new Edge[vertexs.length-1];
        int index = 0;
    
        for (int i = 0; i < edges.length; i++) {
            int start = getIndexByChar(edges[i].start);
            int end = getIndexByChar(edges[i].end);
            //获取start结点的终点
            int startParent = getEnd(ends, start);
            //获取end结点的终点
            int endParent = getEnd(ends, end);
            //如果两个结点的 终点不是同一下标 则记录
            if (startParent != endParent) {
                //将当前边加入到结果集中
                res[index++] = edges[i];
                //同时更新start结点的终点为end结点的终点
                ends[startParent] = endParent;
            }
        }
    
        for (int i = 0; i < res.length; i++) {
            System.out.println(res[i]);
        }
    
    }

    三、迪杰斯特拉算法

      源码:迪杰斯特拉算法

    1,介绍

      迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个结点到其他结点的最短路径。它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。

    2,案例

      看一个应用场景和问题:

    • 战争时期,胜利乡有7个村庄(A, B, C, D, E, F, G) ,现在有六个邮差,从G点出发,需要分别把邮件分别送到 A, B, C , D, E, F 六个村庄
    • 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里
    • 问:如何计算出G村庄到 其它各个村庄的最短距离?
    • 如果从其它点出发到各个点的最短距离又是多少?

        

    3,思路

    • 从指定起点开始,找出所有邻接节点更新起点到邻接节点路径权值和记录的前驱节点,从中选出路径权值最小的一个节点,作为下一轮的起点
    • 从次轮起点开始,重复第一轮的操作
    • 每一轮更新记录的路径权值,是把 “记录得原始起点到该目标节点的路径总权值” 与 “记录中原始起点到本轮起点的路径权值 + 本轮起点到邻接节点的权值” 比较,保留小的
    • 更新了权值的同时要记得更新路径终点的前驱节点
    • 每一轮都将此轮的起点设置为已访问,并且寻找邻接节点时也要跳过那些已访问的
    • 所有节点都"已访问"时结束

    4,代码实现

    思路:1,构建一个vistedvetex对象(pre数组记录到达当前顶点的上一个顶点索引;already数组记录当前顶点是否被访问;dis数组记录从初始顶点出发到达当前顶点的距离2,初始化根据初始顶点,构建:除初始顶点以外所有dis均为最大值(65535),除初始顶点以外的所有顶点均为被访问
         3以index(第一次index为初始顶点,之后均为获取当前dis数组中未被访问的最短距离为index,并标记当前index为已访问)
               作为基准点,获取当前dis中index的值(该值则为从初始结点到index的距离) + index到i(遍历图变化) 距离之和 记为len,
               用len与dis中i的值进行比较:如果len < dis[i],则说明以index作为基准点 更为合适,此时更新dis[i] = len ; pre[i] = index
    /**
     * 迪杰斯特拉算法
     *
     * @param index 初始顶点的坐标
     */
    public void dsj(int index) {
        //初始化visitedVetex
        vv = new VisitedVetex(index, vertex.length);
        //以index作为介质,更新visitedvetex中的dis和pre
        update(index);
        //记录当前存储在dis数组中 并 未被访问的最小节点的距离
        int i;
        //如果当前i==-1 ,则表示dis数组中所有的数据均被访问过
        while ((i = vv.updateArr()) != -1) {
            //以i作为介质  更新vv
            update(i);
        }
    
        //打印
        vv.show();
    }
    
    /**
     * 更新以当前结点为 介质 的visitedvetex的 dis和pre数组
     *
     * @param index 当前结点的索引
     */
    private void update(int index) {
        //遍历以index作为结点  获取 与其连接的所有结点
        for (int i = 0; i < matrix[index].length; i++) {
            //len为  从index到i结点的邻接距离 + 存储在dis数组中的起始点到index的距离
            int len = matrix[index][i] + vv.dis[index];
            //如果len < 存储在dis数组中的起始点到i 结点的距离  并且当前i结点未被访问过(只有i在dis数组中为最小的数时才会被访问)
            if (len < vv.dis[i] && !vv.isVisited(i)) {
                //更新dis数组中 起始点到i的距离为len
                vv.updateDis(i, len);
                //修改当前i结点的前驱结点为index
                vv.updatePre(i, index);
            }
        }
    }

    四、弗洛伊德算法

      源码:弗洛伊德算法

    1,介绍

    • 弗洛伊德(Floyd)算法也是一种用于寻找给定的加权图中顶点间最短路径的算法
    • 弗洛伊德算法(Floyd)计算图中各个顶点之间的最短路径
    • 迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径。
    • 弗洛伊德算法 VS 迪杰斯特拉算法:
      • 迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点的最短路径;
      • 弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径。

    2,案例

    • 胜利乡有7个村庄(A, B, C, D, E, F, G)
    • 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里
    • 问:如何计算出各村庄到其它各村庄的最短距离?

            

    3,思路

    • 设置顶点vi到顶点vk的最短路径已知为Lik,顶点vk到vj的最短路径已知为Lkj,顶点vi到vj的路径为Lij,则vi到vj的最短路径为:min((Lik+Lkj),Lij),vk的取值为图中所有顶点,则可获得vi到vj的最短路径
    • 至于vi到vk的最短路径Lik或者vk到vj的最短路径Lkj,也是以同样的方式获得

    4,代码实现

    /**
     * 弗洛伊德算法 : 以i为中间点 比较j->i->k 的距离与 j->k的距离
     *  如果采用i作为中间顶点距离短则记录i为前驱顶点,并更新距离
     *  如果是j->k 距离更短  则直接记录j
     * @param vetex 顶点集合
     * @param w     邻接矩阵
     */
    public static void floyd(char[] vetex, int[][] w) {
        //存储前驱结点
        char[][] pre = new char[vetex.length][vetex.length];
        // 对pre数组初始化, 注意存放的是前驱顶点的下标
        for (int i = 0; i < vetex.length; i++) {
            Arrays.fill(pre[i], vetex[i]);
        }
    
        //作为中间结点
        for (int i = 0; i < vetex.length; i++) {
            //初始结点
            for (int j = 0; j < vetex.length; j++) {
                //终点
                for (int k = 0; k < vetex.length; k++) {
                    //j->i->k的距离
                    int len = w[j][i] + w[i][k];
                    if (w[j][k] > len) {
                        w[j][k] = len;
                        //记录前驱结点为中间结点
                        pre[j][k] = pre[i][k];
                    }
                }
            }
        }
    }

    五、马踏棋盘算法(骑士周游问题)

      源码:马踏棋盘算法

    1,介绍

      将马随机放在国际象棋的8×8棋盘Board[0~7][0~7]的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格

      

    2,思路

    • 马踏棋盘问题(骑士周游问题)实际上是图的深度优先搜索(DFS)的应用。
    • 编码思路
      • 创建棋盘 chessBoard,是一个二维数组
      • 当前位置设置为已经访问,然后根据当前位置,计算马儿还能走哪些位置,并放入到一个集合中(ArrayList),下一步可选位置最多有8个位置, 每走一步,就使用 step+1
      • 遍历ArrayList中存放的所有位置,看看哪个可以走通,如果走通,就继续,走不通,就回溯
      • 判断马儿是否完成了任务,使用 step 和应该走的步数比较 , 如果没有达到数量,则表示没有完成任务,将整个棋盘置 0
      • 注意:马儿不同的走法(策略),会得到不同的结果,效率也会有影响(优化)

    3,代码实现

    /**
     * 马踏棋盘算法核心
     *
     * @param x    初始棋子在棋盘的哪一行
     * @param y    初始棋子在棋盘的哪一列
     * @param step 当前是第几步
     */
    public void start(int x, int y, int step) {
        //首先存储对应的步数
        chess[x][y] = step;
        //获取当前x,y对应的 visited数组中的索引位置
        int index = x * Y + y;
        //标记为已访问
        visited[index] = true;
        //构建当前point
        Point currentPoint = new Point(x, y);
        List<Point> ps = next(currentPoint);
        //对ps进行排序,排序的规则就是对ps的所有的Point对象的下一步的位置的数目,进行非递减排序
        sort(ps);
        while (!ps.isEmpty()) {
            //将第一个元素移除
            Point removePoint = ps.remove(0);
            //未被访问过,则调用当前
            if (!visited[(removePoint.x) * Y + removePoint.y]) {
                start(removePoint.x, removePoint.y, step + 1);
            }
        }
    
        //回溯
        if (step < X * Y && !finished) {
            chess[x][y] = 0;
            visited[index] = false;
        } else {
            finished = true;
        }
    
    }
    
    /**
     * 对ps集合进行非递减排序
     * @param ps    集合
     */
    private void sort(List<Point> ps) {
        ps.sort((p1, p2) -> {
            //获取当前p1和p2  各自对应的
            List<Point> next1 = next(p1);
            List<Point> next2 = next(p2);
            return next1.size() - next2.size();
        });
    }
    
    private List<Point> next(Point currentPoint) {
        List<Point> ps = new ArrayList<>();
        int x, y;
        //0
        if ((x = currentPoint.x - 1) >= 0 && (y = currentPoint.y + 2) < Y) {
            ps.add(new Point(x, y));
        }
        //1
        if ((x = currentPoint.x + 1) < X && (y = currentPoint.y + 2) < Y) {
            ps.add(new Point(x, y));
        }
        //2
        if ((x = currentPoint.x + 2) < X && (y = currentPoint.y + 1) < Y) {
            ps.add(new Point(x, y));
        }
        //3
        if ((x = currentPoint.x + 2) < X && (y = currentPoint.y - 1) >= 0) {
            ps.add(new Point(x, y));
        }
        //4
        if ((x = currentPoint.x + 1) < X && (y = currentPoint.y - 2) >= 0) {
            ps.add(new Point(x, y));
        }
        //5
        if ((x = currentPoint.x - 1) >= 0 && (y = currentPoint.y - 2) >= 0) {
            ps.add(new Point(x, y));
        }
        //6
        if ((x = currentPoint.x - 2) >= 0 && (y = currentPoint.y - 1) >= 0) {
            ps.add(new Point(x, y));
        }
        //7
        if ((x = currentPoint.x - 2) >= 0 && (y = currentPoint.y + 1) < Y) {
            ps.add(new Point(x, y));
        }
        return ps;
    }
  • 相关阅读:
    开发者入门必读:最值得看的十大机器学习公开课
    ansible 文件模块,很实用
    前端之Bootstrap框架
    47考题整理
    前端之jQuery
    前端之BOM和DOM
    python补充之进制转换、exec、eval、compile
    JavaScript
    css(2)
    前端之form表单与css(1)
  • 原文地址:https://www.cnblogs.com/bbgs-xc/p/14262937.html
Copyright © 2020-2023  润新知