14.常用10大算法

目录

1.二分查找算法(非递归)
2.分治算法
 2.1 分治算法介绍
 2.2 分治算法解决安诺塔问题
3.动态规划算法
4.KMP算法
5.贪心算法
6.普利姆算法(Prim)
7.克鲁斯卡尔算法(Kruskal)
8.迪杰斯特拉算法(Dijkstra)
9.弗洛伊德算法(Floyd)
10.马踏棋盘算法

1.二分查找算法(非递归)

二分查找算法介绍:

二分查找法只适用于从有序的数列中进行查找(比如数字和字母等),将数列排序后再进行查找。

代码实现(JAVA):

package algorithm;

public class BinarySearch {

    public static void main(String[] args) {
        int[] array = {1, 3, 8 , 10, 11, 67, 100};
        System.out.println(search(array, -8));
    }

    /**
     *
     * @param array 待查找的数组(升序为例)
     * @param key 待查找的值
     * @return 返回找到的值的下标,没找到返回-1
     */
    public static int search(int[] array, int key){
        int left = 0, right = array.length-1;
        while (left <=  right){
            int mid = (left+right)/2;
            if (array[mid] > key){
                right = mid - 1;
            }else if (array[mid] < key){
                left = mid + 1;
            }else {
                return mid;
            }
        }
        return -1;
    }

}

有关二分查找算法的详细设计思想和递归实现方式可以参考我的另一篇博客 8.查找算法 > 3.二分查找

2.分治算法

2.1 分值算法介绍

分治算法概述:

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

分治算法的一些经典应用:

  • 二分搜索
  • 大整数乘法
  • 棋盘覆盖
  • 合并排序
  • 快速排序
  • 线性时间选择
  • 最接近点对问题
  • 循环赛日程表
  • 汉诺塔

分治算法的基本步骤:

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

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

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

if |P|≤n0
  then return(ADHOC(P))
//将P分解为较小的子问题 P1 ,P2 ,…,Pk
for i←1 to k
do yi ← Divide-and-Conquer(Pi)   //递归解决Pi
T ← MERGE(y1,y2,,yk)   //合并子问题
return(T)

其中|P|表示问题P的规模;n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解。ADHOC§是该分治法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过n0时直接用算法ADHOC§求解。算法MERGE(y1,y2,…,yk)是该分治法中的合并子算法,用于将P的子问题P1 ,P2 ,…,Pk的相应的解y1,y2,…,yk合并为P的解。

2.2 分值算法解决汉诺塔问题

汉诺塔的传说:

汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
 
假如每秒钟一次,共需多长时间呢?移完这些金片需要5845.54亿年以上,太阳系的预期寿命据说也就是数百亿年。真的过了5845.54亿年,地球上的一切生命,连同梵塔、庙宇等,都早已经灰飞烟灭。

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

如下图所示的A、B、C三根柱子,有3个盘,要将全部盘移动到另外一根柱子C上,根据分治算法的思路可以这样解决:
在这里插入图片描述
将所有盘看作是两个盘:最下面的一个盘和上面所有盘看作一个盘,于是问题就演变为先将一个整体上面的盘移动到中间辅助柱子B上,再将最底下的盘移动到目的柱子C上,最后再将上面一个整体的盘移动到C柱子;整体上面的盘又可以按照该思路只看成两个盘,不断递归类推。

  1. 将第一个盘(最顶上最短的那个盘)移动到C,将第二个盘移动到B,再将第一个盘移动到B,此时实现将上面整体的那一个盘移动到了中间辅助柱子B:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  2. 然后将第三个盘移动到目的柱子C:
    在这里插入图片描述
  3. 对于B柱子上的之前的整体的一个盘,又可以分为最底下的一个盘,和除去最底下那个盘之后上面所有的盘看成一个整体(虽然这里只有一个盘了)。此时A柱子成了中间辅助柱子,将第一个盘移动到A柱子,实现将上面的整体盘移动到辅助柱子上;再将第二个盘移动到C上,实现将下面的盘移动到目标柱子上。
    在这里插入图片描述
    在这里插入图片描述
  4. 最后再将第一个盘移动到目标柱子C上即完整所有盘的移动:
    在这里插入图片描述

汉诺塔思路总结:

  1. 如果只有一个盘, 则直接 A->C
  2. 如果有 n >= 2 个盘的情况,我们总是可以看做是两个盘 1.最下边的盘 2. 其余上面所有的盘,于是:
    2.1 先把最上面的盘 A->B
    2.2 把最下边的盘 A->C
    2.3 把B的所有盘从 B->C

代码实现(JAVA):

package algorithm;

public class Hanoitower {

    public static void main(String[] args) {
        move(4, 'A', 'B', 'C');
    }

    //汉诺塔的移动的方法
    //使用分治算法
    //a,b,c分别为三根柱子的标识(起始柱子、辅助柱子、目标柱子),num为盘的数量
    public static void move(int num, char a, char b, char c) {
        //如果只有一个盘,则看做直接移动到C
        if(num == 1) {
            System.out.println("第 1 个盘从 " + a + "->" + c);
        } else {
            //如果有 n >= 2  情况,我们总是可以看做是两个盘 1.最下边的一个盘 2. 其余上面所有的盘
            //1. 先把 最上面的所有盘 A->B, 移动过程会使用到 c 作为辅助柱子(a为移动起点,b为目标点)
            move(num - 1, a, c, b);//递归不断移动上面的盘到B
            //2. 把最下边的盘 A->C
            System.out.println("第" + num + "个盘从 " + a + "->" + c);
            //3. 把 B 塔的所有盘 从 B->C , 移动过程使用到 a 作为辅助柱子(b为移动起点,c为目标点)
            move(num - 1, b, a, c);//递归不断再将整体盘移动到C
        }
    }
}

运行结果如下,测试为4个盘,可以自己画图用4个盘校验算法的正确性。
在这里插入图片描述

3.动态规划算法

动态规划算法介绍:

动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算。
 
动态规划算法与分治算法类似,都是把大问题拆分成小问题,通过寻找大问题与小问题的递推关系,解决一个个小问题,最终达到解决原问题的效果。但不同的是,分治法在子问题和子子问题等上被重复计算了很多次,而动态规划则具有记忆性,通过填写表把所有已经解决的子问题答案纪录下来,在新问题里需要用到的子问题可以直接提取,避免了重复计算,从而节约了时间,所以在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到。
 
最优性原理是动态规划的基础,最优性原理是指“多阶段决策过程的最优决策序列具有这样的性质:不论初始状态和初始决策如何,对于前面决策所造成的某一状态而言,其后各阶段的决策序列必须构成最优策略”。
 
动态规划一般解题步骤:问题抽象化、建立模型、寻找约束条件、判断是否满足最优性原理、找大问题与小问题的递推关系式、填表、寻找解组成。

一类简单的动态规划问题——背包问题:

背包问题(Knapsack problem)是在1978年提出的,它都可以类似的描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。背包问题主要有三种分支:1.01背包;2.完全背包;3.多重背包。
 
其中01背包比较容易,(甚至可以用贪心来做,但这里不予考虑)。比如01背包问题:假如有1个背包,背包容量是10,有5个物品,编号为1,2,3,4,5,他们都有各自的重量和价格。要求在不超过背包容量的情况下,使背包装载物品的价值最大。现将问题拆分为五个子问题。
1.背容=10,从1号物品中找出该问题的解
2.背容=10,从1号,2号物品中找出该问题的解
3.背容=10,从1号,2号,3号物品中找出该问题的解
4.背容=10,从1号,2号,3号,4号物品中找出该问题的解
5.背容=10,从1号,2号,3号,4号,5号物品中找出该问题的解

背包问题的解决思路:

我们可以将1,2,3,4,5子问题的答案都存入一张表中。因为求解2子问题,需要用到1子问题的答案(2的每一步方案要与1的每一步方案比较,如果2的该步方案优于1所对应的方案。则将2的这步方案标为可行。如果不优于1的,或者不满足问题的约束条件,则舍弃该方案。继续沿用该步所对应的1的方案作为该步的方案)。求解3子问题,需要用到2子问题的答案,一直依次递推到求解5子问题,需要用到4子问题的答案。而5子问题就是原问题。5子问题的答案就是最终原问题的解。

