unity3D教程 : 游戏算法开发(三)

来源:互联网 发布:中国互联网金融数据 编辑:程序博客网 时间:2024/04/30 08:12
六、贪婪法

    贪婪法是一种不追求最优解,只希望得到较为满意解的方法。贪婪法一般可以快速得到满意的解,因为它省去了为找最优解要穷尽所有可能而必须耗费的大量时间。贪婪法常以当前情况为基础作最优选择,而不考虑各种可能的整体情况,所以贪婪法不要回溯。

    例如平时购物找钱时,为使找回的零钱的硬币数最少,不考虑找零钱的所有各种发表方案,而是从最大面值的币种开始,按递减的顺序考虑各币种,先尽量用大面值 的币种,当不足大面值币种的金额时才去考虑下一种较小面值的币种。这就是在使用贪婪法。这种方法在这里总是最优,是因为银行对其发行的硬币种类和硬币面值 的巧妙安排。如只有面值分别为1、5和11单位的硬币,而希望找回总额为15单位的硬币。按贪婪算法,应找1个11单位面值的硬币和4个1单位面值的硬 币,共找回5个硬币。但最优的解应是3个5单位面值的硬币。

    【问题】 装箱问题

    问题描述:装箱问题可简述如下:设有编号为0、1、…、n-1的n种物品,体积分别为v0、v1、…、vn-1。将这n种物品装到容量都为V的若干箱 子里。约定这n种物品的体积均不超过V,即对于0≤i<n,有0<vi≤V。不同的装箱方案所需要的箱子数目可能不同。装箱问题要求使装尽这n种物品的箱 子数要少。

    若考察将n种物品的集合分划成n个或小于n个物品的所有子集,最优解就可以找到。但所有可能划分的总数太大。对适当大的n,找出所有可能的划分要花费的时 间是无法承受的。为此,对装箱问题采用非常简单的近似算法,即贪婪法。该算法依次将物品放到它第一个能放进去的箱子中,该算法虽不能保证找到最优解,但还 是能找到非常好的解。不失一般性,设n件物品的体积是按从大到小排好序的,即有v0≥v1≥…≥vn-1。如不满足上述要求,只要先对这n件物品按它们的 体积从大到小排序,然后按排序结果对物品重新编号即可。装箱算法简单描述如下:

