7. 机器人项目

news2024/9/30 4:55:59

        在 “项目 ”章节中,我会暂时停止向你灌输新的理论,而是让我们一起完成一个程序。学习编程,理论是必要的,但阅读和理解实际程序同样重要。

        本章的课题是建立一个自动机,一个在虚拟世界中执行任务的小程序。我们的自动机将是一个收发包裹的机器人。

梅多菲尔德

梅多菲尔德村并不大。它由 11 个地方组成,中间有 14 条路。可以用这样的道路阵列来描述它:

        村里的道路网构成了一幅图。图是点(村庄中的地点)与线(道路)的集合。这个图将是我们的机器人移动的世界。

        字符串数组并不容易处理。我们感兴趣的是从给定地点可以到达的目的地。让我们把道路列表转换成一个数据结构,告诉我们每个地点可以到达的目的地。

        给定一个边数组后,buildGraph 会创建一个 map 对象,为每个节点存储一个连接节点数组。它使用 split 方法将道路字符串(其形式为 “Start-End”)转换为包含开始和结束的双元素数组,作为单独的字符串。

任务

        我们的机器人将在村子里移动。在不同的地方都有包裹,每个包裹都寄往其他地方。机器人遇到包裹就捡起来,到达目的地后再送出去。

        每到一处,自动机都必须决定下一步去哪里。当所有包裹都送完时,它就完成了任务。

        为了能够模拟这一过程,我们必须定义一个能够描述这一过程的虚拟世界。这个模型会告诉我们机器人在哪里,包裹在哪里。当机器人决定移动到某个地方时,我们需要更新模型,以反映新的情况。

        如果你采用面向对象编程的思维方式,你的第一反应可能是开始为世界中的各种元素定义对象:一个机器人类,一个包裹类,也许还有一个地点类。这些对象可以持有描述其当前状态的属性,例如某个地点的包裹堆,我们可以在更新世界时改变这些属性。

        这是错误的。至少通常是这样。听起来像对象的东西并不自动意味着它在程序中就应该是对象。为应用程序中的每一个概念都编写类的做法,往往会使你的程序成为一个相互关联的对象集合,而每个对象都有自己的内部变化状态。这样的程序往往难以理解,因此很容易被破解。

        取而代之的是,让我们把村子的状态浓缩为定义它的一组最小值。这里有机器人的当前位置和未交付包裹的集合,每个包裹都有一个当前位置和目的地地址。就是这样。

同时,让我们在机器人移动时不改变这个状态,而是为移动后的情况计算一个新的状态。

        移动方法是进行操作的地方。它首先会检查是否有一条路可以从当前位置通往目的地,如果没有,就会返回旧的状态,因为这不是一个有效的移动。

        接下来,该方法会创建一个新状态,将目的地作为机器人的新位置。它还需要创建一组新的包裹--机器人携带的包裹(在机器人当前位置)需要被移动到新位置。此外,还需要交付寄往新地点的包裹,也就是说,需要将它们从未曾交付的包裹中移除。地图调用负责移动,过滤调用负责递送。

        包裹对象在移动时不会改变,而是会重新创建。移动方法为我们提供了一个新的村庄状态,但旧的状态则完全保留。

        移动导致包裹被送达,这反映在下一个状态中。但初始状态描述的仍然是机器人在邮局、包裹未送达的情况。

持久数据

        不会改变的数据结构被称为不可变或持久性数据结构。它们的行为很像字符串和数字,因为它们就是它们,并保持不变,而不是在不同的时间包含不同的内容。

        在 JavaScript 中,几乎所有东西都可以更改,因此在处理本应是持久的值时需要有所克制。有一个名为 Object.freeze 的函数可以更改对象,从而忽略对其属性的写入。如果你想谨慎起见,可以用它来确保你的对象不会被更改。冻结确实需要计算机做一些额外的工作,而忽略更新和让人做错事情一样,都会让人感到困惑。我通常更倾向于告诉人们某个对象不应该被更改,并希望他们记住这一点。

        为什么语言明明希望我改变对象,我却偏偏不改变呢?因为这有助于我理解我的程序。这又与复杂性管理有关。当系统中的对象是固定、稳定的事物时,我可以孤立地考虑对它们的操作--从给定的起始状态移动到爱丽丝的房子,总会产生相同的新状态。当对象随时间发生变化时,这种推理的复杂性就会增加一个全新的维度。

        对于我们在本章中构建的这种小型系统,我们可以处理这种额外的复杂性。但是,我们能构建什么样的系统,最重要的限制在于我们能理解多少。只要能让代码更容易理解,我们就有可能构建一个更宏伟的系统。

        不幸的是,虽然理解一个基于持久性数据结构的系统比较容易,但设计一个系统,尤其是当你的编程语言无法提供帮助时,可能会有点困难。我们将在本书中寻找使用持久化数据结构的机会,但我们也将使用可改变的数据结构。

