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

 前言

    心是一个人的翅膀,心有多大,世界就有多大。很多时候限制我们的,不是周遭的环境,也不是他人的言行,而是我们自己!看不开,放不下,忘不了,把自己囚禁在灰暗的记忆里;不敢想,不自信,不行动,把自己局限在自己的控件里面......如果不能 打破心的桎梏,即使给你整个天空,你也找不到自由地感觉!so fighting!

                                                                                                                          ---------摘自《美文欣赏》

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

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

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

                                                      算法之四:回溯算法

、什么是回溯算法

         按照惯例我们先来说一说回溯算法的基本概念,什么是回溯算法呢?回溯算法实际上就是类似枚举的搜索尝试过程,同时在这个搜索过程之中寻找问题的解,当发现不满足求解条件的时候,就“回溯返回”,尝试别的路径。

        回溯法求解问题在实际过程之中也挺常见的,比如在迷宫问题之中我们就可以使用回溯法求解!回溯法可以说是一种优选搜索法,按照选优条件向前搜索,以求最后达到最终的目标,假如是在迷宫问题之中如果达到出口的时候,那么回溯的过程也就结束了。但是在这个逐步前进的过程之中,发现原先的选择并不是最优或者说是根本达不到目的地,也就是所说的碰到了“障碍物”。那么这时候它就会退回一步重新选择,这种走不通就回退一步的再走的技术为回溯法,而满足回溯状态的某个点就称之为“回溯点”。

        由于回溯法挺容易理解的,所以回溯法也适合很多的实际问题,甚至一些比较复杂的大型的问题,因此也获得一个“通用解题方法”的美称! 

、核心思想

       接下去再来说一说回溯法的核心思想,在包含所有问题的解空间树之中,按照深度优先搜索的策略,从根节点出发深度搜索解空间树。当搜索到某一个节点的时候,需要先判断该节点是否包含问题的解,换句话说检测当前点是否满足条件。如果包含或者说是满足条件,那么就从该节点出发继续搜索下去,如果该节点不包含所求问题的解,那么就逐层向其祖先节点进行回溯!说到这里可能大家也会感觉到其实也就是我们常说的深度优先策略,我们学习图的过程之中经常会使用到深度优先和广度优先的策略!因此对于这种策略也不会陌生,按照这种策略,首先从根节点出发深度搜索解空间树。当搜索到某一个节点的时候,这时候需要判断该节点是否包含问题的解,如果包含,就从该节点继续出发搜索下去,如果该节点包含问题的解,那么这时候就逐层向根节点进行回溯

       总结一下,其实所谓的回溯法就是对隐式图的深度优先搜索算法

       顺便提一句,若用回溯方法求解问题的所有解的时候,如果没有找到最终的目标,那么只有当回溯到根节点的时候才可以,并且根节点的所有可行子树都要已被遍历才可以。但是通常我们只需要某一个解就行了,比如在走迷宫的时候我们只需要找到一个出口就行了(假设迷宫有多个出口)。

、回溯算法的适用场景

       那么什么时候需要用到回溯算法呢?首先使用回算法的时候需要注意一下,需要明确定义问题的解空间,问题的解空间至少包含问题的一个最优解

       在使用的时候我们还需要确定节点的扩展搜索规则,以深度优先方式搜索解空间,并且你还可以在搜索过程之中利用剪枝函数避免无效的搜索。

、实际运用

       那么体怎么使用呢?接下来我们来看一看具体地运用,首先来看一下算法的框架。假设一个问题的解是一个n维向量(a1,a2,a3,....,an),约束的条件是ai(i = 1,2,3,.....,n)之间满足某种条件,并用一个函数式f(ai)来表示。

       说了怎么表示之后,我们就需要用把整个框架搭出来。一般来说有两种常见的方法,一种是递归解法,一种是非递归解法,下面我们就把这两种框架写出来。

      非递归算法框架

int a[n], i;     //n是一个常量,初始化数组
i = 1;
while(i > 0 && (还没有达到出口,即还有路可以走))  //表示还没有回溯到头
{
    if( i > n)       //已经搜索到最终叶子节点
    {
         //已经搜索到了解,可以进行输出了
         //如果不需要搜索出所有的解,这时候就可以结束退出了
     }
     else
     {
          //a[i]第一个可能的值
          while(a[i]在不满足约束条件且在搜索空间之内)
          {
                a[i]下一个可能的值
            }
          if(a[i]在搜索空间之内)
          {
                //此时作为技术功能的i需要进行++操作,表示资源的占用
               i = i +1;
           }
          else
          {
               清理所占的状态空间;            // 回溯
               i = i –1; 
           }
     }
}

          递归框架

      一般来说回溯法还是使用递归方式解决比较好,因为回溯法是对解空间的深度优先搜索,用下面的伪代码来简单模拟实现一下:(k表示搜索的深度,此框架是网上看到的,感觉比较精辟,就拿来用了)