背包问题的解决步骤:

以上述01背包问题为例,作讲解。给出问题参数:

int n = 5;//物品个数n
int capacity = 10;//背包容量capacity
int[] weight = {2, 2, 6, 5, 4};//物重weight,为了方便描述问题,下标1即为第一个物品的重量,将下标0用0值填充
int[] value= {6, 3, 5, 4, 6};//物价value

根据上述定义,为描述方便,首先定义一些变量:Vi表示第 i 个物品的价值,Wi表示第 i 个物品的重量,定义表格V(i,j):当前背包容量为 j 时前 i 个物品(第i个子问题)最佳组合对应的总价值;同时背包问题抽象化为(X1,X2,…,Xn,其中 Xi 取0或1,表示第 i 个物品选或不选)顺序表。详细步骤如下:

  1. 建立模型,即求max(V1X1+V2X2+…+VnXn)
  2. 寻找约束条件,W1X1+W2X2+…+WnXn<capacity
  3. 寻找递推关系式,面对当前商品有两种可能性:
    3.1. 包的容量比该商品重量小,装不下,此时的价值与第i-1个子问题的价值是一样的,即V(i,j)=V(i-1,j)
    3.2. 还有足够的容量可以装该商品,但可能装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即V(i,j)=max{ V(i-1,j),V(i-1,j-w(i))+v(i) }(其中V(i-1,j-w(i))+v(i)表示在当前j容量下,且能放入当前i物品的情况下,放入当前i物品后的将价值加上之前的放的价值的和,即为j容量下放入i物品后的背包中的总价值)。
    由此可以得出递推关系式:
    如果j<w(i),则V(i,j)=V(i-1,j)
    如果j>=w(i),则V(i,j)=max{ V(i-1,j),V(i-1,j-w(i))+v(i) }
  4. 填表,首先初始化边界条件,V(0,j)=V(i,0)=0;
    在这里插入图片描述
    然后一行一行的填表:
    ● 如,i=1,j=1,w(1)=2,v(1)=6,有j<w(1),故V(1,1)=V(1-1,1)=0;
    ● 又如,i=1,j=2,w(1)=2,v(1)=6,有j=w(1),故V(1,2)=max{ V(1-1,2),V(1-1,2-w(1))+v(1) }=max{0,0+6}=6;
    ● 如此下去,填到最后一个,i=5,j=10,w(5)=4,v(5)=6,有j>w(5),故V(5,10)=max{ V(5-1,10),V(5-1,10-w(5))+v(5) }= max{14,9+6}=15;
    填完表如下图:
    在这里插入图片描述
  5. 表格填完,最优解即是V(number,capacity)=V(5,10)=15。

代码实现(JAVA):

package algorithm;

import java.lang.reflect.Array;
import java.util.Arrays;

public class Dynamic {

    public static void main(String[] args) {
        int n = 5;//物品个数n
        int capacity = 10;//背包容量capacity
        int[] weight = {0, 2, 2, 6, 5, 4};//物重weight,为了方便描述问题,下标1即为第一个物品的重量,将下标0用0值填充
        int[] value = {0, 6, 3, 5, 4, 6};//物价value
        knapsackProblem(n, capacity, weight, value);
    }

    /**
     * 01背包问题
     * @param n 物品个数
     * @param capacity 背包容量
     * @param w 物重
     * @param v 物价
     */
    public static void knapsackProblem(int n, int capacity, int[] w, int[] v){
        // 初始化一个动态规划表V(i,j),i表示第几个物品,j表示当前背包的容量,
        // V[i][j]存储的值为:在背包容量为j时的前i个物品最佳组合放入背包的总价值
        // 这里V表格的横纵分别都多一行、一列的目的是为了将问题1的子问题初始为0,且物品和容量为1是的下标也对应为1,是为了方便描述问题
        int[][] V = new int[n+1][capacity+1];
        // 动态规划填表
        for (int i = 1; i < n + 1; i++) {
            for (int j = 1; j < capacity + 1; j++) {
                if (j < w[i]){
                    V[i][j] = V[i-1][j];
                } else{
                    V[i][j] = Math.max(V[i-1][j], V[i-1][ j-w[i] ] + v[i]);
                }
            }
        }
        // 动态规划表的输出
        System.out.println("动态规划表为:");
        for (int i = 0; i < n + 1; i++) {
            for (int j = 0; j < capacity + 1; j++) {
                System.out.printf("%d\t",V[i][j]);
            }
            System.out.println("");
        }
    }
}

背包问题最优解回溯:

通过上面的方法可以求出背包问题的最优解,但还不知道这个最优解由哪些物品组成,故需要根据最优解回溯找出解的组成,根据填表的原理可以有如下的寻解方式:

  1. V(i,j)=V(i-1,j)时,说明没有装入第 i 个物品,则回到V(i-1,j);
  2. V(i,j)=V(i-1,j-w(i))+v(i)时,说明装入了第i个物品,该物品是最优解组成的一部分,随后我们得回到装该物品之前,即回到V(i-1,j-w(i))
  3. 一直遍历到i=0结束为止,所有解的组成都会找到。

如在上述例子中:

  • 最优解为V(5,10)=15,而V(5,10)!=V(4,10),却有V(5,10)=V(4,10-w(5))+v(5)=V(4,6)+6=9+6=15,所以第5件物品被选中;
  • 并且回到V(4,10-w(5))=V(4,6);有V(4,6)=V(3,6)=V(2,6)=9,所以第4件物品、第3件物品没被选中;
  • 继续回到V(2,6);V(2,6)!=V(1,6),却有V(2,6)=V(1,6-w(2))+v(2)=V(1,4)+3=6+3=9,所以第2件物品被选中;
  • 继续回到V(1,6-w(2))=V(1,4);V(1,4)!=V(0,4),却有V(1,4)=V(0,4-w(1))+v(1)=V(0,2)+6=0+6=6,所以第1件物品被选中;
  • 再继续回到V(0,2), i 此时已经为0了,结束遍历。
    在这里插入图片描述

所以最后放入背包的物品为1、2、5;总重量为8<10,总价值15为最大价值。

代码实现(JAVA):

package algorithm;

import java.lang.reflect.Array;
import java.util.Arrays;

public class Dynamic {

    public static void main(String[] args) {
        int n = 5;//物品个数n
        int capacity = 10;//背包容量capacity
        int[] weight = {0, 2, 2, 6, 5, 4};//物重weight,为了方便描述问题,下标1即为第一个物品的重量,将下标0用0值填充
        int[] value = {0, 6, 3, 5, 4, 6};//物价value
        knapsackProblem(n, capacity, weight, value);
    }

    /**
     * 01背包问题
     * @param n 物品个数
     * @param capacity 背包容量
     * @param w 物重
     * @param v 物价
     */
    public static void knapsackProblem(int n, int capacity, int[] w, int[] v){
        // 初始化一个动态规划表V(i,j),i表示第几个物品,j表示当前背包的容量,
        // V[i][j]存储的值为:在背包容量为j时的前i个物品最佳组合放入背包的总价值
        // 这里V表格的横纵分别都多一行、一列的目的是为了将问题1的子问题初始为0,且物品和容量为1是的下标也对应为1,是为了方便描述问题
        int[][] V = new int[n+1][capacity+1];
        // 动态规划填表
        int i = 0, j = 0;
        for (i = 1; i < n + 1; i++) {
            for (j = 1; j < capacity + 1; j++) {
                if (j < w[i]){
                    V[i][j] = V[i-1][j];
                } else {
                    V[i][j] = Math.max(V[i-1][j], V[i-1][ j-w[i] ] + v[i]);
                }
            }
        }
        //回溯查找最优情况
        i = n;
        j = capacity;
        boolean[] item = new boolean[n+1];//存储最优解情况,如第i个物品放入了背包,则item[i]==true;
        while (i>0){
            if (V[i][j] == V[i-1][j]) {
                item[i] = false;
            }else if (j - w[i] >= 0 && V[i][j] == V[i-1][ j-w[i] ] + v[i]) {
                item[i] = true;
                j = j - w[i];
            }
            i--;
        }
        // 动态规划表的输出
        System.out.println("动态规划表为:");
        for (i = 0; i < n + 1; i++) {
            for (j = 0; j < capacity + 1; j++) {
                System.out.printf("%d\t",V[i][j]);
            }
            System.out.println("");
        }
        //最优解输出
        System.out.println("最优解为:");
        for (i = 0; i < n+1; i++) {
            if (item[i]){
                System.out.printf("%d\t", i);
            }
        }
    }
}