[AppleScript] 纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{输入箱子的容积;
 
 
输入物品种数n;
 
 
按体积从大到小顺序,输入各物品的体积;
 
 
预置已用箱子链为空;
 
 
预置已用箱子计数器box_count为0
 
 
for(i=0;i  {从已用的第一只箱子开始顺序寻找能放入物品i 的箱子j;
 
 
if(已用箱子都不能再放物品i)
 
 
{另用一个箱子,并将物品i放入该箱子;
 
 
box_count++
 
 
}
 
 
else
 
 
将物品i放入箱子j;
 
 
}
 
 
}


    上述算法能求出需要的箱子数box_count,并能求出各箱子所装物品。下面的例子说明该算法不一定能找到最优解,设有6种物品,它们的体积分别为: 60、45、35、20、20和20单位体积,箱子的容积为100个单位体积。按上述算法计算,需三只箱子,各箱子所装物品分别为:第一只箱子装物品1、 3;第二只箱子装物品2、4、5;第三只箱子装物品6。而最优解为两只箱子,分别装物品1、4、5和2、3、6。

    若每只箱子所装物品用链表来表示,链表首结点指针存于一个结构中,结构记录尚剩余的空间量和该箱子所装物品链表的首指针。另将全部箱子的信息也构成链表。以下是按以上算法编写的程序。

    【程序】
[AppleScript] 纯文本查看 复制代码
?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# include
 
  
  # include
 
  
  typedef struct ele
 
  
  {int vno;
 
  
  struct ele *link;
 
  
  }ELE;
 
  
  typedef struct hnode
 
  
  {int remainder;
 
  
  ELE*head;
 
  
  Struct hnode *next;
 
  
  }HNODE;
 
  
  voidmain()
 
  
  {int n,i,box_count,box_volume,*a;
 
  
  HNODE*box_h,*box_t,*j;
 
  
  ELE*p,*q;
 
  
  Printf(“输入箱子容积\n”);
 
  
  Scanf(“%d”,&box_volume);
 
  
  Printf(“输入物品种数\n”);
 
  
  Scanf(“%d”,&n);
 
  
  A=(int*)malloc(sizeof(int)*n);
 
  
  Printf(“请按体积从大到小顺序输入各物品的体积:”);
 
  
  For(i=0;i   Box_h=box_t=NULL;
 
  
  Box_count=0;
 
  
  For(i=0;i  {p=(ELE*)malloc(sizeof(ELE));
 
  
  p->vno=i;
 
  
  for(j=box_h;j!=NULL;j=j->next)
 
  
  if(j->remainder>=a[i])break;
 
  
  if(j==NULL)
 
  
  {j=(HNODE*)malloc(sizeof(HNODE));
 
  
  j->remainder=box_volume-a[i];
 
  
  j->head=NULL;
 
  
  if(box_h==NULL)box_h=box_t=j;
 
  
  elsebox_t=boix_t->next=j;
 
  
  j->next=NULL;
 
  
  box_count++;
 
  
  }
 
  
  elsej->remainder-=a[i];
 
  
  for(q=j->next;q!=NULL&&q->link!=NULL;q=q->link);
 
  
  if(q==NULL)
 
  
  {p->link=j->head;
 
  
  j->head=p;
 
  
  }
 
  
  else
 
  
  {p->link=NULL;
 
  
  q->link=p;
 
  
  }
 
  
  }
 
  
  printf(“共使用了%d只箱子”,box_count);
 
  
  printf(“各箱子装物品情况如下:”);
 
  
  for(j=box_h,i=1;j!=NULL;j=j->next,i++)
 
  
  {printf(“第%2d只箱子,还剩余容积%4d,所装物品有;\n”,I,j->remainder);
 
  
  for(p=j->head;p!=NULL;p=p->link)
 
  
  printf(“%4d”,p->vno+1);
 
  
  printf(“\n”);
 
  
  }
 
  
  }


    【问题】 马的遍历

    问题描述:在8×8方格的棋盘上,从任意指定的方格出发,为马寻找一条走遍棋盘每一格并且只经过一次的一条路径。

    马在某个方格,可以在一步内到达的不同位置最多有8个,如图所示。如用二维数组board[ ][ ]表示棋盘,其元素记录马经过该位置时的步骤号。另对马的8种可能走法(称为着法)设定一个顺序,如当前位置在棋盘的(i,j)方格,下一个可能的位置依 次为(i+2,j+1)、(i+1,j+2)、(i-1,j+2)、(i-2,j+1)、(i-2,j-1)、(i-1,j-2)、(i+1,j-2)、 (i+2,j-1),实际可以走的位置尽限于还未走过的和不越出边界的那些位置。为便于程序的同意处理,可以引入两个数组,分别存储各种可能走法对当前位 置的纵横增量。

    4 3

    5 2

    马

    6 1

    7 0

    对于本题,一般可以采用回溯法,这里采用Warnsdoff策略求解,这也是一种贪婪法,其选择下一出口的贪婪标准是在那些允许走的位置中,选择出口最少 的那个位置。如马的当前位置(i,j)只有三个出口,他们是位置(i+2,j+1)、(i-2,j+1)和(i-1,j-2),如分别走到这些位置,这三 个位置又分别会有不同的出口,假定这三个位置的出口个数分别为4、2、3,则程序就选择让马走向(i-2,j+1)位置。

    由于程序采用的是一种贪婪法,整个找解过程是一直向前,没有回溯,所以能非常快地找到解。但是,对于某些开始位置,实际上有解,而该算法不能找到解。对于 找不到解的情况,程序只要改变8种可能出口的选择顺序,就能找到解。改变出口选择顺序,就是改变有相同出口时的选择标准。以下程序考虑到这种情况,引入变 量start,用于控制8种可能着法的选择顺序。开始时为0,当不能找到解时,就让start增1,重新找解。细节以下程序。

    【程序】

[AppleScript] 纯文本查看 复制代码
?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# include
 
  
  int delta_i[ ]={2,1,-1,-2,-2,-1,1,2};
 
  
  int delta_j[ ]={1,2,2,1,-1,-2,-2,-1};
 
  
  int board[8][8];
 
  
  int exitn(int i,int j,int s,int a[ ])
 
  
  {int i1,j1,k,count;
 
  
  for(count=k=0;k<8;k++)
 
  
  {i1=i+delta_i[(s+k)%8];
 
  
  j1=i+delta_j[(s+k)%8];
 
  
  if(i1>=0&&i1<8&&j1>=0&&j1<8&&board[I1][j1]==0)
 
  
  a[count++]=(s+k)%8;
 
  
  }
 
  
  returncount;
 
  
  }
 
  
  int next(int i,int j,int s)
 
  
  {int m,k,mm,min,a[8],b[8],temp;
 
  
  m=exitn(i,j,s,a);
 
  
  if(m==0)return 1;
 
  
  for(min=9,k=0;k  {temp=exitn(I+delta_i[a[k]],j+delta_j[a[k]],s,b);
 
  
  if(temp  {min=temp;
 
  
  kk=a[k];
 
  
  }
 
  
  }
 
  
  returnkk;
 
  
  }
 
  
  voidmain()
 
  
  {int sx,sy,i,j,step,no,start;
 
  
  for(sx=0;sx<8;sx++)
 
  
  for(sy=0;sy<8;sy++)
 
  
  {start=0;
 
  
  do{
 
  
  for(i=0;i<8;i++)
 
  
  for(j=0;j<8;j++)
 
  
  board[i][j]=0;
 
  
  board[sx][sy]=1;
 
  
  I=sx; j=sy;
 
  
  For(step=2;step<64;step++)
 
  
  {if ((no=next(i,j,start))==-1)break;
 
  
  I+=delta_i[no];
 
  
  j+=delta_j[no];
 
  
  board[i][j]=step;
 
  
  }
 
  
  if(step>64)break;
 
  
  start++;
 
  
  }while(step<=64)
 
  
  for(i=0;i<8;i++)
 
  
  {for (j=0;j<8;j++)
 
  
  printf(“%4d”,board[i][j]);
 
  
  printf(“\n\n”);
 
  
  }
 
  
  scanf(“%*c”);
 
  
  }
 
  
  }


    七、分治法

    1、分治法的基本思想

    任何一个可以用计算机求解的问题所需的计算时间都与其规模N有关。问题的规模越小,越容易直接求解,解题所需的计算时间也越少。例如,对于n个元素的 排序问题,当n=1时,不需任何计算;n=2时,只要作一次比较即可排好序;n=3时只要作3次比较即可,…。而当n较大时,问题就不那么容易处理了。要 想直接解决一个规模较大的问题,有时是相当困难的。

    分治法的设计思想是,将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。

    如果原问题可分割成k个子问题

    2、分治法的适用条件

    分治法所能解决的问题一般具有以下几个特征:

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

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

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

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

    上述的第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;第二条特征是应用分治法的前提,它也是大多数问 题可以满足的,此特征反映了递归思想的应用;第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具 备第三条特征,则可以考虑贪心法或动态规划法。第四条特征涉及到分治法的效率,如果各子问题是不独立的,则分治法要做许多不必要的工作,重复地解公共的子 问题,此时虽然可用分治法,但一般用动态规划法较好。

    3、分治法的基本步骤

    分治法在每一层递归上都有三个步骤:

    (1)分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;

    (2)解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题;

    (3)合并:将各个子问题的解合并为原问题的解。

    它的一般的算法设计模式如下:

    Divide_and_Conquer(P)

    if |P|≤n0

    then return(ADHOC(P))

    将P分解为较小的子问题P1、P2、…、Pk

    for i←1 to k

    do

    yi ← Divide-and-Conquer(Pi) △ 递归解决Pi

    T ← MERGE(y1,y2,…,yk) △ 合并子问题

    Return(T)

    其中 |P| 表示问题P的规模;n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解。ADHOC(P)是该分治法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过n0时,直接用算法ADHOC(P)求解。

    算法MERGE(y1,y2,…,yk)是该分治法中的合并子算法,用于将P的子问题P1、P2、…、Pk的相应的解y1、y2、…、yk合并为P的解。

    根据分治法的分割原则,原问题应该分为多少个子问题才较适宜?各个子问题的规模应该怎样才为适当?这些问题很难予以肯定的回答。但人们从大量实践中发 现,在用分治法设计算法时,最好使子问题的规模大致相同。换句话说,将一个问题分成大小相等的k个子问题的处理方法是行之有效的。许多问题可以取k=2。 这种使子问题规模大致相等的做法是出自一种平衡子问题的思想,它几乎总是比子问题规模不等的做法要好。

    分治法的合并步骤是算法的关键所在。有些问题的合并方法比较明显,有些问题合并方法比较复杂,或者是有多种合并方案;或者是合并方案不明显。究竟应该怎样合并,没有统一的模式,需要具体问题具体分析。

    【问题】 大整数乘法

    问题描述:

    通常,在分析一个算法的计算复杂性时,都将加法和乘法运算当作是基本运算来处理,即将执行一次加法或乘法运算所需的计算时间当作一个仅取决于计算机硬件处理速度的常数。

    这个假定仅在计算机硬件能对参加运算的整数直接表示和处理时才是合理的。然而,在某些情况下,我们要处理很大的整数,它无法在计算机硬件能直接表示的 范围内进行处理。若用浮点数来表示它,则只能近似地表示它的大小,计算结果中的有效数字也受到限制。若要精确地表示大整数并在计算结果中要求精确地得到所 有位数上的数字,就必须用软件的方法来实现大整数的算术运算。

    请设计一个有效的算法,可以进行两个n位大整数的乘法运算。

    设X和Y都是n位的二进制整数,现在要计算它们的乘积XY。我们可以用小学所学的方法来设计一个计算乘积XY的算法,但是这样做计算步骤太多,显得效 率较低。如果将每2个1位数的乘法或加法看作一步运算,那么这种方法要作O(n2)步运算才能求出乘积XY。下面我们用分治法来设计一个更有效的大整数乘 积算法。

    图6-3 大整数X和Y的分段

    我们将n位的二进制整数X和Y各分为2段,每段的长为n/2位(为简单起见,假设n是2的幂),如图6-3所示。

    由此,X=A2n/2+B,Y=C2n/2+D。这样,X和Y的乘积为:

    XY=(A2n/2+B)(C2n/2+D)=AC2n+(AD+CB)2n/2+BD (1)

    如果按式(1)计算XY,则我们必须进行4次n/2位整数的乘法(AC,AD,BC和BD),以及3次不超过n位的整数加法(分别对应于式(1)中的 加号),此外还要做2次移位(分别对应于式(1)中乘2n和乘2n/2)。所有这些加法和移位共用O(n)步运算。设T(n)是2个n位整数相乘所需的运 算总数,则由式(1),我们有:

    (2)由此可得T(n)=O(n2)。因此,用(1)式来计算X和Y的乘积并不比小学生的方法更有效。要想改进算法的计算复杂性,必须减少乘法次数。为此我们把XY写成另一种形式:

    XY=AC2n+[(A-B)(D-C)+AC+BD]2n/2+BD (3)

    虽然,式(3)看起来比式(1)复杂些,但它仅需做3次n/2位整数的乘法(AC,BD和(A-B)(D-C)),6次加、减法和2次移位。由此可得:

    (4)

    用解递归方程的套用公式法马上可得其解为T(n)=O(nlog3)=O(n1.59)。利用式(3),并考虑到X和Y的符号对结果的影响,我们给出大整数相乘的完整算法MULT如下:
[AppleScript] 纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
function MULT(X,Y,n);{X和Y为2个小于2n的整数,返回结果为X和Y的乘积XY}
 
 
begin
 
 
S=SIGN(X)*SIGN(Y);{S为X和Y的符号乘积}
 
 
X=ABS(X);
 
 
Y=ABS(Y);{X和Y分别取绝对值}
 
 
ifn=1then
 
 
if(X=1)and(Y=1)then return(S)
 
 
elsereturn(0)
 
 
elsebegin
 
 
A=X的左边n/2位;
 
 
B=X的右边n/2位;
 
 
C=Y的左边n/2位;
 
 
D=Y的右边n/2位;
 
 
ml=MULT(A,C,n/2);
 
 
m2=MULT(A-B,D-C,n/2);
 
 
m3=MULT(B,D,n/2);
 
 
S=S*(m1*2n+(m1+m2+m3)*2n/2+m3);
 
 
return(S);
 
 
end;
 
 
end;

原创粉丝点击