“ 寻友之旅 “ 的三种解决办法

news2025/4/17 17:29:15

题目来源于:稀土掘金

" 寻友之旅 " 的三种解决办法!

本文将分别讲解如何使用BFS双向BFS以及 Dijsktra堆优化的方法来解决此题~ 一起来看看吧!

附Java题解代码!


文章目录

  • " 寻友之旅 " 的三种解决办法!
  • 前言
  • 错误思路——动态规划
  • BFS题解
  • 双向BFS题解
  • Dijsktra(堆优化)题解
    • 什么是“链式前向星”?
  • 总结


前言

这是我在青训营里面遇到的一个主题创作题目,整好最近在复习算法,整合三种解决方案给大家。(本文完全由本人自己总结,如有问题请在评论区指出)

题目——寻友之旅
小青要找小码去玩,他们的家在一条直线上,当前小青在地点 N ,小码在地点 K (0≤N , K≤100 000),并且小码在自己家原地不动等待小青。小青有两种交通方式可选:步行和公交。
步行:小青可以在一分钟内从任意节点 X 移动到节点 X-1 或 X+1
公交:小青可以在一分钟内从任意节点 X 移动到节点 2×X (公交不可以向后走)
请帮助小青通知小码,小青最快到达时间是多久?
输入: 两个整数 N 和 K
输出: 小青到小码家所需的最短时间(以分钟为单位)

错误思路——动态规划

拿到这道题时,我们被小青有两种交通方式可选:步行和公交。(前进一步、后退一步、直达2 * current)这句话吸引了,可能首先想到用动态规划来做。
似乎好像大概是没问题的,那我们根据动态规划的思想:"当前的结果都是由前面的结果推导出来的"进行推导,举例:例如小码的地点在200这个结点(即终点下标为200),我们可以画出的示意图如下图所示:

50
99
100
101
198
199
200
201
202

我们会发现一个问题!!!200可以由199推导过来,而199同样可以由200推导过来,emmm…这不成环了吗?其实,动态规划是拓扑图,一般来说是由当前状态推出下一状态,是无环的! ~好家伙,那这题咋做呢?经过一些思考,我们会想到用搜索来做,dfs和bfs里面,我选bfs,因为dfs每次都要搜到底,容易爆栈,而且里面的控制条件写起来相对bfs可能要更难一些,所以我们尝试用bfs来解此题

BFS题解

bfs找最短,最多就是100000的空间,vis记录一下已经访问过的点
dfs是找到底,可能会爆栈和超时
这个题相当于一维走迷宫,前进一步、后退一步、前进到两倍下标的地方,
二维走迷宫是上下左右走而已
以start为出发点,每次弹出元素时时先记录当前队列中元素的数量,这些数量就是以上一次访问到的点作为起点找出的其他能够访问的点的数量,方便后面弹出指定数量的元素;同时用vis[]来记录已经访问过的点,由于流程是逐层递进的,所以每个初次访问到的点所在的时间一定是最短时间!(举个例子:y点最初通过x点访问到,在后续的情况中,可能还会通过其他点再次访问到该点,而此时的时间一定是大于等于初次访问到y点的。)

代码如下(java):

import java.io.*;
import java.util.*;

public class Main {
    static int n, k;
    static boolean[] vis = new boolean[100001];

    public static void main(String[] args) throws IOException {
        //例如输入:20 38   输出 2
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] line = br.readLine().split(" ");
        n = Integer.parseInt(line[0]);
        k = Integer.parseInt(line[1]);
        if (n == k)
            System.out.println(0);
        else if (n < k) {
            System.out.println(bfs(n, k));
        } else {
            System.out.println(n - k);
        }

    }

    private static int bfs(int start, int target) {
        int time = 0;
        Queue<Integer> queue = new LinkedList<>();
        queue.offer(start);
        vis[start] = true;
        while (!queue.isEmpty()) {
            int length = queue.size();
            for (int i = 0; i < length; i++) {
                Integer current = -1;
                if (!queue.isEmpty())
                    current = queue.poll();
                if (current == target) {
                    return time;
                }
                if (current - 1 >= 0 && !vis[current - 1]) {
                    queue.offer(current - 1);
                    vis[current - 1] = true;
                }
                if (current + 1 <= 100000 && !vis[current + 1]) {
                    queue.offer(current + 1);
                    vis[current + 1] = true;
                }
                if (current * 2 <= 100000 && !vis[current * 2]) {
                    queue.offer(current * 2);
                    vis[current * 2] = true;
                }
            }
            time++;
        }
        return -1;
    }

}

