2007noi冬令营讲义枚举、递归与搜索_第1页
2007noi冬令营讲义枚举、递归与搜索_第2页
2007noi冬令营讲义枚举、递归与搜索_第3页
2007noi冬令营讲义枚举、递归与搜索_第4页
2007noi冬令营讲义枚举、递归与搜索_第5页
已阅读5页,还剩64页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、NOI 2007 冬令营讲座枚举、递归与搜索郭 炜北京大学信息科学技术学院NOI训练到底能给学生什么?乐趣NOI者, 一本万利也!知识和技能友情见识荣誉上好大学的资本ACM国际大学生程序设计竞赛简介ACM International Collegiate Programming Contest (ACM/ICPC) 3人为一队,在5个小时内用1台计算机解决8-12个问题。 每个问题只有对和错两种结果。 每年9月-12月在全球100多个城市举行预赛,有一千多所大学的数千支队参加 预赛优胜的数十支队进入第二年的全球总决赛,每个大学只有一支队伍能进入总决赛ACM/ICPC能带来什么?乐趣知识和技能友

2、情见识荣誉上好大学,找好工作的资本团队精神爱情T2POJ: 例1. 熄灯问题(POJ1222)问题描述:有一个由按钮组成的矩阵,其中每行有6个按钮,共5行。每个按钮的位置上有一盏灯。当按下一个按钮后,该按钮以及周围位置(上边、下边、左边、右边)的灯都会改变一次。即,如果灯原来是点亮的,就会被熄灭;如果灯原来是熄灭的,则会被点亮。在矩阵角上的按钮改变3盏灯的状态在矩阵边上的按钮改变4盏灯的状态其他的按钮改变5盏灯的状态在上图中,左边矩阵中用X标记的按钮表示被按下,右边的矩阵表示灯状态的改变对矩阵中的每盏灯设置一个初始状态。请你按按钮,直至每一盏等都熄灭。与一盏灯毗邻的多个按钮被按下时,一个操作会

3、抵消另一次操作的结果。在下图中,第2行第3、5列的按钮都被按下,因此第2行、第4列的灯的状态就不改变 请你写一个程序,确定需要按下哪些按钮,恰好使得所有的灯都熄灭。输入:第一行是一个正整数N,表示需要解决的案例数。每个案例由5行组成,每一行包括6个数字。这些数字以空格隔开,可以是0或1。0表示灯的初始状态是熄灭的,1表示灯的初始状态是点亮的。输出:对每个案例,首先输出一行,输出字符串“PUZZLE #m”,其中m是该案例的序号。接着按照该案例的输入格式输出5行,其中的1表示需要把对应的按钮按下,0则表示不需要按对应的按钮。每个数字以一个空格隔开。样例输入2 0 1 1 0 1 01 0 0 1

4、 1 1 0 0 1 0 0 1 1 0 0 1 0 1 0 1 1 1 0 0 0 0 1 0 1 0 1 0 1 0 1 1 0 0 1 0 1 1 1 0 1 1 0 0 0 1 0 1 0 0 样例输出PUZZLE #1 1 0 1 0 0 1 1 1 0 1 0 1 0 0 1 0 1 1 1 0 0 1 0 0 0 1 0 0 0 0PUZZLE #2 1 0 0 1 1 1 1 1 0 0 0 0 0 0 0 1 0 0 1 1 0 1 0 1 1 0 1 1 0 1 第2次按下同一个按钮时,将抵消第1次按下时所产生的结果。因此,每个按钮最多只需要按下一次。各个按钮被按下的顺序对

5、最终的结果没有影响对第1行中每盏点亮的灯,按下第2行对应的按钮,就可以熄灭第1行的全部灯。如此重复下去,可以熄灭第1、2、3、4行的全部灯。解题思路第一想法:枚举所有可能的开关状态,对每个状态计算一下最后灯的情况,看是否都熄灭。每个开关有两种状态,一共有30个开关,那么状态数是230,太多,会超时。如何减少枚举的状态数目呢?一个基本思路是,如果存在某个局部,一旦这个局部的状态被确定,那么剩余其他部分的状态只能是确定的一种,或者不多的n种,那么就只需枚举这个局部的状态就行了。解题思路本题是否存在这样的“局部”呢?经过观察,发现第1行就是这样的一个“局部”。因为第1行的各开关状态确定的情况下,这些

