贪心算法(四)——最小代价生成树
问题描述
n个村庄间架设通信线路,每个村庄间的距离不同,如何架设最节省开销?
这个问题中,村庄可以抽象成节点,村庄之间的距离抽象成带权值的边,要求最节约的架设方案其实就是求如何使用最少的边、最小的权值和将图中所有的节点连接起来。 这就是一个最小代价生成树的问题,可以用Prim算法或kruskal算法解决。
- PS1:无向连通图的生成树是一个极小连通子图。
- PS2:生成树是图的一个子图,包括所有的顶点和最少的边(n-1条边)。
- PS3:最小代价生成树就是所有生成树中权值之和最小的那个。
算法思路
算法的目标很明确,就是要在n个节点的图中,找出n-1个节点,并且节点之间连线的权值是最小的。因此总体思路如下:
/**
* @param a:图的邻接矩阵
*/
EdgeSet spanningTree(int[][] a){
// 结果集(边的集合)
EdgeSet solution = new EdgeSet();
// 选出n-1条边
int i = 0;
while( i<n && 还有未检查的边 ){
// 选出一条边
Edge edge = Select(a);
// 判断是否有回路
if ( !hasLoop(edge) ) {
solution.add( edge );
}
}
return solution;
}
上述为最小代价生成树的总体思路,其中选边方式(贪心准则)的不同,就产生不同的最小代价生成树算法。
图的邻接表示法
边节点
一个边节点有一条边 和 一个终止节点组成。
/**
* 边节点(由一条边和一个终止节点构成)
*/
class ENode{
int id;// 终止节点的编号
int weight;// 边的权重
}
图的邻接表示
图用一个Map< String,List>表示,其中String表示节点的编号,List中存储以该节点为起点的所有边节点。
Map<String,List<ENode>>
Prim算法
贪心准则:将整个图分成两部分,一部分已选入生成树,另一部分在生成树之外。每次选的边要求一头在生成树之内,一头在生成树之外,并保证当前边是满足上述条件中最短的一条。重复上述操作,直到选出n-1条边为止。
数据结构
- mark: Map<String,Boolean> mark = new HashMap<>(); 记录指定节点是否已在生成树中。 key表示节点编号,value为boolean型,表示是否已选入生成树中。
- nearest: Map<String,String> nearest; 用于记录最小代价生成树的那条路径。 key表示指定节点的编号; value表示在最小代价生成树中,该节点的前驱节点的编号。
- lowcost: Map<String,Integer> lowcost; 记录指定节点为终点的边的最小权值。 key表示指定节点的编号; value表示在最小代价生成树中,以该节点为终点的边的权值。
- k节点: 最新选入生成树的节点。
算法过程
第一步: 首先初始化数组: 1. mark的值全为false 2. nearest的值全为-1 3. lowcost的值全为Integer.MAX_VALUE。
mark[1] |
mark[2] |
mark[3] |
mark[4] |
mark[5] |
mark[6] |
---|---|---|---|---|---|
false |
false |
false |
false |
false |
false |
lowcost[1] |
lowcost[2] |
lowcost[3] |
lowcost[4] |
lowcost[5] |
lowcost[6] |
---|---|---|---|---|---|
MAX |
MAX |
MAX |
MAX |
MAX |
MAX |
nearest[1] |
nearest[2] |
nearest[3] |
nearest[4] |
nearest[5] |
nearest[6] |
---|---|---|---|---|---|
-1 |
-1 |
-1 |
-1 |
-1 |
-1 |
第二步:
- 将节点1作为起点选入生成树,记为k,mark[1]=true;
- 遍历节点k的所有相邻节点,更新lowcost数组和nearest数组: 设j是节点k相邻节点,并且如果< k,j>这条边的权值小于lowcost[j],则更新lowcost[j]=w< k,j>、nearest[j]=k。
- 在lowcost数组中找到那个权值最小,且不在生成树中的边的节点,将它加入生成树中: 3.1. 遍历lowcost,找出最小值; 3.2. 将该最小值对应的lowcost下标(节点编号)的mark设为true; 3.3. 更新k;
mark[1] |
mark[2] |
mark[3] |
mark[4] |
mark[5] |
mark[6] |
---|---|---|---|---|---|
true |
false |
false |
false |
false |
false |
lowcost[1] |
lowcost[2] |
lowcost[3] |
lowcost[4] |
lowcost[5] |
lowcost[6] |
---|---|---|---|---|---|
MAX |
6 |
1 |
5 |
MAX |
MAX |
nearest[1] |
nearest[2] |
nearest[3] |
nearest[4] |
nearest[5] |
nearest[6] |
---|---|---|---|---|---|
-1 |
1 |
1 |
1 |
0 |
0 |
第三步:
- 此时将节点3记为k;
- 依次遍历与k节点相邻的所有不在生成树中的节点,并更新nearest数组和lowcost数组;
- 遍历lowcost数组,找出尚未选中的最短的边,将该边的终点设为true,并设为k,一直循环下去,直到选出n-1条边为止。
代码实现
/**
* prim算法
* @param graph:图的邻接矩阵
*/
void prim(Map<String,List<Edge>> graph){
// 初始化
Map<String,String> nearest = new HashMap<>();
Map<String,Integer> lowcost = new HashMap<>();
Map<String,Boolean> mark = new HashMap<>();
String k = null;
String end = null;// 记录最后一个节点的id,用于从后向前输出结果
for( String id : graph.keySet() ){
nearest.put( id, null );
lowcost.put( id, Integer.MAX_VALUE );
mark.put( id, false );
k = id;
}
mark.put( id, true );
// 寻找生成树的n-1条边
for(int i=1; i<=graph.size()-1; i++){
// 更新与k相邻的nearest
List<ENode> edges = graph.get( k );
for( ENode edge : edges ){
if ( !mark.get(edge.id) && edge.w < lowcost.get(edge.id) ) {
lowcost.put( edge.id, edge.w );
nearest.put( edge.id, k );
}
}
// 寻找当前lowcost中最短的边
int min = Integer.MAX_VALUE;
for( Map.Entry<String,Integer> entry : lowcost.entrySet() ){
if ( entry.getValue() < min ) {
min = entry.getValue();
k = entry.getKey();
}
}
mark.get( k, true );
end = k;
}
// 输出结果
for ( int i=0; i<graph.size(); i++ ) {
System.out.println( nearest.get(end)+"-"+end+"权值:"+lowcost.get(end) );
end = nearest.get(end);
}
}
时间复杂度
若图中共有n个节点,那么Prim算法的时间复杂度为O(n^2)。
Kruskal算法
贪心准则:将所有的边按照权值递增的顺序排序,每次选一条权值最小的边纳入生成树中,若没有环路则选边成功,若有环路,则选下一条次小的边,直到选满n-1条边为止。
- Golang学习-第三篇 认识Web框架
- Golang学习-第二篇 搭建一个简单的Go Web服务器
- 数据说话:Go语言的Switch和Map性能实测
- Dora.Interception, 为.NET Core度身打造的AOP框架[4]:演示几个典型应用
- Dora.Interception, 为.NET Core度身打造的AOP框架[3]:Interceptor的注册
- Dora.Interception, 为.NET Core度身打造的AOP框架:不一样的Interceptor定义方式
- Dora.Interception,为.NET Core度身打造的AOP框架:全新的版本
- ASP.NET Core的路由[4]:来认识一下实现路由的RouterMiddleware中间件
- 浅谈 Java 并发编程中的若干核心技术
- ASP.NET Core的路由[3]:Router的创建者——RouteBuilder
- ASP.NET Core的路由[2]:路由系统的核心对象——Router
- ASP.NET Core的路由[1]:注册URL模式与HttpHandler的映射关系
- 学习ASP.NET Core, 怎能不了解请求处理管道[6]: 管道是如何随着WebHost的开启被构建出来的?
- 学习ASP.NET Core, 怎能不了解请求处理管道[5]: 中间件注册可以除了可以使用Startup之外,还可以选择StartupFilter
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- PHP图像处理 imagestring添加图片水印与文字水印操作示例
- CentOS6.9下NFS服务安装配置教程
- Python 中 function(#) (X)格式 和 (#)在Python3.*中的注意事项
- PHP 枚举类型的管理与设计知识点总结
- Linux静态链接库使用类模板的快速排序算法
- TP5(thinkPHP5)框架使用ajax实现与后台数据交互的方法小结
- php post换行的方法
- apache实现部署多个网站(一个ip部署多域名)的方法详解
- windows7 32、64位下python爬虫框架scrapy环境的搭建方法
- python实现简单名片管理系统
- PHP7创建COOKIE和销毁COOKIE的实例方法
- PHP实现新型冠状病毒疫情实时图的实例
- 深入浅析Python2.x和3.x版本的主要区别
- 用户态进程如何得到虚拟地址对应的物理地址?
- Linux中利用grep命令如何检索文件内容详解