在这里插入图片描述

4.KMP算法

KMP算法介绍:

Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”。KMP算法是一个解决模式串在文本串是否出现过,如果出现过,得到最早出现的位置的经典算法。常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、James H. Morris、Vaughan Pratt三人于1977年联合发表,故取这3人的姓氏命名此算法。

暴力匹配算法:

对于上述提到的问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置。我们最容易想到的方法和最简单的方法是暴力匹配算法:将P与S第一个字符对齐,从P的第一个字符开始,依次与S中对齐的字符匹配,匹配成功则不断比较P后面的字符,匹配失败则将P整体后移一位与S第二个字符对齐,再执行上面的依次比较操作,假设S的索引为 i ,P的索引为 j ;用代码可描述为:

  • 如果当前字符匹配成功,即S[i] == P[j],则i++,j++,继续匹配下一个字符;
  • 如果失配,即S[i]! = P[j],令i = i - j + 1; j = 0。相当于每次匹配失败时,i 回溯到该次匹配的第一个字符的位置后再后移一位,j 被置为0。然后进行下一次整体比较。

暴力配算法很容易想到,也很好理解和实现,但暴力匹配算法效率低下,做了很多多余的比较。 例如下图中文本串S:BBC ABCDAB ABCDABCDABDE和模式串P:ABCDABD,实际上在P串最后一位D和S串的空格不匹配时,前面的字符已经匹配过了,并且知道BCDA不匹配,所以其实就可以利用该信息将P串直接后移4位,而暴力匹配算法则是无脑后移一位来匹配,这和我们利用起来前面匹配时已有的一些信息相比,就多进行了3次循环和匹配。当需要匹配的字符串很庞大和复杂时,可想而知其运行效率会受多大影响。所以KMP解决的正是这个问题
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

部分匹配表:

在详细介绍KMP算法步骤前,需要了解一个KMP中用到的一个关键顺序表:部分匹配表。首先还需要了解前缀、后缀的概念:
在这里插入图片描述
“部分匹配值”就是”前缀”和”后缀”的最长公共元素的长度。 以上述示例中的”ABCDABD”为例:

在这里插入图片描述
”部分匹配”的实质是,有时候,字符串头部和尾部会有重复。比如,”ABCDAB”之中有两个”AB”,那么它的”部分匹配值”就是 2(”AB”的长度)。上述部分匹配值构成的表为部分匹配表。

KMP算法详细步骤:

同样以上述示例为例:
在这里插入图片描述
因为模式串中的字符A跟文本串中的字符B、B、C、空格一开始就不匹配,所以不必考虑结论,直接将模式串不断的右移一位即可,直到模式串中的字符A跟文本串的第5个字符A匹配成功:
在这里插入图片描述
继续往后匹配,当模式串最后一个字符D跟文本串匹配时失配,显而易见,模式串需要向右移动。但向右移动多少位呢?因为模式串中首尾可能会有重复的字符,故通过部分匹配表可得出下述结论:模式串向右移动的位数 = 已匹配字符数 - 失配字符的上一位字符所对应的部分匹配值。因为此时已经匹配的字符数为6个(ABCDAB),然后根据 “部分匹配表” 可得失配字符D的上一位字符B对应的长度值为2,所以根据之前的结论,可知需要向右移动6 - 2 = 4 位:
在这里插入图片描述
在这里插入图片描述
模式串向右移动4位后,发现C处再度失配,因为此时已经匹配了2个字符(AB),且上一位字符B对应的部分匹配值为0,所以向右移动:2 - 0 =2 位:
在这里插入图片描述
A与空格失配,向右移动1 位:继续比较,发现D与C 失配,故向右移动的位数为:已匹配的字符数6减去上一位字符B对应的部分匹配值2,即向右移动6 - 2 = 4 位:
在这里插入图片描述
在这里插入图片描述
这次移动后发现完全匹配成功,过程结束。
 
通过上述匹配过程可以看出,问题的关键就是寻找模式串中最大长度的相同前缀和后缀,即部分匹配值,找到了模式串中每个字符及其之前的字符串的部分匹配值后,便可基于此匹配。而这个部分匹配值便正是代码实现中的next 数组要表达的含义。

代码实现(JAVA):

package algorithm;

import java.util.Arrays;

public class StringMatch {

    public static void main(String[] args) {
        String source = "BBC ABCDAB ABCDABCDABDE";
        String pattern = "ABCDABD";
        System.out.println("暴力匹配结果:" + violentMatch(source,pattern));
        System.out.println("KMP结果:" + KMP(source,pattern));
    }

    /**
     * 字符串暴力匹配
     * @param source 源字符串
     * @param pattern 要匹配的字符串(模式串)
     * @return 匹配成功返回第一个匹配的下标,失败返回-1
     */
    public static int violentMatch(String source, String pattern){
        int i = 0;//source索引
        int j = 0;//pattern索引
        while (i<source.length() && j<pattern.length()){
            if (source.charAt(i)==pattern.charAt(j)){//匹配成功继续匹配后面的字符
                i++;
                j++;
            }else {//匹配失败则j置零,i后移
                i = i - j + 1;
                j = 0;
            }
        }
        if (j == pattern.length()){//j完全匹配完了,则匹配成功,返回第一个匹配的下标
            return i-j;
        }
        //匹配失败则返回-1
        return -1;
    }

    /**
     * KMP算法
     * @param source 源字符串
     * @param pattern 要匹配的字符串(模式串)
     * @return 匹配成功返回第一个匹配的下标,失败返回-1
     */
    public static int KMP(String source, String pattern){

        int[] next = KMPNext(pattern);
        System.out.println("KMP部分匹配表:"+Arrays.toString(next));
        int i = 0;//source索引
        int j = 0;//pattern索引
        while (i<source.length() && j<pattern.length()) {
            if(j==0 || source.charAt(i) == pattern.charAt(j)) {
                i++;
                j++;
            }else {
                j = next[j-1];
            }
        }
        if(j == pattern.length()) {//完全匹配
            return i - j;
        }
        return -1;
    }

    /**
     * 生成KMP算法所需要的部分匹配表
     * 基本思想是:
     * 1.只有一个字符时公共部分元素为0;
     * 2.有两个字符时,如果这个第二个字符和第一个字符匹配上上了,则将指向第一个字符的指针后移;没匹配上则将这个指针依旧指向第一个字符。最后这个指针的索引值刚好是公共部分元素的最大长度;
     * 3.有三个字符时,以ABCDABD为例,此时索引指针值依旧为0,且第三个字符没有和索引指向的字符匹配上,所以其部分匹配值还是为0;
     * 4.以此类推当有5个字符时,此时的第五个字符和索引0指向的A匹配上了,将索引后移一位值为1,此时该值1刚好为有五个字符时的部分匹配值;
     * 6.有6个字符时,B继续和索引1指向的B匹配上了,索引继续后移一位值为2,部分匹配值为2;
     * 7.到第7个字符时,与索引2指向的字符C不匹配,将索引置0,然后七个字符时的部分匹配值确实为0.
     * 8.以此类推
     * @param pattern
     * @return
     */
    public static int[] KMPNext(String pattern){
        int[] next = new int[pattern.length()];
        next[0] = 0; //如果字符串是长度为1,前缀和后缀都为空,公共部分元素长度为0
        for(int i = 1, j = 0; i < pattern.length(); i++) {//i为后缀索引,j为前缀索引
            if (pattern.charAt(i) != pattern.charAt(j)){//不匹配将j置零,需要重新判断匹配数
                j = 0;
            }
            if (pattern.charAt(i) == pattern.charAt(j)) {//匹配则将j后移,其值刚好是公共元素的最大数量
                j++;
            }
            next[i] = j;
        }
        return next;
    }

}

