游戏常用算法

 

 

 

完全背包问题

题目
有N NN种物品和一个容量为V 的背包,每种物品都有无限件可用。第i ii种物品的费用是w[i] ,价值是v[i] 。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

基本思路
这个问题非常类似于01背包问题,所不同的是每种物品有无限件也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0 00件、取1 11件、取2 22件……等很多种。如果仍然按照解01背包时的思路,令f[i][j] 表示前i 种物品恰放入一个容量为V 的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:

  j是背包的体积

一个简单有效的优化:

剔除价值小费用高的物品,大于V的剔除,然后用计数排序的方法计算处相同物品中价值最高的那个。来优化


 

                                       分治算法

学习链接

 https://blog.csdn.net/boshuzhang/article/details/77896250

https://blog.csdn.net/Loving_Forever_/article/details/54092926

<1. 什么是分治算法?

  • 分解(Divide)将大规模的问题分解成若干个规模更小但形式相同的子问题
  • 解决(Conquer)如果当前问题的规模足够小,并可以直接解决的话,那么直接解决并返回解。否则,继续进行分解并递归求解分解后的子问题。
  • 合并(Merge)将各个子问题合并,最终形成原问题的解。

分成两个相同或者相似的子问题,再把子问题分成更小的问题,一直这样下去.......直到最后,子问题可以简单地求解,还有一点就是把所有求得的子问题合并就是原问题的解。其实在很多场合下都会使用到分治算法,比如说我们常用的归并排序、快速排序都是很常见的分治思想的体现。

<2. 核心思想: 把大问题分解成小问题,逐个击破。

<3. 分治的适用场景

<4. 实际运用:

类似于数学归纳法,找到解决本问题的求解方程公式,然后根据方程公式设计递归程序。

第一步:一定是先找到最小问题规模时的求解方法,一般来说最小问题规模的求解方法是很简单的(就像归并排序之中当问题规模最小的时候,也就是只有一个元素的时候,直接就已经有序了)。

第二步:然后考虑随着问题规模增大时的求解方法,区间划分完了之后,开始考虑规模增大之后应该怎么做,还是以归并排序为例,当划分到每一个元素之后,不能再往下划分了,这时候就需要考虑问题增大时候的求解方法,增大具体方法需要借助另外一个存储空间,这也是归并排序为什么需要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);  
}

青蛙?跳台阶的问题:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法?

首先,当青蛙在面对第一个台阶时,他只有两种选择——跳一步还是跳两步。如果我们定义f(n)代表青蛙跳跃到n层台阶一共的方法数,那么我们可以将问题进行分解两个规模更小,但形式相同的问题:

1

f(n) = f(n - 1) + f(n - 2)

其中f(n - 1)是青蛙选择跳一步后,剩下的子问题,同理f(n - 2)是青蛙选择跳两步后剩下的子问题。这样,我们就把问题进行了分解。

下面再谈谈如何解决,正如上面谈到的解决步骤,如果规模足够小那么直接返回,否则继续降低规模进行递归求解。这时,就是我们要确定边界条件——即当n = 1 和n = 2时的情况。、

如果没有到最小问题求解处,则f(n - 1) + f(n - 2)把问题分解成最小问题,再合并解                                    

int f(int n) {                                                                                                                     
    // 边界条件(解决)                                                                                                            
    if (n < 1) return 0; // 当台阶数目小于1时,那么就返回0种方案数量                                                               
    if (n == 1) return 1; // 当台阶数目为1时,问题的规模已经足够小,我们可以直接想出他的方案数量—— 即1种:1步                      
    if (n == 2) return 2; // 当台阶数目为2时,他的方案数量为2种,即—— 1步+1步,2步                                                 
    return f(n - 1) + f(n - 2); // 分解并合并   如果没有到最小问题求解处,则把问题分解成最小问题,再合并解                                                                                   
}                                                                                                                                  
                                                                                                                                    
