贪心算法
1.贪心算法
1.电台覆盖区域求最优解问题
题目:假设存在如下表的需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号
广播台 | 覆盖地区 |
---|---|
K1 | “北京”, “上海”, “天津” |
K2 | “广州”, “北京”, “深圳” |
K3 | “成都”, “上海”, “杭州” |
K4 | “上海”, “天津” |
K5 | “杭州”, “大连” |
/**
* @author 缪广亮
* @version 1.0
*/
public class GreedyAlgorithm {
public static void main(String[] args) {
// 创建广播电台map
HashMap<String, HashSet<String>> broadcasts = new HashMap<>();
// 将各个电台放入到broadcasts
HashSet<String> hashSet1 = new HashSet<>();
hashSet1.add("北京");
hashSet1.add("上海");
hashSet1.add("天津");
HashSet<String> hashSet2 = new HashSet<>();
hashSet2.add("北京");
hashSet2.add("广州");
hashSet2.add("深圳");
HashSet<String> hashSet3 = new HashSet<>();
hashSet3.add("成都");
hashSet3.add("上海");
hashSet3.add("杭州");
HashSet<String> hashSet4 = new HashSet<>();
hashSet4.add("上海");
hashSet4.add("天津");
HashSet<String> hashSet5 = new HashSet<>();
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<>();
for (HashSet<String> areas : broadcasts.values())
// broadcasts.values()返回的是values的一个集合,而每一个value都是一个集合用addAll
allAreas.addAll(areas);
// 存放选择的电台集合
ArrayList<String> selects = new ArrayList<>();
// 临时的集合,在遍历的过程中,存放遍历过程中的电台覆盖的地区和当前还没有覆盖的地区的交集
HashSet<String> tempSet = new HashSet<>();
// 定义maxKey,保存在一次遍历过程中,能够覆盖最大未覆盖的地区对应的电台的key
// 如果maxKey不为null,则就会加入到selects
String maxKey = null;
while (allAreas.size() > 0) {//allAreas不为0,则表示还没有覆盖到所有地区
// 每次while将maxKey置空
maxKey=null;
for (String key : broadcasts.keySet()) {
// 每次for将tempSet清空
tempSet.clear();
// 当前这个key能够覆盖的地区
HashSet<String> areas = broadcasts.get(key);
tempSet.addAll(areas);
// retainAll方法是求出tempSet和allAreas集合的交集,交集会赋给tempSet
tempSet.retainAll(allAreas);
// 如果当前集合包含的未覆盖的地区的数量,,比maxKey指向的集合地区还要多
// 重置maxKey
// tempSet.size() > broadcasts.get(maxKey).size()) 体现贪心算法的特点 每次选择最优的
if (tempSet.size() > 0 &&
(maxKey == null || tempSet.size() > broadcasts.get(maxKey).size()))
maxKey = key;
}
// maxKey不为空,就应该将maxKey加入到selects
if (maxKey!=null){
selects.add(maxKey);
// 将maxKey指向的广播电台覆盖的地区,从allAreas移除
allAreas.removeAll(broadcasts.get(maxKey));
}
}
System.out.println("选择的结果:"+selects);
}
}
2.最小生成树
修路问题本质就是就是最小生成树问题, 先介绍一下最小生成树(Minimum Cost Spanning Tree),简称 MST。 给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树
- N 个顶点,一定有 N-1 条边
- 包含全部顶点
- N-1 条边都在图中
- 求最小生成树的算法主要是普里姆算法和克鲁斯卡尔算法
1.普利姆算法(prim)
应用场景-修路问题
普利姆算法的图解分析:
1.从A顶点开始处理<A,G>
A-C[7] A-G[2] A-B[5]=>
2.<A,G>开始,将A和G 顶点和他们相邻的还没有访问的顶点进行处理=》<A,G,B> A-C[7] A-B[5] G-B[3] G-E[4] G-F[6]
3.<A,G,B>开始,将A,G,B顶点和他们相邻的还没有访问的顶点进行处理=<A,G,B,E> A-CI7] G-E[4] G-F[6] B-D[9]
......
4.{A,G,B,E}->F//第4次大循环 ,对应边<E,F>权值:5
5.{A,G,B,E,F}->D//第5次大循环,对应边<F,D>权值: 4
6.[A,G,B,E,F,D}->C//第6次大循环,对应边<A,C权值: 7
/**
* @author 缪广亮
* @version 1.0
*/
public class PrimAlgorithm {
public static void main(String[] args) {
char[] data = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int vertex = 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 graph = new MGraph(vertex);
MinTree minTree = new MinTree();
minTree.createGraph(graph, vertex, data, weight);
minTree.showGraph(graph);
minTree.prim(graph,0);
}
}
//创建最小成树
class MinTree {
// 创建邻接矩阵
/**
* @param graph 图对象
* @param vertex 顶点个数
* @param data 各个顶点的值
* @param weight 邻接矩阵
*/
public void createGraph(MGraph graph, int vertex, char[] data, int[][] weight) {
for (int i = 0; i < vertex; i++) {
graph.data[i] = data[i];
for (int j = 0; j < vertex; j++) {
graph.weight[i][j] = weight[i][j];
}
}
}
// prim算法
/**
* @param graph 图
* @param v 从第几个顶点开始
*/
public void prim(MGraph graph, int v) {
// 标记顶点是否已经被访问过
int[] isVisited = new int[graph.vertex];
// 将该数组初始化
// 将当前这个顶点标记为已访问
isVisited[v] = 1;
// h1和h2记录两个顶点的下标
int h1 = -1;
int h2 = -1;
int minWeight = 10000;//先将该值初始化成大值,在后面遍历会被替换
for (int k = 1; k < graph.vertex; k++) {//k从1开始,因为prim算法是graph.vertex-1条边
// 这个是确定每一次生成的子图,哪个顶点和这次遍历的结点距离最近
for (int i = 0; i < graph.vertex; i++) {//i结点表示被访问过的结点
for (int j = 0; j < graph.vertex; j++) {//j结点表示还没有访问过的结点
if (isVisited[i] == 1 && isVisited[j] == 0 && graph.weight[i][j] < minWeight) {
// 替换minWeight(寻找已经访问过的结点和未访问过的节点间的权值最小的边 )
minWeight = graph.weight[i][j];
//记录最小值的下标i、j
h1 = i;
h2 = j;
}
}
}
// 找到一条边是最小
System.out.println("边<" + graph.data[h1] + "," + graph.data[h2] + "> 权值:" + minWeight);
// 将当前这个结点标记为已访问
isVisited[h2] = 1;
// minWeight重置
minWeight = 10000;
}
}
// 显示邻接矩阵
public void showGraph(MGraph graph) {
for (int[] link : graph.weight) {
System.out.println(Arrays.toString(link));
}
}
}
class MGraph {
int vertex;
char[] data;//存放结点的数组
int[][] weight;//存放边,邻接矩阵
public MGraph(int vertex) {
this.vertex = vertex;
data = new char[vertex];
weight = new int[vertex][vertex];
}
}
2.克鲁斯卡尔(kruskal)
应用场景-公交站问题
基本介绍:
克鲁斯卡尔(Kruskal)算法,求加权连通图最小生成树的算法
基本思想:按权值从小到大顺序选择n-1条边,保证n-1条边不够成回路
具体做法:先构造一个只有n顶点的森林,然后按权值从小到大连通网中选择边加入森林中,保证森林不产生回路,直到森林变为一棵树为止
/**
* @author 缪广亮
* @version 1.0
*/
@SuppressWarnings({"all"})
public class KruskalCase {
private int edgeNum;
private char[] vertex;
private int[][] matrix;
// 使用INF表示两个顶点不能联通
private static final int INF = Integer.MAX_VALUE;
public static void main(String[] args) {
char[] vertex = new char[]{'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(vertex, matrix);
kruskalCase.printMatrix();
EData[] edges = kruskalCase.getEdges();
kruskalCase.sortEdges(edges);
System.out.println(Arrays.toString(edges));
kruskalCase.kruskal();
}
public KruskalCase(char[] vertex, int[][] matrix) {
// 初始化顶点和边的个数
int vLen = vertex.length;
// 初始化顶点,复制拷贝的方式
this.vertex = new char[vLen];
for (int i = 0; i < vLen; i++) {
this.vertex[i] = vertex[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];
}
}
// 统计边
for (int i = 0; i < vLen; i++) {
for (int j = i + 1; j < vLen; j++) {
if (this.matrix[i][j] != INF) {
edgeNum++;
}
}
}
}
// 打印邻接矩阵
public void printMatrix() {
System.out.println("邻接矩阵为:\n");
for (int i = 0; i < vertex.length; i++) {
for (int j = 0; j < vertex.length; j++) {
System.out.printf("%12d", matrix[i][j]);
}
System.out.println();
}
}
public void kruskal(){
int index=0;//表示最后结果数组的索引
// 用于保存“已有最小生成树”中每个顶点在最小生成树中的终点
int[] ends=new int[edgeNum];
// 创建结果数组,保存最后的最小生成树
EData[] res = new EData[edgeNum];
// 获取图中所有的边集合
EData[] edges = getEdges();
// 按照边的权值大小进行排序
sortEdges(edges);
// 遍历edges数组,将边添加到最小生成树中时,判断准备加入的边是否形成了回路
for (int i = 0; i < edgeNum; i++) {
// 获取到第i条边的第一个顶点(起点)
int p1=getPosition(edges[i].start);//p1=4
// 获取到第i条边的第二个顶点(终点)
int p2=getPosition(edges[i].end);//p2=5
// 获取p1这个顶点在已有最小生成树中的终点
int m=getEnd(ends,p1);//m=4
// 获取p2这个顶点在已有最小生成树中的终点
int n=getEnd(ends,p2);//n=5
// 是否构成回路
if (m!=n) {//没有构成回路
ends[m] = n;//设置m在“已有最小生成树”中的终点
res[index++] = edges[i];//有一条边加入到res数组
}
}
// 统计并打印最小生成树,输出res
System.out.println("最小生成树为");
for (int i = 0; i < index; i++) {
System.out.print(res[i]+" ");
}
}
/**
* 对边进行排序处理,冒泡排序
*
* @param edges 边的集合
*/
public void sortEdges(EData[] edges) {
for (int i = 0; i < edges.length - 1; i++) {
for (int j = 0; j < edges.length - 1 - i; j++) {
if (edges[j].weight > edges[j + 1].weight) {
EData temp = edges[j];
edges[j] = edges[j + 1];
edges[j + 1] = temp;
}
}
}
}
/**
* @param ch 顶点的值,比如'A'
* @return 返回ch顶点对应的下标,否则-1
*/
public int getPosition(char ch) {
for (int i = 0; i < vertex.length; i++) {
if (vertex[i] == ch)
return i;
}
return -1;
}
/**
* 获取图中的边,放到EData[]数组中,后面我们需要遍历该数组
* 通过matrix邻接矩阵来获取
*
* @return EData[]类型的数组
*/
public EData[] getEdges() {
int index = 0;
EData[] edges = new EData[edgeNum];
for (int i = 0; i < vertex.length; i++) {
for (int j = i + 1; j < vertex.length; j++) {
if (matrix[i][j] != INF)
edges[index++] = new EData(vertex[i], vertex[j], matrix[i][j]);
}
}
return edges;
}
/**
* 获取下标为i的顶点的终点,用于后面判断两个顶点的终点是否相同
* @param ends:数组就是记录了各个顶点对应的终点是哪个,ends数组是在遍历过程中,逐步形成
* @param i:表示传入的顶点对应的下标
* @return 返回的就是下标为i的这个顶点对应的终点的下标
*/
public int getEnd(int[] ends,int i){
while (ends[i]!=0)
i=ends[i];
return i;
}
}
//创建类EData,他的对象实例就表示一条边
class EData {
char start;//一条边的起点
char end;//一条边的终点
int weight;//边的权重
public EData(char start, char end, int weight) {
this.start = start;
this.end = end;
this.weight = weight;
}
@Override
public String toString() {
return "EData{" +
"start=" + start +
", end=" + end +
", weight=" + weight +
'}';
}
}
4.最短路径
1.dijkstra(迪杰斯特拉)
迪杰斯特拉算法的描述:
- 初始化:
- 创建一个距离数组dist[],用于存储起点到各个顶点的最短距离。将起点的距离初始化为0,其他顶点的距离初始化为无穷大(表示尚未找到最短路径)。
- 创建一个集合visited[],用于记录已经找到最短路径的顶点。
- 创建一个前驱数组prev[],用于记录最短路径中每个顶点的前驱顶点。
- 选择最短距离的顶点:
- 从未访问的顶点中选择一个距离最小的顶点,将其标记为已访问。
- 更新起点到该顶点相邻顶点的距离,如果经过该顶点到达相邻顶点的距离小于之前记录的最短距离,则更新最短距离和前驱顶点。
- 重复步骤2,直到所有顶点都被访问:
- 选择未访问的顶点中距离最小的顶点,将其标记为已访问。
- 更新起点到该顶点相邻顶点的距离,如果经过该顶点到达相邻顶点的距离小于之前记录的最短距离,则更新最短距离和前驱顶点。
- 最短路径计算:
- 根据prev数组,从终点开始回溯,可以得到起点到终点的最短路径。
迪杰斯特拉算法的核心思想是通过不断更新起点到各个顶点的最短距离,直到找到起点到终点的最短路径。算法的时间复杂度为O(V^2),其中V是图中顶点的数量。在稀疏图中,可以使用优先队列(例如最小堆)来优化算法的时间复杂度为O((V+E)logV),其中E是图中边的数量。
/**
* @author 缪广亮
* @version 1.0
*/
@SuppressWarnings({"all"})
public class DijkstraAlgorithm {
public static void main(String[] args) {
char[] vertexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int[][] martex = new int[vertexs.length][vertexs.length];
final int N = 65535;//表示结点间不连通
martex[0] = new int[]{N, 5, 7, N, N, N, 2};
martex[1] = new int[]{5, N, N, 9, N, N, 3};
martex[2] = new int[]{7, N, N, N, 8, N, N};
martex[3] = new int[]{N, 9, N, N, N, 4, N};
martex[4] = new int[]{N, N, 8, N, N, 5, 4};
martex[5] = new int[]{N, N, N, 4, 5, N, 6};
martex[6] = new int[]{2, 3, N, N, 4, 6, N};
Graph graph = new Graph(vertexs, martex);
graph.showGraph();
graph.djs(6);
graph.showDijkstra();
}
}
class Graph {
private char[] vertex;
private int[][] matrix;
private VisitedVertex vv;//已经访问的集合
public Graph(char[] vertex, int[][] matrix) {
this.vertex = vertex;
this.matrix = matrix;
}
public void showGraph() {
for (int[] link : matrix) {
System.out.println(Arrays.toString(link));
}
}
public void djs(int index) {
vv = new VisitedVertex(vertex.length, index);
update(index);//更新index下标顶点到周围顶点的距离和周围顶点的前驱顶点
// i=1,因为开始进入djs方法就已经将出发顶点访问完了update(index)
for (int i = 1; i < vertex.length; i++) {
index = vv.updateArr();//选择并返回新的访问顶点
update(index);//更新index下标顶点到周围顶点的距离和周围顶点的前驱顶点
}
}
// 更新index下标顶点到周围顶点的距离和周围顶点的前驱顶点
private void update(int index) {
int len = 0;
//根据遍历我们的邻接矩阵的出发顶点matrix[index].length行
for (int i = 0; i < matrix[index].length; i++) {
// len的含义:出发顶点到index顶点的距离+从index顶点到i顶点的距离之和
len = vv.getDis(index) + matrix[index][i];
// 如果j顶点没有被访问过,并且len小于出发顶点到i顶点的距离,就需要更新
if (!vv.in(i) && len < vv.getDis(i)) {
vv.updatePre(i, index);//更新i顶点的前驱为index顶点
vv.updateDis(i, len);//更新出发顶点到i顶点的距离
}
}
}
// 显示结果
public void showDijkstra() {
vv.show();
}
}
//已访问顶点集合
class VisitedVertex {
// 记录各个顶点是否访问过 1表示已访问,0表示未访问,动态更新
public int[] already_arr;
// 每个下标对应的值为前一个顶点下标,动态更新
public int[] pre_visited;
// 记录出发顶点到其他所有顶点的距离,例如以G为出发点,就会记录G到其他顶点的距离,动态更新
// 求的最短距离就会存放到dis
public int[] dis;
/**
* @param length 表示顶点的个数
* @param index 出发顶点对应的下标 比如G顶点 下标就是6
*/
public VisitedVertex(int length, int index) {
this.already_arr = new int[length];
this.pre_visited = new int[length];
this.dis = new int[length];
// 初始化dis数组
Arrays.fill(dis, 65535);
this.already_arr[index] = 1;
this.dis[index] = 0;//出发顶点的访问距离到自己的距离为0
}
/**
* 功能:判断index顶点是否被访问过
*
* @param index
* @return 如果访问过,就返回true,否则未访问false
*/
public boolean in(int index) {
return already_arr[index] == 1;
}
/**
* 功能:更新出发顶点到index顶点的距离
*
* @param index
* @param len
*/
public void updateDis(int index, int len) {
dis[index] = len;
}
/**
* 功能:更新pre这个顶点的前驱为index顶点
*
* @param pre
* @param index
*/
public void updatePre(int pre, int index) {
pre_visited[pre] = index;
}
/**
* 功能:返回出发顶点到index顶点的距离
*
* @param index
* @return
*/
public int getDis(int index) {
return dis[index];
}
/**
* 继续选择并访问新的访问顶点,就比如G访问完后,就是A顶点作为新的访问顶点(注意不是出发顶点)
*
* @return
*/
public int updateArr() {
int min = 65535, 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;
}
// 显示最后的结果
// 显示三个数组的情况输出
public void show() {
System.out.println("========================");
for (int i : already_arr) {
System.out.print(i + " ");
}
System.out.println();
for (int i : pre_visited) {
System.out.print(i + " ");
}
System.out.println();
for (int i : dis) {
System.out.print(i + " ");
}
System.out.println();
// 显示最后的最短距离
char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int count = 0;
for (int i : dis) {
if (i != 65535) {
System.out.print(vertex[count] + "(" + i + ")" + " ");
} else
System.out.println();
count++;
}
}
}
2.floyd(弗洛伊德)
算法描述:
1)设置顶点vi到顶点vk的最短路径已知为Lik 顶点vk到vj的最短路径已知为Lkj,顶点vidaovj的路径为Lij,则vi到vj的最短路径为: min((Lik+Lkj),Lij),vk的取值为图中所有顶点,则可获得vi到vj的最短路径
2)至于vi到vj的最短路径Lik或者vk到vj的最短路径Lkj,是以同样的方式获得
/**
* @author 缪广亮
* @version 1.0
*/
@SuppressWarnings({"all"})
public class FloydAlgorithm {
public static void main(String[] args) {
char[] vertexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int[][] matrix = new int[vertexs.length][vertexs.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 = new Graph(vertexs.length, vertexs, matrix);
graph.floyd();
graph.show();
}
}
class Graph {
private char[] vertex;
private int[][] dis;//保存从各个顶点出发到其他顶点的距离,最后结果也是保留在该数组
private int[][] pre;//保存到达目的顶点的前驱顶点
public Graph(int length, char[] vertex, int[][] dis) {
this.vertex = vertex;
this.dis = dis;
pre = new int[length][length];
for (int i = 0; i < length; i++) {
Arrays.fill(pre[i], i);
}
}
// 显示pre和dis数组
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.print(vertex[pre[k][i]] + " ");
}
System.out.println();
// 输出dis数组的一行数据
for (int i = 0; i < dis.length; i++) {
System.out.print("(" + vertex[k] + "到" + vertex[i] + "最短路径是" + dis[k][i] + ") ");
}
System.out.println();
}
}
// 弗洛伊德算法
public void floyd() {
int len = 0;//保存距离
// 对中间顶点遍历
for (int k = 0; k < dis.length; k++) {
// 从i顶点开始出发
for (int i = 0; i < dis.length; i++) {
// 到达j顶点终点顶点
for (int j = 0; j < dis.length; j++) {
len = dis[i][k] + dis[k][j];//求出从i顶点出发,经过k中间顶点,到达j顶点距离
if (len < dis[i][j]) {//经过中间顶点的距离小于直连的距离
dis[i][j] = len;//更新距离
pre[i][j] = pre[k][j];//更新前驱顶点
}
}
}
}
}
}