5.贪心算法

贪心算法介绍:

贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅仅是在某种意义上的局部最优解。
 
贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性(即某个状态以后的过程不会影响以前的状态,只与当前状态有关。)所以,对所采用的贪心策略一定要仔细分析其是否满足无后效性。

贪心算法的应用场景:

实际上,贪心算法适用的情况很少。一般对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,就可以做出判断。
 
贪心选择的应用场景一般是:所求问题的整体最优解可以通过一系列局部最优的选择。换句话说,当考虑做何种选择的时候,我们只考虑对当前问题最佳的选择而不考虑子问题的结果。这是贪心算法可行的第一个基本要素。贪心算法以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用贪心算法求解的关键特征。

贪心算法基本思路:

1.建立数学模型来描述问题
2.把求解的问题分成若干个子问题
3.对每个子问题求解,得到子问题的局部最优解
4.把子问题的解局部最优解合成原来问题的一个解
 
示例问题:假设山洞中有 n 种宝物,每种宝物有一定重量 w 和相应的价值 v,毛驴运载能力有限,只能运走 m 重量的宝物,一种宝物只能拿一样,宝物可以分割。那么怎么才能使毛驴运走宝物的价值最大呢?
 
我们可以尝试贪心策略:
(1)每次挑选价值最大的宝物装入背包,得到的结果是否最优?
(2)每次挑选重量最小的宝物装入,能否得到最优解?
(3)每次选取单位重量价值最大的宝物,能否使价值最高?
 
思考一下,如果选价值最大的宝物,但重量非常大,也是不行的,因为运载能力是有限的,所以第 1 种策略舍弃;如果选重量最小的物品装入,那么其价值不一定高,所以不能在总重限制的情况下保证价值最大,第 2 种策略舍弃;而第 3 种是每次选取单位重量价值最大的宝物,也就是说每次选择性价比(价值/重量)最高的宝物,如果可以达到运载重量 m,那么一定能得到价值最大。因此采用第 3 种贪心策略,每次从剩下的宝物中选择性价比最高的宝物。
 
算法设计:
数据结构及初始化:将 n 种宝物的重量和价值并计算其性价比存储在宝物对象中,然后将这些对象存储在顺序表中,按照性价比进行升序排序。根据贪心策略,按照性价比从大到小选取宝物,直到达到毛驴的运载能力。每次选择性价比高的物品,判断是否小于 m(毛驴运载能力),如果小于 m,则放入,sum(已放入物品的价值)加上当前宝物的价值,m 减去放入宝物的重量;如果不小于 m,则取该宝物的一部分 m * p[i],m=0,程序结束。m 减少到 0,则 sum 得到最大值。
 
贪心算法主要是其思想,代码实现比较简单,这里不做实现。

贪心算法的局限:

  • 不能保证求得的最后解是最佳的
  • 不能用来求最大值或最小值的问题
  • 只能求满足某些约束条件的可行解的范围

6.普利姆算法(Prim)

最小生成树:

在图数据结构中,对于含有 n 个顶点的连通图来说可能包含有多种生成树,且生成树中边的数量 = 顶点数 - 1,例如下图中的连通图和它相对应的生成树,可以用于解决实际生活中的问题:假设A、B、C 和 D 为 4 座城市,为了方便生产生活,要为这 4 座城市建立通信。对于 4 个城市来讲,本着节约经费的原则,只需要建立 3 (4-1=3) 个通信线路即可,就如下图中(b)中的任意一种方式。
在这里插入图片描述
但在具体选择采用(b)中哪一种方式时,需要综合考虑城市之间间隔的距离,建设通信线路的难度等各种因素,将这些因素综合起来用一个数值表示,作为这条线路的权值。如下图所示:
在这里插入图片描述
通过综合分析可以得出,对于上图(b)中生成树的几种方案中,选择权值总和为 7 的两种方案最节约经费。
所以,从图的众多生成树中筛选出权值总和最小的生成树,即为该图的最小生成树

普利姆算法的介绍(Prim)和思想:

给定一个连通网,普里姆(Prim)算法可以用以求图的最小生成树。
 
普里姆算法在找最小生成树时,首先将顶点分为两类,一类是在查找的过程中已经包含在树中的(假设为 A 类),剩下的为另一类(假设为 B 类)。对于给定的连通网,起始状态全部顶点都归为 B 类。在找最小生成树时,选定任意一个顶点作为起始点,并将其从 B 类移至 A 类;然后在 B 类中到 A 类中所有顶点的权值最小的邻接顶点,将该邻接顶点从 B 类移至 A 类,如此重复,直到 B 类中没有顶点为止。该过程中所走过的顶点和边就是该连通图的最小生成树。
 
除了普利姆算法(Prim),克鲁斯卡尔(Kruskal)算法也可以用来求最小生成树。

普利姆算法介绍(Prim)的示例演示:

例如,通过普里姆算法查找上图中(a)的最小生成树的步骤为:

  1. 假如从顶点A出发,顶点 B、C、D 到顶点 A 的权值分别为 2、4、2,所以,对于顶点 A 来说,顶点 B 和顶点 D 到 A 的权值最小,假设先找到的顶点 B:
    在这里插入图片描述
  2. 此时以A、B为出发点,找它们的邻接点,顶点 C 到 B 的权值为 3,到 A 的权值为 4;顶点 D 到 A 的权值为 2,到 B 的权值为无穷大(如果之间没有直接通路,设定权值为无穷大)。所以选择权值最小的顶点 D:
    在这里插入图片描述
  3. 最后,只剩下顶点 C,到 A 的权值为 4,到 B 的权值和到 D 的权值一样大,为 3。所以该连通图有两个最小生成树,在算法中任意选择一个即可:
    在这里插入图片描述

代码实现(JAVA):

package algorithm;

public class Prim {

    private class Graph{
        public String[] vertexs;//顶点
        public int[][] edges;//二维数组,记录顶点之间的关系:边
        public int vertexNum;
        public Graph(int vertexNum, String[] vertexs, int[][] edges){
            this.vertexs = vertexs;
            this.edges = edges;
            this.vertexNum = vertexNum;
        }
    }

    private Graph graph;

    public Prim(int vertexNum, String[] vertexs, int[][] edges){
        this.graph = new Graph(vertexNum,vertexs,edges);
    }

    public static void main(String[] args) {
        String[] vertexs = {"A", "B", "C", "D", "E", "F"};
        int[][] edges = new int[][]{
                {0,6,1,5,0,0},
                {6,0,5,0,3,0},
                {1,5,0,5,6,4},
                {5,0,5,0,0,2},
                {0,3,6,0,0,6},
                {0,0,4,2,6,0}
        };
        Prim prim = new Prim(6,vertexs,edges);
        System.out.println("图中各边的权值:");
        prim.showGraph();
        System.out.println("最小生成树为:");
        prim.miniSpanningTree(0);
    }

    /**
     * 普利姆算法
     * @param startV 最小生成树起始顶点
     */
    public void miniSpanningTree(int startV){
        boolean visited[] = new boolean[this.graph.vertexNum];//标记顶点是否为A类(归入生成树中)
        visited[startV] = true;//初始将startV顶点加入生成树
        int sum = 0;//记录最小生成树的权值和
        //每次添加一个顶点到生成树,循环this.graph.vertexNum-1次
        for (int i = 0; i < this.graph.vertexNum-1; i++) {
            int v1 = 0, v2 = 0, min = Integer.MAX_VALUE;//记录顶点的下标、其最小权值邻接点的下标和权值
            for (int j = 0; j < this.graph.vertexNum; j++) {
                //查找已经在树中的顶点的权值最小的邻接点,且该邻接点还没有在树中
                if (visited[j]){
                    for (int k = 0; k < this.graph.vertexNum; k++) {
                        if (this.graph.edges[j][k]!=0 && !visited[k] && this.graph.edges[j][k]<min){//首先是邻接点、然后没有在树中、最后权值还得最小
                            min = this.graph.edges[j][k];
                            v1 = j;
                            v2 = k;
                        }
                    }
                }
            }
            visited[v2] = true;//将该邻接点加入生成树,标记为true
            sum += this.graph.edges[v1][v2];
            System.out.println("["+this.graph.vertexs[v1]+","+this.graph.vertexs[v2]+"]");
        }
        System.out.println("最小生成树权值和为:"+sum);
    }