int main() {                                                                                                                       
    cout<<f(4)<<endl;                                                                                                              
    return 0;                                                                                                                      
} 

 

                                               动态规划算法

<1 、什么是动态规划

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

<2 、核心思想

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

<3. 动态规划的适用场景

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

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

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

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

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

<4 、实际运用

“用”

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

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

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

动态规划的难点在于上述四个步骤的确立。

 

动态规划应用:

一、背包问题

https://blog.csdn.net/yandaoqiusheng/article/details/84782655   //看此博客

题目
有N 件物品和一个容量为V 的背包。第i件物品的费用是w[i](该i 物品需要占用的体积),价值是v[i],求将哪些物品装入背包可使价值总和最大。
基本思路
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
用子问题定义状态:即f[i][j] 表示前i 件物品恰放入一个容量为j 的背包可以获得的最大价值。则其状态转移方程便是:
f[i][j]=max(f[i−1][j],f[i−1][j−w[i]]+v[i]) 

分析问题:

(1)分析最优解的性质  (2) 递归的定义最优解  (3)记忆法保存每个阶段的值[一般是二维数组]   (4)根据计算最优解的时得到的信息构造问题的最优解 

编码思路:

(1)阶段划分  (2)确定状态和状态变量  (3)写出状态方程[根据相邻两个状态之间的关系来写]    (4)寻找边界值   】

 

红线部分理解: 还是前i-1件物品。放入剩余容量为 j−w[i] 的背包中【因为此处的体积要减去第i件物品自身占用背包的体积】。而价值则需要在前 i-1 价值的基础上加上i件物品的价值。

 

上述代码只是举例说明情况【会越界】,下边的才是正确的  j>=w[i]

第i 件物品的费用是w[i] 【该i 物品需要占用的体积】,保证背包中的剩余体积是 j >=放入该物品需要的体积。则:

初始化的细节问题:

我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求"恰好装满背包"时的最优解有的题目则并没有要求必须把背包装满。这两种问法的区别是在初始化的时候有所不同。

 

//参考链接     https://www.cnblogs.com/Christal-R/p/Dynamic_programming.html

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// https://www.cnblogs.com/Christal-R/p/Dynamic_programming.html
/// </summary>
public class Bag : MonoBehaviour
{

    int capacity = 8; //包的体积
    int number = 4;  //物品个数
    int[] w = new int[] { 0,2, 3, 4, 5 };   //物品的体积
    int[] v = new int[] { 0,3, 4, 5, 6 };    //物品的价值

    int[,] V = new int[4+1,8+1];    //因为从i=1开始的,所有这个表需要+1(i,0)=0   (0,j)=0


    int maxValue; //最优解

    private void Start()
    {
        FindMax();
    }
    void FindMax()//动态规划
    {

        int maxI=0, maxJ=0;
        int i, j;  //i第i个商品  j是背包的体积
        //填表
        for (i = 1; i <= number; i++)
        {
            for (j = 1; j <= capacity; j++)
            {
                if (j < w[i])//包装不进,包的体积<物体的体积 
                {
                    V[i,j] = V[i - 1,j];
                    //Debug.Log(i + "      " + j + "      " + V[i, j]);
                }
                else//能装的情况下,取一个价值更大的装进去
                {
                    #region
                    //考虑最大价值的情况下
                    //if (V[i - 1, j] > V[i - 1, j - w[i]] + v[i])//不装价值大
                    //{
                    //    V[i, j] = V[i - 1, j];
                    //    Debug.Log(i + "      " + j + "      " + V[i, j]);
                    //}
                    //else//前i-1个物品的最优解与第i个物品的价值之和更大
                    //{
                    //    V[i, j] = V[i - 1, j - w[i]] + v[i];//****
                    //    Debug.Log(i + "      " + j + "      " + V[i, j]);
                    //}
                    #endregion

                    //返回价值更大的值
                    V[i, j] = Mathf.Max(V[i - 1, j], V[i - 1, j - w[i]] + v[i]);
                    //Debug.Log(i + "      " + j + "      " + V[i, j]);

                    if (maxValue < V[i, j])
                    {
                        //知道装不下时 ,背包中的价值最大是10,即最优解是10
                        maxValue= V[i, j];
                        maxI = i;
                        maxJ = j;
                    }
                }
            }
        }

        FindWhat(maxI, maxJ);
    }