模拟

        一个送货机器人会观察世界,然后决定要向哪个方向移动。因此我们可以说,机器人是一个函数,它接收一个 VillageState 对象,并返回附近一个地方的名称。

        因为我们希望机器人能够记住事物,以便制定和执行计划,所以我们也将它们的内存传递给它们,并允许它们返回一个新的内存。因此,机器人返回的是一个对象,其中既包含它想要移动的方向,也包含下次调用时将返回给它的内存值。

        考虑一下机器人在 “解决 ”给定状态时需要做些什么。它必须通过访问每个有包裹的地点来拾取所有包裹,并通过访问每个有包裹的地点来递送包裹,但只有在拾取包裹后才能递送。

        最笨的策略是什么?机器人可以每轮随机行走。这就意味着,它很有可能最终会碰到所有的包裹,然后也会在某一时刻到达包裹应该送达的地方。

下面就是这种情况:

        请记住,Math.random() 返回一个介于 0 和 1 之间的数字,但总是低于 1。将这个数字乘以数组的长度,然后应用 Math.floor,就能得到数组的随机索引。

        由于这个机器人不需要记住任何东西,因此它忽略了第二个参数(请记住,JavaScript 函数可以调用额外的参数,而不会产生不良影响),并在返回的对象中省略了内存属性。

        要让这个复杂的机器人开始工作,我们首先需要一种方法来创建一个包含一些包裹的新状态。静态方法(这里是通过在构造函数中直接添加一个属性来编写的)是实现这一功能的好地方。

        我们不希望任何包裹从地址相同的地方寄出。因此,当 do 循环得到与地址相同的地址时,就会不断选择新的地址。

让我们创建一个虚拟世界。

由于机器人没有提前做好计划,所以它要转很多圈才能把包裹送到。我们很快就会解决这个问题。

        如果想更直观地了解模拟情况,可以使用本章编程环境中的运行机器人动画(runRobotAnimation)函数。该函数将运行模拟,但不会输出文本,而是显示机器人在村庄地图上移动的画面。

runRobotAnimation 的实现方式暂时还是个谜,但当你读完本书后面讨论 JavaScript 与网页浏览器集成的章节后,你就能猜到它是如何工作的了。

邮车的路线

        我们应该能比随机机器人做得更好。一个简单的改进方法就是借鉴现实世界中的邮件投递方式。如果我们找到一条能经过村里所有地方的路线,那么机器人就可以在这条路线上运行两次,这样就能保证完成任务。下面就是这样一条路线(从邮局开始):

为了实现机器人的路线跟踪功能,我们需要利用机器人内存。机器人会将其余路线保存在内存中,并在每一轮中丢弃第一个元素。

这个机器人已经快很多了。它最多需要转 26 圈(13 步路线的两倍),但通常会更少。

寻路

        不过,盲目地按照固定路线行驶并不能称得上是智能行为。如果机器人能根据需要完成的实际工作调整自己的行为,那么它的工作效率会更高。

        要做到这一点,机器人就必须能够有意识地向某个包裹或必须运送包裹的地点移动。要做到这一点,即使距离目标不止一步之遥,也需要某种寻路功能。

        在图中寻找路线的问题是一个典型的搜索问题。我们可以判断给定的解决方案(路线)是否有效,但不能像计算 2 + 2 那样直接计算解决方案。相反,我们必须不断创造潜在的解,直到找到一个有效的解为止。

        图中可能的路线数量是无限的。但在寻找从 A 到 B 的路线时,我们只对从 A 出发的路线感兴趣。我们也不关心那些两次访问同一地点的路线,因为这些路线绝对不是最有效的路线。因此,这就减少了路线搜索器需要考虑的路线数量。

        事实上,由于我们主要关注的是最短的路线,因此我们要确保在查看较长路线之前先查看较短的路线。一个好的方法是从起点开始 “增长 ”路线,探索每一个尚未到达的地方,直到路线到达目标。这样,我们只会探索那些潜在的有趣路线,而且我们知道我们找到的第一条路线是最短的。