int a[n];
try(int i)
{
    if(i>n)
      输出结果;
    else
    {
        for(j = 下界; j <= 上界; j=j+1)  // 枚举i所有可能的路径
       {
           if(fun(j))               // 满足限界函数和约束条件
              {
                  a[i] = j;
                  ...                         // 其他操作
                 try(i+1);
               回溯前的清理工作(如a[i]置空值等);
              }
          }
     }
 }

       接下来给出一个使用回溯方法解决的具体问题---迷宫问题,下面是一个迷宫类的实现:

//迷宫
template<typename T>
class Maze
{
	typedef Node Pos;
public:
	Maze()
		: m_row(0)
		, m_col(0)
		, m_start(0, 2)
		, m_map(NULL)
	{}
	~Maze()
	{
		for (int i = 0; i < m_row; i++)
		{
			delete[]  m_map[i];
		}
		delete[]  m_map;
	}
	bool SearchPath()            //查找迷宫路径 -------(深度优先,相当于回溯吧)
	{
		Pos  Cur = m_start;
		m_s.Push(Cur);
		m_map[Cur.x][Cur.y] = 2;
		while (!m_s.Empty())
		{
			Pos  next = m_s.Top();
			Cur = next;
			m_map[Cur.x][Cur.y] = 2;
			if (next.x == m_row - 1 || next.y == m_col - 1)
				return true;
			//判断上
			next.x--;
			if (CheckNextAccess(next))
			{
				m_s.Push(next);
				continue;
			}
			next.x++;
			//下
			next.x++;
			if (CheckNextAccess(next))
			{
				m_s.Push(next);
				continue;
			}
			next.x--;
			//左
			next.y--;
			if (CheckNextAccess(next))
			{
				m_s.Push(next);
				continue;
			}
			next.y++;
			//右
			next.y++;
			if (CheckNextAccess(next))
			{
				m_s.Push(next);
				continue;
			}
			//进行回溯
			m_s.Pop();
			//并且把这个位置标记起来
			m_map[next.x][next.y] = 3;
		}
		return false;
	}
	void PrintMap()              //输出迷宫地图
	{
		for (int i = 0; i < m_row; ++i)
		{
			for (int j = 0; j < m_col; ++j)
				cout << m_map[i][j] << " ";
			cout << endl;
		}
	}
	void PrintPath()             //打印路径的坐标 
	{
		Stack<Pos> sk;
		while (!m_s.Empty())
		{
			sk.Push(m_s.Top());
			m_s.Pop();
		}
		while (!sk.Empty())
		{
			cout << "(" << sk.Top().x <<","<< sk.Top().y << ")" << "->";
			sk.Pop();
		}
		cout << endl;
	}
	void SetMap(int  arr[][10], int  row,  int col)                //设置地图 
	{
		m_row = row;
		m_col = col;
		m_map = new T *[m_row];
		for (int i = 0; i < m_row; ++i)
		{
			m_map[i] = new T[m_col];
			for (int j = 0; j < m_col; ++j)
				m_map[i][j] = arr[i][j];
		}
	}
private:
	bool CheckNextAccess(Pos coor)        //判断该坐标能否通过
	{
		if (coor.x >= 0 && coor.x < m_row
			&& coor.y >= 0 && coor.y < m_col
			&& m_map[coor.x][coor.y] == 1)
			return true;
		else
			return false;
	}
private:
	int  m_row;
	int  m_col;
	T **  m_map;
	Stack<Pos>  m_s;
	Pos  m_start;
};

                                                      算法之五:分支界限法       

       说完了回溯算法,那么我们接下去说一说最后一个算法,其实这个算法与回溯法比较相似,所以放在一起讲了,还是像之前一样按照步骤一步一步来!

、什么是回溯算法

       首先需要说明的是,分支界限法与回溯法挺类似的,也是一种在问题的解空间树Tree上来搜索问题解的算法。但是在一般情况下,分支界限法与回溯法的求解目标不同而已,回溯法的求解目标是找出Tree中满足约束条件的所有解不过很多时候我们为了方便只是用了其中一个解而已。而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解