    public void showGraph(){
        for (int i = 0; i < this.graph.vertexNum; i++) {
            for (int j = 0; j < this.graph.vertexNum; j++) {
                System.out.printf("%d\t",this.graph.edges[i][j]);
            }
            System.out.println("");
        }
    }

}

运行结果:

在这里插入图片描述
修改不同起始点,最小生成树可能不同,但其最小权值和都是一样的。
在这里插入图片描述
在这里插入图片描述

普利姆算法(Prim)总结:

普里姆算法的运行效率只与连通网中包含的顶点数相关,而和网所含的边数无关。所以普里姆算法适合于解决边稠密的网,该算法运行的时间复杂度为:O(n2)。

7.克鲁斯卡尔算法(Kruskal)

克鲁斯卡尔算法(Kruskal)的介绍:

上述的普里姆算法从顶点的角度为出发点,时间复杂度为O(n2),更适合与解决边的绸密度更高的连通网。而克鲁斯卡尔算法,从边的角度求网的最小生成树,时间复杂度为O(eloge)。和普里姆算法恰恰相反,更适合于求边稀疏的网的最小生成树。

克鲁斯卡尔算法(Kruskal)的思想:

对于任意一个连通网的最小生成树来说,在要求总的权值最小的情况下,最直接的想法就是将连通网中的所有边按照权值大小进行升序排序,从小到大依次选择。由于最小生成树本身是一棵生成树,所以需要时刻满足以下两点:

  1. 生成树中任意顶点之间有且仅有一条通路,也就是说,生成树中不能存在回路;
  2. 对于具有 n 个顶点的连通网,其生成树中只能有 n-1 条边,这 n-1 条边连通着 n 个顶点。

所以克鲁斯卡尔算法的具体思路是:将所有边按照权值的大小进行升序排序,然后从小到大一一判断,条件为:如果这个边不会与之前选择的所有边组成回路,就可以作为最小生成树的一部分;反之,舍去。直到具有 n 个顶点的连通网筛选出来 n-1 条边为止。筛选出来的边和所有的顶点构成此连通网的最小生成树。
 
判断是否会产生回路的方法为:在初始状态下给每个顶点赋予不同的标记,对于遍历过程的每条边,其都有两个顶点,判断这两个顶点的标记是否一致,如果一致,说明它们本身就处在一棵树中,如果继续连接就会产生回路;如果不一致,说明它们之间还没有任何关系,可以连接,并在连接后将他们的标记改为一致(如后加入的顶点改为与前一个顶点的标记一致)。

克鲁斯卡尔算法(Kruskal)的示例演示:

例如,使用克鲁斯卡尔算法找下图的最小生成树的过程为:
在这里插入图片描述
首先,在初始状态下,对各顶点赋予不同的标记(用颜色区别),如下图所示:
在这里插入图片描述
对所有边按照权值的大小进行排序,按照从小到大的顺序进行判断,首先是(1,3),由于顶点 1 和顶点 3 标记不同,所以可以构成生成树的一部分,遍历所有顶点,将与顶点 3 标记相同的全部更改为顶点 1 的标记,如下图所示:
在这里插入图片描述
其次是(4,6)边,两顶点标记不同,所以可以构成生成树的一部分,并更新所有相同标记顶点的标记为:
在这里插入图片描述
其次是(2,5)边,两顶点标记不同,可以构成生成树的一部分,并更新所有相同标记顶点的标记为:
在这里插入图片描述
然后最小的是(3,6)边,两者标记不同,可以连接,遍历所有顶点,将与顶点 6 标记相同的所有顶点的标记更改为顶点 3 的标记:
在这里插入图片描述
继续选择权值最小的边,此时会发现,权值为 5 的边有 3 个,其中(1,4)和(3,4)各自两顶点的标记一致,如果连接会产生回路,所以舍去,而(2,3)标记不一致,可以选择,将所有与顶点 2 标记相同的顶点的标记全部改为同顶点 3 相同的标记:
在这里插入图片描述
当选取的边的数量相比与顶点的数量小 1 时,说明最小生成树已经生成。所以最终采用克鲁斯卡尔算法得到的最小生成树为上图所示。

代码实现(JAVA):

package algorithm;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Kruskal {

    private class Graph{
        public String[] vertexs;//顶点
        public int[][] edges;//二维数组,记录顶点之间的关系:边
        public int vertexNum;
        public Graph(int vertexNum, String[] vertexs, int[][] edges){
            this.vertexs = vertexs;
            this.edges = edges;
            this.vertexNum = vertexNum;
        }
    }

    private Kruskal.Graph graph;

    public Kruskal(int vertexNum, String[] vertexs, int[][] edges){
        this.graph = new Kruskal.Graph(vertexNum,vertexs,edges);
    }

    public static void main(String[] args) {
        String[] vertexs = {"1", "2", "3", "4", "5", "6"};
        int[][] edges = new int[][]{
                {0,6,1,5,0,0},
                {6,0,5,0,3,0},
                {1,5,0,5,6,4},
                {5,0,5,0,0,2},
                {0,3,6,0,0,6},
                {0,0,4,2,6,0}
        };
        Kruskal kruskal = new Kruskal(6,vertexs,edges);
        System.out.println("图中各边的权值:");
        kruskal.showGraph();
        kruskal.miniSpanningTree();
    }

    /**
     * 克鲁斯卡尔kruskal算法生成最小生成树
     */
    public void miniSpanningTree(){
        //构造边内部类
        class Edge implements Comparable<Edge>{
            int v1, v2;//边的两个顶点的下标
            int weight;//边的权重
            public Edge(int v1, int v2, int weight){
                this.v1 = v1;
                this.v2 = v2;
                this.weight = weight;
            }
            @Override
            public String toString() {
                return "<" + (v1+1) + "," + (v2+1) + "> = " + weight;
            }

            @Override
            public int compareTo(Edge o) {
                return this.weight-o.weight;
            }
        }
        //首先提取所有边到Edge集合中
        List<Edge> edges = new ArrayList<>();
        for (int i = 0; i < this.graph.vertexNum; i++) {
            for (int j = i+1; j < this.graph.vertexNum; j++) {
                if (this.graph.edges[i][j]>0){
                    edges.add(new Edge(i,j,this.graph.edges[i][j]));
                }
            }
        }
        System.out.println("连通图的边为:\n"+edges);
        Collections.sort(edges);//按权值升序排序
        System.out.println("按边权值升序排序后连通图的边为:\n"+edges);
        //初始化顶点的标记,不同即可
        int[] sign = new int[this.graph.vertexNum];
        for (int i = 0; i < sign.length; i++) {
            sign[i] = i;
        }
        //记录被选中加入到最小生成树中的边
        List<Edge> choosed = new ArrayList<>();
        //遍历所有边,当选中了 顶点数-1 个边后即找到最小生成树
        for (int i = 0; i < edges.size() && choosed.size() < this.graph.vertexNum-1; i++) {
            Edge edge = edges.get(i);
            //标记不同,则不会形成回路
            if (sign[edge.v1] != sign[edge.v2]){
                //选中该边
                choosed.add(edge);
                //并更新新加入生成树顶点的标记
                for (int j = 0; j < this.graph.vertexNum; j++) {
                    if (sign[j] == sign[edge.v2]){
                        sign[j] = sign[edge.v1];
                    }
                }
            }
        }
        System.out.println("最小生成树:\n" + choosed);
    }

    public void showGraph(){
        for (int i = 0; i < this.graph.vertexNum; i++) {
            for (int j = 0; j < this.graph.vertexNum; j++) {
                System.out.printf("%d\t",this.graph.edges[i][j]);
            }
            System.out.println("");
        }
    }

}