    //根据最优解回溯找出解的组成表格填完,最优解即是V(number,capacity)=V(4,8)=10,即获得的最大价值是10
    //但还不知道解由哪些商品组成,故要根据最优解回溯找出解的组成,
    //根据填表的原理可以有如下的寻解方式    
     void FindWhat(int i, int j)//寻找解的组成方式,根据填表的原理
    {
        if (i >0)
        {
            if (V[i,j] == V[i - 1,j])//相等   根据最优解回溯  如果V(i,j)=V(i-1,j)时,
//如果商品在装(i-1)个商品时背包中的价值是num (num=5) ,在i时任然等于n(5),则说明在i时就没装
            {
                FindWhat(i - 1, j);
            }
            else if (j - w[i] >= 0 && V[i,j] == V [i - 1 , j - w[i]] + v[i])  
            {
              Debug.Log("装进去的商品序号  "+i);
                FindWhat(i - 1, j - w[i]);//回到装包之前的位置
            }
        }
    }

    //    //void FindMaxBetter()//优化空间后的动态规划
    //    //{
    //    //    int i, j;
    //    //    for (i = 1; i <= number; i++)
    //    //    {
    //    //        for (j = capacity; j >= 0; j--)
    //    //        {
    //    //            if (B[j] <= B[j - w[i]] + v[i] && j - w[i] >= 0)//二维变一维
    //    //            {
    //    //                B[j] = B[j - w[i]] + v[i];
    //    //            }
    //    //        }
    //    //    }
    //    //}
}

树的遍历查找法:更新每次的价值,找到最大价值,可以直接找到最大价值的父节点对应的物体。

树转成二叉树,再使用堆遍历。例如:大顶堆 跟节点最大、左、右结点次之。二叉搜索树:左<跟节点  右>根节点 ,方便动态搜索。

青蛙问题动态规划求解:

最优子结构

如果一个问题的最优解包含了其中子问题的最优解,那么称其具有最优子结构的性质。

什么意思?青蛙在面对n个台阶时的解决方案数是f(n),那么我们知道f(n) = f(n - 1) + f(n - 2)。其中的f(n - 1)与f(n - 2)就是两个子问题的最优解,此时我们可以理解成一个问题的最优解包含了其子问题的最优解,那么这个时候这种问题具有了最优子结构性质。

重叠子问题

这个性质,在我理解是对于上文提到的子问题的补充说明。当解决一个问题时,往往需要依赖于其更小规模的子问题的解,甚至是同时依赖于若干个规模更小的子问题的解,即子问题是被(重复)包含于比其更大的问题中的,所以他是具有重叠子问题的性质。

在这里,多提出一句,这个子问题是在解决当前问题时需要依赖的。即,只有计算了子问题,父问题才可能被求解。这是和贪心算法的重要区别所在。

状态转移方程

1

dp[i] = dp[i - 1] + dp[i - 2]

  • i代表当前问题的规模,即所需要跳过的台阶数。
  • dp[i]代表的是跳过i个台阶的方案数量
int n = 4;                                                                                                                     
    vector<int> dp(n + 1, 0);                                                                                                      
    dp[1] = 1;                                                                                                                     
    dp[2] = 2;                                                                                                                     
    for (int i = 3; i < = n; ++i) {                                                                                                 
        dp[i] = dp[i - 1] + dp[i - 2];                                                                                             
    }                                                                                                                              
    cout<<dp[n]<<endl;                                                                                                             
    return dp[n]; 

 

 

                                           贪心算法

