DSA_常用10种算法(java数据结构与算法)

本文详细介绍了计算图论中的最小生成树问题,如普里姆算法和克鲁斯卡尔算法,以及求解最短路径的迪杰斯特拉算法和弗洛伊德算法。同时,探讨了字符串匹配问题,如暴力匹配和KMP算法。这些算法在解决网络优化、物流路径规划和文本处理等领域有着广泛应用。
摘要由CSDN通过智能技术生成

二分查找算法(非递归)

二分查找算法(非递归)介绍

  1. 前面我们讲过了二分查找算法,是使用递归的方式,下面我们讲解二分查找算法的非递归方式
  2. 二分查找法只适用于从有序的数列中进行查找(比如数字和字母等),将数列排序后再进行查找
  3. 二分查找法的运行时间为对数时间 O(㏒₂n) ,即查找到需要的目标位置最多只需要㏒₂n 步,假设从[0,99]的 队列(100 个数,即 n=100)中寻到目标数 30,则需要查找步数为㏒₂100 , 即最多需要查找 7 次( 2^6 < 100 < 2^7)

二分查找算法(非递归)代码实现

数组 {1,3, 8, 10, 11, 67, 100}, 编程实现二分查找, 要求使用非递归的方式完成.

package cn.chasing.Algorithm.binarySearch;

/**
 * @author 柴柴快乐每一天
 * @create 2021-08-23  10:50 上午
 * <p>
 * 『Stay hungry, stay foolish. 』
 */
public class BinarySearchNoRecur {
    public static void main(String[] args) {
        //测试
        int[] arr = {1,3, 8, 10, 11, 67, 100};
        int index = binarySearch(arr, -100);
        System.out.println("index=" + index);
    }

    /**
     * 二分查找的非递归实现
     * @param arr 待查找的数组,arr是升序排列
     * @param target 需要查找的数
     * @return 返回对应下标,-1表示没有找到
     */
    public static int binarySearch(int[] arr, int target) {
        int left = 0;
        int right = arr.length - 1;
        int mid;
        while (left <= right) {
            mid = (left + right) / 2;
            if (target == arr[mid]) {
                return mid;
            }
            if (target < arr[mid]) {
                right = mid-1;
            } else {
                left = mid + 1;
            }
        }
        return -1;
    }
}

分治算法

分治算法介绍

  1. 分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……

  2. 分治算法可以求解的一些经典问题

    • 二分搜索

    • 大整数乘法

    • 棋盘覆盖

    • 合并排序

    • 快速排序

    • 线性时间选择

    • 最接近点对问题

    • 循环赛日程表

    • 汉诺塔

分治算法的基本步骤

分治法在每一层递归上都有三个步骤:

  1. 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
  2. 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
  1. 合并:将各个子问题的解合并为原问题的解。

分治(Divide-and-Conquer§)算法设计模式如下:

image-20210823161739263

分治算法最佳实践-汉诺塔

汉诺塔的传说

汉诺塔:汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着 64 片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

假如每秒钟一次,共需多长时间呢?移完这些金片需要 5845.54 亿年以上,太阳系的预期寿命据说也就是数百亿年。真的过了 5845.54 亿年,地球上的一切生命,连同梵塔、庙宇等,都早已经灰飞烟灭。

汉诺塔游戏的演示和思路分析:

  1. 如果是有一个盘, A->C

    如果我们有 n >= 2 情况,我们总是可以看做是两个盘

    1.最下边的盘 2. 上面的盘

  2. 先把 最上面的盘 A->B

  3. 把最下边的盘 A->C

  4. 把 B 塔的所有盘 从 B->C

汉诺塔游戏的代码实现:

package cn.chasing.Algorithm.divideAndConquer;

/**
 * @author 柴柴快乐每一天
 * @create 2021-08-23  3:45 下午
 * <p>
 * 『Stay hungry, stay foolish. 』
 */
public class HanoiTower {
    public static void main(String[] args) {
        hanoiTower(3, 'A', 'B', 'C');
    }

    /**
     * 汉诺塔移动的方法,将起始塔a的所有圆盘借助辅助塔b移动到目标塔c
     * @param num 汉诺塔的层数
     * @param a 起始塔
     * @param b 辅助塔
     * @param c 目标塔
     */
    public static void hanoiTower(int num, char a, char b, char c) {
        // 如果只有一个圆盘
        if (num == 1) {
            System.out.println("第1个圆盘从 " + a + "->" + c);
        } else {
            // 当 n >= 2 时,始终可以看做两个圆盘:1. 最底部的一个盘;2. 上面的所有盘
            // 1. 先把最上面所有的盘移动从a->b,移动过程中会用到c
            hanoiTower(num-1, a, c, b);
            // 2. 把最下面的盘移动到a->c
            System.out.println("第" + num + "个盘从 " + a + "->" + c);
            // 3. 把b塔上的所有盘从b->c,移动过程中会用到a
            hanoiTower(num-1, b, a, c);
        }
    }
}

动态规划算法

动态规划算法介绍

  1. 动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解 的处理算法
  2. 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这 些子问题的解得到原问题的解。
  3. 与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。 ( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )
  4. 动态规划可以通过填表的方式来逐步推进,得到最优解.

背包问题

背包问题

有一个背包,容量为 4 磅 , 现有如下物品

image-20210823172716059

  1. 要求达到的目标为装入的背包的总价值最大,并且重量不超出
  2. 要求装入的物品不能重复

思路分析

  1. 背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价 值最大。其中又分 01 背包和完全背包(完全背包指的是:每种物品都有无限件可用)
  2. 这里的问题属于 01 背包,即每个物品最多放一个。而无限背包可以转化为 01 背包。
  3. 算法的主要思想,利用动态规划来解决。每次遍历到的第 i 个物品,根据 weight[i]和 value[i]来确定是否需要将该物品放入背包中。即对于给定的 n 个物品,设 value[i]、weight[i]分别为第 i 个物品的价值和重量,C 为背包的容量。再令 vTable 表示在前 i 个物品中能够装入容量为 j 的背包中的最大价值。则我们有下面的结果:
  1. vTable[i] [0] = vTable[0] [j] = 0;

    表示填入表第一行和第一列是 0

  2. 当 weight[i] > j 时:

    vTable[i] [j] = vTable[i-1] [j]

    当准备加入新增的商品的容量大于当前背包的容量时,就直接使用上一个单元格的装入策略

  3. 当 w[i] <= j时:

    vTable[i] [j] = max{ vTable[i-1] [j], value[i] + vTable[i-1] [j-weight[i]] }

    当 准备加入的新增的商品的容量小于等于当前背包的容量,

装入的方式:

j 为当前动态容量,i为商品下标

vTable [i-1] [j]: 就是上一个单元格的装入的最大值

value[i] : 表示当前商品的价值

