人工智能之搜索策略-A*算法入门

中文原文:http://blog.csdn.net/pojianbing/article/details/5649142

英文原文:http://www.gamedev.net/page/resources/_/technical/artificial-intelligence/a-pathfinding-for-beginners-r2003

A*(读作A星)算法对初学者可能略显复杂.虽然网络上已经有很多讲解A*算法的文章,但大多数都是为那些已经具备基础知识的人写的.而本文特为真正新手准备.
本文并不是要给出这个问题的终极篇,而是阐述一些基础问题,帮你做好拓展阅读的准备,能真正明白他们在说什么.文章最后给出了其中佼佼者的链接供继续阅读.
最后一点,本文并非针对具体方案.你可以把这里讨论的东西转换成任何计算机语言实现.正如你所期望的,文章最后给出了示例代码的下载链接.该示例程序包含C++和Blitz Basic两个版本.如果你仅仅想看一下效果A*算法的执行效果,里面有可执行文件.
我们正在超越自己.我们开始…
 

入门:搜索区

假设有一个人要从A点到B点去中间有一墙之隔,如下图所示.绿色是出发点,红色是终点,蓝色填充的正方形代表墙.

     

首先应该注意到的是我们已经把搜索区域划分成了方格.把搜索区进行简化,这是寻径的第一步.这种特殊的方法可以帮我们把搜索区简化成一个二维的数组.数组的每一项代表方形区域的一个格子,它的状态用'通行'和'禁行'表示.寻径就是要找出从A点到B点要走的格子.一旦路径确立,这个人就可以从一个格子的中心到另一格子的中心直至到达目的地.
这些中心点被称作"节点".当你阅读其他寻径的材料,你会经常看到人们讨论节点.为什么不直接叫它们方格呢?因为很有可能你切分出来的并不是一定是方形.它们实际上有可能是长方形,六角形,三角形,或者任何形状.节点包围在各种形状里,可能被放在任意位置--中心或者沿边缘,或任何其他地方.我们使用这个概念系统,因为它最简单.
 

开始搜索

一旦我们像上面一样把搜索区域简化成可接受数量的节点,下一步就是进行搜索,寻找最短路径.我们从A点开始这项工作,检查邻接区域,并开始向外扩大搜索范围直至找到目标点.
我们按照下面的步骤开始搜索:
1.从A点开始,先把它加入到待计算的open list中.这个open list就像一个购物清单一样.现在清单里面只有一项,后面会有更多.这个清单里面包含了备选路径可能经过的区域或者不经过的区域.基本上这就是一个格子的待查列表.
2.忽略那些有墙,有水,或者非正常的格子,看一下与出发点邻接的所有可到达或者可通过的区域.把它们也加入到open list.这里面每一个节点都把A节点作为它的"父节点"(parent square).父节点是我们在做回溯路径时很重要的一个概念.后面会有详细说明.
3.从准入区域清单中去掉起始点A,并把它加入到closed list中.closed list是指从现在开始就不用再考虑的区域.
 
说到这里,你脑海里应该有一个形象化的东西了,就像下面的图展示的.黑-绿区域的中心是你的开始区域.边缘浅蓝色是表示该区域已经加入了closed list.所有的邻接区域现在都放在open list中待查,它们的边缘被标记成浅绿色,每一个区域都有一个灰色的指针指向父节点,即开始节点.

 

下面,我们从open list列表中选择一个邻接区域,或多或少的重复上面的过程,如下所述.但是应该选择哪个区域呢?F成本最低的那一个!
 

路径评分

决定使用哪一个区域来构成路径,关键在于下面的等式:F=G+H
这里
   G=沿着生成的路径,从开始节点A到指定区域的移动消耗.
   H=估算从指定区域格子到目的地格子(B节点)的移动消耗.通常这被称为启发式,这可能有一点不好理解.之所以这样叫是因为有一个"猜"的行为.由于路上可能遇到各种情况(墙,水,等等),直到我们找到路径才能知道实际的距离.本文会给出一个计算H的算法,网上还可以找到很多其它的方法.
 