运行结果:
在这里插入图片描述

8.迪杰斯特拉算法(Dijkstra)

最短路径情景介绍:

如今出行已经不需要再为找不着路而担心了,车上有车载导航,手机中有导航App。只需要确定起点和终点,导航会自动规划出可行的距离最短的道路。这是最短路径在人们实际生活中最典型的应用。
 
在一个网(有权图)中,求一个顶点到另一个顶点的最短路径的计算方式有两种:迪杰斯特拉(Dijkstra算法)和弗洛伊德(Floyd)算法。迪杰斯特拉算法计算的是有向网中的某个顶点到其余所有顶点的最短路径;弗洛伊德算法计算的是任意两顶点之间的最短路径。
 
最短路径算法既适用于有向网,也同样适用于无向网。下述将主要围绕有向网讲解迪杰斯特拉算法的具体实现。

迪杰斯特拉算法(Dijkstra)的介绍和思想:

Dijkstra算法采用的是一种贪心的策略,声明一个数组dis来保存源点到各个顶点的最短距离,并声明一个集合保存已经找到了最短路径的顶点。

  1. 初始时,原点 s 的路径权重被赋为 0 (dis[s] = 0)。若对于顶点 s 存在能直接到达的边(s,m),则把dis[m]的值设为该边权值,同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大。初始时,已经找到了最短路径的顶点只有 s。
  2. 然后,从dis数组选择还没有确定最短路径的顶点到源点的最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点加入到找到了最短路径顶点的集合中,此时完成找打了第一个顶点到源点的最短路径。
  3. 然后,我们需要看看新加入的顶点是否可以到达其他顶点并且看看通过该顶点后源点到达其他点的路径长度是否比源点直接到达的路径短,如果是,那么就替换这些顶点在dis中的值。
  4. 然后,又从dis中找出最小值,重复上述动作,直到找到了最短路径顶点的集合中包含了图的所有顶点。

迪杰斯特拉算法(Dijkstra)的示例演示:

在这里插入图片描述
如上图所示是一个有向网,在计算 V1 到其它所有顶点之间的最小路径时,迪杰斯特拉算法的步骤为:

  1. 首先第一步先声明一个dis数组,该数组初始化的值为:
    在这里插入图片描述
    已经找到最短路径的顶点的集合初始化为:T = {V1};
  2. 既然是求 v1顶点到其余各个顶点的最短路程,那就先找一个离 1 号顶点最近的顶点。通过数组 dis 可知当前离v1顶点最近,且还没有被标记为找到了到源点的最短路径的是 v3顶点。当选择了v3后,dis[2](下标从0开始)的值就已经从“估计值”变为了“确定值”,即 v1顶点到 v3顶点的最短路程就是当前 dis[2]值。将V3加入到T中。

为什么呢?因为目前离 v1顶点最近的是 v3顶点,并且这个图所有的边都是正数,那么肯定不可能通过第三个顶点中转,使得 v1顶点到 v3顶点的路程进一步缩短了。因为 v1顶点到其它顶点的路程肯定没有 v1到 v3顶点短。

  1. 现在确定了一个顶点到源点v1的最短路径,接下来就要根据这个新入的顶点v3的出度来找其他顶点到源点的最短路径,发现以v3 为弧尾的有: < v3,v4 >,那么我们看看路径:v1–v3–v4的长度是否比v1–v4短,其实这个已经是很明显的了,因为dis[3]代表的就是v1–v4的长度为无穷大,而v1–v3–v4的长度为:10+50=60,所以更新dis[3]的值为60,表示v4通过v3到达源点的路径比v4直接到达源点更近,得到如下结果:
    在这里插入图片描述
  2. 然后,我们又从除dis[2]和dis[0]外的其他值中寻找最小值,发现dis[4]的值最小,通过之前解释的原理,可以知道v1到v5的最短距离就是dis[4]的值,然后,我们把v5加入到集合T中,然后,考虑v5的出度是否会影响我们的数组dis的值,v5有两条出度:< v5,v4>和 < v5,v6>,然后我们发现:v1–v5–v4的长度为:50,而dis[3]的值为60,所以我们要更新dis[3]的值.另外,v1-v5-v6的长度为:90,而dis[5]为100,所以我们需要更新dis[5]的值。更新后的dis数组如下图:
    在这里插入图片描述
  3. 然后,继续从dis中选择未确定的顶点的值中选择一个最小的值,发现dis[3]的值是最小的,所以把v4加入到集合T中,此时集合T={v1,v3,v5,v4},然后,考虑v4的出度是否会影响我们的数组dis的值,v4有一条出度:< v4,v6>,然后我们发现:v1–v5–v4–v6的长度为:60,而dis[5]的值为90,所以我们要更新dis[5]的值,更新后的dis数组如下图:
    在这里插入图片描述
  4. 然后,我们使用同样原理,分别确定了v6和v2的最短路径,最后dis的数组的值如下:
    在这里插入图片描述
  5. 因此,从图中,我们可以发现v1-v2的值为:∞,代表没有路径从v1到达v2。所以我们得到的最后的结果为:
    在这里插入图片描述

代码实现(JAVA):

package algorithm;

import java.util.Arrays;
import java.util.Stack;

public class Dijkstra {

    private class Graph{
        public String[] vertexs;//顶点
        public int[][] edges;//二维数组,记录顶点之间的关系:边
        public int vertexNum;
        public Graph(int vertexNum, String[] vertexs, int[][] edges){
            this.vertexs = vertexs;
            this.edges = edges;
            this.vertexNum = vertexNum;
        }
    }

    private Dijkstra.Graph graph;

    public Dijkstra(int vertexNum, String[] vertexs, int[][] edges){
        this.graph = new Dijkstra.Graph(vertexNum,vertexs,edges);
    }

    public static void main(String[] args) {
        String[] vertexs = {"V1", "V2", "V3", "V4", "V5", "V6"};
        int[][] edges = new int[][]{
                {0, 0, 10, 0, 30, 100},
                {0, 0, 5,  0, 0,  0},
                {0, 0, 0, 50, 0,  0},
                {0, 0, 0,  0, 0,  10},
                {0, 0, 0, 20, 0,  60},
                {0, 0, 0, 0,  0,  0}
        };
        Dijkstra dijkstra = new Dijkstra(6,vertexs,edges);
        System.out.println("图中各边的权值:");
        dijkstra.showGraph();
        dijkstra.findShortestPath(0);
    }

    public void findShortestPath(int S){
        //1.初始化dis数组
        int[] dis = new int[this.graph.vertexNum];
        for (int i = 0; i < dis.length; i++) {
            int weight = this.graph.edges[S][i];
            if (weight!=0){
                dis[i] = weight;
            }else {
                dis[i] = Integer.MAX_VALUE;
            }
        }
        dis[S] = 0;//初始源顶点到自己的最短距离为0
        //2.初始化记录已经确定到源点最短路径顶点的集合,false-未确定,true-确定
        boolean[] visited = new boolean[this.graph.vertexNum];
        visited[S] = true;//初始确定了到S最短距离的顶点为S自己
        //3.记录到源点最短路径上的前驱顶点的下标
        int[] preVertex = new int[this.graph.vertexNum];
        for (int i = 0; i < preVertex.length; i++) {//初始前驱顶点
            if (this.graph.edges[S][i]==0){
                preVertex[i] = -1;
            }
        }
        //遍历dis表,每次确定一个顶点到源点的最短路径
        while (true){
            int k = -1;//记录本次要确定到源顶点距离最短的顶点的下标
            int min = Integer.MAX_VALUE;//记录最短距离值
            for (int i = 0; i < dis.length; i++) {
                if (!visited[i] && min>dis[i]){
                    min = dis[i];
                    k = i;
                }
            }
            if (k == -1) {//所有顶点都确定了到达源点的最短距离
                break;
            }
            //标记该顶点为确定了最短路径
            visited[k] = true;
            //判断该顶点的出度,如果出度顶点经过该k顶点到达源点的距离小于直接到源点的距离,更新dis中的值
            for (int i = 0; i < this.graph.vertexNum; i++) {
                if (this.graph.edges[k][i]!=0 && !visited[i] && dis[k]+this.graph.edges[k][i]<dis[i]){
                    dis[i] = dis[k] + this.graph.edges[k][i];
                    preVertex[i] = k;
                }
            }
        }
        System.out.println("最短路径前驱顶点记录表:"+Arrays.toString(preVertex));
        //输出各顶点到源点的最短路径
        System.out.println("以"+this.graph.vertexs[S]+"为起点到图中各顶点的最短路径为:");
        for (int i = 0; i < this.graph.vertexNum; i++) {
            if (S == i){
                continue;
            }
            System.out.print("源顶点到"+this.graph.vertexs[i]+"的最短路径为:");
            int j = i;
            Stack<String> stack = new Stack<>();//存储逆序路径
            int sum = 0;//记录路径距离总和(权值和)
            if (preVertex[j]==-1){
                System.out.printf("无连通的最短路径\n");
                continue;
            }
            while (preVertex[j]!=-1){
                stack.push(this.graph.vertexs[j]);
                sum += this.graph.edges[preVertex[j]][j];
                j = preVertex[j];
            }
            //逆序输出
            System.out.printf(this.graph.vertexs[S]);
            while (!stack.isEmpty()){
                System.out.printf("->"+stack.pop());
            }
            System.out.println("="+sum);
        }

    }

