Unity3D教程:游戏开发算法(二)

4、递归

    递归是设计和描述算法的一种有力的工具,因为它在复杂算法的描述中被常常采用,为此在进一步介绍其余算法设计方法以前先讨论它。

    能采用递归描述的算法一般有这样的特征:为求解规模为N的问题,设法将它分解成规模较小的问题,而后从这些小问题的解方便地构造出大问题的解,而且这些规模较小的问题也能采用一样的分解和综合方法,分解成规模更小的问题,并从这些更小问题的解构造出规模较大问题的解。特别地,当规模N=1时,能直接得解。

    【问题】 编写计算斐波那契(Fibonacci)数列的第n项函数fib(n)。

    斐波那契数列为:0、一、一、二、三、……,即:

[AppleScript]  纯文本查看 复制代码
?
1
2
3
4
5
6
7
fib ( 0 ) = 0 ;
 
  
  fib ( 1 ) = 1 ;
 
  
  fib ( n ) = fib ( n -1 ) + fib ( n -2 ) (当n > 1 时)。


    写成递归函数有:

[AppleScript]  纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
13
int fib ( int n )
 
  
   { if ( n = = 0 ) return 0 ;
 
  
   if ( n = = 1 ) return 1 ;
 
  
   if ( n > 1 ) return fib ( n -1 ) + fib ( n -2 ) ;
 
  
   }


    递归算法的执行过程分递推和回归两个阶段。在递推阶段,把较复杂的问题(规模为n)的求解推到比原问题简单一些的问题(规模小于n)的求解。例如上例中,求解fib(n),把它推到求解fib(n-1)和fib(n-2)。也就是说,为计算fib(n),必须先计算fib(n-1)和fib(n-2),而计算fib(n-1)和fib(n-2),又必须先计算fib(n-3)和fib(n-4)。依次类推,直至计算fib(1)和fib(0),分别能当即获得结果1和0。在递推阶段,必需要有终止递归的状况。例如在函数fib中,当n为1和0的状况。

    在回归阶段,当得到最简单状况的解后,逐级返回,依次获得稍复杂问题的解,例如获得fib(1)和fib(0)后,返回获得fib(2)的结果,在获得了fib(n-1)和fib(n-2)的结果后,返回获得fib(n)的结果。

    在编写递归函数时要注意,函数中的局部变量和参数知识局限于当前调用层,当递推动入“简单问题”层时,原来层次上的参数和局部变量便被隐蔽起来。在一系列“简单问题”层,它们各有本身的参数和局部变量。

    因为递归引发一系列的函数调用,而且可能会有一系列的重复计算,递归算法的执行效率相对较低。当某个递归算法能较方便地转换成递推算法时,一般按递推算法编写程序。例如上例计算斐波那契数列的第n项的函数fib(n)应采用递推算法,即从斐波那契数列的前两项出发,逐次由前两项计算出下一项,直至计算出要求的第n项。

    【问题】 组合问题

    问题描述:找出从天然数一、二、……、n中任取r个数的全部组合。例如n=5,r=3的全部组合为: (1)五、四、3 (2)五、四、2 (3)五、四、1

    (4)五、三、2 (5)五、三、1 (6)五、二、1

    (7)四、三、2 (8)四、三、1 (9)四、二、1

    (10)三、二、1

    分析所列的10个组合,能够采用这样的递归思想来考虑求组合函数的算法。设函数为void comb(int m,int k)为找出从天然数一、二、……、m中任取k个数的全部组合。当组合的第一个数字选定时,其后的数字是从余下的m-1个数中取k-1数的组合。这就将求m个数中取k个数的组合问题转化成求m-1个数中取k-1个数的组合问题。设函数引入工做数组a[ ]存放求出的组合的数字,约定函数将肯定的k个数字组合的第一个数字放在a[k]中,当一个组合求出后,才将a[ ]中的一个组合输出。第一个数能够是m、m-一、……、k,函数将肯定组合的第一个数字放入数组后,有两种可能的选择,因还未去顶组合的其他元素,继续递归去肯定;或因已肯定了组合的所有元素,输出这个组合。细节见如下程序中的函数comb。

    【程序】