路径是通过反复遍历open list并选择最低F值产生的.这个过程后面会详述.首先近距离看一下如何计算这个等式.
如上所述,G是从开始节点A沿着生成的路径到指定区域的移动消耗.本例中,我们设每水平或垂直移动一个区域消耗移动力10,对角线消耗14移动力.使用这样的数值是因为实际的对角移动距离是2的平方根(别害怕),大约是水平或垂直移动消耗力的1.414倍.简单起见我们使用10和14.这样做使得比例大约正确并且避开了计算平方根和小数.这并不是因为我们笨不喜欢数学,而是使用这些整数计算要快得多.后面你就会发现,如果不使用这样的捷径计算,寻径可能很慢.
 
我们从开始节点沿着一个特定路径到达指定的区域,这个区域的G值消耗就是从它到父节点的消耗,按照正交加上10,对角线加上14.这种计算方法在本例中会有进一步的展现,因为我们的路径超过了一个格的距离.
 
H值可以使用各种方式估算.我们这里使用的是曼哈顿法(Manhattan),它计算从当前区域到达目的地纵横移动所经过的总区域数,忽略对角线移动,忽略路径上可能出现的任何不明障碍.然后乘以我们刚刚定义的纵横移动消耗值:10;这种方法被称作曼哈顿法可能是因为它就像从一地到另一地,计算的是经过的城市数,而你不能从城市中间斜穿而过.
 
读过上面的描述你可能猜想启发式只是一个从当前节点到目的地剩余距离按照直线方式行进的粗略估算.事实并非如此,我们实际上尝试沿着路径估算剩余距离(通常会远一些).我们的估算值越接近实际剩余距离,算法就越快.如果我们高估了剩余距离就不能保证得到的是最短路径了.这就是所谓的"不能接受的启发"(inadmissible heuristic).
 
从技术上讲,本例中,曼哈顿法是不能接受的,因为它略微高估了剩余距离.但我们还是使用它,因为它能够比较简单的突出我们的意图,而且只是轻微的高估.极罕见的场景中曼哈顿法产生的路径不是最短路径而只是一个接近最短.想了解更多可以参考等式和启发式的补充说明.
 
F是G和H之和.我们搜索过程的第一步结果可以看下图.F,G,H三个值被标记在每一个区域.图示中区域在出发区域的右侧,F标在左上角,G左下角,H右下角. 

 

一起看一下这些区域.看一下标有字母的区域G=10,这是因为从它到起始区域只需要水平方向上经过一块区域.起始区域正上方和正下方,以及左右的区域的G都是10.对角线上的区域G值都是14.
 
H值是估算到红色区域的曼哈顿距离:只做水平和垂直运动(并忽略掉障碍物)的距离.按照这个方式计算从该区域经过三块区域到达红色区域,因此H值是30;这块区域正上方的区域是四块区域远所以是40,记住只有横向和纵向移动.你可以看出来其它区域的H值是怎么计算出来的了.
重申一下,每个区域的F值是G和H之和.
 

继续搜索

要继续进行搜索,我们只是简单地从open list中寻找F值最低的项;然后:
4)从open list中删除该项并把它加入closed list中.
5)检查该项所有的邻接区域.忽略那些已经在closed list中的项和不能通过的项(墙,水,或其它非正常地形),然后检查该邻接区域是否在open list中,如果不在就加入其中.该项作为新区域的"父节点";
6)如果邻接区域已经在open list中,检查一下到此区域是否有一个更好的路径.换句话说,检查它的G值比当前区域到它的G值要低.如果不是这种情况什么都不用做.
 
对已经选择的项我们进行如下操作:
    如果新路径G成本较低,改变邻接区域的父节点为选择的项(图中就是改变指针的指向选择的项).最后重新计算邻接区域的F值和G值.如果感觉有点混乱看一下下面的图示:
    好,来看看是怎么操作的.初始化的9块区域在把起始区域转移到结束列表之后,还有8个在open list.这8块区域中具有最低F值40的是右侧的区域.我们把它作为下一区域.下面的图示中我们把它标蓝.

 

首先我们把它从open list中转移到closed list中(这就是标蓝的原因).下面开始检查邻接区域.右面是墙忽略掉.左面是起始点,它已经在结束列表中所以也忽略掉.
 
其它另外四格已经在open list中,所以我们需要检查到这些格的路径是否比使用当前格要好,参考标准是G值.看当前格正上方的格,它的G值是14.如果我们选择从当前格到正上方格,G值为20(从开始点到当前格是10加上纵向移动10).G值20大于14所以不是一个更优路径.看图会有直观感受.从起始格沿对角线过去比起横向走一格再纵向走一格走来得直接.
 