6、开关作用过后,将导致第1行某些灯是亮的,某些灯是灭的。此时要熄灭第1行某个亮着的灯(假设位于第i列),那么唯一的办法就是按下第2行第i列的开关(因为第一行的开关已经用过了,而第3行及其后的开关不会影响到第1行)。因此,为了使第1行的灯全部熄灭,第2行的合理开关状态就是唯一的。解题思路第2行的开关起作用后,为了熄灭第二行的灯,第3行的合理开关状态就也是唯一的,以此类推,最后一行的开关状态也是唯一的。总之,只要第1行的状态定下来,比如叫A,那么剩余行的情况就是确定唯一的了。推算出最后一行的开关状态,然后看看最后一行的开关起作用后,最后一行的所有灯是否都熄灭,如果是,那么A就是一个解的状态。如果不是

7、,那么A不是解的状态,第1行换个状态重新试试。因此,只需枚举第一行的状态,状态数是26 = 64解题思路具体实现用一个矩阵anPuzzle 56表示灯的初始状态anPuzzleij=1:灯(i, j)初始时是被点亮的anPuzzle ij=0:灯(i, j)初始时是熄灭的用一个矩阵anSwitch 56表示要计算的结果anSwitchij=1:需要按下按钮(i, j)anSwitchij=0:不需要按下按钮(i, j) anSwitch0里放着第1行开关的状态,如何进行枚举呢?可以使用六重循环:for( int a0 = 0; a0 2; a0 + ) for( int a1 = 0; a1

8、2; a0 + ) for( int a2 = 0; a2 2; a0 + ) for( int a3 = 0; a3 2; a0 + ) for( int a4 = 0; a4 2; a0 + ) for( int a5 = 0; a5 2; a0 + ) anSwitch00 = a0; anSwitch01 = a1;anSwitch02 = a2; /如果每行开关数目是N那怎么办? 适用于一行有N个开关的 办法: 一个6位二进制数的所有取值正好是64种,让该数的每一位对应于anSwitch0里的一个元素( anSwitch00 对应最高位,anSwitch01对应次高位. ),那么这个

9、二进制数的每个取值正好表示了第一行开关的一种状态。要写一个从二进制数到状态的转换函数void SwitchStatus( int n, int * pSwitchLine); 要写一个让开关起作用的函数 void ApplySwitch( int * pLights, int * pNextLights, int * pSwitchs) ;#include #include #include int T;int anPuzzle66;int anOriPuzzle66;int anSwitch66; int i,j;void ApplySwitch( int * pLights, int *