所谓贪心就是总是在当前情况下做出最为有利的选择,也就是说它不从整体上考虑。它只是做出了某种意义上的局部最优解

贪心算法不像动态规划那样有固定的框架,由于贪心算法没有固定的算法框架,因此怎么样区分有关于贪心算法呢?这就需要一种贪心策略了!贪心算法不是所有情况下都能得到整体最优解,得到的只是一个近似最优解,所以求最值的问题上,一般不用贪心算法,而是用动态规划算法。贪心策略的选择必须满足无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。

二、核心思想

       关于贪心算法,其实没有过多要说的,就简单说一下步骤吧!   
       第一步:建立数学模型来描述问题。

       第二步:把求解的问题分成若干个子问题

       第三步:对每一子问题求解,得到子问题的局部最优解

       第四步:把子问题的解局部最优解合成原来解问题的一个解

三、贪心算法的适用场景

       贪心策略的前提就是尽量保证局部最优解可以产生全局最优解,最美好的贪心策略当然就是希望能通过不断地求局部最优解从而得到全局最优解!

如:上面的背包问题,并不能用弹贪心算法。因为贪心算法只能从某一个方向考虑,比如单单以重量(每次选择重量最轻的),或用价值(每次选择价值最高的),甚至用价格与重量的比值,其实这三者都实际运用过程之中都有问题,基本很难得到最优解。

四、实际运用

 寻路

同时使用路径搜索和运动算法。

我们每次都从 "开启列表" 找出 F 值最小的, 将它从 "开启列表" 中移掉, 添加到 "关闭列表".。再继续找出它周围可以到达的方块,如此循环下去...

用一个实例来完全理解A*寻路算法

<1. 

      取起始点加入到OPen中。

      考察这个点相邻的八个点【即计算相邻点的f,g,h】,检查完后把这个起始点加入到Close中 . 

      记录每个节点的连接,即当前位置之前经过的那个节点。

       

 

<2. 

   计算Open表中8个节点的行走代价,选择最小的作为这一轮的查考对象.

   计算从起始点移动到当前点的移动代价(记录为g),还要计算当前到目标点的代价(h),总移动代价f=g+h.

   计算g: 取它父节点的g,以找相对于父节点的连接方式,增加相应的值。例如:对角线连接,增加1.414,直线连接增加1.(或者     根据和父节点的连接关系。算出节点走回到起始位置所需要的移动代价)。

   计算h: 由于没有到达目标点,所以只能估算h,即启发方式,采用欧立德几何【两节点之间的距离】或者曼哈顿距离等。此处采用曼哈顿距离。即当前格子到目标点。先水平后垂直方向的数量和作为h的估算值。【假设没有障碍物的情况】

   计算f=g+h.首先应该检查移动代价最低的那些节点(第一步图中的选中)。如果有多个节点最有相等的最小值,可以任选一个进行检查。

以上步骤结束后,则选中(3,D)为代价最小的节点,接下来检查这个节点:障碍物无需检查,其他七个节点中3个已经在Open或者Close中,但是之前记录的g值是通过其他路径得到的,因此本轮需要再次计算通过当前节点(3,D)所得到的新g值。见第2步图示。检查结束后,把(3,D)加入到Close中。

 

<3. 

   首先检查Open表,找出其中具有最低代价的节点,即(3,E),(2,D),选择任意一个,例如(3,E),且相邻节点也不需要更新信息,把这个节点加入到Close中。

 

<4. 

   上图中,再次检查Open表,选中最低代价节点(2,D),检查8个相邻点,(2,C)、(3,C)在Open表中;(3,D)、(3,E)在Close中,检查后无需更新信息。对于(1,E)因为有障碍物(2,E)的存在,因此不是直接到达的。检查(1,C)和(1,D)。 此时相邻点检查完毕,将(2,D)d点移出Open,加入Close中。

<5. 

 上图,检查Open,任意选择一个最小代价的,即(3,C)进行检查其相邻点。更新需要更新的g,h ; 并且这个点的父节点变为(3,C) ;