我们在所有准入列表中的邻接四格反复执行这个过程,我们通过这个格子没有发现更优路径存在,所以我们什么都不改动.考察完所有的邻接四格,当前格的操作已经完成,现在要做的就是移动到下一格.
 
遍历一下open list中的格子,现在还有7格,还是选择一个F值最低的.很有意思,我们这里有两个有最低F值54的格.那应该选择哪个呢?这没有关系,考虑到计算速度,直接选择最后加入准入列表的那个.后面你就会发现这种偏见方案更适用于方格情况,特别是你接近目标的时候.但这不要紧.(这种情况的不同处理方式导致两种A*算法可能发现同样长度的不同路径)
我们选择下方的格,见下图:

 

这一次我们检查邻接格子发现右边是墙,和上面一样忽略掉.我们同样要忽略掉墙正下方的一格,为什么呢?如果不切墙角而过,从当前格你不能直接到达正下方的一格.实际过程你肯定是要先往下移动一格然后到达,是一个绕墙角的过程.(注意:在允许切墙角的环境下这一条规则可选.这取决于你的格子如何摆放)
 
现在还剩5格,其中当前格下方的两格还没有加入到open list中,我们将它们加入并把当前格作为它的父节点.
另外三格已经有两个在结束列表中(起始格和当前格正上方一格,上图中使用蓝色标记),我们忽略掉它们.还有最后一格,我们需要检查有没有通过当前格使G值更小的路径出现.结果徒劳;所以我们结束当前格子的操作,准备检查open list中的下一格.重复这个过程直到目标格子进入closed list,如下图所示

 

 
注意开始格下方的两个格子的父节点和前一个图相比已经发生了变化.之前G值为28的格子指向右上方格子,现在它的G值是20并指向正上方的格子.这是在搜索过程中发生的,检查G值发现使用新路径可以使得它的值更小-所以修改了它的父节点并重新计算了F值和G值.这个变化在我们这个例子里面显得并不是太重要.不断地检查过程中可能出现很多可能性使得最终到达目的地格子的路径千差万别.
 
那么怎么样确定一条路径呢?简单讲,从红色格子开始进行回溯,沿着箭头方向从一个格子到它的父节点.最终会回到起始点.如下图所示.从开始点A到目的地B的移动简化成从沿着路径从每一个格子的中心(节点)到下一个格子的中心,直至到达目标.
  

A*算法总结
好吧,我们来回顾一下整个过程,把一步一步的方法放在一起:
1)把开始格子(或节点)添加到open list.
2)反复执行下面的过程:
  a)在准入列表中选择F值最小的,我们称之为当前格子;
  b)把当前格子转移到closed list.
  c)对于当前格子邻接的8个格子进行如下处理:
不能通过或者已经在closed list中就忽略之.否则就进行下一步
如果不在open list中,添加到open list中.并设置其父节点为当前格子.记录该格子的F,G,H值
如果已经在open list中了,那么就以G值为标准看有没有更优路径.G值更小说明路径更优.如果是这样的话就把当前格子作为它的父节点并重新计算该格的G值和F值.如果你保持open list按照F值排序,你可能还要重新排序.
     d)结束直到进行到:
把目的地格子添加到Closed list中,这样就能找到路径了(看下面的注解),或者
没有找到目的地格子,open list已经为空.这种情况就是没有找到路径.
   3)保存路径.从目的地格子开始回溯直到起始格子,那就是你要的路径.
 
:本文较早的版本会建议你目的地格子(节点)已经在准入列表中就可以停止了,而不是以进入结束结束列表为标准.这样做会更快,它几乎都会给你一条路径但并不总能得到.从第二个节点到最后一个节点(目的地节点)移动消耗可能因地形不同而变化-比如两个节点之间有条河.
 

实现备注

现在你已经明白了基本的方法,自己用程序实现时还有一些事情要考虑.下面是我在使用C++和Blitz Basic实现时遇到的问题,但这些要点同样适用于其它语言.
 