    public void showGraph(){
        for (int i = 0; i < this.graph.vertexNum; i++) {
            for (int j = 0; j < this.graph.vertexNum; j++) {
                System.out.printf("%d\t",this.graph.edges[i][j]);
            }
            System.out.println("");
        }
    }
}

运行结果:
在这里插入图片描述

9.弗洛伊德算法(Floyd)

弗洛伊德算法(Floyd)的介绍:

上述的迪杰斯特拉算法,主要解决从网(带权图)中某一顶点计算到其它顶点之间的最短路径问题。如果求有向网中每一对顶点之间的最短路径,使用迪杰斯特拉算法的解决思路是:以每一个顶点为源点,执行迪杰斯特拉算法。这样可以求得每一对顶点之间的最短路径。
 
而对于该问题,求有向网中每一对顶点之间的最短路径,另外一种解决算法为:弗洛伊德算法,该算法相比于使用迪杰斯特拉算法在解决此问题上的时间复杂度虽然相同,都为O(n3),但是弗洛伊德算法的实现形式更简单。

弗洛伊德算法(Floyd)的思想:

弗洛伊德的核心思想是:对于网中的任意两个顶点(例如顶点 A 到顶点 B)来说,之间的最短路径不外乎有 2 种情况:

  1. 直接从顶点 A 到顶点 B 的弧的权值为顶点 A 到顶点 B 的最短路径;
  2. 从顶点 A 开始,经过若干个顶点,最终达到顶点 B,期间经过的弧的权值和为顶点 A 到顶点 B 的最短路径。

所以,弗洛伊德算法的核心为:对于从顶点 A 到顶点 B 的最短路径,拿出网中所有的顶点进行如下判断:Dis(A,K)+ Dis(K,B)< Dis(A,B)(其中,K 表示网中所有的顶点;Dis(A,B) 表示顶点 A 到顶点 B 的距离。)。
 
也就是说,拿出所有的顶点 K,判断经过顶点 K 是否存在一条可行路径比直达的路径的权值小,如果式子成立,说明确实存在一条权值更小的路径,此时只需要更新记录的权值和即可。
 
任意的两个顶点全部做以上的判断,最终遍历完成后记录的最终的权值即为对应顶点之间的最短路径。

弗洛伊德算法(Floyd)的示例演示:

在这里插入图片描述
例如在使用弗洛伊德算法计算上图中的任意两个顶点之间的最短路径时,具体实施步骤为:

  1. 首先,记录顶点之间初始的权值,如下表所示:
    在这里插入图片描述
  1. 依次遍历所有的顶点,假设从 V0 开始,将 V0 作为中间点,看每对顶点之间的距离值是否会更小。最终 V0 对于每对顶点之间的距离没有任何改善。因为对于 V0 来说,由于该顶点只有出度,没有入度,所以没有作为中间点的可能。同理,V1也没有可能。
  1. 将 V2 作为每对顶点的中间点,有影响的为 (V0,V3) 和 (V1,V3):例如,(V0,V3)权值为无穷大,而(V0,V2)+(V2,V3)= 60,比之前的值小,相比而言后者的路径更短;同理 (V1,V3)也是如此。更新的表格为:
    在这里插入图片描述
  2. 以 V3 作为中间顶点遍历各队顶点,更新后的表格为:
    在这里插入图片描述
  3. 以 V4 作为中间顶点遍历各队顶点,更新后的表格为:
    在这里插入图片描述
  4. 而对于顶点 V5 来说,和顶点 V0 和 V1 相类似,所不同的是,V5 只有入度,没有出度,所以对各队顶点的距离不会产生影响。最终采用弗洛伊德算法求得的各个顶点之间的最短路径如上图所示。

代码实现(JAVA):

package algorithm;

import java.util.Arrays;
import java.util.Stack;

public class Floyd {

    private class Graph{
        public String[] vertexs;//顶点
        public int[][] edges;//二维数组,记录顶点之间的关系:边
        public int vertexNum;
        public Graph(int vertexNum, String[] vertexs, int[][] edges){
            this.vertexs = vertexs;
            this.edges = edges;
            this.vertexNum = vertexNum;
        }
    }

    private Floyd.Graph graph;

    public Floyd(int vertexNum, String[] vertexs, int[][] edges){
        this.graph = new Floyd.Graph(vertexNum,vertexs,edges);
    }

    public static void main(String[] args) {
        String[] vertexs = {"V0", "V1", "V2", "V3", "V4", "V5"};
        int[][] edges = new int[][]{
                {0, 0, 10, 0, 30, 100},
                {0, 0, 5,  0, 0,  0},
                {0, 0, 0, 50, 0,  0},
                {0, 0, 0,  0, 0,  10},
                {0, 0, 0, 20, 0,  60},
                {0, 0, 0, 0,  0,  0}
        };
        Floyd floyd = new Floyd(6,vertexs,edges);
        System.out.println("图中各边的权值:");
        floyd.showGraph(floyd.graph.edges, floyd.graph.vertexNum, floyd.graph.vertexNum);
        floyd.findShortestPath();
    }

    /**
     * 弗洛伊德算法
     * 其中P二维数组存放各对顶点的最短路径经过的顶点(前驱顶点),D二维数组存储各个顶点之间的权值
     */
    public void findShortestPath(){
        //对P数组和D数组进行初始化
        int[][] D = new int[this.graph.vertexNum][this.graph.vertexNum];
        int[][] P = new int[this.graph.vertexNum][this.graph.vertexNum];
        for (int i = 0; i < this.graph.vertexNum; i++) {
            for (int j = 0; j < this.graph.vertexNum; j++) {
                D[i][j] = this.graph.edges[i][j];
                if (this.graph.edges[i][j]==0){
                    P[i][j] = -1;//初始前驱顶点
                }else {
                    P[i][j] = i;
                }
            }
        }
        //拿出每个顶点作为中间顶点,更新最短路径D
        for (int k = 0; k < this.graph.vertexNum; k++) {
            //对于第k个顶点作为中间顶点来说,遍历网中任意两个顶点i和j,判断其间接的距离是否更短
            for (int i = 0; i < this.graph.vertexNum; i++) {
                for (int j = 0; j < this.graph.vertexNum; j++) {
                    //判断经过顶点k的距离是否更短,如果判断成立,则存储距离更短的路径
                    //必须同时满足k是i的出度,是j的入度,且间接路径还更短,由于数组中不连通的权值规定为0,所以为0时一定需要中间点
                    if (D[i][k]!=0 && D[k][j]!=0 && (D[i][j]==0 || D[i][j] > D[i][k]+D[k][j]) ){
                        D[i][j] = D[i][k]+D[k][j];
                        P[i][j] = k;
                    }
                }
            }
        }
        //输出D表和P表
        System.out.println("弗洛伊德算法后的D表:");
        this.showGraph(D, D.length, D.length);
        System.out.println("弗洛伊德算法后的P表:");
        this.showGraph(P, P.length, P.length);
        //输出任意两个顶点间的最短路径
        for (int i = 0; i < D.length; i++) {
            for (int j = 0; j < D.length; j++) {
                if (i==j){
                    continue;
                }
                System.out.print(this.graph.vertexs[i]+"->"+this.graph.vertexs[j]+"的最短路径为:");
                int p = j;
                Stack<String> stack = new Stack<>();//存储逆序路径
                int sum = 0;//记录路径距离总和(权值和)
                if (P[i][p]==-1){
                    System.out.printf("无连通的最短路径\n");
                    continue;
                }
                while (P[i][p]!=-1){
                    stack.push(this.graph.vertexs[p]);
                    sum += this.graph.edges[P[i][p]][p];
                    p = P[i][p];
                }
                //逆序输出
                System.out.printf(this.graph.vertexs[i]);
                while (!stack.isEmpty()){
                    System.out.printf("->"+stack.pop());
                }
                System.out.println("="+sum);
            }
        }
    }