vTable [i-1] [j-weight[i]] : 装入 i-1 商品,到剩余空间 j-weight[i]的最大值

当w[i] <= j时: vTable[i] [j] = max{vTable[i-1] [j], value[i] + vTable[i-1] [j-weight[i]] }

图解

image-20210823174258692

代码实现

package cn.chasing.Algorithm.dynamic;

/**
 * @author 柴柴快乐每一天
 * @create 2021-08-23  5:44 下午
 * <p>
 * 『Stay hungry, stay foolish. 』
 */
public class Knapsack {
    public static void main(String[] args) {
        // 物体的重量
        int[] weight = {0, 1, 4, 3};
        // 物体的价值
        int[] value = {0, 1500, 3000, 2000};
        // 背包的容量
        int capacity = 4;
        // 物体的个数
        int n = value.length;

        // 创建二维数组状态表,vTable[i][j] 表示从前i个物品中,装入到容量为j的背包中的最大价值
        // n个物品,第一个物品0磅,capacity+1是因为要保证表头为0 1 2 3 4,不加1就没有4
        int[][] vTable = new int[n][capacity + 1];
        // 存储最优解的路径
        int[][] path = new int[n][capacity+1];

        // 初始化第一行和第一列,使其都为0,但但是数组默认值为0,所以可以不处理

        // 进行动态规划,从非0行列开始处理
        for (int i = 1; i < vTable.length; i++) {
            for (int j = 1; j < vTable[0].length; j++) {
                if (weight[i] > j) {
                    vTable[i][j] = vTable[i - 1][j];
                } else {
                    vTable[i][j] = Math.max(vTable[i - 1][j], value[i] + vTable[i - 1][j - weight[i]]);
                    path[i][j] = 1;
                }
            }
        }

        // 输出状态表
        for (int i = 0; i < vTable.length; i++) {
            for (int j = 0; j < vTable[i].length; j++) {
                System.out.print(vTable[i][j] + " ");
            }
            System.out.println();
        }

        System.out.println("=========================");


        int i = path.length - 1;
        int j = path[0].length - 1;
        while (i > 0 && j > 0) {
            if (path[i][j] == 1) {
                System.out.printf("第%d个商品放入到背包\n", i);
                j -= weight[i];
            }
            i--;
        }

    }
}

KMP 算法

应用场景-字符串匹配问题

字符串匹配问题::

  1. 有一个字符串 str1= ““硅硅谷 尚硅谷你尚硅 尚硅谷你尚硅谷你尚硅你好””,和一个子串 str2=“尚硅谷你尚硅你”
  2. 现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1

暴力匹配算法

如果用暴力匹配的思路,并假设现在 str1 匹配到 i 位置,子串 str2 匹配到 j 位置,则有:

  1. 如果当前字符匹配成功(即 str1[i] == str2[j]),则 i++,j++,继续匹配下一个字符
  2. 如果失配(即 str1[i]! = str2[j]),令 i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为 0。
  3. 用暴力方法解决的话就会有大量的回溯,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间。(不可行!)
  4. 暴力匹配算法实现.
  5. 代码

KMP 算法介绍

  1. KMP 是一个解决模式串在文本串是否出现过,如果出现过,最早出现的位置的经典算法
  2. Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP 算法”,常用于在一个文本串 S 内查找一个模式串 P 的出现位置,这个算法由 Donald Knuth、Vaughan Pratt、James H. Morris 三人于 1977 年联合发表,故取这 3 人的 姓氏命名此算法.
  3. KMP 方法算法就利用之前判断过信息,通过一个 next 数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过 next 数组找到,前面匹配过的位置,省去了大量的计算时间
  4. 参考资料:https://www.cnblogs.com/ZuoAndFutureGirl/p/9028287.html

KMP 算法最佳应用-字符串匹配问题

字符串匹配问题::

  1. 有一个字符串 str1= “BBC ABCDAB ABCDABCDABDE”,和一个子串 str2=“ABCDABD”

  2. 现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1

  3. 要求:使用 KMP 算法完成判断,不能使用简单的暴力匹配算法.

思路分析图解

举例来说,有一个字符串 Str1 = “BBC ABCDAB ABCDABCDABDE”,判断,里面是否包含另一个字符串 Str2 = “ABCDABD”?

  1. 首先,用 Str1 的第一个字符和 Str2 的第一个字符去比较,不符合,关键词向后移动一位
  1. 重复第一步,还是不符合,再后移
image-20210823223903419
  1. 一直重复,直到 Str1 有一个字符与 Str2 的第一个字符符合为止
image-20210823223917084
  1. 接着比较字符串和搜索词的下一个字符,还是符合。
image-20210823224001788
  1. 遇到 Str1 有一个字符与 Str2 对应的字符不符合。
image-20210823224027643
  1. 这时候,想到的是继续遍历 Str1 的下一个字符,重复第 1 步。(其实是很不明智的,因为此时 BCD 已经比较过了,没有必要再做重复的工作,一个基本事实是,当空格与 D 不匹配时,你其实知道前面六个字符是”ABCDAB”。 KMP 算法的想法是,设法利用这个已知信息,不要把”搜索位置”移回已经比较过的位置,继续把它向后移,这样就提高了效率。)
image-20210823224109484
  1. 怎么做到把刚刚重复的步骤省略掉?可以对 Str2 计算出一张《部分匹配表》,这张表的产生在后面介绍
image-20210823224138276
  1. 已知空格与 D 不匹配时,前面六个字符”ABCDAB”是匹配的。查表可知,最后一个匹配字符 B 对应的”部分匹配值”为 2,因此按照下面的公式算出向后移动的位数:

    移动位数 = 已匹配的字符数 - 对应的部分匹配值

    因为 6 - 2 等于 4,所以将搜索词向后移动 4 位。

  2. 因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为 2(”AB”),对应的”部分匹配值” 为 0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移 2 位。

image-20210823224307110
  1. 因为空格与 A 不匹配,继续后移一位。
image-20210823224329964
  1. 逐位比较,直到发现 C 与 D 不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动 4 位。
image-20210823224351636
  1. 逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动 7 位,这里就不再重复了。
image-20210823224416179
  1. 介绍《部分匹配表》怎么产生的,先介绍前缀,后缀是什么
image-20210823224501353

“部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度。以”ABCDABD”为例,

  • ”A”的前缀和后缀都为空集,共有元素的长度为 0;
  • ”AB”的前缀为[A],后缀为[B],共有元素的长度为 0;
  • ”ABC”的前缀为[A, AB],后缀为[BC, C],共有元素的长度 0;
  • ”ABCD”的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为 0;
  • ”ABCDA”的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为”A”,长度为 1;
  • ”ABCDAB”的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为”AB”,长度为 2;
  • ”ABCDABD”的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为 0。
  1. 部分匹配”的实质是,有时候,字符串头部和尾部会有重复。比如,”ABCDAB”之中有两个”AB”,那么它的”部分匹配值”就是 2(”AB”的长度)。搜索词移动的时候,第一个”AB”向后移动 4 位(字符串长度- 部分匹配值),就可以来到第二个”AB”的位置。