双向BFS题解

分别从起点和终点出发进行搜索,这里将起点走过的地点标记为1,终点走过来的路标记为2,如果能相遇(vis[current] + vis[other] == 3),则已经找到最短时间。

代码如下(java):

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;

public class Main {
    static int n, k;
    static int[] vis = new int[100001];//前队标记1  后队标记2  相加得3 即为相遇
    static int[] dis = new int[100001];
    static Queue<Integer> frontQueue = new LinkedList<>();//从起点开始搜
    static Queue<Integer> backQueue = new LinkedList<>();//从终点开始搜

    public static void main(String[] args) throws IOException {
        //例如输入:20 38   输出 2
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] line = br.readLine().split(" ");
        n = Integer.parseInt(line[0]);
        k = Integer.parseInt(line[1]);
        if (n == k)
            System.out.println(0);
        else if (n < k) {
            System.out.println(dbfs(n, k));
        } else {
            System.out.println(n - k);
        }

    }

    private static int dbfs(int start, int target) {
        int flag;
        int length;
        Queue<Integer> curQueue;
        frontQueue.offer(start);
        backQueue.offer(target);
        vis[start] = 1;
        vis[target] = 2;
        while (!frontQueue.isEmpty() && !backQueue.isEmpty()) {
            if (frontQueue.size() <= backQueue.size()) {
                flag = 1;
                length = frontQueue.size();
            } else {
                flag = 0;
                length = backQueue.size();
            }
            for (int i = 0; i < length; i++) {
                Integer curPoll;
                if (flag == 1) {
                    curPoll = frontQueue.poll();
                    curQueue = frontQueue;
                } else {
                    curPoll = backQueue.poll();
                    curQueue = backQueue;
                }
                if (curPoll - 1 >= 0 && vis[curPoll - 1] == 0) {
                    curQueue.offer(curPoll - 1);
                    vis[curPoll - 1] = 1;
                    dis[curPoll - 1] = dis[curPoll] + 1;
                } else if (curPoll - 1 >= 0 && vis[curPoll - 1] != 0) {
                    if (vis[curPoll] + vis[curPoll - 1] == 3) {
                        return dis[curPoll] + 1 + dis[curPoll - 1];
                    }
                }
                if (curPoll + 1 <= 100000 && vis[curPoll + 1] == 0) {
                    curQueue.offer(curPoll + 1);
                    vis[curPoll + 1] = 1;
                    dis[curPoll + 1] = dis[curPoll] + 1;
                } else if (curPoll + 1 <= 100000 && vis[curPoll + 1] != 0) {
                    if (vis[curPoll] + vis[curPoll + 1] == 3) {
                        return dis[curPoll] + 1 + dis[curPoll + 1];
                    }
                }
                if (curPoll * 2 <= 100000 && vis[curPoll * 2] == 0) {
                    curQueue.offer(curPoll * 2);
                    vis[curPoll * 2] = 1;
                    dis[curPoll * 2] = dis[curPoll] + 1;
                } else if (curPoll * 2 <= 100000 && vis[curPoll * 2] != 0) {
                    if (vis[curPoll] + vis[curPoll * 2] == 3) {
                        return dis[curPoll] + 1 + dis[curPoll * 2];
                    }
                }
            }
        }
        return -1;
    }

}

Dijsktra(堆优化)题解

