游戏与常用的五大算法---上篇

 前言

      什么时候,我们之间竟然变得这么生疏

      什么时候,我想见到你,却又害怕见到你

      什么时候,才能在我身边,告诉我。其实,你一直都在

                                   -----------《仙剑奇侠传》

PS:为了方便大家阅读,个人认为比较重要的内容-------红色字体显示

                                      个人认为可以了解的内容-------紫色字体显示

---------------------------------------------------------------------------

-----------------------------------------------分-割-线--------------------------------------------

            最近感觉好忙啊,不过每天也都过得很充实,希望这样保持下去,一直到毕业。好久没有提笔写博客了,尽然已经有半个月之多了,不过今天来讨论一下游戏与算法,主要准备从常用的五大算法入手,顺便讨论一下,游戏与算法之间的关系。其实游戏与算法真的密不可分!如果没有了这些算法,那么游戏几乎就无法运作,加上本身游戏对于性能的要求就很高,所以一款游戏的游戏必然要求有让人拍案叫绝的算法!

                                                          算法之一:分治算法

、什么是分治算法

      首先来说一说什么是分治法,“分治”二字顾名思义,就是“分而治之”的意思,说的通俗一点就是步步为营各个击破,再来解释分而治之的意思,其实也就是把一个问题(一般来说这个问题都是比较复杂的)分成两个相同或者相似的子问题,再把子问题分成更小的问题,一直这样下去.......直到最后,子问题可以简单地求解,还有一点就是把所有求得的子问题合并就是原问题的解。其实在很多场合下都会使用到分治算法,比如说我们常用的归并排序、快速排序都是很常见的分治思想的体现。

、核心思想

      说完了分治算法的概念,我们就该谈一谈分治算法的思想及策略

      分治法的思想:将一个难以直接解决的大问题,分解成规模较小的相同问题,接下来就是刚刚说的八个字:步步为营、各个击破。

      怎么样才能达到这种状态呢?我们需要用什么方法呢?首先假设遇到一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决。不过有时候却没有很好地思路去解,这时候如果你发现如果n取得比较小的情况下,很容易解决,那么我们就应该将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。

       不过在使用的时候还要多说几句:

       假设我们遇到一个规模为n的问题,这个问题可分割成k个子问题,1<k≤n,且这些子问题都可解并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法

、分治算法的适用场景

       知道了分治算法的原理,接下来的自然是归结到一个“用”字上面,怎么使用呢?要使用之前肯定要知道什么样的条件下可以使用或者说是适合使用分治算法。

      1) 该问题的规模缩小到一定的程度就可以容易地解决

      2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质

      3) 利用该问题分解出的子问题的解可以合并为该问题的解;

      4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题

       需要注意的是:第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加,所以说第一条不能作为重要的依据。

      第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、

      第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法

      第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好

、实际运用

       具体到实际运用的过程之中,归结到到游戏上面的话,其实用的还是挺常见的。最常见的就是在RGP游戏之中,主角会经常获得道具,有时候我们会想给这些道具按个数的多少拍个序,那么最常见的做法就是按一下数量这个按钮。按下之后就会给这些道具内容进行排序了!一般来说快排是用的最多的,但是归并也很常见,恰好这两者都是分治算法的体现。 

     总结一下在实际过程之中怎么运用,以下三步是分治思想的惯用套路

     实际上就是类似于数学归纳法,找到解决本问题的求解方程公式,然后根据方程公式设计递归程序。
第一步:一定是先找到最小问题规模时的求解方法,一般来说最小问题规模的求解方法是很简单的(就像归并排序之中当问题规模最小的时候,也就是只有一个元素的时候,直接就已经有序了)。
第二步:然后考虑随着问题规模增大时的求解方法,区间划分完了之后,开始考虑规模增大之后应该怎么做,还是以归并排序为例,当划分到每一个元素之后,不能再往下划分了,这时候就需要考虑问题增大时候的求解方法,增大具体方法需要借助另外一个存储空间,这也是归并排序为什么需要O(N)的额外存储空间。
第三步:找到求解的递归函数式后(各种规模或因子),设计递归程序即可。
      来看一个具体例子吧,下面是一个归并排序的例子:
//---------------------------归并排序之中问题增大时候的求解方法---------------------------------
void Merge(int sourceArr[], int tempArr[], int startIndex, int midIndex, int endIndex)
{
	int  i = startIndex, j = midIndex + 1, k = startIndex;
	while (i != midIndex + 1 && j != endIndex + 1)
	{
		if (sourceArr[i] >= sourceArr[j])
			tempArr[k++] = sourceArr[j++];
		else
			tempArr[k++] = sourceArr[i++];
	}
	while (i != midIndex + 1)
		tempArr[k++] = sourceArr[i++];
	while (j != endIndex + 1)
		tempArr[k++] = sourceArr[j++];
	for (int index = startIndex; index <= endIndex; ++index)
		sourceArr[index] = tempArr[index];
}
//---------------------------------归并排序划分为子问题------------------------------------------
void  MergeSort1(int sourceArr[], int tempArr[], int startIndex, int endIndex)		//内部递归使用
{
	int midIndex = 0;
	if (startIndex < endIndex)
	{
		midIndex = startIndex + (endIndex - startIndex) / 2;
		MergeSort1(sourceArr, tempArr, startIndex, midIndex);
		MergeSort1(sourceArr, tempArr, midIndex + 1, endIndex);
		Merge(sourceArr, tempArr, startIndex, midIndex, endIndex);
	}
}
//----------------------------------------优化方法---------------------------------------------
void MergeSort2(int sourceArr[], int tempArr[], int startIndex, int endIndex)
{
	int midIndex = 0;
	if ((endIndex - startIndex) >= 50)              // 大于50个数据的数组进行归并排序  
	{
		midIndex = startIndex + (endIndex - startIndex) / 2;
		MergeSort2(sourceArr, tempArr, startIndex, midIndex);
		MergeSort2(sourceArr, tempArr, midIndex + 1, endIndex);
		Merge(sourceArr, tempArr, startIndex, midIndex, endIndex);
	}
	else                                            // 小于50个数据的数组进行插入排序
		InsertSort(sourceArr + startIndex, endIndex - startIndex + 1);
}</span></span></font>

       来看一看优化与不优化两者时间实验结果比较:


       最后再来看一看游戏之中排序的应用(一般是归并排序或者是快速排序)吧,一般来说归并排序在文件的排序用的比较多,而快速排序在大多数情况都适用,如下图所示(图为《仙剑四》买物品的场景,游戏确实有点老了,而且仙剑六都已经出了,仙剑七也正在开发过程之中,但是个人还是认为仙剑四和五前最为经典,所以电脑上一直保留着,自己希望能多多研究这样的经典游戏),对于物品的选择,如果我们物品很多,但是你希望按价格高低排序看一看的,这时候排序就派上用场了,点一下价格,就会按照价格降序排列!


                                                      算法之二:动态规划算法

、什么是动态规划

      关于什么是动态规划呢?用通俗一点的话来说就是“边走边看”,注意和回溯法这种先把一条道走到黑的方法区别开来,总的来说就是前面的知道了,后面的也可以根据前面的推导出来了。好了通俗的话说到这了,下面用正规一点的语言总结一下:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。

、核心思想

       其实在刚开始接触的时候,很容易把动态规划与分治算法混在一起,不过这两者还真的有些类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段。不过动态规划之中前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

       由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。

       与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解),但是分治法不同,分治法一般最后才把这些子问题合并,但是在这之前他们是互不干扰的,所以分治法只要一直往下划分即可。

、动态规划的适用场景

       动态规划适用的场景还是挺多的,而且什么笔试的时候也很喜欢考,这样的题目都有一个特点,就是如果你知道要使用动态规划区解这个题,那么做起来回很方便,很快速,代码量不多,但却很考验思维。这也是为什么动态规划出现地比较多的原因,甚至在一些什么ACM大赛上,动态规划也是一个易考点。

       高中里我们都学过线性规划,使用来求最优解的方法,动态规划与它也有点类似,所以说动态规划本质上来说还是规划,是不断进行决策的问题,一般用于求解最(优)值;而分治是一种处理复杂问题的方法,不仅仅只用于解决最值问题(而且我们一般也不用它来求最值,你想一串数字如果特别多,你想找一个最大的出来,用了一个排序是不是有一点奢侈呢,比较游戏与效率要求真的很高)。

         所以如果能用动态规划来解决的问题,通常要满足以下三点要求:

        (1)最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。

        (2)无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关

        (3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)