image-20210823224704000

到此 KMP 算法思想分析完毕!

image-20210824111225163

代码实现

package cn.chasing.kmp;

import java.util.Arrays;

/**
 * @author 柴柴快乐每一天
 * @create 2021-08-23  11:02 下午
 * <p>
 * 『Stay hungry, stay foolish. 』
 */
public class KMP {

    public static void main(String[] args) {
        String str1 = "BBC ABCDAB ABCDABCDABDE";
        String str2 = "ABCDABD";
//        String str2 = "AABAAA";

        int[] next = kmpNext(str2);
        System.out.println("next = " + Arrays.toString(next));
        int index = kmpSearch(str1, str2, next);
        System.out.println(index);
    }

    public static int kmpSearch(String str, String dest, int[] next) {
        for (int i = 0, j = 0; i < str.length(); i++) {
            // 需要处理str.charAt(i) != dest.charAt(j),去调整j的位置
            while (j > 0 && str.charAt(i) != dest.charAt(j)) {
                j = next[j-1];
            }
            if (str.charAt(i) == dest.charAt(j)) {
                j++;
            }
            if (j == dest.length()) {
                return i-j+1;
            }
        }
        return -1;
    }

    /**
     * 获取到一个字符串的部分匹配表
     * @param dest 目标字符串
     * @return 返回该字符串的部分匹配表
     */
    public static int[] kmpNext(String dest) {
        // 创建一个next数组保存部分匹配值
        int[] next = new int[dest.length()];
        // 字符串长度为1,则部分匹配值的值为0
        next[0] = 0;
        // j一直在前缀活动,i在找对应的后缀
        for (int i = 1, j = 0; i < dest.length(); i++) {
            // 当 dest.charAt(i) != dest.charAt(j),需要从next[j-1]获取新的j
            // 直到发现有 dest.charAt(i) == dest.charAt(j) 时才退出, j > 0防止next[j-1]越界
            while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
                // j表示已经匹配上的位数。失配时,
                // 之前我们比较模式串和文本串时遇到冲突回退到next数组前一位代表的下标位置,现在我们也是匹配只不过匹配的时前缀(相当于模式串)和后缀(相当于文本串)
                 j = next[j-1];
            }

            if (dest.charAt(i) == dest.charAt(j)) {
                j++;
            }

            next[i] = j;
        }
        return next;
    }
}

贪心算法

贪心算法介绍

  1. 贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法
  2. 贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果

贪心算法注意事项和细节

  1. 贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果
  2. 比如上题的算法选出的是 K1, K2, K3, K5,符合覆盖了全部的地区
  3. 但是我们发现 K2, K3,K4,K5 也可以覆盖全部地区,如果 K2 的使用成本低于 K1,那么我们上题的 K1, K2, K3, K5 虽然是满足条件,但是并不是最优的.

贪心算法最佳应用-集合覆盖

问题描述

假设存在如下表的需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号

image-20210825101949234

思路分析:

如何找出覆盖所有地区的广播台的集合呢,使用穷举法实现,列出每个可能的广播台的集合,这被称为幂集。假设总的有 n 个广播台,则广播台的组合总共有 2ⁿ -1 个,假设每秒可以计算 10 个子集, 如图:

image-20210825102118391

使用贪婪算法,效率高:

  1. 目前并没有算法可以快速计算得到准备的值, 使用贪婪算法,则可以得到非常接近的解,并且效率高。选择策略上,因为需要覆盖全部地区的最小集合:
  2. 遍历所有的广播电台, 找到一个覆盖了最多未覆盖的地区的电台(此电台可能包含一些已覆盖的地区,但没有关系)
  3. 将这个电台加入到一个集合中(比如 ArrayList), 想办法把该电台覆盖的地区在下次比较时去掉。
  4. 重复第 1 步直到覆盖了全部的地区

分析的图解:

image-20210825102245103

代码实现

package cn.chasing.Algorithm.greedy;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;

/**
 * @author 柴柴快乐每一天
 * @create 2021-08-25  10:45 上午
 * <p>
 * 『Stay hungry, stay foolish. 』
 */
public class GreedyAlgorithm {
    public static void main(String[] args) {
        // 创建广播电台,放到Map
        HashMap<String, HashSet<String >> broadcasts = new HashMap<>();
        // 将各个电台放到broadcasts
        HashSet<String> hashSet1 = new HashSet<String>();
        hashSet1.add("北京");
        hashSet1.add("上海");
        hashSet1.add("天津");

        HashSet<String> hashSet2 = new HashSet<String>();
        hashSet2.add("广州");
        hashSet2.add("北京");
        hashSet2.add("深圳");

        HashSet<String> hashSet3 = new HashSet<String>();
        hashSet3.add("成都");
        hashSet3.add("上海");
        hashSet3.add("杭州");


        HashSet<String> hashSet4 = new HashSet<String>();
        hashSet4.add("上海");
        hashSet4.add("天津");

        HashSet<String> hashSet5 = new HashSet<String>();
        hashSet5.add("杭州");
        hashSet5.add("大连");

        //加入到map
        broadcasts.put("K1", hashSet1);
        broadcasts.put("K2", hashSet2);
        broadcasts.put("K3", hashSet3);
        broadcasts.put("K4", hashSet4);
        broadcasts.put("K5", hashSet5);

        //allAreas 存放所有的地区
        HashSet<String> allAreas = new HashSet<String>();
        allAreas.add("北京");
        allAreas.add("上海");
        allAreas.add("天津");
        allAreas.add("广州");
        allAreas.add("深圳");
        allAreas.add("成都");
        allAreas.add("杭州");
        allAreas.add("大连");

        // 创建ArrayList,存放选择的电台集合
        ArrayList<String> selects = new ArrayList<>();

        // 定义一个临时的集合,在遍历过程中,存放该电台覆盖区域和当前allAreas剩余未覆盖区域的交集
        HashSet<String> tempSet = new HashSet<>();

        // 定义maxKey,保存在遍历过程中,能够覆盖最大未覆盖区域的key
        // maxAreaSize是能够覆盖最大未覆盖区域的tempSet的size
        // 当maxKey != null,加入到selects
        String maxKey;
        Integer maxAreaSize = null;
        // 当allAreas 里还有元素,这说明还有未覆盖到的区域
        while (allAreas.size() > 0) {
            // 每次遍历比较前,需将上一次的maxKey清空
            maxKey = null;

            // 遍历broadcasts,取出对应key
            for (String key : broadcasts.keySet()) {
                // 每一次for都要清空临时set
                tempSet.clear();
                // 获取当前这个key可以覆盖的地区
                HashSet<String> areas = broadcasts.get(key);
                tempSet.addAll(areas);
                // 求出tempSet和allAreas交集,结果会直接赋给tempSet
                tempSet.retainAll(allAreas);
                // 第一次maxKey为null,通过判断makKey == null 进入if赋初始maxKey;
                // 之后每一次for循环则比较当前key和maxKey对应覆盖区域交集的大小,通过判断tempSet.size() > maxAreas更新maxKey
                if (tempSet.size() > 0 && (maxKey == null || tempSet.size() > maxAreaSize)) {
                    maxKey = key;
                    maxAreaSize = tempSet.size();
                }
            }
            // 一轮比较结束后,如果maxKey != null, 就把maxKey加入selects
            if (maxKey != null) {
                selects.add(maxKey);
                // 将maxKey指向的广播电台覆盖的区域,从allAreas中去掉
                allAreas.removeAll(broadcasts.get(maxKey));
            }
        }

        // K1, K2, K3, K5
        System.out.println("得到的选择结果是:" + selects);
    }
}