1.其它的元素(避免碰撞):如果你已经仔细看过我的样例代码,你就会发现它完全忽略屏幕上的其它元素.不同的游戏,这可能是可以接受,也可能不是.如果在寻径算法中考虑其它元素并让它们自行运动,那么我建议你只考虑停下的或者寻径过程中邻接的元素,把它们当前的位置当成不能通过的.对于那些移动的邻接元素你可以避免碰撞找一个替代路径.(详解#2)
 
如果你选择考虑正在运动元素和寻径过程不邻接的元素,你需要写一个方法对他们在任意时间任意位置的情况做出预测来实现躲避.否则你可能得到一个锯齿形路径仅仅是为了躲避一个已经不在那个位置上的元素.
你当然还需要一些碰撞检测代码,因为不管当时计算出来的路径多好事情会随着时间变化,当发生碰撞必须重新寻径或者其它元素在运动而且不是迎头相撞,等待直到路径没有障碍.
 
这些提示足以让你开始了.如果你想了解更多,下面有一些有用的链接:
 
Steering Behavior for Autonomous Characters:Craig Reynold在寻径过程中的转向处理略有不同,但它可以与寻径算法集成在一起实现更完整的避免碰撞系统.
The Long and Short of Steering in Computer Games: 一个有关转向和寻径的学术研究,这是一个PDF
Coordinated Unit Movement:对形成和基于组的运动两部系列著作的第一部,作者是帝国时代的设计师Dave Pottinger
Implementing Coordinated Movement:Dave Pottinger两部著作的第二部.
 
2.不同地貌的消耗:本教程程序使用的地形有两种:通行的和禁行的.但是如果一个格子是可以通过的,但是消耗特别大呢?沼泽,丘陵,地牢楼梯所有这些地形都可以通过但是比平地消耗较大.同样路径也会有一个成本比较小的.
 
这个问题通过计算G值时添加地形消耗就可以解决了.为那些地形简单的添加一下消耗加成就可以.A*算法里面添加一下寻找最低消耗的操作就能很容易处理好.我用的例子里面地形只有通行和禁行两种因此A*就是寻找一个最短最直接的路径.但在各种消耗不同的地形上最低消耗的路径可能包含最远距离的路径,就像绕道而不是直接穿越沼泽.
 
还有一个要考虑的就是被专业人士称作"影响映射"的东西.正如上面说的消耗不等的地形,你可以设计一个额外的加成系统应用到寻径中.想象一下你有一个山地区域防守上面有很多防御元素.每一次有人企图通过都会被挫败,如果你愿意大可以创建一个地图到处都是大屠杀.这将教会计算机选择一条更安全的路径.避免部队仅仅因为最短就选择一条路径,因为它也可能更危险.还有一种可能性就是在移动元素的路径旁边使用威慑元素.A*算法的一个缺点就是当一群元素都想到一个类似的目的地时,由于采取相同或者类似的路径,路径会出现比较严重的重叠.添加惩罚性到节点上有助于保证分离度,减少碰撞.不要把这些节点当成不能通过处理,因为你依然希望万不得已多个元素还是可以挤在一条通道上.另外惩罚的原则是只惩罚附近路径上的元素而不是所有路径,否则就会产生无处可躲的奇怪现象.并且惩罚节点出现的位置是沿着当前的路径和之后要经过的路径,而不是已近走过的路径.
 
3.处理尚未探索到的区域:电脑总是知道如何移动,包括地图还没有探索开的情况,你玩过这样的游戏吗?根据不同的游戏,寻径做得非常好.幸运的是,这是一个比较容易处理的问题.
答案是为每一个玩家和电脑对手创建一个单独的"knownWalkability"数组(每个玩家而不是每个元素--那将使用更多的内存).每一个数组都包含了玩家已经探索开的区域,地图的其它部分假设是可以通过的,直到证明并非如此.使用这种方法,元素可以漫游到死角,做出错误的选择直到他们探索开周围.一旦地图被探索开,寻径就可以正常工作了.
 
4.平滑路径:尽管A*算法会给你一个最短成本最低的路径,但是它不会给你提供一条平滑的路径.可以看一下我们图7给出的最终路径.那条路径第一步就是其实点右下方的格子,我们的路径能平滑一点从正下方开始么?有几种方法可以解决这一问题.计算路径过程中一旦发现方向改变就增加惩罚值,同时修改G值.或者你可以在路径计算之后,在邻接的地方选择一个节点使得路径更为平滑.这个话题更多的讨论请移步这里查看:
 Toward More Realistic Pathfinding,  来自Gamasutra.com 作者 Marco Pinter.
 
5.非方形搜索区域:我们的例子使用的都是2D方形布局.你不必拘泥于此.你可以使用不规则的地区.想一下棋盘游戏Risk里面的国家.你可以为类似那样的游戏想象一个寻径场景.要做到这一点,你需要建立一个表来存储国家的邻接情况和从一个国家到另外一个国家的G值.你可能还需要一个方法来估算H值.剩下的就和上面例子的处理方式差不多了.除了使用邻接区域,你要看一下邻接国家,把邻接国家也添加到open list中.类似的,你需要为一个固定的地形图建立一个航点系统.航点通常是一条路径上的关键点:比如隧道或者地牢.作为一个游戏的设计者预先分配航点.如果两个航点之间没有障碍物可以直接相连就认为二者是"邻接".正如Risk的例子,你会把这个邻接信息到一个查询表中,并在构建open list过程中使用.你可能也会记录G值(当然就是节点间的直线距离)和H值(节点到目的地的直线距离).其它过程照旧.
Amit patel 已经写了一个简短的文章来研究一些替代方案.另外一个例子是isometric RPG地图使用的非方形搜索区,移步查看我的文章: Two-Tiered A* Pathfinding
 
6.速度相关:当开发自己的A*程序时,或者用我写的那个程序,你最终会发现寻径占用了大量的CPU时间.特别是你有一定数量需要寻径的元素而且地图又够大.如果读过网络上一些文章,你会发现事实的确如此,甚至星际争霸和帝国时代的设计者也深以为然.如果你发现由于寻径导致游戏变慢,下面有一些加速方法:
 考虑一个更小的地图或者少一些影响寻径的元素.
不要同时做多个寻径,把他们放在一个队列里面分散到几个游戏周期进行.如果你的游戏周期是40秒,没有人会注意到.但是如果同时进行寻径运算玩家会注意到游戏变慢了.
在你的地图上考虑使用较大的方形(不管你使用的到底是什么图形).这就大大减少了寻径需要搜索的节点总数.如果你野心勃勃,你可以设计出来两套寻径系统,在不同的路径长度下使用.专业人士就是这样做的,他们先使用大区域进行寻径,然后切换到一个更精细的范围进行寻径.如果你对这个话题感兴趣,请移步查看我的文章: Two-Tiered A* Pathfinding
对于较长的路径,考虑进入游戏之前对路径进行预计算.
考虑预处理地图,找出那些地区是不能通过的,我们把这些地区称为"孤岛".在现实中,他们可以是岛屿或者任何其他从周围无法进入的地区(比如被墙).A*算法的一个缺点就是如果你告诉它需要寻找一个区域,它就会寻找整个地图,直到找到每一个可以进入的节点已经处理放入了open list或者Closed list.这会浪费很多CPU时间.可以通过提前判断是否可以进入来预防(比如洪水填充路径或者类似的情况),记录在数组等类似的结构中,寻径之前先进行这个检查.
在一个拥挤的,迷宫式的环境中,考虑标记出节点不会导致任何地方成为死角.这些地区可以在地图编辑中预先手工定制.或者你野心勃勃开发一个算法可以自动识别识别这种区域.所有的死胡同节点都会分配一个独立的号码标示.接着,你就可以放心地忽略掉寻径中的死角,除非开始节点或者目的地节点就被围在一个死胡同中,你就停下来吧.
 
7.维护Open List:这实际上是A*算法中最耗时的操作.每一次你访问open list,你需要找到具有最低F值的格子.有很多方法可以做到这一点.你可以根据需要保存路径项或者遍历整个表找到F值最小的格子.这很简单但是真正寻径过程中这是相当耗时的.可以维护一个SortedList然后每一次就取列表的第一个就是你想要的最小F值消耗节点.我写程序的时候最初就使用这个方法.
 
这个方法在小地图中表现不错,但它不是最快的方案.严谨的A*算法开发者为了加速会使用一种叫二叉堆(binary heap)的结构,现在我的代码就使用这个结构.就我的经验,这种方法大多数情景中可以加快2-3倍,长路径情况下速度呈几何级数加快(快10倍以上).如果你有兴趣了解更多二叉堆的内容,请看我的文章:Using Binary Heaps in A* Pathfinding.
 
另一个可能的瓶颈是你在寻径过程中需要清空和维护的数据结构.我个人喜欢全都存放在数组中.节点可以动态地,按照面对对象的方式创建,记录,维护.我发现创建删除对象需要消耗额外的时间这间接地影响了速度.如果你使用了数组,每一次调用之间都要做清理.最后要做的就是每一次寻径结束之后要全部进行初始化,特别是地图特别大的时候.我通过使用二维数组WhichList(x,y) 指示每一个节点是在open list还是closed list避免了这种情况.寻径结束之后我并不初始化这个数组而是每一次寻径之后重置onClosedListOnOpenList的值,每次寻径递增5或者类似的值.使用这个方法,算法就可以比较安全的忽略掉前一次寻径的数据.我也把F,G,H值存放在数组中.这样我每一次就直接覆盖已有值而不必寻径结束之后清除整个数组了.存储二维数组需要使用更多的内存,因此这里有一个权衡,无论如何你都应该选择一个自己感觉舒服的数据结构.

 

 8.Dijkstra的算法:尽管A*的被普遍认为是最好的寻路算法(上文赘述),至少有另外一个算法,有其适用场景 - Dijkstra算法。 Dijkstra算法本质与A*相同,只是没有启发式(H值始终为0)。因为它没有启发性,同样扩大了它在各个方向搜索。正如你可能想象到的,因此Dijkstra探索通常到达最终目标之前探索一个更大的区域。这就是通常情况下它比A*算法慢的原因.那为什么还要用它?有时候我们不知道目的地在那里.假设你有一个资源采集的游戏元素,需要出去采集某种资源,它就可以知道多个采集区,但是只选择一个最近的.这种情况Dijkstra比A *好,因为我们不知道哪一个是最近的。唯一的办法就是反复使用A *找到每一个距离,然后选择一条路径.很可能有很多种类似的情况:需要找到多个位置,选择一个最近的.但又不知道它在哪里或者哪一个可能是最近的.

 进一步阅

好了,现在你对一些高级话题已经具备了基础知识和印象.从这点考虑,我建议你深入看一下我的源代码.代码包有两个版本,一个C++版本一个使Blitz Basic版本.两个版本都有大量的评论,相对而言也比较容易懂.链接在此:

  • Sample Code: A* Pathfinder (2D) Version 1.9

如果你不了解C++或者Blitz Basic,可以找到两个C++版本的exe程序.Blitz Basic版本可以通过在其官网下载Blitz Basic 3D(不是Blitz Plus)运行.

你还应该考虑阅读下面推荐的网页.通读过本教程,它们应该容易懂了多了:

  • Amit’s A* Pages: 这是一个被广泛引用的页面,作者Amit patel;但如果你没有首先阅读本文由可能会感觉迷惑.值得一看,尤其是Amit对这个话题自己的观点.
  • Smart Moves: Intelligent Path Finding: 作者Bryan Stout发表在 Gamasutra.com,要求注册之后阅读.免费注册,为了这篇文章也值得注册一下更不必说还有其它资源.Bryan用Delphi写的程序帮助我学习了A*算法,同样也是我A*程序背后的灵感.它还描述了A*算法的一些可选方案.
  • Terrain Analysis:这是一个高级话题,但是非常有趣,作者是Dave Pottinger, Ensemble工作室的专家.这家伙协调帝国时代之王者时代的开发.不要期望能全读懂,但这篇有趣的文章可以给你产生很多自己的想法.文中包含了对Mip-Mappinginfluence Mapping的讨论以及其它一些关于寻径和人工智能的高级话题.

 

还有一些站点值得关注:

 

  • aiGuru: Pathfinding
  • Game AI Resource: Pathfinding
  • GameDev.net: Pathfinding

 

我同样强烈推荐下面几本书,里面都有有若干分支讲到寻径和其它AI话题.它们都附有样例代码的CD.两本我都买了,如果你使用下面的链接购买,我还能拿点Amazon的回扣 :)

 


好吧,就这么多.我非常乐意能见到你恰巧在程序中使用了本文的某个观点.你可以通过下面的方式联系我:

 

万分期待,祝你好运


原文链接:http://www.policyalmanac.org/games/aStarTutorial.htm

----End---

你可能感兴趣的:(人工智能(AI))