usaco教程.docx
《usaco教程.docx》由会员分享,可在线阅读,更多相关《usaco教程.docx(49页珍藏版)》请在冰豆网上搜索。
usaco教程
USACO教程
CompleteSearch枚举搜索
思想:
写枚举搜索时应遵循KISS原则(Keepitsimplestupid,译为“写最单纯愚蠢的程序”,意思是应把程序写得尽量简洁),竞赛时写程序的最终目标就是在限制时间内求出解,而不需太在意否还有更快的算法。
枚举搜索具有强大的力量,他用直接面向答案并尝试所有方案的方法发现答案。
这种算法几乎总是解题时你第一个想到的方法。
如果它能在规定的时间与空间限制内找出解,那么它常常很容易编写与调试。
这就意味着你可以有时间去解答其他难题,即那些不能显示枚举算法强大的题目。
如果你面对一道可能状态小于两百万的题目,那么你可以考虑使用枚举搜索。
尝试所有的状态,看看它们是否可行。
小心!
小心!
有时,题目不会直接要求你使用枚举算法。
例题1:
派对灯[IOI98]
在一次IOI派对上有N个灯和4个灯开光,第一个开关可以使所有灯改变状态(关上开着的灯,开启关着的灯),第二个开关可以改变所有偶数位上灯的状态,第三个开关可以改变所有奇数位上灯的状态,第四个开关控制着灯1、4、7、10……(3n+1)。
告诉你N的值和所有按开关的次数(小于10,000次)。
并告诉你某些灯的状态(例如:
7号灯是关着的,10号灯是开着的)请编程输出所有灯可能的最后状态。
很明显,每次按开关你都要偿试4种可能。
那么总共要试410000次(大约106020),那意味着你没有足够的时间去使用枚举搜索,但是我们将枚举方法改进一下,那就可以使用枚举了。
因为无论有多少个灯,由于开关控制的特殊性,都会出现6个灯一次循环的情况,即1号灯的状态永远与7号灯,13号灯,19号灯……相同,2号灯的状态也永远与8号灯,14号灯,20号灯……相同。
同样,无论你按了多少次开关,按同一个开关两次就相当于没有按该开关,那么每一个开关就只需要考虑按一次或没有按,那么这题的枚举量就很小了。
例题2:
时钟调整[IOI94]
有九个钟被摆放在一个3X3的矩阵中,它们各自指向12:
00,9:
00,6:
00,3:
00中的一种,你的目的是将它们的指针全部调向12:
00。
很遗憾,每一次调整你都只能从九种调整方案中选择一种执行(九种方案已被从1到9编号),每一种方案可以改变固定钟的状态(例如:
方案1控制钟1,2,3,方案2控制钟1,4,7,方案3控制钟5,6,8,9……),即将方案指定的所有钟向前拨快3小时(使时针向顺时针方向旋转90度),请你输出一个数列,使得按该数列表示方案执行后,所有钟都指向12:
00。
并且如果把整个序列看作一个数,要求该数最小。
最容易想到的方法是用递归枚举1到9的方案在该步使用。
很可怕,由于递归的层数在此没有限定,所以将用掉9k的时间(k为层数),那可能是相当巨大的。
其实,不用紧张,细心的你一定会发现:
当一个方案执行4次后,就相当于没有执行,又因为题目要求输出最优解,那么任意一个方案都没有必要4次以上执行。
也就是说,只需要枚举每一个方案的4种情况(没执行,执行1,2,3次)就可以了。
他仅有49,约262,072此枚举,我们的计算机1s内就可以算完,应该算是极快了。
类似问题:
挤牛奶[USACO1996初赛]
给出一个挤牛奶的顺序(农夫A在300秒到1000秒时挤牛奶,农夫B从700秒到1200秒),要求输出:
最长的有人挤牛奶的时间。
最长的没人挤牛奶的时间。
完全数牛与完全数牛群[USACO1995决赛]
如果一个数可以由它的某几个约数相加得到,那么我们叫它完全数,如28=1+2+4+7+14。
而一对完全对数就是指两个数都可以由对方的约数相加得出。
同样一个完全数组就是一个数组的第一个数可以由第二个数的约数加和得到,第二个数也可以由第三个数的约数相加得到……最后一个数可以由第一个数的约数加和得到。
现在FarmerJohn已经将它的牛儿们编好了号(1到32000)请找出其中所有的完全数牛与完全数牛群。
贪心算法
样例:
牛棚修理[1999USACO春季公开赛]
FarmerJohn有一列牛棚,在一次暴风中,牛棚的一整面墙都被吹倒了,但还好不是每一间牛棚都有牛。
FarmerJohn决定卖木料来修理牛棚,然而,刻薄的木材提供商却只能提供有限块的木料(木料的长度不限),现在告诉你关着牛的牛棚号,和提供的木材个数N,你的任务是编程求出最小的木块长度和。
(1<=N<=50)
贪心思想:
贪心思想的本质是每次都形成局部最优解,换一种方法说,就是每次都处理出一个最好的方案。
例如:
在样例中,若已经发现N=5时的最优解,那么我们可以直接利用N=5的最优解构成N=4的最优解,而不用去考虑那些N=4时的其他非最优解。
贪心算法的最大特点就是快。
通常,二次方级的存储要浪费额外的空间,而且很不幸,那些空间经常得不出正解。
但是,当使用贪心算法时,这些空间可以帮助算法更容易实现且更快执行。
贪心的难点:
贪心算法有两大难点:
如何贪心:
怎样才能从众多可行解中找到最优解呢?
其实,大部分都是有规律的。
在样例中,贪心就有很明显的规律。
但你得到了N=5时的最优解后,你只需要在已用上的5块木板中寻找最靠近的两块,然后贴上中间的几个牛棚,使两块木板变成一块。
这样生成的N=4的解必定最优。
因为这样木板的浪费最少。
同样,其他的贪心题也会有这样的性质。
正因为贪心有如此性质,它才能比其他算法要快。
贪心的正确性:
要证明贪心性质的正确性,才是贪心算法的真正挑战,因为并不是每次局部最优解都会与整体最优解之间有联系,往往靠贪心生成的解不是最优解。
这样,贪心性质的证明就成了贪心算法正确的关键。
一个你想出的贪心性质也许是错的,即使它在大部分数据中都是可行的,你必须考虑到所有可能出现的特殊情况,并证明你的贪心性质在这些特殊情况中仍然正确。
这样经过千锤百炼的性质才能构成一个正确的贪心。
在样例中,我们的贪心性质是正确的。
如下:
假设我们的答案盖住了较大的空牛棚连续列,而不是较小的。
那么我们把那部分盖空牛棚的木板锯下来,用来把较小的空牛棚连续列盖住,还会有剩余。
那么锯掉它们!
还给木材商!
同时我们的解也变小了。
也就是说,我们获得更优的解。
所以,靠盖住较大空牛棚连续列的方法无法获得最优解,我们也应该尽量贪心那些距离小的木板合并。
如果仍有一个空牛棚连续列与我们的答案盖住的那个相同,我们同样使用上述的方法。
会发现获得的新解与原解相同,那么不论我们选哪个,结果都将一样。
由此可见,如果我们合并的两块木板间距离最短,那么总能获得最优解。
所以,在解题的每一步中,我们都只需要寻找两块距离最小的木板并合并它们。
这样,我们获得的解必定最优。
结论:
如果有贪心性质存在,那么一定要采用!
因为它容易编写,容易调试,速度极快,并且节约空间。
几乎可以说,它是所有算法中最好的。
但是应该注意,别陷入证明不正确贪心性质的泥塘中无法自拔,因为贪心算法的适用范围并不大,而且有一部分极难证明,若是没有把握,最好还是不要冒险,因为还有其他算法会比它要保险。
类似问题:
三值排序问题[IOI1996]
有一个由N个数值均为1、2或3的数构成的序列(N<=1000),其值无序,现要求你用最少的交换次数将序列按升序顺序排列。
算法:
排序后的序列分为三个部分:
排序后应存储1的部分,排序后应存储2的部分和排序后应存储3的部分,贪心排序法应交换尽量多的交换后位置正确的(2,1)、(3,1)和(3,2)数对。
当这些数对交换完毕后,再交换进行两次交换后位置正确的(1,2,3)三个数。
分析:
很明显,每一次交换都可以改变两个数的位置,若经过一次交换以后,两个数的位置都由错误变为了正确,那么它必定最优。
同时我们还可发现,经过两次交换后,我们可以随意改变3个数的位置。
那么如果存在三个数恰好为1,2和3,且位置都是错误的,那么进行两次交换使它们位置正确也必定最优。
有由于该题具有最优子结构性质,我们的贪心算法成立。
货币系统--一个反例[已删节]
奶牛王国刚刚独立,王国中的奶牛们要求设立一个货币系统,使得这个货币系统最好。
现在告诉你一个货币系统所包含的货币面额种类(假设全为硬币)以及所需要找的钱的大小,请给出用该货币系统找出该钱数,并且要求硬币数尽量少。
算法:
每次都选择面额不超过剩余钱数但却最大的一枚硬币。
例如:
有货币系统为{1,2,5,10},要求找出16,那么第一次找出10,第二次找出5,第三次找出1,恰好为最优解。
错误分析:
其实可以发现,这种算法并不是每一次都能构成最优解。
反例如:
货币系统{1,5,8,10},同样找16,贪心的结果是10,5,1三枚,但用两枚8的硬币才是最优解。
因为这样,贪心的性质不成立,如此解题也是错的。
拓扑排序
给你一些物品的集合,然后给你一些这些物品的摆放顺序的约束,如"物品A应摆放在物品B前",请给出一个这些物品的摆放方案,使得所有约束都可以得到满足。
算法:
对于给定的物品创建一个有向图,A到B的弧表示"物品A应摆放在物品B前”。
以任意顺序对每个物品进行遍历。
每当你找到一个物品,他的入度为0,那么贪心地将它放到当前序列的末尾,删除它所有的出弧,然后对它的出弧指向的所有结点进行递归,用同样的算法。
如果这个算法遍历了所有的物品,但却没有把所有的物品排序,那就意味着没有满足条件的解。
SearchTechniques搜索方式
样例:
n皇后问题[经典问题]
将n个皇后摆放在一个nxn的棋盘上,使得每一个皇后都无法攻击到其他皇后。
深度优先搜索(DFS)
显而易见,最直接的方法就是把皇后一个一个地摆放在棋盘上的合法位置上,枚举所有可能寻找可行解。
可以发现在棋盘上的每一行(或列)都存在且仅存在一个皇后,所以,在递归的每一步中,只需要在当前行(或列)中寻找合法格,选择其中一个格摆放一个皇后。
1search(col)
2 iffilledallcolumns
3 printsolutionandexit
4 foreachrow
5 ifboard(row,col)isnotattacked
6 placequeenat(row,col)
7 search(col+1)
8 removequeenat(row,col)
从search(0)开始搜索,由于在每一步中可选择的节点较少,该方法可以较快地求解:
当一定数量的皇后被摆放在棋盘上后那些不会被攻击到的节点的数量将迅速减少。
这是深度优先搜索的一个经典例题,该算法总是能尽可能快地抵达搜索树的底层:
当k个皇后被摆放到棋盘上时,可以马上确定如何在棋盘上摆放下一个皇后,而不需要去考虑其他的顺序摆放皇后可能造成的影响(如当前情况是否为最优情况),该方法有时可以在找到可行解之前避免复杂的计算,这是十分值得的。
深度优先搜索具有一些特性,考虑下图的搜索树:
该算法用逐步加层的方法搜索并且适当时回溯,在每一个已被访问过的节点上标号,以便下次回溯时不会再次被搜索。
绘画般地,搜索树将以如下顺序被遍历:
复杂度:
假设搜索树有d层(在样例中d=n,即棋盘的列数)。
再假设每一个节点都有c个子节点(在样例中,同样c=n,即棋盘的行数,但最后一层没有子节点,除外)。
那么整个搜索花去的时间将与cd成正比,是指数级的。
但是其需要的空间较小,除了搜索树以外,仅需要用一个栈存储当前路径所经过的节点,其空间复杂度为O(d)。
样例:
骑士覆盖问题[经典问题]
在一个nxn的棋盘中摆放尽量少的骑士,使得棋盘的每一格都会被至少一个骑士攻击到。
但骑士无法攻击到它自己站的位置.
广度优先搜索(BFS)
在这里,最好的方法莫过于先确定k个骑士能否实现后再尝试k+1个骑士,这就叫广度优先搜索。
通常,广度优先搜索需用队列来帮助实现。
1process(state)
2 foreachpossiblenextstatefromthisone
3 enqueuenextstate
4search()
5 enqueueinitialstate
6 while!
empty(queue)
7 state=getstatefromqueue
8 process(state)
广度优先搜索得名于它的实现方式:
每次都先将搜索树某一层的所有节点全部访问完毕后再访问下一层,再利用先前的那颗搜索树,广度优先搜索以如下顺序遍历:
首先访问根节点,而后是搜索树第一层的所有节点,之后第二层、第三层……以此类推。
复杂度:
广度优先搜索所需的空间与深度优先搜索所需的不同(n皇后问题的空间复杂度为O(n)),广度优先搜索的空间复杂取决于每层的节点数。
如果搜索树有k层,每个节点有c个子节点,那么最后将可能有ck个数据被存入队列,这个复杂度无疑是巨大的。
所以在使用广度优先搜索时,应小心处理空间问题。
迭代加深搜索(ID)
广度优先搜索可以用迭代加深搜索代替。
迭代加深搜索实质是限定下界的深度优先搜索,即首先允许深度优先搜索搜索k层搜索树,若没有发现可行解,再将k+1后再进行一次以上步骤,直到搜索到可行解。
这个“模仿广度优先搜索”搜索法比起广搜是牺牲了时间,但节约了空间。
1truncated_dfsearch(hnextpos,depth)
2 ifboardiscovered
3 printsolutionandexit
4 ifdepth==0
5 return
6 forifromnextposton*n
7 putknightati
8 truncated_dfsearch(i+1,depth-1)
9 removeknightati
10dfid_search
11 fordepth=0tomax_depth
12 truncated_dfsearch(0,depth)
复杂度:
ID时间复杂度与DFS的时间复杂度(O(n))不同,另一方面,它要更复杂,某次DFS若限界k层,则耗时ck。
若搜索树共有d层,则一个完整的DFS-ID将耗时c0+c1+c2+...+cd。
如果c=2,那么式子的和是cd+1-1,大约是同效BFS的两倍。
当c>2时(子节点的数目大于2),差距将变小:
ID的时间消耗不可能大于同效BFS的两倍。
所以,但数据较大时,ID-DFS并不比BFS慢,但是空间复杂度却与DFS相同,比BFS小得多。
算法选择:
当你已经知道某题是一道搜索题,那么选择正确的搜索方式是十分重要的。
下面给你一些选择的依据。
简表:
搜索方式时间空间使用情况
DFSO(ck)O(k)必须遍历整棵树,要求出解的深度或经的过节点,或者你并不需要解的深度最小。
BFSO(cd)O(cd)了解到解十分靠近根节点,或者你需要解的深度最小。
DFS+IDO(cd)O(d)需要做BFS,但没有足够的空间,时间却很充裕。
d:
解的深度
k:
搜索树的深度
d<=k
记住每一种搜索法的优势。
如果要求求出最接近根节点的解,则使用BFD或ID。
而如果是其他的情况,DFS是一种很好的搜索方式。
如果没有足够的时间搜出所有解。
那么使用的方法应最容易搜出可行解,如果答案可能离根节点较近,那么就应该用BFS或ID,相反,如果答案离根节点较远,那么使用DFS较好。
还有,请仔细小心空间的限制。
如果空间不足以让你使用BFS,那么请使用迭代加深吧!
类似问题:
超级质数肋骨[USACO1994决赛]
一个数,如果它从右到左的一位、两位直到N位(N是)所构成的数都是质数,那么它就称为超级质数。
例如:
233、23、2都是质数,所以233是超级质数。
要求:
读入一个数N(N<=9),编程输出所有有N位的超级质数。
这题应使用DFS,因为每一个答案都有N层(最底层),所以DFS是最好的.
Betsy的旅行[USACO1995资格赛]
一个正方形的小镇被分成NxN(2<=N<=6)个小方格,Besty要从左上角的方格到达左下角的方格,并且经过每一次方格都恰好经过一次。
编程对于给定的N计算出Besty能采用的所有的旅行路线的数目。
这题要求求出解的数量,所以整颗搜索树都必须被遍历,这就与可行解的位置与出解速度没有关系了。
所以这题可以使用BFS或DFS,又因为DFS需要的空间较少,所以DFS是较好的.
奶牛运输[USACO1995决赛]
奶牛运输公司拥有一辆运输卡车与牧场A,运输公司的任务是在A,B,C,D,E,F和G七个农场之间运输奶牛。
每两个农场之间的路程(可以用floyed改变)已给出。
每天早晨,运输公司都必须确定一条运输路线,使得运输的总距离最短。
但必须遵守以下规则:
农场A是公司的基地。
每天的运输都必须从这开始并且在这结束。
卡车任何时刻都只能最多承载一头奶牛。
给出的数据是奶牛的原先位置与运输结束后奶牛所在的位置。
而你的任务是在上述规则内寻找最短的运输路线。
在发现最优解时必须比较所有可行解,所以必须遍历整棵搜索树。
所以,可以用DFS解题.
横越沙漠[1992IOI]
一群沙漠探险者正尝试着让他们中的一部分人横渡沙漠。
每一个探险者可以携带一定数量的水,同时他们每天也要喝掉一定量的水。
已知每个探险者可携带的水量与每天需要的水量都不同。
给出每个探险者能携带的水量与需要的水量与横渡沙漠所需的天数,请编程求出最多能有几个人能横渡沙漠。
所有探险者都必须存活,所以有些探险者在中途必须返回,返回时也必须携带足够的水。
当然,如果一个探险者返回时有剩余的水(除去返回所需的水以外),他可以把剩余的水送给他的一个同伴,如果它的同伴可以携带的话。
这题可以分成两个小问题,一个是如何为探险者分组,另一个是某些探险者应在何处返回。
所以使用ID-DFS是可行的。
首先尝试每一个探险者能否独自横渡,然后是两个人配合,三个人配合。
直到结束。
GraphTheory
图论知识
何为图?
正式地说,图G是由:
所有结点的集合V
所有边]的集合E
构成的,简写成G(V,E)。
我们可以把结点假设成一个“地点”,而结点集合就是一个所有地点的集合。
同样,边可以被假设成连接两个“地点”的一条“路”;那么边的集合就可以认为是所有这样的“路”的集合。
表示方法:
图通常用如下方法表示;结点是点或者圈,而边是直线或者曲线。
在上图中,结点V={1,2,3,4,5,6},边E={(1,3),(1,6),(2,5),(3,4),(3,6)}.
每一个结点都是集合V中的一个数字,每条边都是集合E中的成员,注意并不是每个节点都有边与其他结点相连,这样没有边与其他结点相连的结点被称作孤立结点。
有时,边会与一些数值关联,类似表示边的长度或花费。
我们把这些数字称作边的权
这样边有权的图被称为边权图。
类似的,我们还定义了点权图,即每个结点都有一个权。
图论的几个例子
奶牛的电信(USACO锦标赛1996)
农夫约翰的奶牛们喜欢通过电邮保持联系,于是她们建立了一个奶牛电脑网络,以便互相交流。
这些机器用如下的方式发送电邮:
如果存在一个由c台电脑组成的序列a1,a2,...,a(c),且a1与a2相连,a2与a3相连,等等,那么电脑a1和a(c)就可以互发电邮。
很不幸,有时候奶牛会不小心踩到电脑上,农夫约翰的车也可能碾过电脑,这台倒霉的电脑就会坏掉。
这意味着这台电脑不能再发送电邮了,于是与这台电脑相关的连接也就不可用了。
有两头奶牛就想:
如果我们两个不能互发电邮,至少需要坏掉多少台电脑呢?
请编写一个程序为她们计算这个最小值和与之对应的坏掉的电脑集合。
图:
每个结点表示一台电脑,而边就相对应的成了连接各台电脑的缆线。
骑马修栅栏
农民John每年有很多栅栏要修理。
他总是骑着马穿过每一个栅栏并修复它破损的地方。
John是一个与其他农民一样懒的人。
他讨厌骑马,因此从来不两次经过一个一个栅栏。
你必须编一个程序,读入栅栏网络的描述,并计算出一条修栅栏的路径,使每个栅栏都恰好被经过一次。
John能从任何一个顶点(即两个栅栏的交点)开始骑马,在任意一个顶点结束。
每一个栅栏连接两个顶点,顶点用1到500标号(虽然有的农场并没有500个顶点)。
一个顶点上可连接任意多(>=1)个栅栏。
所有栅栏都是连通的(也就是你可以从任意一个栅栏到达另外的所有栅栏)。
你的程序必须输出骑马的路径(用路上依次经过的顶点号码表示)。
我们如果把输出的路径看成是一个500进制的数,那么当存在多组解的情况下,输出500进制表示法中最小的一个(也就是输出第一个数较小的,如果还有多组解,输出第二个数较小的,等等)。
输入数据保证至少有一个解。
图:
农民John从一个栅栏交叉点开始,经过所有栅栏一次。
因而,图的结点就是栅栏交叉点,边就是栅栏。
骑士覆盖问题
在一个nxn的棋盘中摆放尽量少的骑士,使得棋盘的每一格都会被至少一个骑士攻击到。
但骑士无法攻击到它自己站的位置.
图:
这里的图较为特殊,棋盘的每一格都是一个结点,如果骑士能从这一格跳到另一格,这就说在这两个格相对应的结点之间有一条边。
穿越栅栏[1999USACO春季公开赛]
农夫
John在外面的田野上搭建了一个巨大的用栅栏围成的迷宫。
幸运的是,他在迷宫的边界上留出了两段栅栏作为迷宫的出口。
更幸运的是,他所建造的迷宫是一个
完美的迷宫:
即你能从迷宫中的任意一点找到一条走出迷宫的路。
这是一个W=5,H=3的迷宫:
+-+-+-+-+-+
| |
+-++-+++
| |||
++-+-+++
|| |
+-++-+-+-+
如上图的例子,栅栏的柱子只出现在奇数行或奇数列。
每个迷宫只有两个出口。
图:
如上图,图的每一个位置都是一个结点,如果两个相邻的位置之间没有被栅栏分开,则说在这两个位置相对应的结点之间有一条边。
用语:
我们再看刚才的图:
如果有一条边起点与终点都是同一个结点,我们就称它为环边,表示为(v,v),上图中没有环边。
简单图是指一张没有环边且边在边集E中不重复出现的图。
与简单图相对的是复杂图。
在我们的讨论中不涉及复杂图,所有的图都是简单图