10、pNextLights, int * pSwitchs) for( int i = 0;i 0 )pLightsi-1 = 1 - pLightsi-1;pLightsi = 1 - pLightsi;if( i 5) pLightsi+1 = 1 - pLightsi+1;pNextLightsi = 1 - pNextLightsi;void SwitchStatus( int n, int * pSwitch)for( i = 0;i 6 ;i + ) pSwitchi = (1 (6 - i - 1) & n ;if( pSwitchi) pSwitchi = 1;void Outpu

11、tResult(int t)cout PUZZLE # t endl;for( int i = 0;i 5; i + ) for( int j = 0; j 6; j + ) cout anSwitchij;if( j 5 ) cout ;cout T;for( int t = 0; t T; t + ) for( i = 0;i 5; i + )for( j = 0; j anOriPuzzleij;for( int n = 0; n 64; n + ) memcpy( anPuzzle,anOriPuzzle,sizeof(anPuzzle);SwitchStatus( n, anSwit

12、ch0);for( int k = 0; k 5; k + ) ApplySwitch( anPuzzlek,anPuzzlek+1,anSwitchk);memcpy( anSwitchk+1, anPuzzlek,sizeof(anPuzzlek); bool bOk = true;for( k = 0; k 6; k + ) if( anPuzzle4k ) bOk = false;break;if( bOk ) OutputResult(t+1);break; / for( int n = 0; n 64; n + ) 问题描述:在韩国,有一种小的青蛙。每到晚上,这种青蛙会跳越稻田,从

13、而踩踏稻子。农民在早上看到被踩踏的稻子,希望找到造成最大损害的那只青蛙经过的路径。每只青蛙总是沿着一条直线跳越稻田,而且每次跳跃的距离都相同。例2:讨厌的青蛙(POJ1054)如下图所示,稻田里的稻子组成一个栅格,每棵稻子位于一个格点上。而青蛙总是从稻田的一侧跳进稻田,然后沿着某条直线穿越稻田,从另一侧跳出去如下图所示,可能会有多只青蛙从稻田穿越。青蛙的每一跳都恰好踩在一棵水稻上,将这棵水稻拍倒。有些水稻可能被多只青蛙踩踏。当然,农民所见到的是图4中的情形,并看不到图3中的直线,也见不到别人家田里被踩踏的水稻,。根据图4,农民能够构造出青蛙穿越稻田时的行走路径,并且只关心那些在穿越稻田时至少踩

14、踏了3棵水稻的青蛙。因此,每条青蛙行走路径上至少包括3棵被踩踏的水稻。而在一条青蛙行走路径的直线上,也可能会有些被踩踏的水稻不属于该行走路径不是一条行走路径:只有两棵被踩踏的水稻是一条行走路径,但不包括(2,6)上的水道不是一条行走路径:虽然有3棵被踩踏的水稻,但这三棵水稻之间的距离间隔不相等请你写一个程序,确定:在一条青蛙行走路径中,最多有多少颗水稻被踩踏。例如,图4的答案是7,因为第6行上全部水稻恰好构成一条青蛙行走路径程序输入:从标准输入设备上读入数据。第一行上两个整数R、C,分别表示稻田中水稻的行数和列数,1R、C5000。第二行是一个整数N,表示被踩踏的水稻数量, 3N5000。在剩

15、下的N行中,每行有两个整数,分别是一颗被踩踏水稻的行号(1R)和列号(1C),两个整数用一个空格隔开。而且,每棵被踩踏水稻只被列出一次。程序输出:从标准输出设备上输出一个整数。如果在稻田中存在青蛙行走路径,则输出包含最多水稻的青蛙行走路径中的水稻数量,否则输出0。样例输入6 714 2 1 6 6 4 2 2 5 2 6 2 7 3 4 6 1 6 2 2 3 6 3 6 4 6 5 6 7 样例输出7解题思路每条青蛙行走路径中至少有3棵水稻假设一只青蛙进入稻田后踩踏的前两棵水稻分别是(X1,Y1)、 (X2,Y2)。那么:青蛙每一跳在X方向上的步长dX = X2 - X1 、在 Y方向上的步

16、长dY = Y2 - Y1 (X1 - dX,Y1 - dY)需要落在稻田之外当青蛙踩在水稻(X,Y)上时,下一跳踩踏的水稻是(X + dX,Y + dY)将路径上的最后一棵水稻记作(XK,YK),(XK + dX,YK + dY)需要落在稻田之外解题思路:猜测一条路径猜测的办法需要保证:每条可能的路径都能够被猜测到从输入的水稻中任取两棵,作为一只青蛙进入稻田后踩踏的前两棵水稻,看能否形成一条穿越稻田的行走路径解题思路:猜测一条路径猜测的过程需要尽快排除错误的答案:猜测(X1,Y1)、 (X2,Y2)就是所要寻找的行走路径上的前两棵水稻。当下列条件之一满足时,这个猜测就不成立青蛙不能经过一跳从

17、稻田外跳到(X1,Y1)上按照(X1,Y1)、 (X2,Y2)确定的步长,从(X1,Y1)出发,青蛙最多经过(MAXSTEPS - 1)步,就会跳到稻田之外。 MAXSTEPS是当前已经找到的最好答案选择合适的数据结构选择合适的数据结构:采用的数据结构需要与问题描述中的概念对应方案1:struct int x, y; plants5000;方案2:int plantsRow5000, plantsCol5000;显然方案1更符合问题本身的描述设计的算法要简洁尽量使用C提供的函数完成计算的任务:猜测一条行走路径时,需要从当前位置(X,Y)出发上时,看看(X + dX,Y + dY)位置的水稻水稻

18、是否被踩踏方案1:自己写一段代码,看看(X + dX,Y + dY) 是否在数组plants中;方案2:先用QSORT对plants中的元素排序,然后用BSEARCH从中查找元素(X + dX,Y + dY)显然基于方案2设计的算法更简洁、更容易实现、更不容易出错误通常,所选用的数据结构对算法的设计有很大影响注意,一个有n个元素的数组,每次取两个元素,遍历所有取法的代码写法:for( int i = 0; i n 1 )for( int j = i + 1; j n; j + ) ai = ;aj = ;二分查找函数:void *bsearch(const void *key, const v

19、oid *base, size_t nelem, size_t width, int (_USERENTRY *fcmp)(const void *, const void *); 查到返回地址,查不到返回空指针参考程序#include #include int r, c, n;struct PLANT int x, y;PLANT plants5001;PLANT plant;int pare( const void *ele1, const void *ele2 );int searchPath(PLANT secPlant, int dX, int dY) void main()int

20、i,j, dX, dY, pX, pY, steps, max = 2;scanf(%d%d, &r, &c);scanf(%d, &n);for (i = 0; i n; i+)scanf(%d%d, &plantsi.x, &plantsi.y);qsort(plants, n, sizeof(PLANT), pare);for (i = 0; i n - 2; i+)for ( j = i + 1; j n -1 ; j+) dX = plants j .x - plantsi.x; dY = plants j .y - plantsi.y; pX = plants i .x - dX;

21、 pY = plants i .y - dY; if (pX = 1 & pY = 1)continue; if (plants i .x + (max - 1) * dX r)break; pY = plants i .y + (max - 1) * dY; if ( pY c | pY max) max = steps; if ( max = 2 ) max = 0;printf(%dn, max);int pare( const void *ele1, const void *ele2 )PLANT *p1, *p2;p1 = (PLANT*) ele1;p2 = (PLANT*) el

22、e2;if ( p1-x = p2-x ) return(p1-y - p2-y);return ( p1-x - p2-x );int searchPath(PLANT secPlant, int dX, int dY)PLANT plant;int steps;plant.x = secPlant.x + dX;plant.y = secPlant.y + dY;steps = 2;while (plant.x = 1 & plant.y = 1) if (!bsearch(&plant, plants, n, sizeof(PLANT), pare) steps = 0;break;pl

23、ant.x += dX; plant.y += dY;steps+;return(steps);例3:木棒问题(POJ1011)问题描述:乔治拿来一组等长的木棒,将它们随机地裁断,使得每一节木棒的长度都不超过50个长度单位。然后他又想把这些木棒恢复到为裁截前的状态,但忘记了木棒的初始长度。请你设计一个程序,帮助乔治计算木棒的可能最小长度。每一节木棒的长度都用大于零的整数表示输入:由多个案例组成,每个案例包括两行。第一行是一个不超过64的整数,表示裁截之后共有多少节木棒。第二行是经过裁截后,所得到的各节木棒的长度。在最后一个案例之后,是零。输出:为每个案例,分别输出木棒的可能最小长度。每个案例占

24、一行。解题思路初始状态:有N节木棒最终状态:这N节木棒恰好被拼接成若干根等长的棍子(裁前的木棒称为棍子)枚举什么?枚举所有有可能的棍子长度。从最长的那根木棒的长度一直枚举到木棒长度总和的一半,对每个假设的棍子长度试试看能否拼齐所有棍子在拼接过程中,要给用过的木棒做上标记,以免重复使用拼好前i根棍子,结果发现第i+1根拼不成了,那么就要推翻第i根的拼法,重拼第i根.直至有可能推翻第1根棍子的拼法搜索题,首先要解决一个问题:按什么顺序搜索?把木棒按长度排序。每次选木棒的时候都尽量先选长的。搜索题,还要解决一个问题:如何剪枝(就本题而言,即尽可能快地发现一根拼好的棍子需要被拆掉,以及尽量少做结果不能

25、成功的尝试。 剪枝1: 每次开始拼一根新棍子的时候,必定选剩下的木棒里最长的一根,作为该棍子的第一根木棒。对此决不后悔。剪枝2:不要希望通过仅仅替换已拼好棍子的最后一根木棒就能够改变失败的局面。已拼接部分未拼接部分长度为L的棍子长度为L1的未拼接木棒,共有N1根长度为L2的未拼接木棒,共有N2根长度为L3的未拼接木棒,共有N3根如果某次拼接选择长度为L1的木棒,导致最终失败,则尝试下一根木棒时,要跳过所有长度为L1的木棒。剪枝3:boolDfs(int nUnusedSticks, int nLeft ) ; 表示: 当前有nUnusedSticks根未用木棒,而且当前正在拼的那根棍子比假定的

26、棍子长度短了nLeft, 那么在这种情况下能全部否拼成功。Dfs的基本递推关系:boolDfs(int nUnusedSticks, int nLeft) .找一根长度不超过nLeft的木棒(假设长为len,拼在当前棍子上,然后return Dfs(nUnusedSticks 1,nLeft len );Dfs的终止条件之一:boolDfs(int nTotalSticks,int nUnusedSticks, int nLeft ) if( nUnusedSticks = 0 & nLeft = 0)return true;#include #include #include int T,

27、S;int L;int anLength65;int anUsed65;int i,j,k;int Dfs(int nUnusedSticks, int nLeft);int pare( const void * e1, const void * e2) int * p1, * p2;p1 = (int * ) e1;p2 = (int * ) e2;return * p2 - * p1;main()while(1) cin S;if( S = 0 )break;int nTotalLen = 0;for( int i = 0; i anLengthi;nTotalLen += anLengt

28、hi;qsort(anLength,S,sizeof(int) pare); for( L = anLength0; L = nTotalLen / 2; L + ) if( nTotalLen % L)continue;memset( anUsed, 0,sizeof(anUsed);if( Dfs( S,L) cout L nTotalLen / 2 )cout nTotalLen endl; / whileint Dfs( int nUnusedSticks, int nLeft)/ nLeft表示当前正在拼的棍子和 L 比还缺的长度if( nUnusedSticks = 0 & nLe

29、ft = 0 )return true;if( nLeft = 0 ) /一根刚刚拼完nLeft = L; /开始拼新的一根for( int i = 0;i S;i +) if( !anUsedi & anLengthi 0 ) if( anUsedi-1 = false & anLengthi = anLengthi-1)continue; /剪枝3anUsedi = 1;if ( Dfs( nUnusedSticks - 1, nLeft - anLengthi)return true;else anUsedi = 0;/说明不能用i作为/第条,/那么要拆以前的/棍子,i还可能用/在以前的

30、棍子里, /因此要anUsedi = 0;if( anLengthi = nLeft | nLeft = L)return false;/剪枝2、1return false;例4:红与黑(POJ1979)问题描述:有一间长方形的房子,地上铺了红色、黑色两种颜色的正方形瓷砖。你站在其中一块黑色的瓷砖上,只能向相邻的黑色瓷砖移动。请写一个程序,计算你总共能够到达多少块黑色的瓷砖。输入:包括多个数据集合。每个数据集合的第一行是两个整数W和H,分别表示x方向和y方向瓷砖的数量。W和H都不超过20。在接下来的H行中,每行包括W个字符。每个字符表示一块瓷砖的颜色,规则如下.:黑色的瓷砖#:红色的瓷砖:黑色

31、的瓷砖,并且你站在这块瓷砖上。该字符在每个数据集合中唯一出现一次当在一行中读入的是两个零时,表示输入结束。输出:对每个数据集合,分别输出一行,显示你从初始位置出发能到达的瓷砖数(记数时包括初始位置的瓷砖)样例输入:6 9 .#. .# . . . . . #.# .#.#. 0 0样例输出:45 设函数 dfs( r,c) 表示从 (r,c) 点开始所能走到的格子数目(走的过程中不允许经过红格子和该函数执行前就已经被标记过的黑格子),而且该函数执行过程中走过一个格子就会将其标记。那么:如果 (r,c)已经被标记,则dfs(r,c) = 0否则:dfs(r,c) = 1 + dfs( r -1,

32、c) + dfs( r+ 1,c) +dfs( r, c -1 ) + dfs( r,c+1); 在执行等号右边前,需将点 (r,c)标记当然还要考虑(r,c)在边界上的情况#include #includeint R, C;char anBlock2121;int anMarked2121;int i,j,k;int nManR, nManC;int Dfs(int r,int c);main()while( 1) cin C R;if( C = 0 & R = 0 )break;memset( anMarked,0,sizeof( anMarked);for( i = 0;i R;i +

33、) for( j = 0; j anBlockij;if( anBlockij = ) nManR = i;nManC = j; /for( i = 0;i R;i + ) cout Dfs( nManR, nManC) 0 & anBlockr-1c = . & anMarkedr-1c = 0 )nTotals += Dfs(r-1,c);if( r 0 & anBlockrc-1 = . & anMarkedrc-1 = 0 )nTotals += Dfs(r,c-1);if( c C -1 & anBlockrc+1 = . & anMarkedrc+1 = 0 )nTotals +=

34、 Dfs(r,c+1);return nTotals;例5:陪审团的人选 在遥远的国家佛罗布尼亚,嫌犯是否有罪,须由陪审团决定。陪审团是由法官从公众中挑选的。先随机挑选n个人作为陪审团的候选人,然后再从这n个人中选m人组成陪审团。选m人的办法是: 控方和辩方会根据对候选人的喜欢程度,给所有候选人打分,分值从0到20。为了公平起见,法官选出陪审团的原则是:选出的m个人,必须满足辩方总分和控方总分的差的绝对值最小。如果有多种选择方案的辩方总分和控方总分的之差的绝对值相同,那么选辩控双方总分之和最大的方案即可。例5:陪审团的人选 为叙述问题方便,现将任一选择方案中,辩方总分和控方总分之差简称为“辩控差”,辩方总分和控方总分之和称为“辩控和”。第i个候选人的辩方总分和控方总分之差记为V(i),辩方总分和控方总分之和记为S(i)。现用f(j,k)表示,取j

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论