、核心思想

       之前我们在说回溯法的时候说过,回溯法其实就是我们通常所说的深度优先策略,而现在所说的分支界限法就是另一种策略---广度优先策略。

       所谓“分支”就是采用广度优先的策略,依次搜索E-结点的所有分支,也就是所有相邻结点,抛弃不满足约束条件的结点,其余结点加入活结点表。然后从表中选择一个结点作为下一个E-结点,继续搜索。

       选择下一个E-结点的方式不同,则会有几种不同的分支搜索方式。

   1)FIFO搜索                                    2)LIFO搜索                         3)优先队列式搜索

、回溯算法的适用场景

       由于分支界限法是广度优先的策略,所以一般用来求最优解的比较多,我们在背包问题之中也可以使用分支界限法来解决,当然在使用的过程之中我们可以使用剪枝来优化,这样可以提高搜索的效率。

、实际运用

       在说实际运用之前还是先说一说使用的过程:

       由于求解目标不同,导致分支限界法与回溯法在解空间树T上的搜索方式也不相同。回溯法以深度优先的方式搜索解空间树T,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树T

       分支限界法的搜索策略是:在扩展结点处,先生成其所有的儿子结点(分支),然后再从当前的活结点表中选择下一个扩展对点。为了有效地选择下一扩展结点,以加速搜索的进程,在每一活结点处,计算一个函数值(限界),并根据这些已计算出的函数值,从当前活结点表中选择一个最有利的结点作为扩展结点,使搜索朝着解空间树上有最优解的分支推进,以便尽快地找出一个最优解。
       分支限界法常以广度优先或以最小耗费(最大效益)优先的方式搜索问题的解空间树。问题的解空间树是表示问题解空间的一棵有序树,常见的有子集树和排列树。在搜索问题的解空间树时,分支限界法与回溯法对当前扩展结点所使用的扩展方式不同。在分支限界法中,每一个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点,就一次性产生其所有儿子结点。在这些儿子结点中,那些导致不可行解或导致非最优解的儿子结点被舍弃,其余儿子结点被子加入活结点表中。此后,从活结点表中取下一结点成为当前扩展结点,并重复上述结点扩展过程。这个过程一直持续到找到所求的解或活结点表为空时为止。

       由于分支界限法用的是广度优先策略,所以下面用一个具体的例子来表示广度优先策略:

template<typename  T>         //广度优先策略
class Maze
{
	typedef Node Pos;
public:
	Maze()
		:m_row(0)
		, m_col(0)
		, m_start(0, 2)
		, m_map(NULL)
		, m_book(NULL)
	{}
	~Maze()
	{
		for (int i = 0; i < m_row; ++i)
		{
			delete[] m_map[i];
			delete[] m_book[i];
		}
		delete[]  m_map;
		delete[]  m_book;
	}

