, l4 e! H# Y. L: B. d<TR>& M0 J/ H- j o7 t
<TD width=74><IMG src="http://www.frontfree.net/articles/pages/0000000688/title.gif" border=1></TD>! V# }! d3 a, m6 {0 q
<TD vAlign=top width="100%"># E' }- R2 ^ e/ H# _4 {
<TABLE height="100%" cellSpacing=0 cellPadding=0 width="100%" border=0>$ z, J; ]& r: B9 K8 e" |
9 y. x( _& L9 n( }
<TR>4 Y1 T% u3 O. y
<TD class=artitle vAlign=top colSpan=2>ACM竞赛试题分析</TD></TR>: l7 M4 o6 K& U- F- R6 ]
<TR vAlign=top>. }. J: j5 p* f
<TD align=left>原创:怒火之袍</TD>; Z7 O0 L( }( K6 ?' H) o# |8 \ S0 T
<TD class=text vAlign=top align=right>2002年12月1日 </TD></TR></TABLE></TD></TR> " m2 B Z8 v _% H9 X: i: S<TR>, B) [& L% D6 ^% o
<TD class=arcontent colSpan=3> * y( y6 t6 L+ \7 H, L8 I<>上一次笔者简单地介绍了ACM/ICPC程序设计竞赛的基本信息和发展状况,这次则试着向大家展示一下其中一个比较简单的题目,并作出一些粗浅的分析,希望能以此让大家有个更感性的认识。</P>; e1 ]! ^; H k" A0 I8 \/ }
<>从“蛇和梯子”的问题谈对信息的过滤处理</P> / Z; n* t* ^1 Q. l. o+ y. e( H<>2002年11月2日 阿拉伯和北非地区第5届地区赛 题目G 蛇和梯子</P># a) w" \# U* B: T. R D A
<>问题简述:“蛇和梯子”是一个在N*N(0<N<=20)的方格棋盘上进行的游戏。(见下图)</P> + u2 C8 |9 X$ u, w< align=center><IMG src="http://www.frontfree.net/articles/pages/0000000688/pic001.jpg" border=0></P> : u5 l3 \+ q2 V4 M) X& o& _( ~8 o<>方格从1到N的平方编号。除了1号和最后1号方格,其他的格子有可能有蛇或梯子存在(蛇和梯子的数量及具体位置由输入确定,它们的数量都在100之内并且蛇和梯子不能临近放置,也就是在任何了放置两者首尾的方格之间至少还有一个未放置任何东西的格子)。开始的时候玩家把他们的标志物放在1号格子中。玩家轮流以扔骰子的方式移动他们的指示物。如果一个指示物到达了一条蛇的嘴部,则把它移回蛇的尾部。如果一个指示物到达了一个梯子的底部则将它移动到梯子的顶部。如果你是一个可以自由控制骰子的高手,现在请求出你至少需要扔几次骰子才能到达标为N^2的格子。(比如在上图所示例一中,你的方案应该是走4步到达5并由梯子上升到16,再走4步到达20并由梯子上升到33,然后走3步。这样,你一共需要扔3次骰子。而在例二中,你的方案应该是连扔4个6。)</P> ) s. w6 y* v( M- u<>比较容易看出,这个问题是不能用贪心算法解决的,因为你不能保证在这步到达一个数码比较大的格子就意味着最好的效率(即使是通过梯子到达了一个这步所能到达的最大号码的格子),也就是说,号码的大小并不能代表从这个格子到达终点所需要的步数上的多少,这就带给我们一个启发:蛇和梯子真的需要看成是两个东西来分别处理么?实际上确实是不需要的,蛇和梯子的本质就是我们经常在游戏中说的“单向传送点”,只不过梯子的底部是入口而顶部是出口,蛇的嘴部是入口而尾部是出口罢了,对于他们的描述完全可以选择相同的结构:</P> . I, E d; {6 @4 C<>struct SnakeAndLadder</P>/ @0 a( l, x; n0 G
<>{</P> 9 ?" c: h2 s" \. _<> int from,to;</P> # L- Y# b# V) [- ~<>};</P>" U8 I4 w9 W9 s
<>接下来要考虑的是解决问题的方法。贪心算法被否定之后,我们的选择可能会是搜索,对于本题所采用的搜索显然应该以广度优先的方式进行,但是稍加分析我们就会发现如果单纯地采用广度优先搜索会产生许多重复的结点,现在我们将指示物处于某格的结点简称为结点X,那么比如在例1中,第1步过后,队列中存放的结点是2,23,4,16,6,7,在第二步时,当结点2成为扩展结点时将生成结点23、4、16、6、7、8,其中只有8不存在于当前活结点队列中,即使加以判断,不把重复的结点再次加入队列中,那至少也需要对活结点队列进行搜索。实际上我们完全有更好的方法。</P>+ @& c _0 f! k- O, ^, o! p# y5 o
<>应该意识到,采用树状结构和搜索的方法处理问题其重要的一点是利用祖先结点的差异性来对儿子结点做不同的处理。然而在本题中,儿子结点的生成只依赖于父结点的信息而与其它祖先结点无关,所以采用树来描述这个过程其实是多余的。在走了若干步之后,对于一个特定的格子实际上只有两种状态的区分:</P> ( S; ~3 Y+ o5 w( X<>1、在走了这些步数之后存在一种方案使指示物位于此格中;</P> 6 r% X. ]$ q- o' `. i<>2、不存在这样的一种方案。</P> $ n2 S& m+ F* y/ D<>所以我们可以用一个N^2大小的数组来描述若干步之后可以到达的格子的集合,其中每一个元素描述一个格子的状态,0表示不存在一种方案到达,1表示存在至少一种方案到达。这样,我们从表示第n步状态的数组,完全可以推出表示第n+1步状态的数组,而且在第n+1步状态的数组得到之后,表示第n步状态的数组也就不再存在利用价值了。一旦数组中表示最后一个格子的元素成为1,就表示可以通过这个步数完成任务了。</P> 8 t7 M( W( n4 I# a<>比如在例1中,描述棋盘状态的数组其变化过程应该是:</P>" V3 f7 Z# p# c' v7 V4 K
<TABLE width="100%" bgColor=#000000 border=0> ' V: C& o6 f$ }" K; a# O, Y ! W4 l+ k B: C9 f<TR>: G1 L" a( t/ I* g" d7 N3 a- ^
<TD width="35%" bgColor=#666666>7 N4 ^& T3 v2 p# |4 H! l" z
<DIV class=unnamed1 align=center><B><FONT color=#ffffff>描述状态</FONT></B></DIV></TD>2 k) J1 u% V5 R. J: T
<TD width="65%" bgColor=#666666> 2 S2 g0 c% x1 D O' p3 n<DIV align=center> 5 `% `2 O( p3 l* w5 b* C4 @2 L<><B><FONT color=#ffffff>数组元素的内容(从表示第1个格的元素排列到表示最后一个格的元素)</FONT></B></P></DIV></TD></TR>) y) a* k. Q; _
<TR> $ v9 K- i1 E1 `1 V; D<TD class=unnamed1 width="35%" bgColor=#ffffff>起始状态</TD>+ m6 \# ~0 O3 Z# }" k! O/ F
<TD class=unnamed1 width="65%" bgColor=#ffffff>100000000000000000000000000000000000</TD></TR> / {7 P( x2 B/ j$ B<TR> " j h# E5 t# k K5 a0 ?& s<TD class=unnamed1 width="35%" bgColor=#e0e0e0>第1步之后</TD> % ?9 P# s4 `7 {$ N5 h9 [7 z9 f- m<TD class=unnamed1 width="65%" bgColor=#e0e0e0>010101100000000100000010000000000000</TD></TR> ( K$ R3 l$ k3 ^" z" [4 E4 X/ ~<TR>5 {. z' a& Z6 B" h9 |) x
<TD class=unnamed1 width="35%" bgColor=#ffffff>第2步之后</TD> " {8 y: f( l- l2 [ B3 \+ B<TD class=unnamed1 width="65%" bgColor=#ffffff>000101111111100111101111111110001000</TD></TR>* K6 q1 n3 G$ f; L" i% p
<TR> 3 X4 D& D- J1 _( X. u<TD class=unnamed1 width="35%" bgColor=#e0e0e0>第3步之后</TD>/ ~+ o! R' ^( y! R ]! R
<TD class=unnamed1 width="65%" bgColor=#e0e0e0>000001111111111111101111111111111101</TD></TR></TABLE> 0 z5 v: t1 x/ P1 e2 m, V7 i* N<>到第3步之后,数组的最后一个元素已经变为了1,这即表明存在一种方案,使得我们扔3次骰子就可以完成任务。以下是实现此算法的主要部分代码,数组下标0——size*size-1的元素分别表示了从第1格到最后一格的状态,step记录步数,obstacle是struct SnakeAndLadder的向量,描述了蛇和梯子的传送入口和出口:</P>& u3 E' C! P( y4 p9 N! ]8 l
<TABLE cellSpacing=0 cellPadding=0 width="100%" bgColor=#e0e0e0 border=0> 5 y' N' Z1 n) A0 D$ t- U) d8 j$ Y7 |- l. R
<TR>$ s8 Q/ L6 ^, [0 G; X* M% a- g
<TD> 4 |1 n: i2 I! a<><FONT color=#006600>//初始化状态数组和步数记录</FONT></P>$ l& G9 }5 C! z7 i1 @& m. E7 O8 \9 Z
<><B><FONT color=#0000ff>for</FONT></B> (j=1;j<size*size;j++)3 z8 V5 h( F& ~- L) u5 Y! ~
grid[j]=0;, q5 F+ M* [* r; W# b/ S
grid[0]=1;7 l( E+ v; c: o* k, }% o) f9 P
step=0; 6 W/ K; D! A2 g7 M8 _5 Y</P> 4 V7 e( m( M; R& Z0 K% I9 m<><B><FONT color=#0000ff>while</FONT></B> (grid[size*size-1]==0)2 z, I/ y w; i, u
{ 0 ~, h6 i5 W. O8 s6 N<FONT color=#0000ff><B> for</B></FONT> (j=0;j<size*size;j++) & j% C& S6 Z5 X. \1 q4 r2 I gridbak[j]=grid[j];; R3 A' H M: e8 W
<B><FONT color=#0000ff>for</FONT></B> (j=0;j<size*size;j++)+ _* s; y, ?+ x3 D ^7 O: q
grid[j]=0;; B% Q/ F% O3 z+ r: M$ A, ]8 @
<B><FONT color=#0000ff>for</FONT></B> (j=0;j<size*size-1;j++)<FONT color=#006600>//搜索所有的格子(最后一格不用搜索因为在过程结束前它一定为0)</FONT>2 P2 Q( h+ _1 n3 f
{ ( M/ ~3 o" M* u4 b6 r <B><FONT color=#0000ff>if </FONT></B>(gridbak[j]==0)//若在上一步无法到达此格则跳出 - x% x8 F9 W0 P! E, v5 d/ Z <FONT color=#0000ff><B>continue</B></FONT>; 0 ^6 S$ W3 P. E <FONT color=#0000ff><B>for</B></FONT> (k=1;k<=6;k++); g; s2 n# ], V- p/ { b3 [/ A1 w
{ 4 P6 @+ L8 X8 }: t* |' e9 s test=0; & U0 z% z# S* {: `+ y <FONT color=#0000ff><B>if</B></FONT> (j+k>size*size-1)1 D! h7 ~5 J8 T. ^
<B><FONT color=#0000ff>break</FONT></B>; 5 c3 ~9 ^& O* n8 h" J2 T8 W <FONT color=#0000ff><B> for</B></FONT> (l=0;l<obstacle.size();l++)<FONT color=#006600>//如果此格是一个传送入口,将传送出口的格子可达</FONT> # V) ~* @' R& `; v6 O; ?5 R {3 e& [. k/ P8 R' \9 J
<FONT color=#0000ff><B>if</B></FONT> (obstacle[l].from==j+k) ; v4 ?2 s2 a9 d4 i1 K. K4 L {. f r- P1 C5 j2 b3 M
grid[obstacle[l].to]=1;4 } _. [. g! T; x7 x8 r9 X3 a0 f" u8 o
test=1; ( _- }# O# e8 K: l" N! K( v <FONT color=#0000ff><B>break</B></FONT>; B6 g1 I2 ?9 U }5 X9 L2 V m u, a6 a- {
} 8 {* S% e! \; I) C7 g& l <FONT color=#0000ff><B>if</B></FONT> (test==0 && grid[j+k]==0) 1 J+ E3 p8 J( M9 F! X) @ grid[j+k]=1;, o. p: T% ^) s& c
} / N2 ?2 T1 h1 b& F+ E) ? }& `2 j2 a9 O! I U9 f! ?
step++;//步数加一 $ A4 O/ ^* g! C} . u4 x' T5 a t; |9 q$ J& K</P></TD></TR></TABLE> 8 W4 G6 o# I+ V过程执行完毕后,输出step即可。 1 y: U% [3 u5 K3 W1 u$ r* h+ v t1 s; I0 Z4 B/ m& j0 L% d1 _6 N( w 0 i$ a5 ?) e8 y+ v+ C1 y5 M% f5 ]/ t7 P
' k Y7 f$ H: T8 c<>另外,笔者认为此题规定棋盘为N*N大小只是为了输入方便,举例画图方便,实际上多大的棋盘都是一样可以看作一条直线,数组也是用一维的便足够了。</P> 9 S. I, I# _- B) N, j/ m<>通过对这道题的分析,笔者想说的是,ACM竞赛试题其实有一些并没有多大的绝对难度,但是它通过一些附加的信息来制造干扰和人为复杂化,这样,一些没有经验和实力不足的选手在有限的时间内对题目的解决思路和总体难度分析都要产生偏差,可实力雄厚的人则能一眼看透问题的实质所在并以最简洁的算法实现,这是通过大量练习才能取得的。并不是所有的算法爱好者都一定要通过参加竞赛来提高,但是竞赛中的一些问题确实向我们展示了一些值得注意、思考和总结的方法,希望大家都能充分利用这些资源取得属于自己的进步。</P></TD></TR></TABLE> , c- ?6 C5 K+ Z$ O( p