我们把题目中的时间想象成路径,求“最短时间”是不是就变成了求“最短路径”问题了?(不过此题较为特殊,因为所有的移动方式所花费的时间都为1,即所有边的长度都为1。)
但是我们需要注意样例范围:(0≤N , K≤100 000)
朴素算法的时间复杂度是n²,而一般题目给的数据都是差不多le5,这时候肯定会爆
于是乎,我们想到用堆优化来降低时间复杂度,将时间复杂度从n²降到nlogn+m
堆优化了每次找离起点最近的点的时间复杂度
用“链式前向星”来创建图(如果不清楚这种建图方式可以先看下文部分【什么是“链式前向星”?】)

代码如下(java):

import java.io.*;
import java.util.*;

public class Main {
    static int[] head, next, ends;
    static int[] times;//结果
    static int n = 100000, m = 300000;//最多有n个顶点,m条边
    static int start, target, total = 0;//++total:从第一条边到最后一条边

    public static void main(String[] args) throws IOException {
        //例如输入:20 38   输出 2
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] line = br.readLine().split(" ");
        start = Integer.parseInt(line[0]);
        target = Integer.parseInt(line[1]);
        if (start == target)
            System.out.println(0);
        else if (start < target) {
            head = new int[m + 1];//表示以 i 为起点的最后一条边的编号
            next = new int[m + 1];//存储与当前边起点相同的上一条边的编号
            ends = new int[m + 1];//存储边的终点
            times = new int[n + 1];
            Arrays.fill(head, -1);//初始化
            for (int i = 0; i <= n; i++) {
                if (i - 1 >= 0) add(i, i - 1);
                if (i + 1 <= n) add(i, i + 1);
                if (i * 2 <= n) add(i, i * 2);
            }
            dijkstra(start);
            System.out.println(times[target]);
        } else {
            System.out.println(start - target);
        }
    }

    private static void dijkstra(int startPoint) {
        for (int i = 1; i <= n; i++) {
            times[i] = Integer.MAX_VALUE;//初始化时,应当赋上最坏的情况
        }
        Queue<Integer> queue = new LinkedList<>();
        queue.offer(startPoint);
        times[startPoint] = 0;//起始位置,应当赋上最好的情况
        while (!queue.isEmpty()) {
            int x = queue.poll();//当前点
            //链式前向星的遍历方法,遍历出以x为起点的所有边
            for (int i = head[x]; i != -1; i = next[i]) {//i表示:第 i 条边
                int j = ends[i];//第 i 条边的终点
                if (times[j] > times[x] + 1) {//如果length(起点-->终点) > length(起点 --> 当前点) + length(当前点 --> 终点)
                    times[j] = times[x] + 1;//更新起点到终点的最短距离
                    queue.offer(j);//并将这个终点入队,以便之后通过该点访问其他顶点
                }
            }
        }
    }

    private static void add(int start, int end) {
        ends[++total] = end;
        next[total] = head[start];//以start为起点的上一条边的编号,即:与这个边起点相同的上一条边的编号
        head[start] = total;//更新以start为起点的上一条边的编号
    }

}

或许有同学会不太清楚上述的建图方式,这里单独讲一下 ↓ 也可以看我的 这篇文章

什么是“链式前向星”?

如果说邻接表是不好写但效率好,邻接矩阵是好写但效率低的话,前向星就是一个相对中庸的数据结构。前向星固然好写,但效率并不高。而在优化为链式前向星后,效率也得到了较大的提升。虽然说,世界上对链式前向星的使用并不是很广泛,但在不愿意写复杂的邻接表的情况下,链式前向星也是一个很优秀的数据结构。 ——摘自《百度百科》

链式前向星其实就是静态建立的邻接表;
时间效率为O(n)、空间效率也为O(n)、遍历效率也为O(n);
对于下面的数据:第一行5个顶点、7条边。接下来是边的起点,终点和权值。如:边1 -> 2 权值为1。

5 7 
1 2 1 
2 3 2 
3 4 3 
1 3 4
4 1 5
1 5 6
4 5 7

*链式前向星存的是以【1,n】为起点的边的集合,对于上面的数据输出就是:

