当前位置: 首页 > article >正文

(JAVA)贪心算法、加权有向图与求得最短路径的基本论述与实现

1. 贪心算法

1.1 贪心算法的概述:

贪心算法是一种对某些求最优解问题的更简单、更迅速的设计技术

贪心算法的特点是一步一步地进行,常以当前情况为基础根据某个优化测度作最优选择,而不考虑各种可能的整体情况,省去了为找最优解要穷尽所有可能而必须耗费的大量时间。

贪心算法采用自顶向下,以迭代的方法做出相继的贪心选择,每做一次贪心选择,就将所求问题简化为一个规模更小的子问题,通过每一步贪心选择,可得到问题的一个最优解。

虽然每一步上都要保证能获得局部最优解,但由此产生的全局解有时不一定是最优的,所以贪心算法不要回溯

1.2 贪心算法适用的问题

贪心策略的前提是:局部最优策略能导致产生全局最优解

实际上,贪心算法使用的情况比较少,一般对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析可以做出判断。

1.3 贪心算法的实现框架

从问题的某一初始解出发:

while (能朝给定总目标前进一步)
{ 
    利用可行的决策,求出可行解的一个解元素;
}

所有解元素组合成问题的一个可行解。

1.4 贪心策略的选择

用贪心算法只能通过解局部最优解的策略来达到全局最优解,因此一定要注意判断问题是否适合采用贪心算法策略,找到解是否一定是问题的最优解。

1.5 例题分析

1.5.1 分糖果问题

n个小朋友玩完游戏后,老师准备给他们发糖果;每个人有一个分数a[i],如果比左右的人分数高,那么糖果也要比左右的多,并且每个小朋友至少有一颗。问老师最少准备多少糖果?

这个题目不能直接用动态规划去解,比如用dp[i]表示前i个人需要的最少糖果数。
因为(前i个人的最少糖果数)这种状态表示会收到第i+1个人的影响,如果a[i]>a[i+1],那么第i个人应该比第i+1个人多。即是这种状态表示不具备无后效性。
如果是我们分配糖果,我们应该怎么分配?

  • 答案是:从分数最低的开始。
    按照分数排序,从最低开始分,每次判断是否比左右的分数高。

假设每个人分c[i]个糖果,那么对于第i个人有c[i]=max(c[i-1],c[c+1])+1;

(c[i]默认为0,如果在计算i的时候,c[i-1]为0,表示i-1的分数比i高)

但是,这样解决的时间复杂度为O(NLogN),主要瓶颈是在排序。如果提交,会得到Time Limit Exceeded的提示。

  • 因此我们需要对贪心的策略进行优化:
    我们把左右两种情况分开看。
    • 如果只考虑比左边的人分数高时,容易得到策略:
      从左到右遍历,如果a[i]>a[i-1],则有c[i]=c[i-1]+1;否则c[i]=1。
    • 再考虑比右边的人分数高时,此时我们要从数组的最右边,向左开始遍历:
      如果a[i]>a[i+1], 则有c[i]=c[i+1]+1;否则c[i]不变;
  • 这样讲过两次遍历,我们可以得到一个分配方案,并且时间复杂度是O(N)

1.5.2 小船过河问题

n个人要过河,但是只有一艘船;船每次只能做两个人,每个人有一个单独坐船的过河时间a[i],如果两个人(x和y)一起坐船,那过河时间为a[x]和a[y]的较大值。问最短需要多少时间,才能把所有人送过河?

题目给出关键信息:1、两个人过河,耗时为较长的时间;
还有隐藏的信息:2、两个人过河后,需要有一个人把船开回去;
要保证总时间尽可能小,这里有两个关键原则:应该使得两个人时间差尽可能小(减少浪费),同时船回去的时间也尽可能小(减少等待)。
先不考虑空船回来的情况,如果有无限多的船,那么应该怎么分配?

  • 答案:每次从剩下的人选择耗时最长的人,再选择与他耗时最接近的人。