[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
# include
 
 
# define MAXN 100
 
 
int a[MAXN];
 
 
void comb ( int m , int k )
 
 
{ int i , j;
 
 
for ( i = m;i > = k;i --)
 
 
{ a[k] = i;
 
 
if ( k > 1 )
 
 
comb ( i -1 , k -1 ) ;
 
 
else
 
 
{ for ( j = a[ 0 ];j > 0 ;j --)
 
 
printf ( “% 4 d” , a[j] ) ;
 
 
printf ( “\n” ) ;
 
 
}
 
 
}
 
 
}
 
 
void main ( )
 
 
{ a[ 0 ] = 3 ;
 
 
comb ( 5 , 3 ) ;
 
 
}


    【问题】 背包问题

    问题描述:有不一样价值、不一样重量的物品n件,求从这n件物品中选取一部分物品的选择方案,使选中物品的总重量不超过指定的限制重量,但选中物品的价值之和最大。

    设n件物品的重量分别为w0、w一、…、wn-1,物品的价值分别为v0、v一、…、vn-1。采用递归寻找物品的选择方案。设前面已有了多种选择的方案,并保留了其中总价值最大的方案于数组option[ ],该方案的总价值存于变量maxv。当前正在考察新方案,其物品选择状况保存于数组cop[ ]。假定当前方案已考虑了前i-1件物品,如今要考虑第i件物品;当前方案已包含的物品的重量之和为tw;至此,若其他物品都选择是可能的话,本方案能达到的总价值的指望值为tv。算法引入tv是当一旦当前方案的总价值的指望值也小于前面方案的总价值maxv时,继续考察当前方案变成无心义的工做,应终止当前方案,当即去考察下一个方案。由于当方案的总价值不比maxv大时,该方案不会被再考察,这同时保证函数后找到的方案必定会比前面的方案更好。

    对于第i件物品的选择考虑有两种可能:

    (1) 考虑物品i被选择,这种可能性仅当包含它不会超过方案总重量限制时才是可行的。选中后,继续递归去考虑其他物品的选择。

    (2) 考虑物品i不被选择,这种可能性仅当不包含物品i也有可能会找到价值更大的方案的状况。

    按以上思想写出递归算法以下:

[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
try ( 物品i,当前选择已达到的重量和,本方案可能达到的总价值tv )
 
 
{ / * 考虑物品i包含在当前方案中的可能性 * /
 
 
if ( 包含物品i是能够接受的 )
 
 
{ 将物品i包含在当前方案中;
 
 
if ( i   try ( i + 1 , tw + 物品i的重量 , tv ) ;
 
 
else
 
 
/ * 又一个完整方案,由于它比前面的方案好,以它做为最佳方案 * /
 
 
以当前方案做为临时最佳方案保存;
 
 
恢复物品i不包含状态;
 
 
}
 
 
/ * 考虑物品i不包含在当前方案中的可能性 * /
 
 
if ( 不包含物品i仅是可男考虑的 )
 
 
if ( i   try ( i + 1 , tw , tv - 物品i的价值 )
 
 
else
 
 
/ * 又一个完整方案,因它比前面的方案好,以它做为最佳方案 * /
 
 
以当前方案做为临时最佳方案保存;
 
 
}


    为了理解上述算法,特举如下实例。设有4件物品,它们的重量和价值见表:

    物品 0 1 2 3

    重量 5 3 2 1

    价值 4 4 3 1

    并设限制重量为7。则按以上算法,下图表示找解过程。由图知,一旦找到一个解,算法就进一步找更好的佳。如能断定某个查找分支不会找到更好的解,算法不会在该分支继续查找,而是当即终止该分支,并去考察下一个分支。

    按上述算法编写函数和程序以下:

    【程序】

[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
# include
 
 
# define N 100
 
 
double limitW , totV , maxV;
 
 
int option[N] , cop[N];
 
 
struct { double weight;
 
 
double value ;
 
 
} a[N];
 
 
int n;
 
 
void find ( int i , double tw , double tv )
 
 
{ int k;
 
 
/ * 考虑物品i包含在当前方案中的可能性 * /
 
 
if ( tw + a[i].weight < = limitW )
 
 
{ cop[i] = 1 ;
 
 
[ / i][ / i] if ( i   else
 
 
{ for ( k = 0 ;k   option[k] = cop[k];
 
 
maxv = tv;
 
 
}
 
 
cop = 0 ;
 
 
}
 
 
/ * 考虑物品i不包含在当前方案中的可能性 * /
 
 
if ( tv - a. value > maxV )
 
 
if ( i   else
 
 
{ for ( k = 0 ;k   option[k] = cop[k];
 
 
maxv = tv - a. value ;
 
 
}
 
 
}
 
 
void main ( )
 
 
{ int k;
 
 
double w , v;
 
 
printf ( “输入物品种数\n” ) ;
 
 
scanf ( ( “%d” , & n ) ;
 
 
printf ( “输入各物品的重量和价值\n” ) ;
 
 
for ( totv = 0.0 , k = 0 ;k   { scanf ( “% 1 f% 1 f” , & w , & v ) ;
 
 
a[k].weight = w;
 
 
a[k]. value = v;
 
 
totV + = V;
 
 
}
 
 
printf ( “输入限制重量\n” ) ;
 
 
scanf ( “% 1 f” , & limitV ) ;
 
 
maxv = 0.0 ;
 
 
for ( k = 0 ;k   find ( 0 , 0.0 , totV ) ;
 
 
for ( k = 0 ;k   if ( option[k] ) printf ( “% 4 d” , k + 1 ) ;
 
 
printf ( “\n总价值为%. 2 f\n” , maxv ) ;
 
 
}


    做为对比,下面以一样的解题思想,考虑非递归的程序解。为了提升找解速度,程序不是简单地逐一辈子成全部候选解,而是从每一个物品对候选解的影响来造成值得进一步考虑的候选解,一个候选解是经过依次考察每一个物品造成的。对物品i的考察有这样几种状况:当该物品被包含在候选解中依旧知足解的总重量的限制,该物品被包含在候选解中是应该继续考虑的;反之,该物品不该该包括在当前正在造成的候选解中。一样地,仅当物品不被包括在候选解中,仍是有可能找到比目前临时最佳解更好的候选解时,才去考虑该物品不被包括在候选解中;反之,该物品不包括在当前候选解中的方案也不该继续考虑。对于任一值得继续考虑的方案,程序就去进一步考虑下一个物品。

    【程序】

[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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# include
 
 
  # define N 100
 
 
 double limitW;
 
 
 int cop[N];
 
 
 struct ele { double weight;
 
 
 double value ;
 
 
  } a[N];
 
 
 int k , n;
 
 
 struct { int ;
 
 
 double tw;
 
 
 double tv;
 
 
  } twv[N];
 
 
 void next ( int i , double tw , double tv )
 
 
  { twv. = 1 ;
 
 
 twv.tw = tw;
 
 
 twv.tv = tv;
 
 
  }
 
 
 double find ( struct ele * a , int n )
 
 
  { int i , k , f;
 
 
 double maxv , tw , tv , totv;
 
 
 maxv = 0 ;
 
 
  for ( totv = 0.0 , k = 0 ;k   totv + = a[k]. value ;
 
 
 next ( 0 , 0.0 , totv ) ;
 
 
 i = 0 ;
 
 
 While ( i > = 0 )
 
 
  { f = twv.;
 
 
 tw = twv.tw;
 
 
 tv = twv.tv;
 
 
 switch ( f )
 
 
  { case 1 : twv. + + ;
 
 
  if ( tw + a.weight < = limitW )
 
 
  if ( i   { next ( i + 1 , tw + a.weight , tv ) ;
 
 
 i + + ;
 
 
  }
 
 
  else
 
 
  { maxv = tv;
 
 
  for ( k = 0 ;k   cop[k] = twv[k].! = 0 ;
 
 
  }
 
 
 break;
 
 
  case 0 : i --;
 
 
 break;
 
 
 default : twv. = 0 ;
 
 
  if ( tv - a. value > maxv )
 
 
  if ( i   { next ( i + 1 , tw , tv - a. value ) ;
 
 
 i + + ;
 
 
  }
 
 
  else
 
 
  { maxv = tv - a. value ;
 
 
  for ( k = 0 ;k   cop[k] = twv[k].! = 0 ;
 
 
  }
 
 
 break;
 
 
  }
 
 
  }
 
 
  return maxv;