	bool  SearchPath()					//查找迷宫路径
	{
		queue<Node>  my_queue;
		assert(m_map);
		my_queue.push(m_start);
		Pos  cur = m_start;
		m_book[cur.x][cur.y] = 0;
		int steps = 0;
		while (!my_queue.empty())
		{
			Pos  next = my_queue.front();
			cur = next;
			m_map[cur.x][cur.y] = 2;
			steps = m_book[cur.x][cur.y] + 1;
			if (cur.x == m_row - 1 && m_start.x != m_row - 1
				||cur.y == m_col - 1 && m_start.y != m_col - 1
				||cur.x == 0 && m_start.x != 0
				||cur.y == 0 && m_start.y !=0)
				break;
			//上
			next.x--;
			if (CheckNextAccess(next))
			{
				my_queue.push(next);
				m_book[next.x][next.y] = steps;
			}
			next.x++;
			//下
			next.x++;
			if (CheckNextAccess(next))
			{
				my_queue.push(next);
				m_book[next.x][next.y] = steps;
			}
			next.x--;
			//左
			next.y--;
			if (CheckNextAccess(next))
			{
				my_queue.push(next);
				m_book[next.x][next.y] = steps;
			}
			next.y++;
			//右
			next.y++;
			if (CheckNextAccess(next))
			{
				my_queue.push(next);
				m_book[next.x][next.y] = steps;
			}
			next.y--;
			my_queue.pop();
		}
		if (!my_queue.empty())
			m_s.Push(cur);
		while (!m_s.Empty())
		{
			Pos  next = m_s.Top();
			if (m_book[next.x][next.y] == 0)
				return true;
			//上
			next.x--;
			if (CheckPrevAccess(next))
			{
				m_s.Push(next);
				continue;
			}
			next.x++;
			//下
			next.x++;
			if (CheckPrevAccess(next))
			{
				m_s.Push(next);
				continue;
			}
			next.x--;
			//左
			next.y--;
			if (CheckPrevAccess(next))
			{
				m_s.Push(next);
				continue;
			}
			next.y++;
			//右
			next.y++;
			if (CheckPrevAccess(next))
			{
				m_s.Push(next);
				continue;
			}
			next.y--;
		}
		return true;
	}
	void  SetMap(T arr[][10], int row, int col)	//设置地图											
	{
		assert(arr);
		if (row <= 0 || col <= 0)
			return;
		m_row = row;
		m_col = col;
		m_map = new T *[m_row];
		m_book = new T *[m_row];
		for (int i = 0; i < m_row; ++i)
		{
			m_map[i] = new T[m_col];
			m_book[i] = new T[m_col];
			for (int j = 0; j < m_col; ++j)
			{
				m_map[i][j] = arr[i][j];
				m_book[i][j] = -1;
			}
		}
	}
	void  PrintMap()										//打印地图
	{
		cout << "迷宫地图中所走过的路径" << endl;
		for (int i = 0; i < m_row; i++)
		{
			for (int j = 0; j < m_col; j++)
			{
				cout << m_map[i][j] << " ";
			}
			cout << endl;
		}
		cout << endl << endl;                  //打印辅助数组  
		cout << "辅助数组中所有入队的坐标" << endl;
		for (int i = 0; i < m_row; i++)
		{
			for (int j = 0; j < m_col; j++)
			{
				printf("%2d ", m_book[i][j]);
			}
			cout << endl;
		}
	}
	void  PrintPath()										//打印路径
	{
		while (!m_s.Empty())
		{
			cout << "(" << m_s.Top().x << "," << m_s.Top().y << ")" << endl;
			m_s.Pop();
		}
	}
private:
	bool  CheckNextAccess(Pos  coor)			//判断下一个坐标能否通过
	{
		if (coor.x >= 0 && coor.x < m_row
			&& coor.y >= 0 && coor.y < m_col
			&& m_map[coor.x][coor.y] == 1)
			return  true;
		else
			return  false;
	}
	bool  CheckPrevAccess(Pos  coor)				//判断前一个坐标能否通过
	{
		if (coor.x >= 0 && coor.x < m_row
			&& coor.y >= 0 && coor.y < m_col
			&& m_book[coor.x][coor.y] == (m_book[m_s.Top().x][m_s.Top().y] - 1))
			return true;
		else
			return false;
	}
private:
	int   m_row;
	int   m_col;
	T ** m_map;
	T ** m_book;
	MyStack<Pos> m_s;
	Pos  m_start;
};                                                  

                                                                             两者的区别 
       有一些问题其实无论用回溯法还是分支限界法都可以得到很好的解决,但是另外一些则不然。也许我们需要具体一些的分析——到底何时使用分支限界而何时使用回溯呢?

       虽然两者有一些相似点,不过还是有区别的,回溯法和分支限界法的一些区别: 

       1)方法对解空间树的搜索方式       2) 存储结点的常用数据结构      3)结点存储特性常用应用    

       说的通俗一点就是两者最终得到的结果不同,回溯法深度优先搜索堆栈活结点的所有可行子结点被遍历后才被从栈中弹出找出满足约束条件的所有解分支限界法广度优先或最小消耗优先搜索队列、优先队列每个结点只有一次成为活结点的机会找出满足约束条件的一个解或特定意义下的最优解


  • 13
    点赞
  • 76
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
算法一: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]
五大常用算法是动态规划、分治、递归、贪心和回溯。 动态规划是一种将问题分解成子问题并保存子问题解的方法。通过求解子问题,可以逐步推导出原始问题的解。动态规划通常用于求解最优化问题,例如最长公共子序列、最短路径等。 分治是将原问题划分成多个相互独立的子问题,然后通过递归的方式求解子问题,并将子问题的解合并成原问题的解。分治算法常用于排序、快速幂等问题。 递归是通过函数调用自身来解决问题的方法。递归算法在问题定义可以被分解为较小规模或更简单情况的时候很有用。例如,计算一个数的阶乘,就可以使用递归实现。 贪心算法是一种选择当前最优策略的方法,即在每一步选取最优解,最终得到全局最优解的算法。贪心算法常用于解决无后效性的问题,例如最小生成树、哈夫曼编码等。 回溯是一种通过穷举搜索所有可能的解空间,找到满足条件的解的方法。回溯算法在解决组合问题、排序问题、子集和问题等方面很有效。回溯算法通过递归的方式逐步构建解,当发现当前解不满足条件时,会回退到上一步继续搜索其他可能的解。 这五种常用算法在不同的问题领域中都有广泛应用,每种算法都有自己的特点和适用范围。在解决具体问题时,可以根据问题的性质和要求选择最适合的算法进行求解。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值