再考虑只有一条船的情况,假设有A/B/C三个人,并且耗时A<B<C。
那么最快的方案是:A+B去, A回;A+C去;总耗时是A+B+C。(因为A是最快的,让其他人来回时间只会更长,减少等待的原则
如果有A/B/C/D四个人,且耗时A<B<C<D,这时有两种方案:
1、最快的来回送人方式,A+B去;A回;A+C去,A回;A+D去; 总耗时是B+C+D+2A (减少等待原则)
2、最快和次快一起送人方式,A+B先去,A回;C+D去,B回;A+B去;总耗时是 3B+D+A (减少浪费原则)
对比方案1、2的选择,我们发现差别仅在A+C和2B;

  • 为何方案1、2差别里没有D?

因为D最终一定要过河,且耗时一定为D。

如果有A/B/C/D/E 5个人,且耗时A<B<C<D<E,这时如何抉择?
仍是从最慢的E看。(参考我们无限多船的情况)

  1. 方案1,减少等待;先送E过去,然后接着考虑四个人的情况;
  2. 方案2,减少浪费;先送E/D过去,然后接着考虑A/B/C三个人的情况;(4人的时候的方案2)

到5个人的时候,我们已经明显发了一个特点:问题是重复,且可以由子问题去解决。

根据5个人的情况,我们可以推出状态转移方程

  • dp[i] = min(dp[i - 1] + a[i] + a[1], dp[i - 2] + a[2] + a[1] + a[i] + a[2]);

再根据我们考虑的1、2、3、4个人的情况,我们分别可以算出dp[i]的初始化值:

dp[1] = a[1];
dp[2] = a[2];
dp[3] = a[2]+a[1]+a[3];
dp[4] = min(dp[3] + a[4] + a[1], dp[2]+a[2]+a[1]+a[4]+a[2]);

由上述的状态转移方程和初始化值,我们可以推出dp[n]的值。

1.5.3 背包问题

问题描述 有一个背包,背包容量是M=150。有7个物品,物品可以分割成任意大小。要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。

在这里插入图片描述

**问题分析: **

  1. 目标函数: ∑pi最大,使得装入背包中的所有物品pi的价值加起来最大。

  2. 约束条件:装入的物品总重量不超过背包容量:∑wi<=M( M=150)

  3. 贪心策略:

    • 选择价值最大的物品
    • 选择价值最大的物品
    • 选择单位重量价值最大的物品

有三个物品A,B,C,其重量分别为{30,10,20},价值分别为{60,30,80},背包的容量为50,

  • 分别应用三种贪心策略装入背包的物品和获得的价值如下图所示:

在这里插入图片描述

算法的三种策略:

  • 算法设计:
  1. 计算出每个物品单位重量的价值
  2. 按单位价值从大到小将物品排序
  3. 根据背包当前所剩容量选取物品
  4. 如果背包的容量大于当前物品的重量,那么就将当前物品装进去。否则,那么就将当前物品舍去,然后跳出循环结束。

2. 加权有向图

与加权无向图不同的时。这种图的特点是边有方向性,即边从一个顶点指向另一个顶点,并且每条边都有一个与之相关的权重,这个权重通常代表了从一点到另一点的成本或距离。

2.1 加权有向图边的表示

2.1.1 API设计

类名DirectedEdge
构造方法DirectedEdge(int v,int w,double weight):通过顶点v和w,以及权重weight值构造一个边对象
成员方法1. public double weight():获取边的权重值
2. public int from():获取有向边的起点
3. public int to():获取有向边的重点
成员变量1. private final int V:起点
2. private final int W:终点
3. private final double weight:当前边的权重

2.2 加权有向图的实现

2.2.1 API 设计

类名EdgeWeightedDigraph
构造方法EdgeWeightedDigraph(DirectedEdge V):创建一个包含V个顶点但不包含边的有向图
成员方法1. public int V():获取图中顶点的数量
2. public int E():获取图中边的数量
3. public void addEdge(DirectedEdge e):向有向图中添加一条边e
4. public Queue<DirectedEdge> adj(int v):获取由v指出的边所连接的所有顶点
5. public Queue<DirectedEdge> edges():获取加权有向图的所有边
成员变量1. private final int V:记录顶点数量
2. private int E:记录边数量
3. private Queue<DirectedEdge>[] adj:邻接表

2.2.2 代码实现

package com.renecdemo.weighted;

import com.renecdemo.graph.Queue;

/**
 * 加权有向图
 */
public class EdgeWeightedDigraph {
    private final int V;// 记录顶点数量
    private int E;// 记录边数量
    private Queue<DirectedEdge>[] adj;// 邻接表

    public EdgeWeightedDigraph(int v) {
        this.V = v;
        this.E = 0;
        this.adj = new Queue[V];

        for (int i = 0; i < adj.length; i++) {
            adj[i] = new Queue<>();
        }
    }

    public int V(){
        return V;
    }

    public int E(){
        return E;
    }

    /**
     * 向有向图中添加一条边e
     * @param e
     */
    public void addEdge(DirectedEdge e){
        // 因为e是有方向的,所以只需要让e出现在起点的邻接表中即可
        int v = e.from();
        adj[v].enqueue(e);

        E++;
    }

    /**
     * 获取由 v顶点 指出的边所连接的所有顶点 - 返回该顶点所在的邻接表即可
     * @param v 顶点
     * @return
     */
    public Queue<DirectedEdge> adj(int v){
        return adj[v];
    }

    /**
     * 获取加权有向图的所有边
     * @return
     */
    public Queue<DirectedEdge> edges(){
        // 遍历图中的每一个顶点
        Queue<DirectedEdge> allEdges = new Queue<>();
        for (int v = 0; v < V; v++) {
            for (DirectedEdge e : adj[v]) {
                allEdges.enqueue(e);
            }
        }

        return allEdges;
    }

}

3. 最短路径

有了加权有向图之后,我们立刻就可以联想到在实际生活汇总的使用场景,例如在一副地图中,找到顶点a和顶点b之间的路径,这条路径可以是距离最短,也可以是时间最短,也可以是费用最小等,如果我们把距离/时间/费用看作是成本,那么就需要找到顶点a和顶点b之间成本最小的路径。

3.1 最短路径的 定义以及性质

3.1.1 定义:

  • 在一副加权有向图中,从顶点s到顶点t的最短路径是所有从顶点s到顶点t的路径中总权重最小的那条路径

在这里插入图片描述

3.1.2 性质:

  1. 路径具有方向性
  2. 权重不一定等价于距离,权重可以是距离、时间、花费等内容,权重最小指的是成本最低
  3. 只考虑连通图,一副图中并不是所有的顶点都是可达的,如果s和t不可达,那么它们之间也就不存在最短路径。为了简化问题,这里只考虑连通图
  4. 最短路径不一定是唯一的。从一个顶点到达另一个顶点的权重最小的路径可能会有很多条,这里只需要找出一条即可

3.1.3 最短路径树

给定一副加权有向图和一个顶点s,以s为起点的一颗最短路径树是图的一副子图,它包含顶点s以及从s可达的所有顶点。

这颗有向图的根节点为s,树的每条路径都是有向图中的一条最短路径

3.2 最短路径树的API设计

类名DijkstraSP
构造方法pulbic DijkstraSP(EdgeWeightedDigraph G,int s):根据一副加权有向图G和顶点s,创建一个计算顶点为s的最短路径树对象
成员方法1. private void relax(EdgeWeightedDigraph G,int v):松弛图G中的顶点
2. public double disTo(int v):获取从顶点s到顶点v的最短路径的总权重
3. public boolean hasPathTo(int v):判断从顶点s到顶带你v是否可达
4. public Queue<DirectedEdge> pathTo(int v):查询从起点s到顶点v的最短路径中所有的边
成员变量1. private DirectedEdge[] edgeTo:索引代表顶点,指表示从顶点s到当前顶点的最短路径上的最后一条边
2. private double[] distTo:索引代表顶点,值从顶点s到当前的最短路径的总权重
3. private IndexMinPriorityQueue<Double> pq:存放树种顶点到非树中顶点之间的有效横切边

3.2 松弛技术

3.2.1 松弛技术的概述

松弛这个词来源于生活:一条橡皮筋沿着两个顶点的某条路径紧紧展开,如果这两个顶点之间的路径不止一条,还有存在更短的路径,那么把皮筋转移到更短的路径上,皮筋就可以放松了。

在这里插入图片描述

松弛这种简单的原理刚好可以用来计算最短路径树。

在我们的API中,需要用到两个成员变量edgeTo和distTo,分别存储边和权重。一开始给定一幅图G和顶点s,我们只知道图的边以及这些边的权重,其他的一无所知,此时初始化顶点s到顶点s的最短路径的总权重disto[s]=0;顶点s到其他顶点的总权重默认为无穷大,随着算法的执行,不断的使用松弛技术处理图的边和顶点,并按一定的条件更新edgeTo和distTo中的数据,最终就可以得到最短路径树。

3.2.2 边的松弛:

放松边v->w意味着检查从s到w的最短路径是否先从s到v,然后再从v到w?

  • 如果是,则v-w这条边需要加入到最短路径树中,更新edgeTo和distTo中的内容:edgeTo[w]=表示v->w这条边的DirectedEdge对象,distTo[w]=distTo[v]+v->w这条边的权重;

  • 如果不是,则忽略v->w这条边。

在这里插入图片描述

3.2.3顶点的松弛:

顶点的松弛是基于边的松弛完成的,只需要把某个顶点指出的所有边松弛,那么该顶点就松弛完毕。例如要松弛顶点v,只需要遍历v的邻接表,把每一条边都松弛,那么顶点v就松弛了。

如果把起点设置为顶点0,那么找出起点0到顶点6的最短路径0->2->7>3->6的过程如下:

在这里插入图片描述

3.3 Dijkstra 算法的代码实现

package com.renecdemo.dijkstra;

import com.renecdemo.graph.Queue;
import com.renecdemo.queue.IndexMinPriorityQueue;
import com.renecdemo.weighted.DirectedEdge;
import com.renecdemo.weighted.EdgeWeightedDigraph;

/**
 * Dijkstra算法
 */
public class DijkstraSP {
    // 索引代表顶点,值表示从顶点s到当前顶点的最短路径上的最后一条边
    private DirectedEdge[] edgeTo;
    // 索引代表顶点,值从顶点s到当前的最短路径的总权重
    private double[] distTo;
    // 存放树种顶点到非树中顶点之间的有效横切边
    private IndexMinPriorityQueue<Double> pq;

    /**
     * 根据一副加权有向图G和顶点s,创建一个计算顶点为s的最短路径树对象
     * @param G
     * @param s
     */
    public DijkstraSP(EdgeWeightedDigraph G, int s){
        // 初始化成员变量
        this.edgeTo = new DirectedEdge[G.V()];
        this.distTo = new double[G.V()];

        // 初始化权重数组,所有边的权重都为Double类型的无限大
        for (int i = 0; i < distTo.length; i++) {
            distTo[i] = Double.POSITIVE_INFINITY;
        }

        this.pq = new IndexMinPriorityQueue<>(G.V());

        // 找到图G中以顶点s为起点的最短路径树
        // 默认让s进入到最短路径树中
        distTo[s] = 0.0;
        pq.insert(s,0.0);

        while (!pq.isEmpty()){
            relax(G,pq.delMin()); // 弹出最新规划出的顶点
        }
    }

    /**
     * 松弛图G中的顶点 - 找出最短路径树
     * @param G 指定的图
     * @param v 起点所至的顶点
     */
    private void relax(EdgeWeightedDigraph G,int v){
        // 遍历 v顶点 的邻接表
        for (DirectedEdge e : G.adj(v)) {
            // 获取到该边的终点
            int w = e.to();// 通过e边 达到 v顶点 的另一个顶点

            /*
                通过松弛技术,判断从s到w的最短路径是否需要先从顶点s到顶点v,如何再由顶点v到顶点w
                判断:从起点到达 v顶点的权重+e边的权重 是否小于 起点直接到达 w顶点的权重
             */
            if (distTo(v)+e.weight() < distTo(w)){
                // 变换
                distTo[w] = distTo(v)+e.weight();
                edgeTo[w] = e;

                // 判断pq中是否已经存在顶点w
                if (pq.contains(w)){
                    pq.changeItem(w,distTo(w));
                }else {
                    pq.insert(w,distTo(w));// 更新pq队列
                }
            }
        }
    }

    /**
     * 获取从顶点s到顶点v的最短路径的总权重
     * @param v
     * @return
     */
    public double distTo(int v){
        return distTo[v];
    }

    /**
     * 判断从顶点s到顶带你v是否可达
     * @param v
     * @return
     */
    public boolean hasPathTo(int v){
        return distTo[v]<Double.POSITIVE_INFINITY;
    }

    /**
     * 查询从起点s到顶点v的最短路径中所有的边
     * @param v
     * @return
     */
    public Queue<DirectedEdge> pathTo(int v){
        // 判断顶点s到顶点v是否可达
        if (!hasPathTo(v)){
            return null;
        }

        // 创建队列对象
        Queue<DirectedEdge> allEdges = new Queue<>();
        // 顶点逆推
        while (true){
            // 得到v顶点的边
            DirectedEdge e = edgeTo[v];
            if (e==null){
                break;
            }

            allEdges.enqueue(e);
            // 更新 v顶点 —— e.from():获得e边的除了v顶点的另外一个顶点
            v = e.from();
        }
        // 返回队列
        return allEdges;
    }
}

4. 前置文章

  1. 浅入数据结构 “堆” - 实现和理论
  2. 开始熟悉 “二叉树” 的数据结构
  3. 队列 和 符号表 两种数据结构的实现
  4. 队列的进阶结构-优先队列
  5. 2-3树思想与红黑树的实现与基本原理
  6. B树和B+树的实现原理阐述
  7. 图的基本原理和API实现
  8. 有向图与拓扑排序的实现原理与基本实现
  9. 加权无向图和最小生成树的实现与原理概述

5. ES8 如何使用?

快来看看这篇好文章吧~~!!
😊👉(全篇详细讲解)ElasticSearch8.7 搭配 SpringDataElasticSearch5.1 的使用


http://www.kler.cn/news/356074.html

相关文章:

  • 【达梦数据库】获取表字段信息SQL
  • 单片机原理及应用笔记:单片机的结构原理与项目实践
  • 动态分层强化学习(DHRL)算法详解
  • FFmpeg 4.3 音视频-多路H265监控录放C++开发三 :安装QT5.14.2, 并将QT集成 到 VS2019中。
  • 鸿蒙网络编程系列13-使用Request部件上传文件到服务端示例
  • 51单片机的智能空调【proteus仿真+程序+报告+原理图+演示视频】
  • JDBC远程连接mysql报错:NotBefore: Sat Mar 30 16:37:41 UTC 2024
  • Ubuntu中vscode如何选择ROS版本
  • 什么是SYN flood,如何处理
  • 创客项目秀|基于XIAO ESP32C3的Night Lamp With Chinese Traditional Pane项目
  • IntelliJ IDEA 快捷键大全(也适用全家桶其他编辑器)
  • AndroidStudio实验报告——实验一、二
  • BEV相关
  • 记录使用appium+夜神模拟器测试多设备时selenium和appium版本不兼容带来的问题
  • 远控代码的重构-远控网络编程的设计上
  • engintime linux lab 实验 增加系统调用函数max,Iam,Whoami
  • PetaLinux工程的常用命令——petalinux-create
  • 【工具变量】上市公司企业公共数据开放及ZF数据开放-含代码及数据(2023-2008年)
  • 面试题:被问的几率最大的前端面试题!!
  • 互联网语言 互联网开发 互联网架构