再计算还没进行检查的点(2,B)的g,h,把它加入到Open中。相邻节点检查完毕后,把它移入到Close中。

下图所示: 

重复上述步骤,直到目标点也进入到Open中,沿着箭头回溯,找到从起点到目标点的路径。箭头方向看向选中点,注意对于Open表中重复的相邻点的箭头方向是不更新,只更新g,h。

最终得到下图: 

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

 

                                              四、 回溯法、

一、什么是回溯算法

回溯法也称试探法,它的基本思想是:从问题的某一种状态(初始状态)出发,搜索从这种状态出发所能达到的所有“状态”,当一条路走到“尽头”的时候(不能再前进),再后退一步或若干步,从另一种可能“状态”出发,继续搜索,直到所有的“路径”(状态)都试探过。这种不断“前进”、不断“回溯”寻找解的方法,就称作“回溯法”。满足回溯状态的某个点就称之为“回溯点”。

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

二、核心思想

接下去再来说一说回溯法的核心思想,在包含所有问题的解空间树之中按深度优先搜索的策略,从根节点出发深度搜索解空间树。当搜索到某一个节点的时候,先检测当前点是否满足条件,如果满足就从该节点出发继续搜索下去,否则不满足那么就逐层向其祖先节点进行回溯。其实也就是深度优先策略,首先从根节点出发深度搜索解空间树。当搜索到某一个节点的时候,这时候需要判断该节点是否包含问题的解,如果包含,就从该节点继续出发搜索下去,如果该节点不包含问题的解,那么这时候就逐层向根节点进行回溯


回溯法: 就是对隐式图的深度优先搜索算法,先一条道走到黑,走到不能走时,再回溯到上一步重新选择。

贪心算法:每一步都找最优的解,且每一步都找能走的节点【路】,每一步决策时互相独立,无后效性。

动态规划:每一步都会影响到下一步的决策,且有后效性。

【  

隐式图:仅给出初始结点、目标结点以及生成子结点的约束条件(题意隐含给出),根据扩展规则,不停的扩展结点,直到扩展节点中包含目标节点的过程。(例如: 对于回溯法只要没有障碍物就成,而A*还要考虑路径代价)。

算法: 启发式搜索(A*)

结点扩展次序受问题的性质和目标结点的一般方向的影响,扩展结点时要估计由初始结点经结点至目标结点的路径代价。它只需要产生全部的状态空间的部分结点及相互关系就可以求解问题了,因此搜索效率高。

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

三、回溯算法的适用场景

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

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

四、实际运用

      具体运用,首先来看一下算法的框架。假设一个问题的解是一个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]置空值等);
              }
          }
     }
 }

使用回溯方法解决的具体问题:

树的深度优先算法。

                                                                                                                                    
/**                                                                                                                                
 * int count 方案总数                                                                                                              
 * int target 目标—— 剩余的台阶数                                                                                                  
 */                                                                                                                               
void dfs(int& count, int target) {                                                                                                 
    // 边界条件                                                                                                                    
    if (target < = 2) {                                                                                                             
        count += target; // 当剩余一个台阶是即累加一种方案,剩余两个台阶时累加两种方案                                             
        return;                                                                                                                    
    }                                                                                                                              
                                                                                                                                    
    // 下面是两个基本点选择一步和选择两步                                                                                          
    // 选择一步                                                                                                                    
    dfs(count, target - 1);                                                                                                        
                                                                                                                                    
    // 选择两步                                                                                                                    
    dfs(count, target - 2);                                                                                                        
}                                                                                                                                  
                                                                                                                                    
int main() {                                                                                                                       
    int count = 0;                                                                                                                 
    dfs(count, 4);                                                                                                                 
    cout<<count<<endl;                                                                                                             
    return 0;                                                                                                                      
} 

 

 

 

 

 

 

                                              五:分支界限法

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
算法一: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]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值