1 //以1为起点的边的集合
1 5 6 
1 3 4 
1 2 1 

2 //以2为起点的边的集合 
2 3 2

3 //以3为起点的边的集合 
3 4 3

4 //以4为起点的边的集合
4 5 7
4 1 5 

5 //以5为起点的边不存在

我们先对上面的7条边进行编号第一条边是0以此类推编号【0~6】。
然后我们要知道两个变量的含义:

  • Next,表示与这个边起点相同的上一条边的编号。
  • head[ i ]数组,表示以 i 为起点的最后一条边的编号。

head数组一般初始化为-1, 为什么是 -1后面会讲到。加边函数是这样的:

//java版本 
static int total = 0;//++total:记录从第一条边到最后一条边 
private static void add(int start, int end, long weight) {//链式前向星的创建方法 
    ends[++total] = end; 
    weights[total] = weight; 
    next[total] = head[start];//以start为起点的上一条边的编号,即:与这个边起点相同的上一条边的编号 
    head[start] = total;//更新以start为起点的上一条边的编号 
}

我们只要知道next,head数组表示的含义,根据上面的数据就可以写出下面的过程:
对于1 2 1这条边:end[0] = 2; next [0] = -1; head[1] = 0;

对于2 3 2这条边:end[1]= 3; next [1]= -1; head[2] = 1;

对于3 4 3这条边:end[2] = 4; next [2]= -1; head[3] = 2;

对于1 3 4这条边:end[3] = 3; next [3]= 0; head[1] = 3;

对于4 1 5这条边:end[4] = 1; next [4]= -1; head[4] = 4;

对于1 5 6这条边:end[5] = 5; next [5]= 3; head[1] = 5;

对于4 5 7这条边:end[6] = 5; next [6]= 4; head[4] = 6;

遍历函数是这样的:

//java版本
static int[] head;//表示以 i 为起点的最后一条边的编号
static int[] next;//存储与当前边起点相同的上一条边的编号
static int[] ends;//存储边的终点
static long[] weights;//权值

//链式前向星的遍历方法,遍历出以x为起点的所有边
for (int i = head[x]; i != -1; i = next[i]) {//i表示:第 i 条边
    System.out.println(i + "这条边的终点:" + ends[i] + "这条边的权值:" + weights[i]);
}
/**
第一层for循环是找每一个点,依次遍历以【1,n】为起点的边的集合。
第二层for循环是遍历以 i 为起点的所有边,k首先等于head[ i ],
注意head[ i ]中存的是以 i 为起点的最后一条边的编号。
然后通过next[ j ]来找下一条边的编号。我们初始化head为-1,
所以找到你最后一个边(也就是以 i 为起点的第一条边)时,
你的next[ j ]为 -1作为终止条件。
*/

现在再回头去看代码,是不是更容易理解了呢?(* ̄︶ ̄)

相关的题还有:蓝桥王国,评论区里面有我的题解 ↓ 欢迎大家来踩踩~

image.png


总结

三种方法都能做出此题(或许还有更多方法),但是我们必须思考:如果这个题变个形————比如把第二个条件改一下:“小青可以在times[x]分钟内从任意节点X移动到节点2*X ”,那这个题就变成了一个带权的图,用bfs就不太行了。bfs就是特殊的最短路,边权为1的最短路可以用bfs,而堆优化可以有效降低朴素dijsktra的时间复杂度,OI必备,希望大家仔细理解后将其掌握!

大家还有其他解法吗?欢迎在评论区留言讨论!~

文章粗浅,如果本文对大家有帮助的话,希望可以点赞支持下~~~ (* ̄︶ ̄)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/397351.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何将两个或多个PDF文件合并成一个?这3个方法可以看看

在工作中&#xff0c;有时候我们需要把两个或多个PDF文件合并成一个&#xff0c;这样一来&#xff0c;可以方便阅读、修改&#xff0c;还能快速打印文件。 下面分享3个工具&#xff0c;看看如何将两个或多个PDF文件合并成一个文件。 方法一&#xff1a;使用美图工具 如果PDF文…