普里姆算法

普里姆算法介绍

普利姆(Prim)算法求最小生成树,也就是在包含 n 个顶点的连通图中,找出只有(n-1)条边包含所有 n 个顶点的

连通子图,也就是所谓的极小连通子图

普利姆的算法如下:

  1. 设 G=(V,E)是连通网,T=(U,D)是最小生成树,V,U 是顶点集合,E,D 是边的集合

  2. 若从顶点 u 开始构造最小生成树,则从集合 V 中取出顶点 u 放入集合 U 中,标记顶点 v 的 visited[u]=1

  3. 若集合 U 中顶点 ui 与集合 V-U 中的顶点 vj 之间存在边,则寻找这些边中权值最小的边,但不能构成回路,将顶点 vj 加入集合 U 中,将边(ui,vj)加入集合 D 中,标记 visited[vj]=1

  4. 重复步骤②,直到 U 与 V 相等,即所有顶点都被标记为访问过,此时 D 中有 n-1 条边

  5. 提示: 单独看步骤很难理解,我们通过代码来讲解,比较好理解.

  6. 图解普利姆算法

image-20210825155938529

普里姆算法最佳实践(修路问题)

image-20210825160125299

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

思路:

将 10 条边,连接即可,但是总的里程数不是最小。正确的思路,就是尽可能的选择少的路线,并且每条路线最小,保证总里程数最少。

最小生成树

修路问题本质就是就是最小生成树问题, 先介绍一下最小生成树(Minimum Cost Spanning Tree),简称 MST。

给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树

  1. N 个顶点,一定有 N-1 条边
  1. 包含全部顶点

  2. N-1 条边都在图中

  3. 举例说明(如图:)

  4. 求最小生成树的算法主要是普里姆算法和克鲁斯卡尔算法

image-20210825160229203

代码实现

package cn.chasing.Algorithm.prim;

import java.util.Arrays;

/**
 * @author 柴柴快乐每一天
 * @create 2021-08-25  4:07 下午
 * <p>
 * 『Stay hungry, stay foolish. 』
 */
public class PrimAlgorithm {
    public static void main(String[] args) {
        //测试看看图是否创建ok
        char[] data = new char[]{'A','B','C','D','E','F','G'};
        int verNum = data.length;
        //邻接矩阵的关系使用二维数组表示,10000这个大数,表示两个点不联通
        int[][] weight = new int[][] {
                {10000,5,7,10000,10000,10000,2},
                {5,10000,10000,9,10000,10000,3},
                {7,10000,10000,10000,8,10000,10000},
                {10000,9,10000,10000,10000,4,10000},
                {10000,10000,8,10000,10000,5,4},
                {10000,10000,10000,4,5,10000,6},
                {2,3,10000,10000,4,6,10000}
        };
        // 创建MGraph对象
        MGraph graph = new MGraph(verNum, data, weight);
        // 创建一个MinTree对象
        MinTree minTree = new MinTree(graph);
        minTree.showGraphWeight();

        minTree.prim(1);

    }
}

/**
 * 创建最小生成树
 */
class MinTree {
    private MGraph mGraph;

    public MinTree(MGraph mGraph) {
        this.mGraph = mGraph;
    }

    /**
     * 显示图的邻接矩阵
     */
    public void showGraphWeight() {
        for (int[] link : this.mGraph.weight) {
            System.out.println(Arrays.toString(link));
        }
    }

    /**
     * 编写prim算法,得到最小生成树
     * @param v 表示从图的第几个顶点开始生成A->0 B->1
     */
    public void prim(int v) {
        // 标记顶点是否被访问过,默认值为0,表示未访问过
        int[] visited = new int[this.mGraph.verNum];
        // 把当前节点标记已访问
        visited[v] = 1;
        // h1, h2记录联通的两点下标
        int h1 = -1, h2 = -1;
        int minWeight = 10000;
        // prim算法结束后,会产生的 边 = 顶点-1
        for (int e = 0; e < this.mGraph.verNum - 1; e++) {

            // 确定当前生成的 子图<X1, X2> 跟哪一个节点Y 距离最近
            // i 表示已经访问过的节点,j表示未访问的。实际不知道访问没有,直接都遍历,拉进循环判断
            for (int i = 0; i < this.mGraph.verNum; i++) {
                for (int j = 0; j < this.mGraph.verNum; j++) {
                    if (visited[i] == 1 && visited[j] == 0 && mGraph.weight[i][j] < minWeight) {
                        minWeight = mGraph.weight[i][j];
                        h1 = i;
                        h2 = j;
                    }
                }
            }
            // 找到一条边最小了
            System.out.println("边<" + mGraph.data[h1] + ", " + mGraph.data[h2] + "> 权值:" + minWeight);
            // 将当前节点标记为已访问
            visited[h2] = 1;
            minWeight = 10000;
        }
    }
}

class MGraph {

    /**
     * @param verNum 表示图的节点个数
     * @param data 存放节点数据
     * @param weight 存放边,即邻接矩阵
     */
    int verNum;
    char[] data;
    int[][] weight;

    public MGraph() {
    }

    /**
     * 初始化图(村庄)
     * @param verNum 图对应的顶点个数
     * @param data 图各个顶点的值
     * @param weight 图的邻接矩阵
     */
    public MGraph (int verNum, char[] data, int[][] weight) {
        this.verNum = verNum;
        this.data = new char[verNum];
        this.weight = new int[verNum][verNum];

        for (int i = 0; i < verNum; i++) {
            this.data[i] = data[i];
            for (int j = 0; j < verNum; j++) {
                this.weight[i][j] = weight[i][j];
            }
        }
    }
}

