本文主要是介绍算法之广度优先,深度优先,拓扑,贪心,并查集,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- 1 图算法
- 1.1 广度优先搜索
- 1.2 深度优先搜索
- 1.3 拓扑排序
- 1.4 贪心算法
- 1.4.1 定义
- 1.4.2 示例
- 1.5 并查集(不相交集合数据结构)
- 1.5.1 并查集讲解
- 1.5.2 Kruskal算法
- 1.5.3 Prim算法
- 1.8 Bellman-Ford算法
1 图算法
地图数据常常可以用图(Graph)
这类数据结构表示,那么在图结构
中常用的搜索算法也可以应用到路径规划中。
首先是两种针对无权图的基本图搜索算法:深度优先搜索(Depth First Search, DFS
)、广度优先搜索(Breadth First Search, BFS
)。它们的区别在于openlist
(后面介绍)所选用的数据结构类型不同,前者使用栈,后者使用队列;之后引入一种启发式搜索算法:贪婪最佳优先算法(Greedy Best First Search, GBFS
),用来提高搜索效率,但是不能确保找到最优路径;最后介绍两种在路径规划中非常经典的算法:Dijkstra算法
、A*算法
,前者是广度优先算法(BFS)在带权图中的扩展,后者则是在前者中加入启发函数得到的算法,兼顾效率和完备性
https://zhuanlan.zhihu.com/p/346666812?utm_medium=social&utm_oi=1096915858005323776
1.1 广度优先搜索
广度优先搜索算法(Breadth-First Search,BFS
)是一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。BFS
并不使用经验法则算法。
广度优先搜索算法
是图
的基本算法之一,图
是用来保存多对多
的关系的数据结构,相对于树一对多
的关系更为复杂,所以难度也会比树结构难一点,图的存储一般有连接表表示跟链接矩阵
表示,相比来说链接矩阵的方式更为常用,也就是用数组来存储。而广度优先搜索算法其实就是图的遍历过程,数组的遍历大家都会,数组的遍历按照下标的顺序来遍历的,而图的遍历也有自己的方式,图是多对多的关系,我们可以按照各个节点直接的关联关系来遍历他们,这样便衍生出来深度优先跟广度优先,广度优先是先访问与跟当前节点相关联的所有节点,而深度优先则是按照关联关系一层层的遍历。如下图:
public class BFS { private LinkedList list=new LinkedList(); public void BFS(int[][] map){ boolean[] visited=new boolean[map.length]; for (int i = 0; i < visited.length; i++) { if (!visited[i]) { list.addLast(i); while (!list.isEmpty()) { //访问队列里面的第一个节点 int now=(Integer) list.removeFirst(); visited[now]=true; // 将与该节点关联的且没有访问的节点加入到队列中。 for (int j = 0; j < visited.length; j++) { if (map[now][j]==1&&!visited[j]) { list.addLast(j); } } } } } }
}
1.2 深度优先搜索
说完广度优先搜索后,我们来看图的另一种遍历形式,深度优先搜索算法,深度优先总是对刚发现的节点的出发边进行探索,直到该节点的所有出发边都被发现为止。一旦所有的出发边都被发现,搜索就回溯到前驱结点,来搜索前驱结点的出发边。反复进行直到全部遍历。我们用递归
跟栈
两种方式进行实现,其实归根到底递归也是栈实现的。
递归实现:
public class DFS { private boolean[] visited; public void dfs(int[][]map){ visited=new boolean[map.length]; for (int i = 0; i < visited.length; i++) { if (!visited[i]) { core(map, i); } } } public void core(int[][]map,int node){ System.out.println(node); visited[node]=true; for (int i = 0; i < visited.length; i++) { if (map[node][i]==1&&!visited[i]) { core(map,i); } } }
}
栈实现:
private LinkedList list=new LinkedList(); private boolean[] visited; public void dfs(int[][] map){ visited=new boolean[map.length]; for (int i = 0; i < visited.length; i++) { if (!visited[i]) { list.addLast(i); while (!list.isEmpty()) { int now=(Integer) list.getLast(); visited[now]=true; System.out.println(now); int link=getlink(map, now); if (link!=-1) { list.add(link); }else { list.removeLast(); } } } } } public int getlink(int[][]map,int node){ for (int i = 0; i < map.length; i++) { if (map[node][i]==1&&!visited[node]) { return i; } } return -1; }
1.3 拓扑排序
拓扑排序
是图中所有节点的一种线性次序,首先拓扑排序是对有向无环图
来说的,如果不满足这个条件则无法拓扑排序,拓扑排序
就是遵循一定的规则对节点的排序,规则就是假设在图中有一条边是从a节点
出发指向b节点
,则在拓扑排序中b节点
不可能排在a的前面。其实就是一直寻找入度为0的节点。我们也可以理解成b的完成必须依赖a的完成,a没有完成则b无法进行,其实这样理解的话我们先前说的线性规划也是一种拓扑排序,像我们平时的起床过程也是一种拓扑排序。
拓扑排序的实现:
public class TopoSort { public void topsort(int[][]map){ int[] into=new int[map.length]; for (int i = 0; i < map.length; i++) { for (int j = 0; j < map.length; j++) { if (map[i][j]==1) { into[j]++; } } } for (int i = 0; i < into.length; i++) { int j=0; while (into[j]!=0) { j++; if (j>into.length) { return; } } System.out.println(j); into[j]=-1; for (int k = 0; k < into.length; k++) { if (map[j][k]==1) { into[k]--; } } } }
1.4 贪心算法
1.4.1 定义
贪心算法相比动态规划更简单,也更高效。它总是做出局部最优选择,希望这样可以得到全局的最优选择。所以这种方法不能保证得到最优解,但是很多问题却都可以用这种方法。
贪心算法是一种基于贪心策略
的算法,它在每一步选择中都采取当前状态下最优选择
,以期望最终得到全局最优解。贪心算法通常用于求解最优化问题,如最小生成树、最短路径、背包问题等。
贪心算法的基本思想是:每一步都选择当前最优解,不考虑未来的后果
。这种贪心策略可能会导致局部最优解,但并不一定能得到全局最优解。因此,在使用贪心算法时,需要证明贪心策略的正确性,即证明每一步的最优解能够导致全局最优解。
以下是一个简单的贪心算法示例:
假设有一组硬币,面值分别为1、5、10、50、100元,现在需要用最少的硬币凑出总值为200元的钱数。使用贪心算法,每一步都选择当前面值最大的硬币,直到凑出总值为200元为止。
具体步骤如下:
- 选择面值最大的硬币100元,剩余金额为100元;
- 选择面值最大的硬币100元,剩余金额为0元;
- 总共使用了2枚硬币,得到最优解。
在这个例子中,贪心算法得到了全局最优解,因为每一步选择的最优解都能够导致全局最优解。但是,对于其他问题,贪心算法可能会得到局部最优解,而不是全局最优解。因此,在使用贪心算法时,需要仔细分析问题,证明贪心策略的正确性。
1.4.2 示例
假设我们有n个活动,只有一个教室,求在这个教室中一天最多可以举办多少活动(同一时间只能举办一个活动)。下面给出的是活动的开始时间跟结束时间。
输入:按每个活动完成时间排序(升序),开始时间数组S,完成时间数组F
输出:表示最大活动数的数组{X1,X2,…Xn},其中Xi=0(没选择)或者1(选择)
活动序号(i) | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|
活动开始(Si) | 1 | 3 | 0 | 5 | 3 | 5 | 6 | 8 | 8 | 2 | 12 |
活动结束(Fi) | 4 | 5 | 6 | 7 | 9 | 9 | 10 | 11 | 12 | 14 | 16 |
动态规划:
public class DPActity { public void actity(){ Scanner sc=new Scanner(System.in); int[][] map=new int[25][25]; for (int i = 0; i < 11; i++) { map[sc.nextInt()][sc.nextInt()]=1; } for(int i=1;i<map.length;i++){ for(int j=0;j<=i;j++){ if(map[j][i]==1){ map[0][i]=max(map[0][i-1], 1+map[0][j]); }else{ map[0][i]=max(map[0][i-1],map[0][i]); } } } System.out.println(map[0][24]); } public int max(int a,int b){ if(a>b){ return a; }else{ return b; } } public static void main(String[] args) { new DPActity().actity(); } }
虽然这样写的感觉代码也不多,但是如果我们用贪心的做法你会发现非常简单,而且好理解。因为我们的数值倒是按照结束时间排序的,所以一直选择结束时间最短的则获得最优解。在这里选择结束时间最早的活动就是我们的决策点。而贪心算法关键就是在选择决策点上。顺便一提像01背包问题不能用贪心算法来解但是分数 背包可以解决。
public void greedy(int[]s,int[]f){ int n=s.length; int k=1; for (int i = 2; i < n; i++) { if (s[i]>=f[k]) { k=i; } } System.out.println(k); }
1.5 并查集(不相交集合数据结构)
1.5.1 并查集讲解
在java
中经典的数据结构基本都给实现好了,我们可以直接调用,但是并查集
这种数据结构却没有很好的替代工具
,在这里我们自己去实现并查集数据结构
。首先我们先要去了解什么是并查集
并查集
是一种非常经典常用的数据结构。像求连通子图跟我们下面要研究的最小生成树问题都会用到并查集。
并查集
:就是能够实现若干个不相交集合
,较快的合并
和判断
元素所在集合的操作。不相交集合
:一个不相交集合维护了一个不相交动态集合{s1,s2,s3……}
我们用一个代表标示每一个集合,他是这个集合的成员。我们不关心哪个成员作为代表,仅关心2次查询这个集合时放回结果应该相同(如果我们不修改集合)。
并查集主要就有三个方法:
make-set(x)
:建立一个新集合唯一元素就是x
,因为是不相交集合所以x
不会出现在其他集合中union(x,y)
:将包含x
和y
的2个动态集合合并成一个新集合。find-set(x)
:返回一个指针,这个指针指向包含x
的集合代表
这样说是不是有点懵,我们来看一个图。
上图两棵树就是一个不相交集合,a数的代表就是a而e树代表就是e,我们在a树上查找b则返回a而查找c也返回a说明b与c在同一结合上,而查找f返回e,说明b与e是在两个结合上,他们两个是不相交的。而union
操作就是将两颗树合并成一棵树。
我们先给出一个一般的实现,数组表示不相交集合保存的各个集合的元素。初始化时:
每个元素都指向自己的父节点也就是自己。这种方式每个节点都指向自己的上一节点。而只有代表节点指向的是自己。所以我们根据这个来搜索代表节点。
public class Ufsarry { private int[] node; private int max; public void makeset(int n){ node=new int[n+1]; max=n; //所有的节点都是不相交集合,代表节点都指向自己。 for (int i = 0; i < node.length; i++) { node[i]=i; } } public int findset(int x){ while (x!=node[x]) { x=node[x]; } return x; } public void union(int x,int y){ node[x]=y; }
}
下面我们要说的就是并查集优化
,按秩合并
与路径压缩
。听着好像很高深,其实很哄人的。两张图就可以解释。
第一张图就是路径压缩
,我们之前都是指向的上一节点,而压缩
则是直接指向代表节点。
按秩合并
则是我们外加一个属性来记录集合的秩。而秩多的则说明树比较高,我们将矮的树嫁接在高的树上,这样总的高度较小。而如果高度相同,则只需要将一个树嫁接过去,给他的秩加一即可。
public class Ufs { private int[] father; private int[] rank; public void makeset(int x){ father[x]=x; rank[x]=0; } public int findset(int x){ if (father[x]!=x) { father[x]=findset(father[x]); } return father[x]; } public void union(int x,int y){ x=findset(x); y=findset(y); if (x==y) { return; }else { if (rank[x]>rank[y]) { father[y]=x; }else{ if (rank[x]==rank[y]) { rank[y]++; } father[x]=y; } } }
}
优化后的代码并不比之前的代码复杂。我们这里是用的数组来实现的,当然也可以用链表来实现。我们下面要说的Kruskal算法
的效率就要看,我们写的并查集的实现。而按秩合并与路径压缩是目前已知渐进时间最快的Kruskal算法
实现方式。
1.5.2 Kruskal算法
说完并查集我们接着再来看这个Kruskal
算法(克鲁斯卡尔算法
)
什么是最小生成树呢,很形象的一个形容就是铺自来水管道,一个村庄有很多的农舍,其实这个村庄我们可以看成一个图,而农舍就是图上的每个节点,节点之间有很多的路径,而铺自来水管道目的就是为了让每家都能用上自来水,而至于自来水怎么铺就不关心了,而铺管子的人就会想如何才能最节省材料,那么最省材料的一条路径就是我们这个图的最小生成树。
而如何去构建一个最小生成树呢?这个就用到了我们之前说的贪心策略
。 这里的觉得点就是一直寻找安全边,所以构建最小生成树的过程可以描述成, 循环一直寻找安全边加入到树中,直到树中包含所有节点
什么是安全边?
安全边的定义就是假设
集合A
是我们的最小生成树的子集,每一步都是选择一条边是A的还是最小生成树的子集,则那条边就是安全边
根据安全边选择策略
不同有两种最短路径算法,分别是Kruskal算法
跟prim算法
。我们先来说Kruskal算法
。首先我们先看一下图
大家可能已经看出来了,kruskal算法
寻找安全边的方式,就是在所有的边中找最小的表
,只要两个节点是两个不相交集合
,就加入到最小生成树
中,直到所有的节点都连接起来
大体伪代码描述:
- 循环所有的边;
- 构建一个最小优先队列;
- 构建一个并查集;
- 直到构建成最小生成树{ 从优先队列取出一个值,判断两个节点是不是不相交集合,是否加入到最小树中。 }
- 结束
javabean示例:
public class Bian implements Comparable<Bian>{ private int left; private int right; private int value; public Bian(int i, int j, int k) { this.left=i; this.right=j; this.value=k; }此处省去getset方法@Override public int compareTo(Bian o) { if (this.getValue()>o.getValue()) { return -1; } return 1; }
}
并查集
class Ufs{ private int[] father; private int[] rank; public void makeset(int max) { father = new int[max]; rank = new int[max]; for (int i = 0; i < father.length; i++) { father[i] = i; } } public void union(int x, int y) { if (rank[x] > rank[y]) { father[y] = x; } else { if (rank[x] == rank[y]) { rank[y]++; } father[x] = y; } } public int findset(int x) { if (father[x] != x) { father[x] = findset(father[x]); } return father[x]; }
}
核心代码其实就是一个循环过程,而之前的代码也全是在先前的准备工作
public void kruskal(int[][] map) { Ufs ufs=new Ufs(); ufs.makeset(map.length); ArrayList list=new ArrayList(); for (int i = 0; i < map.length; i++) { for (int j = 0; j < map[i].length; j++) { if(map[i][j]!=0){ Bian b=new Bian(i,j,map[i][j]); list.add(b); } } } int max=0; while(max<=map.length-1 && list.size()!=0 ){ Collections.sort(list); Bian b=(Bian) list.remove(0); int x=b.getLeft(); int y=b.getRight(); if(ufs.findset(x)!=ufs.findset(y)){ ufs.union(x, y); System.out.println("连接"+x+","+y+"路径长度为"+b.getValue()); max++; } } }
1.5.3 Prim算法
prim算法
是另一种最小生成树算法。他的安全边选择策略跟kruskal
略微不同,这点我们可以通过一张图先来了解一下。
prim算法
的安全边是从与当前生成树 相连接
的边中选择一条最短的一条,并且该边是应是生成树与生成树外一点的连接
所以我们prim算法
用汉字描述的过程应为:
- 初始化
- 构造最小优先队列,将所有节点都加入到最小优先队列中,所有节点的
key
设置为无穷大,开始节点设置成0
。 - 循环,直到队列为空
{取出key
值最小的节点加入到生成树中,变量与key
相连接的边,看是否属于树中的节点相连,如果不是且key
值比队列中节点的key
值小则更新队列中该节点的key值
,最小优先队列排序} - 结束
下面的代码就是按照刚才的过程实现的,有很多需要优化的地方,当然算法还需要根据具体的题目,选择最好的写法,这里只是给出思想。
public class Prim { private int MAX=1000; private int NULL=-1; public void prim(int[][]map){ Node[] nodes=new Node[map.length]; ArrayList list=new ArrayList(); for (int i = 1; i < map.length; i++) { list.add(new Node(i, NULL, MAX)); } list.add(new Node(0,NULL,0)); while(!list.isEmpty()){ Collections.sort(list); Node n=(Node) list.remove(1); System.out.println(n.getValue()+","+n.getLink()+","+n.getKey()); nodes[n.getValue()]=n; for (int i = 0; i < map.length; i++) { if(map[n.getValue()][i]!=0&&nodes[i]==null){ for (Object o : list) { Node node=(Node) o; if(node.getKey()==i&&map[n.getLink()][i]<node.getValue()){ node.setLink(n.getValue()); node.setKey(map[n.getLink()][i]); } } } } } }
}
class Node implements Comparable<Node>{ private int value; private int link; private int key; 省去getset方法public Node(int value, int link, int key) { this.value = value; this.link = link; this.key = key; } @Override public int compareTo(Node o) { if (this.key>o.getKey()) { return -1; } return 1; } }
1.8 Bellman-Ford算法
Bellman-Ford算法
(音译贝尔曼福特算法
),单源最短路径算法,它是图算法
之一,前面说的贪心,图的遍历,动态规划都是它的基础,单源最短路径其实说的就是图中节点到节点的最短路径。就像我们使用百度地图从哪到哪一样,找出最近的距离,而单源最短路径问 题不只是两点之间的路径,他有很多的变形,像单目的地最短路径问题,单节点对最短路径问题,所有节点对最短路径问题,最短路径的最优子结构问题。
在介绍这类算法之前我们先规定节点的基本属性,我们规定节点都有一个key值
,key值
记录的是 开始节点到本节点的最小距离
,每个节点
也都有一个p指针
指向它的前驱节点
。这里我们规定一个操作叫做松弛操作
,我们的算法也是最终基于这个操作的。 松弛操作就是去更新key的值
节点B
的key
值为8
,表示从开始节点到B节点之前的最短估计距离是8,而节点A
的key
值3
,是说从开始节点到A节点最短估计是3,当我们发现这个边 时,从A到B的距离比较近,所以我们去更新B的key值,同时把B的前驱节点设置成A。这个过程就是松弛操作
我们说的Bellman-Ford算法
是最简单的算法,就是从开始节点开始循环每一条边,对他进行松弛操作。最后得到的路径就是最短路径
public class BellmanFord { private int[] rank; private int max=1000; public boolean bellmanford(int[][]map,int start,int end){ init(map.length, start); for (int i = 0; i < map.length; i++) { for (int j = 0; j < map[i].length; j++) { if (map[i][j]!=0) { relex(i,j,map[i][j]); } } } for (int i = 0; i < map.length; i++) { for (int j = 0; j < map.length; j++) { if (rank[j]>rank[i]+map[i][j]) { return false; } } } return true; } public void init(int max,int start){ rank=new int[max]; for (int i = 0; i < rank.length; i++) { rank[i]=max; } rank[start]=0; } public void relex(int s,int e,int length){ if(rank[e]>rank[s]+length){ rank[e]=rank[s]+length; } }
}
这篇关于算法之广度优先,深度优先,拓扑,贪心,并查集的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!