下面的函数可以实现这一功能:

        探索必须按照正确的顺序进行--先到达的地方必须先被探索。我们不能一到达一个地方就立即探索,因为这意味着从那里到达的地方也要立即探索,以此类推,即使可能还有其他更短的路径尚未探索。

        因此,该函数保留了一个工作列表。这是一个数组,列出了下一步应该探索的地点,以及到达这些地点的路线。开始时,它只有起始位置和一条空路线。

        然后,搜索会选择列表中的下一个项目进行探索,这意味着它会查看从该地点出发的所有道路。如果其中一条是目标,就可以返回一条完成的路线。否则,如果我们之前没有搜索过这个地方,就会在列表中添加一个新项目。如果我们以前查看过这个地方,由于我们首先查看的是短路线,所以我们要么已经找到了一条更长的通往这个地方的路线,要么已经找到了一条和现有路线一样长的路线,我们就不需要再去探索它了。

        你可以把它想象成一张由已知路线组成的网,从起点位置爬出,向四面八方均匀延伸(但绝不会纠缠在一起)。一旦第一个线程到达目标位置,该线程就会被追溯到起点,从而得到我们的路线。

        我们的代码不会处理工作列表中没有工作项的情况,因为我们知道我们的图是连通的,这意味着每个位置都可以从所有其他位置到达。我们总能找到两点之间的路线,而且搜索不会失败。

        这个机器人将其记忆值作为移动方向列表,就像路线跟踪机器人一样。只要该列表为空,它就必须想出下一步该怎么做。它会选择第一个未投递的包裹,如果该包裹还未被取走,它就会规划出一条前往该包裹的路线。如果包裹已被取走,但仍需送达,那么机器人就会创建一条前往送货地址的路线。

让我们看看它是怎么做的。

        这个机器人通常在大约 16 个回合内完成运送 5 个包裹的任务。这比 routeRobot 稍微好一些,但仍绝对不是最佳状态。我们将在练习中继续完善它。

练习
测量机器人

        仅仅让机器人解决几个场景是很难对它们进行客观比较的。也许其中一个机器人只是碰巧完成了更容易的任务或它擅长的任务,而另一个机器人却没有。

        编写一个函数 compareRobots,接收两个机器人(以及它们的起始内存)。它应该生成 100 个任务,让两个机器人分别解决这些任务。完成后,它应该输出每个机器人完成每个任务的平均步数。

为了公平起见,请确保每个任务都分配给两个机器人,而不是每个机器人生成不同的任务。

代码

//机器人比较方法
function runRobotForCompare(state, robot, memory) {
    for (let turn = 0; ; turn++) {
        //执行完毕
        if (state.parcels.length == 0) {
            return turn;
        }
        //告诉机器人移动去哪里
        let action = robot(state, memory);
        //机器人往目标方向移动
        state = state.move(action.direction);
        //更新内存信息
        memory = action.memory;
    }
}

//比较机器人
function compareRobots(robot1, memory1, robot2, memory2) {
    let rd = VillageState.random();
    let robot1Count = runRobotForCompare(rd, robot1, memory1);
    let robot2Count = runRobotForCompare(rd, robot2, memory2);
    console.log("robot1,count:", robot1Count, "robot2,count:", robot2Count);
}
compareRobots(routeRobot, [], goalOrientedRobot, []);

机器人效率

        您能编写一个比 targetOrientedRobot 更快完成交付任务的机器人吗?如果您观察该机器人的行为,它会做哪些明显愚蠢的事情?如何改进?

如果您解决了前面的练习,您可能想使用您的 compareRobots 函数来验证您是否改进了机器人。

代码:使用迪杰斯特拉算法,减少执行次数?

持久组

        标准 JavaScript 环境中提供的大多数数据结构都不太适合持久化使用。数组有 slice 和 concat 方法,可以让我们在不损坏旧数组的情况下轻松创建新数组。但是,例如 Set,却没有任何方法可以在添加或删除一个项目后创建一个新的集合。

        编写一个新类 PGroup,类似于第 6 章中的 Group 类,用于存储一组值。与 Group 类一样,它也有 add、delete 和 has 方法。不过,它的 add 方法应该返回一个新的 PGroup 实例,并添加给定的成员,而旧的成员则保持不变。同样,delete 方法应创建一个不包含给定成员的新实例。该类应适用于任何类型的值,而不仅仅是字符串。在使用大量数值时,该类的效率不必很高。

        构造函数不应该是类的接口的一部分(尽管你肯定希望在内部使用它)。取而代之的是一个空实例 PGroup.empty,它可以用作起始值。为什么只需要一个 PGroup.empty 值,而不是每次都创建一个新的空映射?