克鲁斯卡尔算法

克鲁斯卡尔算法介绍

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

应用场景-公交站问题

看一个应用场景和问题:

image-20210825225432343

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

克鲁斯卡尔算法图解说明

以城市公交站问题来图解说明 克鲁斯卡尔算法的原理和步骤:

在含有 n 个顶点的连通图中选择 n-1 条边,构成一棵极小连通子图,并使该连通子图中 n-1 条边上权值之和达到最小,则称其为连通网的最小生成树。

image-20210825225539751

例如,对于如上图 G4 所示的连通网可以有多棵权值总和不相同的生成树。

image-20210825225608466

以上图 G4 为例,来对克鲁斯卡尔进行演示(假设,用数组 R 保存最小生成树结果)。

image-20210825225736149

image-20210825225752248

  1. 将边<E,F>加入 R 中。

    边<E,F>的权值最小,因此将它加入到最小生成树结果 R 中。

  2. 将边<C,D>加入 R 中。

    上一步操作之后,边<C,D>的权值最小,因此将它加入到最小生成树结果 R 中。

  3. 将边<D,E>加入 R 中。

    上一步操作之后,边<D,E>的权值最小,因此将它加入到最小生成树结果 R 中。

  4. 将边<B,F>加入 R 中。

    上一步操作之后,边<C,E>的权值最小,但<C,E>会和已有的边构成回路;因此,跳过边<C,E>。同理,跳过边<C,F>。将边<B,F>加入到最小生成树结果 R 中。

  5. 将边<E,G>加入 R 中。

    上一步操作之后,边<E,G>的权值最小,因此将它加入到最小生成树结果 R 中。

  6. 将边<A,B>加入 R 中。

    上一步操作之后,边<F,G>的权值最小,但<F,G>会和已有的边构成回路;因此,跳过边<F,G>。同理,跳过边<B,C>。将边<A,B>加入到最小生成树结果 R 中。

此时,最小生成树构造完成!它包括的边依次是:

<E,F> <C,D> <D,E> <B,F> <E,G> <A,B>

克鲁斯卡尔算法分析

根据前面介绍的克鲁斯卡尔算法的基本思想和做法,我们能够了解到,克鲁斯卡尔算法重点需要解决的以下两个问题:

  1. 对图的所有边按照权值大小进行排序。
  2. 将边添加到最小生成树中时,怎么样判断是否形成了回路。

问题一很好解决,采用排序算法进行排序即可。

问题二,处理方式是:记录顶点在"最小生成树"中的终点,顶点的终点是"在最小生成树中与它连通的最大顶点"。

然后每次需要将一条边添加到最小生存树时,判断该边的两个顶点的终点是否重合,重合的话则会构成回路。

如何判断是否构成回路

image-20210825230209356

在将<E,F> <C,D> <D,E>加入到最小生成树 R 中之后,这几条边的顶点就都有了终点:

  • C 的终点是 F。
  • D 的终点是 F。
  • E 的终点是 F。
  • F 的终点是 F。

关于终点的说明:

  1. 就是将所有顶点按照从小到大的顺序排列好之后;某个顶点的终点就是"与它连通的最大顶点"。

  2. 因此,接下来,虽然<C,E>是权值最小的边。但是 C 和 E 的终点都是 F,即它们的终点相同,因此,将<C,E> 加入最小生成树的话,会形成回路。这就是判断回路的方式。也就是说,我们加入的边的两个顶点不能都指向同一个终点,否则将构成回路。

代码实现

package cn.chasing.Algorithm.kruskal;

import java.util.Arrays;

/**
 * @author 柴柴快乐每一天
 * @create 2021-08-25  11:05 下午
 * <p>
 * 『Stay hungry, stay foolish. 』
 */
public class KruskalCaseDemo {
    private static final int INF = Integer.MAX_VALUE;

    public static void main(String[] args) {
        char[] vertexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
        //克鲁斯卡尔算法的邻接矩阵
        int matrix[][] = {
                /*A*//*B*//*C*//*D*//*E*//*F*//*G*/
                /*A*/ {   0,  12, INF, INF, INF,  16,  14},
                /*B*/ {  12,   0,  10, INF, INF,   7, INF},
                /*C*/ { INF,  10,   0,   3,   5,   6, INF},
                /*D*/ { INF, INF,   3,   0,   4, INF, INF},
                /*E*/ { INF, INF,   5,   4,   0,   2,   8},
                /*F*/ {  16,   7,   6, INF,   2,   0,   9},
                /*G*/ {  14, INF, INF, INF,   8,   9,   0}};

        KruskalCase kruskalCase = new KruskalCase(vertexs, matrix);
        kruskalCase.showMatrix();
        kruskalCase.kruskal();

    }
}

class KruskalCase {
    /**边的个数*/
    private int edgeNum;
    private char[] vertexes;
    private int[][] matrix;
    private Edge[] edges;
    private static final int INF = Integer.MAX_VALUE;


    /**
     * 创建最小生成树,实现克鲁斯卡尔算法
     * @param vertexes 顶点数组
     * @param matrix    邻接矩阵
     */
    public KruskalCase(char[] vertexes, int[][] matrix) {
        int vLen = vertexes.length;
        // 初始化顶点,复制拷贝数据的方式
        this.vertexes = new char[vLen];
        for (int i = 0; i < vLen; i++) {
            this.vertexes[i] = vertexes[i];
        }
        //初始化边, 使用的是复制拷贝的方式
        this.matrix = new int[vLen][vLen];
        for(int i = 0; i < vLen; i++) {
            for(int j= 0; j < vLen; j++) {
                this.matrix[i][j] = matrix[i][j];
            }
        }
        // 统计边的条数,AB和BA是一条边,统计上三角,不算对角线,自己跟自己没有算边
        for (int i = 0; i < vLen; i++) {
            for (int j = i+1; j < vLen; j++) {
                if (this.matrix[i][j] != INF) {
                    edgeNum++;
                }
            }
        }
    }

    /**
     * 打印邻接矩阵
     */
    public void showMatrix() {
        System.out.println("邻接矩阵为: \n");
        for(int i = 0; i < vertexes.length; i++) {
            for(int j=0; j < vertexes.length; j++) {
                System.out.printf("%12d", matrix[i][j]);
            }
            System.out.println();//换行
        }
    }