、实际运用

       前面说了这么多,还是得归结到一个""字上面,什么情况下适用呢?具体到游戏上又应该用在什么什么上面呢?先来说一说怎么用吧!这里我用一下我之前看到的一段总结的比较好的话来说明一下怎么动态规划怎么使用!

      动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。

                               初始状态→│决策1│→│决策2│→…→│决策n│→结束状态

                                                      动态规划决策过程示意图

    (1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。

    (2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。

    (3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程

    (4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

      一般,只要解决问题的阶段状态状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。

实际应用中可以按以下几个简化的步骤进行设计:

    (1)分析最优解的性质,并刻画其结构特征。

    (2)递归的定义最优解。

    (3)以自底向上自顶向下记忆化方式(备忘录法)计算出最优值,一般我们可以把需要记忆的内容放在一个全局变量或者一个多维数组之中。

    (4)根据计算最优值时得到的信息,构造问题的最优解。

     不过在具体地操作过程之中还是有几点需要说明一下的:

      动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。

      使用动态规划求解问题,最重要的就是确定动态规划三要素

      (1)问题的阶段             (2)每个阶段的状态         (3)从前一个阶段转化到后一个阶段之间的递推关系。

      递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。

       确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。

                               f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}

       网上看到一个通用的动态规划算法的通用架子,如下:
  for(j=1; j<=m; j=j+1) // 第一个阶段
    xn[j] = 初始值; 
  for(i=n-1; i>=1; i=i-1)// 其他n-1个阶段
    for(j=1; j>=f(i); j=j+1)//f(i)与i有关的表达式
      xi[j]=j=max(或min){g(xi-1[j1:j2]), ......, g(xi-1[jk:jk+1])};
    t = g(x1[j1:j2]); // 由子问题的最优解求解整个问题的最优解的方案
    print(x1[j1]);
  for(i=2; i<=n-1; i=i+1)
  {  
      t = t-xi-1[ji];
      for(j=1; j>=f(i); j=j+1)
        if(t=xi[ji])
            break;
  }

       基本用法介绍完了,我们可以来看一看使用动态规划的典型例子,首先就是典型的问题:背包问题。背包问题在我看来就是使用有限的资源,尽可能的创造出更多的价值。背包问题原题是给定n种物品和一背包。物品i的重量是wi,其价值为vi,背包的容量为C。问应如何选择装入背包的物品,使得装入背包中物品的总价值最大?

       接下来我们先把背包问题解决了,然后在说一说在游戏之中背包问题引出的动态规划思想的体现。

       先给出一个具体地背包问题,题目如下:

       有编号分别为a,b,c,d,e的五件物品,它们的重量分别是2,2,6,5,4,它们的价值分别是6,3,5,4,6,现在给你个承重为10的背包,如何让背包里装入的物品具有最大的价值总和

       一看到最大最小值的问题,我们首先应该想一想是否可以使用动态规划解决这个问题呢?一般来说求最值问题,最常用的或者说是最先想到的就应该是动态规划。之前通过上面的分析,我们对于背包问题应该有了一定地思路,不过就是写代码的问题了,关于这道题目的分析过程,这里给出一个链接地址:点这里
       上面给出的链接文章之中对于背包问题进行了很好的分析,所以有需要的可以点进去看一下,不过个人感觉他的代码给出的解释太少,所以自己写了一个,大家可以参考参考。

//--------------------------------------------背包问题------------------------------------------
const int Bag_Capacity = 10;			//背包的总容量
const int Weight[] = { 0, 2, 2, 6, 5, 4 };	//用于存放物品重量的数组,其中0号位置没有用到,只是为了方便而已
const int Value[] = { 0, 6, 3, 5, 4, 6 };	//用于存放物品价值的数组,同样的0号位置没有用到
const int nCount = sizeof(Weight) / sizeof(Weight[0]) - 1;//物品的总个数
int  GoodList[nCount + 1];			//物品的存在序列(1表示存在,0表示不存在)

void  Package(int mor[][11], const int Wei[], const int Val[], const int size)
{
	//通常来说背包问题采用自底向上的方式解决比较好,所以我们假定先放的是最后一个物品,也就是Wei[size]
	//通过自底向上的方式来设置mor这个数组比较好
	//首先进行参数检测
	if (NULL == mor || NULL == Wei || NULL == Val)
		return;
	//在放入第一个元素,也就是Wei[n]
	for (int index = 0; index <= Bag_Capacity; ++index)
	{
		//判断是否可以放背包,index从0到背包的最大容量,可以理解为index是一个试探变量
		//因为物品重量都是整数,所以一定存在某一个值正好等于物品重量(在物品重量小于背包重量的前提之下)
		//如果比背包重量大的话,直接不放如,那么总价值为0(放第一个物品)
		if (index < Wei[size])			
			mor[size][index] = 0;			
		else                                    
			mor[size][index] = Val[size];
	}
	//接下来就是动态规划的体现,对剩下的n-1个物品放入,也就是填充mor数组
	for (int row = size - 1; row >= 0; --row)
	{
		for (int col = 0; col <= Bag_Capacity; ++col)
		{
			if (col < Wei[row])				//这里保持和下面一样就可以了
				mor[row][col] = mor[row + 1][col];
			else                                                  //这里需要理解一下
			{
				mor[row][col] = mor[row + 1][col] > mor[row + 1][col - Wei[row]] + Val[row] ?
					mor[row + 1][col] : mor[row + 1][col - Wei[row]] + Val[row];
			}
		}
	}
}

void GetList(int mor[][11], const int size)
{
	//现在的目的就是为了得到了一个序列,关于物品是否存在的序列
	int  index = Bag_Capacity;
	int i = 0;
	for (i = 1; i <= size - 1; ++i)					//判断前n-1个物品是存在
	{
		if (mor[i][index] == mor[i + 1][index])
			GoodList[i] = 0;
		else
		{
			GoodList[i] = 1;
			index = index - Weight[i];
		}
	}
	//对于最后一个问题,那么只需要判断相应位置是否为0即可
	GoodList[i] = mor[i][index] ? 1 : 0;
}

int main()
{
	int Memory[6][11] = { 0 };
	Package(Memory, Weight, Value, nCount);
	//先把整个过程打印出来
	for (int row = 1; row <= nCount; ++row)
	{
		for (int col = 0; col <= Bag_Capacity; ++col)
			printf("%4d", Memory[row][col]);	//使用printf在这里比较方便指定行宽
		cout << endl;
	}
	GetList(Memory, nCount);
	cout << "最优解为:" << endl;
	for (int idx = 1; idx <= nCount; ++idx)
		cout << GoodList[idx];
	cout << endl;
	return 0;
}</span></span></span>
       关于背包问题在实际游戏之中的运用,我觉得在策略性游戏之中比较有用,记得小学三年级玩的星际争霸的时候,当时还小,而且游戏还是英文的,所以经常打不过电脑。当时就觉得电脑很强(虽然后面打电脑觉得很简单),不过现在看来AI制造部队的时候会不会也是采用类似背包的思想呢?在当前有限的资源下,制造最强战斗力呢?当然还有一个问题,就是资源是不断变化的(除非矿石都已经被采完了),这种情况肯定比背包复杂的不是一点点,所以我认为在这里AI肯定有一套自己的策略用来生产部队。动态规划是一个不错的方法,当然实际肯定会复杂的多。毕竟还有外界因素的影响

       还有一个感觉可能符合的是今年寒假期间刚刚发行的《三国志13》,里面采用了与《三国志12》完全不同的画风,感觉是大地图上宏伟了很多,来看一张截图:


       从上图我们可以看到,这一代玩家可以扮演任意一个角色,而且可以去执行任务。但是需要钱,不同的人执行所需要的金钱也是不同的(同智力成反比),智力越高,所花的金钱越少,所以说这里就需要AI选择了。怎么样花最少的金钱,获得最大的发展。智力就相当于我们上面背包问题里面的重量,执行人物效果又可以对应于背包问题之中的价值。从而选择对于总价值最高的建设方式,尽快提升城市的繁荣程度。个人感觉这一代的AI比上一代的AI明显会思考了很多,而且发展也快了很多。当然游戏里面肯定设计复杂很多,所以说AI的设计真的是一个很大的研究方向,总之应该设计这样的AI,会简单模拟人的思考。用最少的资源,尽快建设城市,训练部队(这两者怎么取舍,这也是一个大问题),而且这些还用到了一些博弈论里面的知识,所以这里就不在赘述了!

                                                            算法之三:贪心算法

、什么是贪心算法

       刚刚上面讲了动态规划,接下来讲一讲贪心算法。解释一下贪心算法,从字面上先解释一下,所谓贪心就是总是在当前情况下做出最为有利的选择,也就是说它不从整体上考虑。它只是做出了某种意义上的局部最优解

       需要说明的一点就是,贪心算法不像动态规划那样有固定的框架,由于贪心算法没有固定的算法框架,因此怎么样区分有关于贪心算法呢?这就需要一种贪心策略了!利用它来区分各种贪心算法。还有需要说明的就是它与动态规划最本质的区别就是贪心算法不是所有情况下都能得到整体最优解,而且往往来说得到的只是一个近似最优解,所以说如果是求最值的问题上,我们一般不用贪心算法,而是采用动态规划算法。

       另外,贪心策略的选择必须满足无后效性,这是很重要的一点,说的具体一点就是某个状态以后的过程不会影响以前的状态,只与当前状态有关。所以我们在使用贪心算法的时候一点要看一看是否满足无后效性。

、核心思想

       关于贪心算法,其实没有过多要说的,就简单说一下步骤吧!  
       第一步:建立数学模型来描述问题。
       第二步:把求解的问题分成若干个子问题
       第三步:对每一子问题求解,得到子问题的局部最优解
       第四步:把子问题的解局部最优解合成原来解问题的一个解。

、贪心算法的适用场景

       由于贪心算法求出来的解并不是最优解,也就注定在某些要求结果精确的情况之中无法使用,有人可能会认为贪心算法用到的并不多,而且贪心策略的前提就是尽量保证局部最优解可以产生全局最优解,最美好的贪心策略当然就是希望能通过不断地求局部最优解从而得到全局最优解!

       就拿刚刚的背包问题来说,显然使用贪心算法是无法得出答案的(一般情况下不能,不过也有很小的可能恰好是全局最优解),因为贪心策略只能从某一个方向考虑,比如单单以重量(每次选择重量最轻的),或者用价值(每次选择价值最高的),甚至用价格与重量的比值,其实这三者都实际运用过程之中都有问题,基本很难得到最优解。

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

       不过还是给出使用贪心算法的一般框架吧:

       //从问题的某一初始解出发;
        while (能朝给定总目标前进一步)
        { 
             利用可行的决策,求出可行解的一个解元素;
        }
        //由所有解元素组合成问题的一个可行解;</span></span></span>

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

、实际运用

      因为在实际过程之中我们都希望通过贪心求得最值,所以说在实际之中运用的不是特别多,最小生成树算是一种。但是在游戏之中贪心算法特别常见!因为对于游戏来说尽可能快求得一个解,从而提高游戏性能显得更为重要,哪怕这个解不是最优解,只要他快,而且最好能让他尽可能的接近最优解的话,那么这样的算法有何尝不是一种好算法呢?在游戏之中贪心算法用的最普遍的就是寻路

  先引用一段网上关于寻路的一段话:

       我们尝试解决的问题是把一个游戏对象(game object)从出发点移动到目的地。路径搜索(Pathfinding)的目标是找到一条好的路径——避免障碍物、敌人,并把代价(燃料,时间,距离,装备,金钱等)最小化。运动(Movement)的目标是找到一条路径并且沿着它行进。把关注的焦点仅集中于其中的一种方法是可能的。一种极端情况是,当游戏对象开始移动时,一个老练的路径搜索器(pathfinder)外加一个琐细的运动算法(movement algorithm)可以找到一条路径,游戏对象将会沿着该路径移动而忽略其它的一切。另一种极端情况是,一个单纯的运动系统(movement-only system)将不会搜索一条路径(最初的“路径”将被一条直线取代),取而代之的是在每一个结点处仅采取一个步骤,同时考虑周围的环境。同时使用路径搜索(Pathfinding)和运动算法(movement algorithm)将会得到最好的效果。

                                                                   A*寻路算法

   接下来就来讲一讲游戏之中常用或者说是2D游戏之中最常用的算法---A*寻路算法!当然寻路算法不止 A* 这一种,还有递归, 非递归, 广度优先, 深度优先, 使用堆栈等等, 有兴趣的可以研究研究~~

   先从背景知识开始吧!在计算机科学中,A*算法广泛应用于寻路和图的遍历。最早是于1968年,由Peter HartNils Nilsson 和Bertram Raphael3人在斯坦福研究院描述了 该算法。是对Dijkstra算法的一种扩展。是一种高效的搜索算法。

                                       寻路的步骤

   总结出下面的寻路六部曲大家先看看下面这张图,因为下面的步骤都是基于这两张图的(一张是开始的图,一张是最终找到了的图)


第一步:从起点A开始, 把它作为待处理的方格存入一个"开启列表", 开启列表就是一个等待检查方格的列表

第二步:寻找起点A周围可以到达的方格, 将它们放入"开启列表", 并设置它们的"父方格"为A

第三步:从"开启列表"中删除起点 A, 并将起点A 加入"关闭列表", "关闭列表"中存放的都是不需要再次检查的方格

       

    注:图中浅绿色描边的方块表示已经加入"开启列表" 等待检查.淡绿色又有点接近淡蓝色描边的起点 A 表示已经放入"关闭列表" , 它不需要再执行检查

        从 "开启列表" 中找出相对最靠谱的方块, 什么是最靠谱? 它们通过公式 F=G+H 来计算,F也叫作启发函数

                F = G + H

                G 表示从起点 A 移动到网格上指定方格的移动耗费 (可沿斜方向移动).

                H 表示从指定的方格移动到终点 B 的预计耗费 (关于H的取法有很多种,最常见的也是用最多的就是曼哈顿算法,两点之间的横坐标之差与纵坐标之差的和,需要注意的是用曼哈顿算法不一定能得到最优路径 而且如果采用曼哈顿算法,那么严格意义上来说只能叫A搜索,不能叫A*搜索,由于采用这个方法说起来简单,实现起来也比较简单,适合初学者,所以本文就采用了曼哈顿算法。A*本身不限制H使用的估计算法,如max(dx,dy)、sqrt(dx*dx+dy*dy)、min(dx,dy)*(0.414)+max(dx+dy)这些都可以(可惜曼哈顿算法dx+dy不在此列),记住一点,只要你能保证H值恒小于实际路径长,A*就是成立的。你甚至可以取一个常数0,这样A*就退化为广搜了)。

        我们还是采用曼哈顿算法来说明吧,因为这样写起来方便,就暂时不去区分A算法与A*算法了!假设横向移动一个格子的耗费为10, 为了便于计算, 沿斜方向移动一个格子耗费是14.。为了更直观的展示如何运算 FGH, 图中方块的左上角数字表示 F, 左下角表示 G, 右下角表示 H。

        从 "开启列表" 中选择 F 值最低的方格 C (绿色起始方块 A 右边的方块), 然后对它进行如下处理:

       第四步:把它从 "开启列表" 中删除, 并放到 "关闭列表" 中

       第五步: 检查它所有相邻并且可以到达 (障碍物和 "关闭列表" 的方格都不考虑) 的方格. 如果这些方格还不在 "开启列表" 里的话, 将它们加入 "开启列表", 计算这些方格的 G,,H 和 F 值各是多少, 并设置它们的 "父方格" 为 C

       第六步: 如果某个相邻方格 D 已经在 "开启列表" 里了, 检查如果用新的路径 (就是经过C 的路径) 到达它的话, G值是否会更低一些,,如果新的G值更低, 那就把它的 "父方格" 改为目前选中的方格 C, 然后重新计算它的 F 值和 G 值 (H 值不需要重新计算, 因为对于每个方块, H 值是不变的).。如果新的 G 值比较高, 就说明经过 C 再到达 D 不是一个明智的选择,,因为它需要更远的路, 这时我们什么也不做.

       上述已构成了一个子问题的求解过程,所以就这样, 我们每次都从 "开启列表" 找出 F 值最小的, 将它从 "开启列表" 中移掉, 添加到 "关闭列表".。再继续找出它周围可以到达的方块,如此循环下去...

       那么什么时候停止呢? —— 当我们发现 "开始列表" 里出现了目标终点方块的时候, 说明路径已经被找到。

       最后一个问题就是如何返回路径呢?

       别忘了,我们还保存了”父节点“呢,最后从目标格开始, 沿着每一格的父节点移动直到回到起始格, 这就是路径

       最后用一张动态图作为结束吧!(关于A*算法的代码,后面会补上!)

    https://img-blog.csdn.net/20160309141620011

       其实还有两个常用的算法没有说完,一个是回溯法、一个是分支界限法。这两个算法有些类似,也有区别。所以准备下回一起讲!就暂时写到这吧!


  • 31
    点赞
  • 166
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
算法一:A*寻路初探 From GameDev.net 译者序:很久以前就知道了A*算法,但是从未认真读过相关的文章,也没有看过代码,只是脑子里有个模糊的概念。这次决定从头开始,研究一下这个被人推崇备至的简单方法,作为学习人工智能的开始。 这 篇文章非常知名,国内应该有不少人翻译过它,我没有查找,觉得翻译本身也是对自身英文水平的锻炼。经过努力,终于完成了文档,也明白的A*算法的原理。毫 无疑问,作者用形象的描述,简洁诙谐的语言由浅入深的讲述了这一神奇的算法,相信每个读过的人都会对此有所认识(如果没有,那就是偶的翻译太差了-- b)。 原文链接:http://www.gamedev.net/reference/articles/article2003.asp 以下是翻译的正文。(由于本人使用ultraedit编辑,所以没有对原文中的各种链接加以处理(除了图表),也是为了避免未经许可链接的嫌疑,有兴趣的读者可以参考原文。 会者不难,A*(念作A星)算法对初学者来说的确有些难度。 这篇文章并不试图对这个话题作权威的陈述。取而代之的是,它只是描述算法的原理,使你可以在进一步的阅读中理解其他相关的资料。 最后,这篇文章没有程序细节。你尽可以用任意的计算机程序语言实现它。如你所愿,我在文章的末尾包含了一个指向例子程序的链接。 压缩包包括C++和Blitz Basic两个语言的版本,如果你只是想看看它的运行效果,里面还包含了可执行文件。 我们正在提高自己。让我们从头开始。。。 序:搜索区域 假设有人想从A点移动到一墙之隔的B点,如下图,绿色的是起点A,红色是终点B,蓝色方块是中间的墙。 [图1] 你 首先注意到,搜索区域被我们划分成了方形网格。像这样,简化搜索区域,是寻路的第一步。这一方法把搜索区域简化成了一个二维数组。数组的每一个元素是网格 的一个方块,方块被标记为可通过的和不可通过的。路径被描述为从A到B我们经过的方块的集合。一旦路径被找到,我们的人就从一个方格的中心走向另一个,直 到到达目的地。 这些中点被称为“节点”。当你阅读其他的寻路资料时,你将经常会看到人们讨论节点。为什么不把他们描述为方格呢?因为有可 能你的路径被分割成其他不是方格的结构。他们完全可以是矩形,六角形,或者其他任意形状。节点能够被放置在形状的任意位置-可以在中心,或者沿着边界,或 其他什么地方。我们使用这种系统,无论如何,因为它是最简单的。 开始搜索 正如我们处理上图网格的方法,一旦搜索区域被转化为容易处理的节点,下一步就是去引导一次找到最短路径的搜索。在A*寻路算法中,我们通过从点A开始,检查相邻方格的方式,向外扩展直到找到目标。 我们做如下操作开始搜索: 1,从点A开始,并且把它作为待处理点存入一个“开启列表”。开启列表就像一张购物清单。尽管现在列表里只有一个元素,但以后就会多起来。你的路径可能会通过它包含的方格,也可能不会。基本上,这是一个待检查方格的列表。 2,寻找起点周围所有可到达或者可通过的方格,跳过有墙,水,或其他无法通过地形的方格。也把他们加入开启列表。为所有这些方格保存点A作为“父方格”。当我们想描述路径的时候,父方格的资料是十分重要的。后面会解释它的具体用途。 3,从开启列表中删除点A,把它加入到一个“关闭列表”,列表中保存所有不需要再次检查的方格。 在这一点,你应该形成如图的结构。在图中,暗绿色方格是你起始方格的中心。它被用浅蓝色描边,以表示它被加入到关闭列表中了。所有的相邻格现在都在开启列表中,它们被用浅绿色描边。每个方格都有一个灰色指针反指他们的父方格,也就是开始的方格。 [图2] 接着,我们选择开启列表中的临近方格,大致重复前面的过程,如下。但是,哪个方格是我们要选择的呢?是那个F值最低的。 路径评分 选择路径中经过哪个方格的关键是下面这个等式: F = G + H 这里: * G = 从起点A,沿着产生的路径,移动到网格上指定方格的移动耗费。 * H = 从网格上那个方格移动到终点B的预估移动耗费。这经常被称为启发式的,可能会让你有点迷惑。这样叫的原因是因为它只是个猜测。我们没办法事先知道路径的长 度,因为路上可能存在各种障碍(墙,水,等等)。虽然本文只提供了一种计算H的方法,但是你可以在网上找到很多其他的方法。 我们的路径是通过反复遍历开启列表并且选择具有最低F值的方格来生成的。文章将对这个过程做更详细的描述。首先,我们更深入的看看如何计算这个方程。 正 如上面所说,G表示沿路径从起点到当前点的移动耗费。在这个例子里,我们令水平或者垂直移动的耗费为10,对角线方向耗费为14。我们取这些值是因为沿对 角线的距离是沿水平或垂直移动耗费的的根号2(别怕),或者约1.414倍。为了简化,我们用10和14近似。比例基本正确,同时我们避免了求根运算和小 数。这不是只因为我们怕麻烦或者不喜欢数学。使用这样的整数对计算机来说也更快捷。你不就就会发现,如果你不使用这些简化方法,寻路会变得很慢。 既然我们在计算沿特定路径通往某个方格的G值,求值的方法就是取它父节点的G值,然后依照它相对父节点是对角线方向或者直角方向(非对角线),分别增加14和10。例子中这个方法的需求会变得更多,因为我们从起点方格以外获取了不止一个方格。 H 值可以用不同的方法估算。我们这里使用的方法被称为曼哈顿方法,它计算从当前格到目的格之间水平和垂直的方格的数量总和,忽略对角线方向。然后把结果乘以 10。这被成为曼哈顿方法是因为它看起来像计算城市中从一个地方到另外一个地方的街区数,在那里你不能沿对角线方向穿过街区。很重要的一点,我们忽略了一 切障碍物。这是对剩余距离的一个估算,而非实际值,这也是这一方法被称为启发式的原因。想知道更多?你可以在这里找到方程和额外的注解。 F的值是G和H的和。第一步搜索的结果可以在下面的图表中看到。F,G和H的评分被写在每个方格里。正如在紧挨起始格右侧的方格所表示的,F被打印在左上角,G在左下角,H则在右下角。 [图3] 现在我们来看看这些方格。写字母的方格里,G = 10。这是因为它只在水平方向偏离起始格一个格距。紧邻起始格的上方,下方和左边的方格的G值都等于10。对角线方向的G值是14。 H 值通过求解到红色目标格的曼哈顿距离得到,其中只在水平和垂直方向移动,并且忽略中间的墙。用这种方法,起点右侧紧邻的方格离红色方格有3格距离,H值就 是30。这块方格上方的方格有4格距离(记住,只能在水平和垂直方向移动),H值是40。你大致应该知道如何计算其他方格的H值了~。 每个格子的F值,还是简单的由G和H相加得到 继续搜索 为了继续搜索,我们简单的从开启列表中选择F值最低的方格。然后,对选中的方格做如下处理: 4,把它从开启列表中删除,然后添加到关闭列表中。 5,检查所有相邻格子。跳过那些已经在关闭列表中的或者不可通过的(有墙,水的地形,或者其他无法通过的地形),把他们添加进开启列表,如果他们还不在里面的话。把选中的方格作为新的方格的父节点。 6,如果某个相邻格已经在开启列表里了,检查现在的这条路径是否更好。换句话说,检查如果我们用新的路径到达它的话,G值是否会更低一些。如果不是,那就什么都不做。 另一方面,如果新的G值更低,那就把相邻方格的父节点改为目前选中的方格(在上面的图表中,把箭头的方向改为指向这个方格)。最后,重新计算F和G的值。如果这看起来不够清晰,你可以看下面的图示。 好了,让我们看看它是怎么运作的。我们最初的9格方格中,在起点被切换到关闭列表中后,还剩8格留在开启列表中。这里面,F值最低的那个是起始格右侧紧邻的格子,它的F值是40。因此我们选择这一格作为下一个要处理的方格。在紧随的图中,它被用蓝色突出显示。 [图4] 首先,我们把它从开启列表中取出,放入关闭列表(这就是他被蓝色突出显示的原因)。然后我们检查相邻的格子。哦,右侧的格子是墙,所以我们略过。左侧的格子是起始格。它在关闭列表里,所以我们也跳过它。 其 他4格已经在开启列表里了,于是我们检查G值来判定,如果通过这一格到达那里,路径是否更好。我们来看选中格子下面的方格。它的G值是14。如果我们从当 前格移动到那里,G值就会等于20(到达当前格的G值是10,移动到上面的格子将使得G值增加10)。因为G值20大于14,所以这不是更好的路径。如果 你看图,就能理解。与其通过先水平移动一格,再垂直移动一格,还不如直接沿对角线方向移动一格来得简单。 当我们对已经存在于开启列表中的4个临近格重复这一过程的时候,我们发现没有一条路径可以通过使用当前格子得到改善,所以我们不做任何改变。既然我们已经检查过了所有邻近格,那么就可以移动到下一格了。 于 是我们检索开启列表,现在里面只有7格了,我们仍然选择其中F值最低的。有趣的是,这次,有两个格子的数值都是54。我们如何选择?这并不麻烦。从速度上 考虑,选择最后添加进列表的格子会更快捷。这种导致了寻路过程中,在靠近目标的时候,优先使用新找到的格子的偏好。但这无关紧要。(对相同数值的不同对 待,导致不同版本的A*算法找到等长的不同路径。) 那我们就选择起始格右下方的格子,如图。 [图5]
五大常用算法是动态规划、分治、递归、贪心和回溯。 动态规划是一种将问题分解成子问题并保存子问题解的方法。通过求解子问题,可以逐步推导出原始问题的解。动态规划通常用于求解最优化问题,例如最长公共子序列、最短路径等。 分治是将原问题划分成多个相互独立的子问题,然后通过递归的方式求解子问题,并将子问题的解合并成原问题的解。分治算法常用于排序、快速幂等问题。 递归是通过函数调用自身来解决问题的方法。递归算法在问题定义可以被分解为较小规模或更简单情况的时候很有用。例如,计算一个数的阶乘,就可以使用递归实现。 贪心算法是一种选择当前最优策略的方法,即在每一步选取最优解,最终得到全局最优解的算法。贪心算法常用于解决无后效性的问题,例如最小生成树、哈夫曼编码等。 回溯是一种通过穷举搜索所有可能的解空间,找到满足条件的解的方法。回溯算法在解决组合问题、排序问题、子集和问题等方面很有效。回溯算法通过递归的方式逐步构建解,当发现当前解不满足条件时,会回退到上一步继续搜索其他可能的解。 这五种常用算法在不同的问题领域中都有广泛应用,每种算法都有自己的特点和适用范围。在解决具体问题时,可以根据问题的性质和要求选择最适合的算法进行求解。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值