    public void showGraph(int[][] map, int m, int n){
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                System.out.printf("%d\t",map[i][j]);
            }
            System.out.println("");
        }
    }
}

运行结果:

图中各边的权值:
0	0	10	0	30	100	
0	0	5	0	0	0	
0	0	0	50	0	0	
0	0	0	0	0	10	
0	0	0	20	0	60	
0	0	0	0	0	0	
弗洛伊德算法后的D表:
0	0	10	50	30	60	
0	0	5	55	0	65	
0	0	0	50	0	60	
0	0	0	0	0	10	
0	0	0	20	0	30	
0	0	0	0	0	0	
弗洛伊德算法后的P表:
-1	-1	0	4	0	4	
-1	-1	1	2	-1	3	
-1	-1	-1	2	-1	3	
-1	-1	-1	-1	-1	3	
-1	-1	-1	4	-1	3	
-1	-1	-1	-1	-1	-1	
V0->V1的最短路径为:无连通的最短路径
V0->V2的最短路径为:V0->V2=10
V0->V3的最短路径为:V0->V4->V3=50
V0->V4的最短路径为:V0->V4=30
V0->V5的最短路径为:V0->V4->V5=90
V1->V0的最短路径为:无连通的最短路径
V1->V2的最短路径为:V1->V2=5
V1->V3的最短路径为:V1->V2->V3=55
V1->V4的最短路径为:无连通的最短路径
V1->V5的最短路径为:V1->V2->V3->V5=65
V2->V0的最短路径为:无连通的最短路径
V2->V1的最短路径为:无连通的最短路径
V2->V3的最短路径为:V2->V3=50
V2->V4的最短路径为:无连通的最短路径
V2->V5的最短路径为:V2->V3->V5=60
V3->V0的最短路径为:无连通的最短路径
V3->V1的最短路径为:无连通的最短路径
V3->V2的最短路径为:无连通的最短路径
V3->V4的最短路径为:无连通的最短路径
V3->V5的最短路径为:V3->V5=10
V4->V0的最短路径为:无连通的最短路径
V4->V1的最短路径为:无连通的最短路径
V4->V2的最短路径为:无连通的最短路径
V4->V3的最短路径为:V4->V3=20
V4->V5的最短路径为:V4->V3->V5=30
V5->V0的最短路径为:无连通的最短路径
V5->V1的最短路径为:无连通的最短路径
V5->V2的最短路径为:无连通的最短路径
V5->V3的最短路径为:无连通的最短路径
V5->V4的最短路径为:无连通的最短路径

Process finished with exit code 0

10.马踏棋盘算法

马踏棋盘算法介绍:

马踏棋盘算法也被称为骑士周游问题,将马随机放在国际象棋的8×8棋盘的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格。
在这里插入图片描述
游戏演示地址:马踏棋盘

马踏棋盘游戏实现思路:

马踏棋盘问题(骑士周游问题)实际上是图的深度优先搜索(DFS)的应用。

  1. 第一种方式是可以使用回溯(就是深度优先搜索)来解决,假如马儿踏了53个点,发现已经走到尽头,没办法,那就只能回退了,查看其他的路径,就在棋盘上不停的回溯…该思路类似迷宫问题,可参考我的另一篇博客笔记:迷宫问题-4.2.1 求解一条可行路径
  2. 但上述方法的性能很大程度上取决于其马儿跳的方向的决策,回溯过程会消耗大量资源。所以可以使用贪心算法(greedyalgorithm)对上述算法进行优化:对于可以走的下一步位置,对这个下一步位置按照其可以走的下一步的位置数量进行升序排序,这样调整马儿跳的决策后,很大程度上减少了回溯的过程。

代码实现(JAVA):

package algorithm;

import java.awt.Point;
import java.util.ArrayList;
import java.util.Comparator;

public class HorseChessboard {

    private static int X; // 棋盘的列数
    private static int Y; // 棋盘的行数
    //创建一个数组,标记棋盘的各个位置是否被访问过
    private static boolean visited[];
    //使用一个属性,标记是否棋盘的所有位置都被访问
    private static boolean finished; // 如果为true,表示成功

    public static void main(String[] args) {
        System.out.println("骑士周游算法,开始运行~~");
        //测试骑士周游算法是否正确
        X = 8;
        Y = 8;
        int row = 1; //马儿初始位置的行,从1开始编号
        int column = 1; //马儿初始位置的列,从1开始编号
        //创建棋盘
        int[][] chessboard = new int[X][Y];
        visited = new boolean[X * Y];//初始值都是false
        //测试一下耗时
        long start = System.currentTimeMillis();
        traversalChessboard(chessboard, row - 1, column - 1, 1);
        long end = System.currentTimeMillis();
        System.out.println("共耗时: " + (end - start) + " 毫秒");

        //输出棋盘的最后情况
        for(int[] rows : chessboard) {
            for(int step: rows) {
                System.out.print(step + "\t");
            }
            System.out.println();
        }
    }

    /**
     * 完成骑士周游问题的算法
     * @param chessboard 棋盘
     * @param row 马儿当前的位置的行
     * @param column 马儿当前的位置的列
     * @param step 是第几步 ,初始位置是第1步 
     */
    public static void traversalChessboard(int[][] chessboard, int row, int column, int step) {
        chessboard[row][column] = step;
        visited[row * X + column] = true; //标记该位置已经访问
        //获取当前位置可以走的下一个位置的集合 
        ArrayList<Point> ps = next(new Point(column, row));
        //对ps进行排序,排序的规则就是对ps的所有的Point对象的下一步的位置的数目,进行非递减排序
        sort(ps);
        //遍历 ps
        while(!ps.isEmpty()) {
            Point p = ps.remove(0);//取出下一个可以走的位置
            //判断该点是否已经访问过
            if(!visited[p.y * X + p.x]) {//说明还没有访问过
                traversalChessboard(chessboard, p.y, p.x, step + 1);
            }
        }
        //判断马儿是否完成了任务,使用   step 和应该走的步数比较 , 
        //如果没有达到数量,则表示没有完成任务,将整个棋盘置0
        //说明: step < X * Y  成立的情况有两种
        //1. 棋盘到目前位置,仍然没有走完
        //2. 棋盘处于一个回溯过程
        if(step < X * Y && !finished ) {
            chessboard[row][column] = 0;
            visited[row * X + column] = false;
        } else {
            finished = true;
        }

    }

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

    //根据当前这个一步的所有的下一步的选择位置,进行非递减排序, 减少回溯的次数
    public static void sort(ArrayList<Point> ps) {
        ps.sort(new Comparator<Point>() {

            @Override
            public int compare(Point o1, Point o2) {
                // TODO Auto-generated method stub
                //获取到o1的下一步的所有位置个数
                int count1 = next(o1).size();
                //获取到o2的下一步的所有位置个数
                int count2 = next(o2).size();
                if(count1 < count2) {
                    return -1;
                } else if (count1 == count2) {
                    return 0;
                } else {
                    return 1;
                }
            }

        });
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值