    /**
     * 通过顶点的值返回其对应下标
     * @param c 顶点的值,如 A, B
     * @return  返回c对应的下标,找不到则返回-1
     */
    public int getPosition(char c) {
        for (int i = 0; i < vertexes.length; i++) {
            if (vertexes[i] == c) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 获取图中的边的信息,存放到Edge[]数组中
     * 通过matrix邻接矩阵来获取
     * 数组形如[ ['A','B', 12], ['B','F',7], ..... ]
     */
    public void setEdges() {
        int index = 0;
        this.edges = new Edge[edgeNum];
        for (int i = 0; i < vertexes.length; i++) {
            for (int j = i+1; j < vertexes.length; j++) {
                if (matrix[i][j] != INF) {
                    // i始终比j小,对应后面ends[end1] = end2
                    edges[index++] = new Edge(vertexes[i], vertexes[j], matrix[i][j]);
                }
            }
        }
    }

    /**
     * 对edges数组按照权值进行排序
     */
    public void sortEdges() {
        Arrays.sort(this.edges);
    }

    /**
     * 获取下标为i的顶点的终点下标,用于后面判断两个终点是否相同
     * @param ends  终点数组。数组动态记录各个顶点在加入子图后对应终点是哪个。ends在加入过程中动态更新
     * @param i 传入的顶点的对应下标
     * @return  下标为i的顶点其对应的终点下标
     */
    public int findEnd(int[] ends, int i) {
        // 终点数组ends[0,0,0,0,5,0,0,0,0,0,0,0]
        while (ends[i] != 0) {
            i = ends[i];
        }
        // 节点在未加入子图时候,终点默认就是自己
        return i;
    }

    public void kruskal() {
        int index = 0;
        // 用于保存“已有最小生成树”中的每个顶点在其中的终点,该数组动态更新
        int[] ends = new int[edgeNum];
        // 创建结果数组,保存最后的最小生成树
        Edge[] res = new Edge[vertexes.length-1];

        // 获取图中所有边的集合(12条)
        this.setEdges();
        System.out.println("图的边的集合=" + Arrays.toString(edges) + " 共"+ edges.length);

        // 按照边的权值将边排序
        this.sortEdges();
        System.out.println("图的边的集合=" + Arrays.toString(edges) + " 共"+ edges.length);

        // 遍历edges数组,将边添加到最小生成树中,判断准备加入的边是否会形成回路
        for (int i = 0; i < edgeNum; i++) {
            // 获取边的两个顶点
            int p1 = getPosition(edges[i].start);
            int p2 = getPosition(edges[i].end);
            // 分别获取这两个点对应的终点,ends初始化为[0,0,0,0,0,0,0,0,0,0,0,0]
            int end1 = findEnd(ends, p1);
            int end2 = findEnd(ends, p2);

            if (end1 != end2) {
                // 未构成回路,则替m对应的字符,设置在最小生成树中的终点,如<E, F>。
                /**
                 * @See setEdges() i永远比j小,导致start对应字符永远比end小
                 */
                ends[end1] = end2;
                res[index++] = edges[i];
            }
        }

        // 统计并打印最小生成树
        //<E,F> <C,D> <D,E> <B,F> <E,G> <A,B>。
        System.out.println("最小生成树为");
        for(int i = 0; i < index; i++) {
            System.out.println(res[i]);
        }
    }

}

class Edge implements Comparable<Edge>{
    char start;
    char end;
    int weight;

    /**
     * 对象实例表示一条边
     * @param start 边的一个顶点
     * @param end   边的另一个顶点
     * @param weight    边的权值
     */
    public Edge(char start, char end, int weight) {
        this.start = start;
        this.end = end;
        this.weight = weight;
    }

    @Override
    public int compareTo(Edge o) {
        return this.weight - o.weight;
    }

    @Override
    public String toString() {
        return "Edge [<" + start + ", " + end + ">= " + weight + "]";
    }
}

迪杰斯特拉算法

迪杰斯特拉(Dijkstra)算法介绍

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

迪杰斯特拉(Dijkstra)算法过程

  1. 设置出发顶点为 v,顶点集合 V{v1,v2,vi…},v 到 V 中各顶点的距离构成距离集合 Dis,Dis{d1,d2,di…},Dis集合记录着 v 到图中各顶点的距离(到自身可以看作 0,v 到 vi 距离对应为 di)

  2. 从 Dis 中选择值最小的 di 并移出 Dis 集合,同时移出 V 集合中对应的顶点 vi,此时的 v 到 vi 即为最短路径

  3. 更新 Dis 集合,更新规则为:比较 v 到 V 集合中顶点的距离值,与 v 通过 vi 到 V 集合中顶点的距离值,保留值较小的一个(同时也应该更新顶点的前驱节点为 vi,表明是通过 vi 到达的)

  4. 重复执行两步骤,直到最短路径顶点为目标顶点即可结束

迪杰斯特拉(Dijkstra)算法最佳应用-最短路径

image-20210826150030246

问题描述

  1. 战争时期,胜利乡有 7 个村庄(A, B, C, D, E, F, G) ,现在有六个邮差,从 G 点出发,需要分别把邮件分别送到A, B, C , D, E, F 六个村庄

  2. 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5 公里

  3. 问:如何计算出 G 村庄到 其它各个村庄的最短距离?

  4. 如果从其它点出发到各个点的最短距离又是多少?

思路图解

image-20210826150309957

代码实现

package cn.chasing.Algorithm.dijkstra;

import java.util.Arrays;

/**
 * @author 柴柴快乐每一天
 * @create 2021-08-26  3:09 下午
 * <p>
 * 『Stay hungry, stay foolish. 』
 */
public class DijkstraAlgorithm {
    public static void main(String[] args) {
        char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
        //邻接矩阵
        int[][] matrix = new int[vertex.length][vertex.length];
        // 表示不可以连接
        final int N = 65535;
        matrix[0]=new int[]{N,5,7,N,N,N,2};
        matrix[1]=new int[]{5,N,N,9,N,N,3};
        matrix[2]=new int[]{7,N,N,N,8,N,N};
        matrix[3]=new int[]{N,9,N,N,N,4,N};
        matrix[4]=new int[]{N,N,8,N,N,5,4};
        matrix[5]=new int[]{N,N,N,4,5,N,6};
        matrix[6]=new int[]{2,3,N,N,4,6,N};
        //创建 Graph对象
        Graph graph = new Graph(vertex, matrix);
        //测试, 看看图的邻接矩阵是否ok
        graph.showGraph();
        //测试迪杰斯特拉算法
        graph.dijkstra(6);
        graph.showDijkstra();
    }
}

class Graph {
    private char[] vertex;
    private int[][] matrix;
    private Vertex ver;

    public Graph(char[] vertex, int[][] matrix) {
        this.vertex = vertex;
        this.matrix = matrix;
    }

    public void showDijkstra() {
        ver.showRes();
    }

    public void showGraph() {
        for (int[] ints : matrix) {
            System.out.println(Arrays.toString(ints));
        }
    }

    /**
     * 更新index顶点到其他顶点的距离,更新顶点的前驱顶点
     * 即更新dis,pre_visited
     * @param index 访问顶点的下标
     */
    private void update(int index) {
        // 表示 出发顶点 到index顶点的距离
        int distance = 0;
        // 遍历matrix数组第index行的数据
        for (int i = 0; i < matrix[index].length; i++) {
            distance = ver.getDis(index) + matrix[index][i];
            if (!ver.isVisited(i) && distance < ver.getDis(i)) {
                ver.updateDis(distance, i);
                // 将index节点的前驱节点设置为i
                ver.updatePre(i, index);
            }
        }
    }

    /**
     * 迪杰斯特拉算法实现
     * @param index 出发顶点的下标
     */
    public void dijkstra(int index) {
        this.ver = new Vertex(vertex.length, index);
        // 更新距离和前驱节点
        update(index);
        for (int i = 0; i < vertex.length; i++) {
            // 选择并返回新的访问节点
            index = ver.updateArr();
            // 更新距离和前驱节点
            update(index);
        }
    }
}

class Vertex {
    /**
     * already_arr  记录各个顶点是否访问过
     * pre_visited  每个顶点对应的前一个顶点下标,动态更新
     * dis  记录出发顶点到其他所有顶点的距离,会动态更新,始终保持最短距离
     */
    public int[] already_arr;
    public int[] pre_visited;
    public int[] dis;

    /**
     *
     * @param number    表示顶点的个数
     * @param index 出发顶点对应的下标
     */
    public Vertex (int number, int index) {
        this.already_arr = new int[number];
        this.pre_visited = new int[number];
        this.dis = new int[number];
        // 初始化dis数组,到所有节点距离都为最大值,到自身为0
        Arrays.fill(dis, 65535);
        this.dis[index] = 0;
        // 设置出发节点已被访问
        this.already_arr[index] = 1;
    }

    /**
     * 判断下标为index的顶点是否被访问
     * @param index 下标
     * @return 已被访问则返回true,否则返回false
     */
    public boolean isVisited (int index) {
        return already_arr[index] == 1;
    }

    /**
     * 返回出发顶点到index顶点的距离
     * @param index 顶点下标
     * @return  到该顶点的距离
     */
    public int getDis(int index) {
        return dis[index];
    }

    /**
     * 更新出发顶点到index顶点的距离
     * @param len   新距离
     * @param index 顶点下标
     */
    public void updateDis (int len, int index) {
        dis[index] = len;
    }


    /**
     * 更新index顶点的前驱节点
     * @param pre 新前驱节点
     * @param index 待更新节点
     */
    public void updatePre(int pre, int index) {
        pre_visited[index] = pre;
    }

    /**
     * 继续选择并返回新的访问顶点,但是并没有更换出发节点
     * @return  返回新访问顶点
     */
    public int updateArr() {
        int min = 65535;
        int index = 0;
        for (int i = 0; i < already_arr.length; i++) {
            if (already_arr[i] == 0 && dis[i] < min) {
                min = dis[i];
                index = i;
            }
        }
        // 更新index顶点被访问
        already_arr[index] = 1;
        return index;
    }

    /**
     * 显示最后的结果,即将3个数组的情况输出
     */
    public void showRes() {
        System.out.println("===================");
        // 输出already_arr
        for (int i : already_arr) {
            System.out.print(i + " ");
        }
        System.out.println();
        // 输出pre_visited
        for (int i : pre_visited) {
            System.out.print(i + " ");
        }
        System.out.println();
        // 输出dis
        for (int di : dis) {
            System.out.print(di + " ");
        }
        System.out.println();

        char[] vertexes = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
        int count = 0;
        for (int di : dis) {
            if (di != 65535) {
                System.out.println(vertexes[count++] + "(" + di + ") ");
            } else {
                System.out.println("N ");
            }
        }
        System.out.println();
    }

}

弗洛伊德算法

弗洛伊德(Floyd)算法介绍

  1. 和 Dijkstra 算法一样,弗洛伊德(Floyd)算法也是一种用于寻找给定的加权图中顶点间最短路径的算法。该算法名称以创始人之一、1978 年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名
  2. 弗洛伊德算法(Floyd)计算图中各个顶点之间的最短路径
  3. 迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径。
  4. 弗洛伊德算法 VS 迪杰斯特拉算法:迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点的最短路径;弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径。

弗洛伊德(Floyd)算法图解分析

  1. 设置顶点 vi 到顶点 vk 的最短路径已知为 Lik,顶点 vk 到 vj 的最短路径已知为 Lkj,顶点 vi 到 vj 的路径为 Lij,则 vi 到 vj 的最短路径为:min((Lik+Lkj),Lij),vk 的取值为图中所有顶点,则可获得 vi 到 vj 的最短路径

  2. 至于 vi 到 vk 的最短路径 Lik 或者 vk 到 vj 的最短路径 Lkj,是以同样的方式获得

image-20210827094937947

image-20210827094954849

image-20210827095019051

image-20210827095046107

弗洛伊德(Floyd)算法最佳应用-最短路径

image-20210827095113531

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

代码实现

package cn.chasing.Algorithm.floyd;

import java.util.Arrays;

/**
 * @author 柴柴快乐每一天
 * @create 2021-08-27  9:56 上午
 * <p>
 * 『Stay hungry, stay foolish. 』
 */
public class FloydAlgorithm {
    public static void main(String[] args) {
        // 测试看看图是否创建成功
        char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
        //创建邻接矩阵
        int[][] matrix = new int[vertex.length][vertex.length];
        final int N = 65535;
        matrix[0] = new int[] { 0, 5, 7, N, N, N, 2 };
        matrix[1] = new int[] { 5, 0, N, 9, N, N, 3 };
        matrix[2] = new int[] { 7, N, 0, N, 8, N, N };
        matrix[3] = new int[] { N, 9, N, 0, N, 4, N };
        matrix[4] = new int[] { N, N, 8, N, 0, 5, 4 };
        matrix[5] = new int[] { N, N, N, 4, 5, 0, 6 };
        matrix[6] = new int[] { 2, 3, N, N, 4, 6, 0 };

        //创建 Graph 对象
        Graph graph = new Graph( vertex,matrix);
        //调用弗洛伊德算法
        graph.floyd();
        graph.show();
    }

}

class Graph {
    /**
     * vertex   存放顶点的数组
     * dis  表示从各个顶点到其他结点的距离,动态更新,最后结果储存在此数组中
     * pre  表示各节点的到达目标节点的中间节点
     */
    private char[] vertex;
    private int[][] dis;
    private int[][] pre;

    public Graph (char[] vertex, int[][] matrix) {
        int len = vertex.length;
        this.vertex = vertex;
        this.dis = matrix;
        this.pre = new int[len][len];
        for (int i = 0; i < len; i++) {
            Arrays.fill(pre[i], i);
        }
    }

    public void show() {
        //为了显示便于阅读,我们优化一下输出
        char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
        for (int k = 0; k < dis.length; k++) {
            // 先将pre数组输出的一行
            for (int i = 0; i < dis.length; i++) {
                System.out.printf(vertex[pre[k][i]] + "\t\t\t");
            }
            System.out.println();
            // 输出dis数组的一行数据
            for (int i = 0; i < dis.length; i++) {
                System.out.print("("+vertex[k]+"->"+vertex[i]+"=" + dis[k][i] + ")\t");
            }
            System.out.println();
            System.out.println();
        }
    }

    public void floyd() {
        int len = 0;
        int disLen = dis.length;
        //对中间顶点遍历, k 就是中间顶点的下标 [A, B, C, D, E, F, G]
        for (int k = 0; k < disLen; k++) {
            //从i顶点开始出发 [A, B, C, D, E, F, G]
            for (int i = 0; i < disLen; i++) {
                //到达j顶点  [A, B, C, D, E, F, G]
                for (int j = 0; j < disLen; j++) {
                    /// => 求出从i顶点出发,经过k中间顶点,到达j顶点距离
                    // ik之间也可能还有其他节点
                    len = dis[i][k] + dis[k][j];
                    if (len < dis[i][j]) {
                        // 更新距离
                        dis[i][j] = len;
                        // 更新终点的前驱节点
                        // i到j会经过的中间节点必然是k到j要经过的中间节点
                        pre[i][j] = pre[k][j];
                    }
                }
            }
        }
    }
}

运行结果

image-20210827164600967

马踏棋盘算法

马踏棋盘算法介绍和游戏演示

  1. 马踏棋盘算法也被称为骑士周游问题
  2. 将马随机放在国际象棋的 8×8 棋盘 Board[0~7][0~7]的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部 64 个方格
  3. 游戏演示: http://www.4399.com/flash/146267_2.htm

image-20210827095259491

思路图解

  1. 马踏棋盘问题(骑士周游问题)实际上是图的深度优先搜索(DFS)的应用。
  2. 如果使用回溯(就是深度优先搜索)来解决,假如马儿踏了 53 个点,如图:走到了第 53 个,坐标(1,0),发现已经走到尽头,没办法,那就只能回退了,查看其他的路径,就在棋盘上不停的回溯……

image-20210827095500330

image-20210827095521704

代码实现

package cn.chasing.Algorithm.knightTour;

import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;

/**
 * @author 柴柴快乐每一天
 * @create 2021-08-27  5:09 下午
 * <p>
 * 『Stay hungry, stay foolish. 』
 */
public class KnightTourAlgorithm {
    public static void main(String[] args) {
        KnightTour knightTour = new KnightTour(8, 8);
        int row = 1;
        int column = 1;
        knightTour.knightTour(row-1, column-1, 1);

        for (int[] ints : knightTour.chessBoard) {
            System.out.println(Arrays.toString(ints));
        }
    }
}

class KnightTour {
    /**
     * X    列数
     * Y    行数
     * visited  标记各个位置是否被访问过
     * finished 标记是否所有位置都已被访问
     */
    private int X;
    private int Y;
    private boolean[] visited;
    private boolean finished;
    public int[][] chessBoard;

    public KnightTour(int X, int Y) {
        this.X = X;
        this.Y = Y;
        this.visited = new boolean[X*Y];
        this.finished = false;
        this.chessBoard = new int[X][Y];
    }

    /**
     * 根据当前位置(Point对象),计算马儿还能走哪些位置,并放到一个集合中,最多有8个位置
     * @param curPoint  当前马儿的位置
     * @return  返回能走位置的集合
     */
    public ArrayList<Point> next(Point curPoint) {
        // 创建一个ArrayList
        ArrayList<Point> points = new ArrayList<>();
        // 创建一个Point
        Point p = new Point();
        // 表示马儿可以走p这个位置
        if ((p.x = curPoint.x - 2) >= 0 && (p.y = curPoint.y) >= 0) {
            points.add(new Point(p));
        }
        //判断马儿可以走6这个位置
        if((p.x = curPoint.x - 1) >=0 && (p.y=curPoint.y-2)>=0) {
            points.add(new Point(p));
        }
        //判断马儿可以走7这个位置
        if ((p.x = curPoint.x + 1) < X && (p.y = curPoint.y - 2) >= 0) {
            points.add(new Point(p));
        }
        //判断马儿可以走0这个位置
        if ((p.x = curPoint.x + 2) < X && (p.y = curPoint.y - 1) >= 0) {
            points.add(new Point(p));
        }
        //判断马儿可以走1这个位置
        if ((p.x = curPoint.x + 2) < X && (p.y = curPoint.y + 1) < Y) {
            points.add(new Point(p));
        }
        //判断马儿可以走2这个位置
        if ((p.x = curPoint.x + 1) < X && (p.y = curPoint.y + 2) < Y) {
            points.add(new Point(p));
        }
        //判断马儿可以走3这个位置
        if ((p.x = curPoint.x - 1) >= 0 && (p.y = curPoint.y + 2) < Y) {
            points.add(new Point(p));
        }
        //判断马儿可以走4这个位置
        if ((p.x = curPoint.x - 2) >= 0 && (p.y = curPoint.y + 1) < Y) {
            points.add(new Point(p));
        }
        return points;
    }

    /**
     * 将每一个能走的位置的下一个能走位置个数进行非递减排序,减少回溯的次数
     * @param points    能走的位置集合
     */
    public void sortNext(ArrayList<Point> points) {
        points.sort(new Comparator<Point>() {
            @Override
            public int compare(Point o1, Point o2) {
                // 获取到o1的下一步的所有位置个数
                int count1 = next(o1).size();
                int count2 = next(o2).size();
                if (count1 < count2) {
                    return -1;
                } else if (count1 == count2) {
                    return 0;
                } else {
                    return 1;
                }
            }
        });
    }

    public void knightTour(int row, int column, int step) {
        this.chessBoard[row][column] = step;
        // 标记该位置已访问
        this.visited[row*X+column] = true;
        // 获取当前位置下一步可以走哪些位置
        ArrayList<Point> next = next(new Point(column, row));
        // 对next进行排序,贪心优化
        sortNext(next);
        // 遍历next
        while (!next.isEmpty()) {
            // 取出下一个可以走的位置
            Point p = next.remove(0);
            // 判断该节点是否访问过
            if (!visited[p.y*X + p.x]) {
                // 没有访问过则继续
                knightTour(p.y, p.x, step+1);
            }
        }

        // 判断马儿是否完成任务,如果没有,则将整个棋盘置为0
        // step < X*Y
        //1. 棋盘到目前位置,仍然没有走完
        //2. 棋盘处于回溯状态
        if (step < X*Y && !finished) {
            chessBoard[row][column] = 0;
            visited[row*X + column] = false;
        } else {
            finished = true;
        }
    }

}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值