【Spring AOP】如何统一“拦截器校验、数据格式返回、异常返回”处理?

目录 一、Spring 拦截器 1.1、背景 1.2、实现步骤 1.3、拦截原理 二、 统一url前缀路径 2.1、方法一&#xff1a;在系统的配置文件中设置 2.2、方法二&#xff1a;在 application.properies 中配置 三、统一异常处理 四、统一返回数据返回格式处理 4.1、背景 4.2、…

PTA:L1-025 正整数A+B、L1-026 I Love GPLT、L1-027 出租(C++)

目录 L1-025 正整数AB 问题描述&#xff1a; 实现代码&#xff1a; L1-026 I Love GPLT 问题描述&#xff1a; 实现代码&#xff1a; L1-027 出租 问题描述&#xff1a; 实现代码&#xff1a; 原理思路&#xff1a; 出租那道题有点意思哈 L1-025 正整数AB 问题描述…

【Java学习笔记】13.Java StringBuffer 和 StringBuilder 类

Java StringBuffer 和 StringBuilder 类 当对字符串进行修改的时候&#xff0c;需要使用 StringBuffer 和 StringBuilder 类。 和 String 类不同的是&#xff0c;StringBuffer 和 StringBuilder 类的对象能够被多次的修改&#xff0c;并且不产生新的未使用对象。 在使用 St…

Tomcat8安装

1、前置环境 Tomcat 8 对应jdk 1.8 版本&#xff1b;如果你的jdk版本是8以上&#xff0c;则安装对应的tomcat版本。 jdk8 官方下载安装时&#xff0c;先安装jdk版本&#xff0c;最后单独安装jre。所以电脑会有两套jre&#xff0c;一套是jdk中的jre&#xff0c;位于 \jre 目录下…

客户案例|三强联手,深度集成,实现四方共赢

关键发现&#xff1a; 用户痛点&#xff1a;以现有ERP系统台账表单模式管理设备&#xff0c;已经不能满足伯恩业务增长所需的设备管理优化与革新的要求。 解决方案&#xff1a;利用西门子Mendix低代码平台与SAP PM模块进行集成开发的联合解决方案&#xff0c;为实现客户设备资…

3.8 并查集

并查集 题目链接 用途 维护集合 将两个集合合并询问两个元素是否在一个集合当中 实现思路 用树的形式维护集合每个集合用一棵树表示&#xff0c;树根的编号就是整个集合的编号&#xff0c;每个节点存储他的父节点&#xff0c;p[x]表示节点x的父节点判断树根的方法:p[x]x求…

运维视角:rabbitmq教程(三)镜像集群

上期回顾 RabbitMQ集群中节点包括内存节点、磁盘节点。内存节点就是将所有数据放在内存&#xff0c;磁盘节点将数据放在磁盘上。如果在投递消息时&#xff0c;打开了消息的持久化&#xff0c;那么即使是内存节点&#xff0c;数据还是安全的放在磁盘。那么内存节点的性能只能体现…

如何编写BI项目之ETL文档

XXXXBI项目之ETL文档 xxx项目组 ------------------------------------------------1---------------------------------------------------------------------- 目录 一 、ETL之概述 1、ETL是数据仓库建构/应用中的核心…

Linux基础命令-kill向进程发送信号

Linux基础命令-setfacl设置文件ACL策略规则 Kill 一.命令介绍 先使用帮助文档查看命令的信息 NAME kill - terminate a process kill命令的主要功能就是向进程发送信号&#xff0c;这里我们主要用来终止结束进程的&#xff0c;与它的英文单词含义相同&#xff0c;在Linux系统…

matlab在管理学中的应用简matlab基础【三】

规划论及MATLAB计算 1、线性规划 问题的提出 例1. 某工厂在计划期内要安排甲、乙两种产品的生产&#xff0c;已知生产单位产品所需的资源A、B、C的消耗以及资源的计划期供给量&#xff0c;如下表&#xff1a; 问题&#xff1a;工厂应分别生产多少单位甲、乙产品才能使工厂获…