代码:

class PGroup {
  arr = [];
  add(val) {
    if (!this.has(val)) {
      this.arr.push(val);
      return this;
    }
  }

  delete(val) {
    this.arr = this.arr.filter(n => n !== val);
    return this;
  }

  has(val) {
    return this.arr.indexOf(val) !== -1;
  }

  static empty() {
    return new PGroup;
  }
}
let a = PGroup.empty().add('a');
let ab = a.add('ab');
console.log(a.has('a'));
console.log(a.has('ab'));
let c = ab.delete('a');
console.log(c.has('a'));

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

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

相关文章

plt.bar函数介绍及实战

目录 plt.bar() 函数实战 plt.bar() 函数 plt.bar() 函数是 Matplotlib 中用于创建柱状图的函数。它用于在图形中绘制一个或多个柱状图,通常用于展示类别型数据的数量或大小的比较。 基本语法: plt.bar(x, height, width0.8, bottomNone, aligncenter…

【css】常见布局概述

本文将对css的常见布局方案进行概述,给大家提供系统化的布局解决方案参考。 一、流式布局 二、浮动布局 三、定位布局 四、弹性布局 五、网格布局 一、流式布局 顾名思义,该布局基于dom的文档流进行布局,是最常用、最原始的布局方式。 …

ThinkPHP发送邮件教程:从配置到发送指南!

ThinkPHP发送邮件功能实现策略?Thinkphp如何发邮件? ThinkPHP作为一个流行的PHP框架,提供了强大的邮件发送功能,使得开发者可以轻松地在应用中集成邮件发送功能。AokSend将详细介绍如何在ThinkPHP中配置和发送邮件。 ThinkPHP发…

Goland 设置GOROOT报错 The selected directory is not a valid home for Go SDK

问题描述 将go版本从1.16升级到1.22时配置GoRoot报错了如下图问题 The selected directory is not a valid home for Go SDK起因的是我的这个goland比较老了,2020年的。所以需要设置下版本 解决 OK,说一下解决办法: 找到go的安装路径&am…

Tomcat架构解析

Tomcat: 是基于JAVA语言的轻量级应用服务器,是一款完全开源免费的Servlet服务器实现。 1. 总体设计 socket: 其实就是操作系统提供给程序员操作“网络协议栈”的接口,你能通过socket的接口,来控制协议,实现网络通信,达…

.Net 6.0 监听Windows网络状态切换

上次发了一个文章获取windows网络状态&#xff0c;判断是否可以访问互联网。传送门&#xff1a;获取本机网络状态 这次我们监听网络状态切换&#xff0c;具体代码如下&#xff1a; public class WindowsNetworkHelper {private static Action<bool>? _NetworkStatusCh…

《Programming from the Ground Up》阅读笔记:p117-p146

《Programming from the Ground Up》学习第8天&#xff0c;p117-p146总结&#xff0c;总计30页。 一、技术总结 1.共享函数用法示例 (1)不使用共享函数 linux.s&#xff1a; # filename:linux.s# system call numbers(按数字大小排列&#xff0c;方便查看) .equ SYS_READ,…

FreeRTOS学习笔记一——FreeRTOS介绍

RTOS学习笔记&#xff0c;主要参考正点原子教程 目录 FreeRTOS特点任务调度方式抢占式调度时间片调度 任务状态状态转换任务列表 FreeRTOS特点 实现多个任务功能划分延时函数实现任务调度高优先级抢占低优先级每个任务都有自己的栈空间 注意&#xff1a; 中断可以打断任意任务…

Spring依赖注入推荐使用构造函数注入而非@Autowired

版权声明 本文原创作者:谷哥的小弟作者博客地址:http://blog.csdn.net/lfdfhl在Spring框架中,依赖注入(Dependency Injection, DI)是实现组件之间松耦合的关键技术。Spring支持多种依赖注入方式,其中构造函数注入和基于@Autowired注解的注入是两种常见的方法。然而,Spri…

活体检测标签之2.4G有源RFID--SI24R2F+

首先从客户对食品安全和可追溯性的关注切入&#xff0c;引出活体标签这个解决方案。接着分别阐述活体标签在动物养殖和植物产品方面的应用&#xff0c;强调其像 “身份证” 一样记录重要信息&#xff0c;让客户能够了解食品的来源和成长历程&#xff0c;从而放心食用。最后呼吁…

手机USB连接不显示内部设备,设备管理器显示“MTP”感叹号,解决方案

进入小米驱动下载界面&#xff0c;等小米驱动下载完成后&#xff0c;解压此驱动文件压缩包。 5、小米USB驱动安装方法&#xff1a;右击“计算机”&#xff0c;从弹出的右键菜单中选择“管理”项进入。 6、在打开的“计算机管理”界面中&#xff0c;展开“设备管理器”项&…

VS开发 - 静态编译和动态编译的基础实践与混用

目录 1. 基础概念 2. 直观感受一下静态编译和动态编译的体积与依赖项目 3. VS运行时库包含哪些主要文件&#xff08;从VS2015起&#xff09; 4. 动态库和静态库混用的情况 5. 感谢清单 1. 基础概念 所谓的运行时库&#xff08;Runtime Library&#xff09;就是WINDOWS系统…

【易上手快捷开发新框架技术】nicegui标签组件lable用法庖丁解牛深度解读和示例源代码IDE运行和调试通过截图为证

传奇开心果微博文系列 序言一、标签组件lable最基本用法示例1.在网页上显示出 Hello World 的标签示例2. 使用 style 参数改变标签样式示例 二、标签组件lable更多用法示例1. 添加按钮动态修改标签文字2. 点击按钮动态改变标签内容、颜色、大小和粗细示例代码3. 添加开关组件动…

RFID系统如何革新资产信息数字化管理

在现代企业中&#xff0c;资产管理的有效性直接影响整体运营效率和成本控制。为了应对传统资产管理中存在的诸多挑战&#xff0c;越来越多的公司开始采用RFID系统&#xff0c;以实现资产信息的数字化管理&#xff0c;从而提高资产利用率和管理透明度。 RFID系统的主要优势 高…

【USB】USB1.0、USB1.1、USB2.0、USB3.0、USB4.0介绍及最大速率说明

USB 1.0 and USB 1.1 1995 年&#xff0c;成立了 USB-IF&#xff0c;该组织于次年宣布推出 USB 1.0。USB 1.0 规定的数据速率为 1.5 Mbit/s&#xff08;低带宽或低速&#xff09;&#xff0c;最大输出电流为 5V/500mA。但不幸的是&#xff0c;这个 USB 版本也很少被制造商采用。…

C++ string的基本运用详细解剖

string的基本操作 一.与C语言中字符串的区别二.标准库中的string三.string中常用接口的介绍1.string中常用的构造函数2.string类对象的容量操作函数3.string类对象的访问及遍历操作4.string类对象的修改操作5.string类的非成员函数6.string中的其他一些操作 一.与C语言中字符串…

网页WebRTC电话和软电话哪个好用?

关于WebRTC电话与软件电话哪个更好用&#xff0c;这实际上取决于多个因素&#xff0c;并没有一个绝对的答案。不过&#xff0c;我可以根据WebRTC技术的一些特点&#xff0c;以及与传统软件电话相比的优劣势&#xff0c;为你提供一个清晰的对比。 首先&#xff0c;让我们了解一下…

python画图|放大和缩小图像

在较多的画图场景中&#xff0c;需要对图像进行局部放大&#xff0c;掌握相关方法非常有用&#xff0c;因此我们很有必要一起学习 【1】官网教程 首先是进入官网教程&#xff0c;找到学习资料&#xff1a; https://matplotlib.org/stable/gallery/subplots_axes_and_figures…

uniapp在线打包的ios后调用摄像头失败的解决方法

uniapp在线打包的ios后调用摄像头失败的解决方法 解决方法&#xff1a; 由于未选中打包模块的配置 当你在测试时发现能够正常的开启摄像头&#xff0c;但是当你对其进行在线打包后&#xff0c;发现当你点击启用摄像头时&#xff0c;没有反应&#xff0c;或者是打开是黑屏状态…

STM32F103C8----3-1 LED闪烁(跟着江科大学STM32)

一&#xff0c;电路图 接线图 面包板的的使用请参考&#xff1a;《面包板的使用_面包板的详细使用方法-CSDN博客》 二&#xff0c;目的/效果 2.1 推婉输出 外部供电&#xff08;熄的时间长&#xff09; 2.2 推婉输出 内部供电(亮的时间长) 三&#xff0c;创建Keil项目 详…