相亲交友直播APP源码

一、什么是亲交友直播APP源码&#xff1f; 亲交友直播APP源码是一款婚恋交友类型的APP&#xff0c;可以帮助单身男女在网络平台就可以进行相亲交友。APP源码分两端&#xff0c;一端是用户端&#xff0c;另外一端是后台端。采用的技术&#xff0c;前端是安卓IOS&#xff0c;后端…

SCI期刊写作必备(二):代码|手把手绘制目标检测领域YOLO论文常见的性能对比折线图,一键生成YOLOv7等主流论文同款图表,包含多种不同功能风格对比图表

绘制一个原创属于自己的YOLO模型性能对比图表 具体绘制操作参考:(附Python代码,直接一键生成,精度对比图表代码 ) 只需要改动为自己的mAP、Params、FPS、GFlops等数值即可,一键生成 多种图表风格📈,可以按需挑选 文章目录 绘制一个原创属于自己的YOLO模型性能对比图…

二、HTTP协议02

文章目录一、HTTP状态管理Cookie和Session二、HTTP协议之身份认证三、HTTP长连接与短连接四、HTTP中介之代理五、HTTP中介之网关六、HTTP之内容协商七、断点续传和多线程下载一、HTTP状态管理Cookie和Session HTTP的缺陷无状态。Cookie和Session就用来弥补这个缺陷的。 Cooki…

Kafka 位移主题

Kafka 位移主题位移格式创建位移提交位移删除位移Kafka 的内部主题 (Internal Topic) : __consumer_offsets (位移主题&#xff0c;Offsets Topic) 老 Consumer 会将位移消息提交到 ZK 中保存 当 Consumer 重启后&#xff0c;能自动从 ZK 中读取位移数据&#xff0c;继续消费…

Kafka和RabbitMQ有哪些区别,各自适合什么场景?

目录标题1. 消息的顺序2. 消息的匹配3. 消息的超时4. 消息的保持5. 消息的错误处理6. 消息的吞吐量总结1. 消息的顺序 有这样一个需求&#xff1a;当订单状态变化的时候&#xff0c;把订单状态变化的消息发送给所有关心订单变化的系统。 订单会有创建成功、待付款、已支付、已…

C++面向对象编程之三:初始化列表、类对象作为类成员、静态成员

初始化列表C提供了初始化列表语法&#xff0c;可以用于成员属性初始化。语法规则&#xff1a;无参构造函数():属性1(值1), 属性2(值2), ... { }有参构造函数(形参1, 形参2, ...):属性1(形参1), 属性2(形参2), ... { }example&#xff1a;写一个怪物类&#xff0c;有怪物id和血量…

【产品设计】ToB 增删改查显算传

入职培训时技术leader说&#xff1a;“我不需要你们太聪明&#xff0c;做好基础的增删改查就可以了。”看似很简单的活&#xff0c;要做好并不容易。基础的坑在哪里呢&#xff1f; 一、 增&#xff08;新增、创建、导入&#xff09; 1. 明确表字段类型 新增的业务是由不同类型…

Android Studio开发APP

1.下载Android Studio 官网下载:Android Studio for Window ... 百度云下载:android-studio-bundle-141.1903250-windows.exe Android Studio 是谷歌推出的一个Android集成开发工具,基于IntelliJ IDEA. 类似 Eclipse ADT,Android Studio 提供了集成的 Android 开发工具用…

js 时间戳转换,全网最牛业务逻辑解析,逐行解析代码,千万不要错过,里面有些错误场景你可能还不知道

一、注意事项 1、js只识别13位的时间戳 2、10位的是unix时间戳 二、小试牛刀--日期类型转换成时间戳 可先将js字符串转换为date类型&#xff0c;再转换为时间戳类型。 字符串格式为 yyyy-MM-dd HH:mm:ss或者yyyy-MM-dd //把字符串转换成时间格式 let date new Date(2023-11-12…