Matrix67's profileMatrix67's SpaceBlogLists Tools Help

Blog


    December 15

    给大家推荐一些书

        近来有很多问该看什么书的。我说一下个人意见(仅代表个人意见)。大家可以按照这样的顺序来阅读这些书(时间足够的话):
     
        《算法导论》
        《数据结构与算法分析——C语言描述》
        《组合数学》
     
        这三本书必看,都是机械工业出版社出版的,翻译质量嘛——尽管有些别扭(翻译的东西都这样),但肯定看得懂。
        第一本的中译本是才出版的,比原来那个盗版的要好得多。
        第二本是Mark Allen Weiss写的,第二版。
        第三本是Richard A.Brualdi写的,第四版。
        如果你英文好的话,最好看原版。
        有人会问我为什么喜欢国外的教材。这是因为,国外的教材各个章节安排得很好,体系性更强,看起来更轻松(保证你能看懂),而且更具有启发性。这些教材的习题安排得很好,绝对是可以经过独立思考想出来的题目。和国内很多教材扔出一大堆概念和公式不同,阅读国外教材是循序渐进的一个学习过程。
     
        以下两本书的话,有兴趣就看吧。
        《离散数学》,第六版,Richard Johnsonbaugh,电子工业出版社。
        《How to Ace Calculus: The Streetwise Guide》系列,中译本叫做“微积分之XXXX”,湖南科学技术出版社。当成看小说吧,很有意思,是我见过的最不像教材的教材了。
     
        最后需要看的是刘汝佳和黄亮的《算法艺术与信息学竞赛》。这里面有很多概念上的讲解是错误的,但是题目讲解的资源很丰富。当前面的书看完了后,拿最后这一本当作题库来实战演练吧。书里的概念讲解部分就不必看了,直接消化里面的例题,一道一道地消化。第三部分的计算几何可以仔细学习一下,因为这部分内容之前的书好像涵盖得不多。
     
        还有,选择什么样的题库。个人首推USACO。大家可以自己了解一下这个与众不同的OJ,它基本上是一个“个人的教练”,并不参与网络排名。你大概需要话半年的时间完成所有的题目。做USACO需要你的认真态度和耐心。千万别看中译和别人的解答。整个USACO的任务完成之后,你基本上就无敌了。
    December 14

    X200果然跑不动Xgl

        这几天没更新是因为我在搞Linux。我的本本硬盘小,当初装系统时没有狠下心;前几天看到了Mandriva 2007的新特性,经不住诱惑,一狠心就把我IBM的隐藏分区格了,我知道这意味着如果我的Win系统坏了的话就没救了(隐藏分区里的可是正版XP啊),不过想到XP坏了就装Vista,也就坦然了。
        说到Vista,不过就是多了个Aero。如果你不用Aero的话,自己滚回XP去。但是,你有见过Linux的Xgl+Compiz吗?这是Mandriva 2007最大的新特性了:集成了3D桌面。如果你还没见过的话,第一次看到这些特效会让你大吃一惊,这个和Aero根本不是一个档次的。最终结果是:我的ATI X200不支持:(  有用X200的同志看到这个帖子以后,你就不必再去试了,我各种方法都试过了,系统都重装了好几次。乖乖地等官方驱动来支持吧。
     
        这几天我已经完全变成夜猫子了,早上6:30睡觉,下午4:30醒,一到晚上精神就来了。幸好附近有一个24小时营业的吃的地方,不然我准被饿死。
        说到24小时,一个月后的今天,呵呵!想起就激动!(有没有跟我一样激动的?)
     
        接下来的安排:写完生成函数的讲解,然后重返OCR,争取做一份过年大礼。
    December 02

    Matrix67的OI点滴(四):König定理的证明

        如果你看不清楚第二个字母,下面有一个大号字体版本:
     
    Matrix67的OI点滴(四):König定理的证明
     
        本文将是这一系列里最短的一篇,因为我只打算把König定理证了,其它的废话一概没有。
        以下五个问题我可能会在以后的文章里说,如果你现在很想知道的话,网上去找找答案:
        1. 什么是二分图;
        2. 什么是二分图的匹配;
        3. 什么是匈牙利算法;(
    http://matrix-67.spaces.live.com/Blog/cns!1p4LfxPH2nEZ7Y2ZGSNt_Llw!201.entry)
        4. König定理证到了有什么用;
        5. 为什么o上面有两个点。
     
        König定理是一个二分图中很重要的定理,它的意思是,一个二分图中的最大匹配数等于这个图中的最小点覆盖数。如果你还不知道什么是最小点覆盖,我也在这里说一下:假如选了一个点就相当于覆盖了以它为端点的所有边,你需要选择最少的点来覆盖所有的边。比如,下面这个图中的最大匹配和最小点覆盖已分别用蓝色和红色标注。它们都等于3。这个定理相信大多数人都知道,但是网络上给出的证明并不多见。有一些网上常见的“证明”明显是错误的。因此,我在这里写一下这个定理的证明,希望对大家有所帮助。
     

        假如我们已经通过匈牙利算法求出了最大匹配(假设它等于M),下面给出的方法可以告诉我们,选哪M个点可以覆盖所有的边。
        匈牙利算法需要我们从右边的某个没有匹配的点,走出一条使得“一条没被匹配、一条已经匹配过,再下一条又没匹配这样交替地出现”的路(交错轨,增广路)。但是,现在我们已经找到了最大匹配,已经不存在这样的路了。换句话说,我们能寻找到很多可能的增广路,但最后都以找不到“终点是还没有匹配过的点”而失败。我们给所有这样的点打上记号:从右边的所有没有匹配过的点出发,按照增广路的“交替出现”的要求可以走到的所有点(最后走出的路径是很多条不完整的增广路)。那么这些点组成了最小覆盖点集:右边所有没有打上记号的点,加上左边已经有记号的点。看图,右图中展示了两条这样的路径,标记了一共6个点(用“√”表示)。那么,用红色圈起来的三个点就是我们的最小覆盖点集。
        首先,为什么这样得到的点集点的个数恰好有M个呢?答案很简单,因为每个点都是某个匹配边的其中一个端点。如果右边的哪个点是没有匹配过的,那么它早就当成起点被标记了;如果左边的哪个点是没有匹配过的,那就走不到它那里去。而一个匹配边又不可能左端点是标记了的,同时右端点是没标记的(不然的话右边的点就可以经过这条边到达了)。因此,最后我们圈起来的点与匹配边一一对应。
        其次,为什么这样得到的点集可以覆盖所有的边呢?答案同样简单。不可能存在某一条边,它的左端点是没有标记的,而右端点是有标记的。原因如下:如果这条边不属于我们的匹配边,那么左端点就可以通过这条边到达(从而得到标记);如果这条边属于我们的匹配边,那么右端点不可能是一条路径的起点,于是它的标记只能是从这条边的左端点过来的(想想匹配的定义),左端点就应该有标记。
        最后,为什么这是最小的点覆盖集呢?这当然是最小的,不可能有比M还小的点覆盖集了,因为要覆盖这M条匹配边至少就需要M个点(再次回到匹配的定义)。
        证完了。
     
     
    Matrix67原创
    做人要厚到 转贴请注明出处
    November 29

    Matrix67的OI点滴(三):KMP算法

        如果机房马上要关门了,或者你急着要和MM约会,请直接跳到第六个自然段。
     
        我们这里说的KMP不是拿来放电影的(虽然我很喜欢这个软件),而是一种算法。KMP算法是拿来处理字符串匹配的。换句话说,给你两个字符串,你需要回答,B串是否是A串的子串(A串是否包含B串)。比如,字符串A="I'm matrix67",字符串B="matrix",我们就说B是A的子串。你可以委婉地问你的MM:“假如你要向你喜欢的人表白的话,我的名字是你的告白语中的子串吗?”
        解决这类问题,通常我们的方法是枚举从A串的什么位置起开始与B匹配,然后验证是否匹配。假如A串长度为n,B串长度为m,那么这种方法的复杂度是O(mn)的。虽然很多时候复杂度达不到mn(验证时只看头一两个字母就发现不匹配了),但我们有许多“最坏情况”,比如,A="aaaaaaaaaaaaaaaaaaaaaaaaaab",B="aaaaaaaab"。我们将介绍的是一种最坏情况下O(n)的算法(这里假设m<=n),即传说中的KMP算法。
        之所以叫做KMP,是因为这个算法是由Knuth、Morris、Pratt三个提出来的,取了这三个人的名字的头一个字母。这时,或许你突然明白了AVL树为什么叫AVL,或者Bellman-Ford为什么中间是一杠不是一个点。有时一个东西有七八个人研究过,那怎么命名呢?通常这个东西干脆就不用人名字命名了,免得发生争议,比如“3x+1问题”。扯远了。
        个人认为KMP是最没有必要讲的东西,因为这个东西网上能找到很多资料。但网上的讲法基本上都涉及到“移动(shift)”、“Next函数”等概念,这非常容易产生误解(至少一年半前我看这些资料学习KMP时就没搞清楚)。在这里,我换一种方法来解释KMP算法。
     
        假如,A="abababaababacb",B="ababacb",我们来看看KMP是怎么工作的。我们用两个指针i和j分别表示,A[i-j+1..i]与B[1..j]完全相等。也就是说,i是不断增加的,随着i的增加j相应地变化,且j满足以A[i]结尾的长度为j的字符串正好匹配B串的前j个字符(j当然越大越好),现在需要检验A[i+1]和B[j+1]的关系。当A[i+1]=B[j+1]时,i和j各加一;什么时候j=m了,我们就说B是A的子串(B串已经整完了),并且可以根据这时的i值算出匹配的位置。当A[i+1]<>B[j+1],KMP的策略是调整j的位置(减小j值)使得A[i-j+1..i]与B[1..j]保持匹配且新的B[j+1]恰好与A[i+1]匹配(从而使得i和j能继续增加)。我们看一看当i=j=5时的情况。
     
        i = 1 2 3 4 5 6 7 8 9 ……
        A = a b a b a b a a b a b
        B = a b a b a c b
        j = 1 2 3 4 5 6 7
     
        此时,A[6]<>B[6]。这表明,此时j不能等于5了,我们要把j改成比它小的值j'。j'可能是多少呢?仔细想一下,我们发现,j'必须要使得B[1..j]中的头j'个字母和末j'个字母完全相等(这样j变成了j'后才能继续保持i和j的性质)。这个j'当然要越大越好。在这里,B[1..5]="ababa",头3个字母和末3个字母都是"aba"。而当新的j为3时,A[6]恰好和B[4]相等。于是,i变成了6,而j则变成了4:
     
        i = 1 2 3 4 5 6 7 8 9 ……
        A = a b a b a b a a b a b
        B =     a b a b a c b
        j =     1 2 3 4 5 6 7
     
        从上面的这个例子,我们可以看到,新的j可以取多少与i无关,只与B串有关。我们完全可以预处理出这样一个数组P[j],表示当匹配到B数组的第j个字母而第j+1个字母不能匹配了时,新的j最大是多少。P[j]应该是所有满足B[1..P[j]]=B[j-P[j]+1..j]的最大值。

        再后来,A[7]=B[5],i和j又各增加1。这时,又出现了A[i+1]<>B[j+1]的情况:
     
        i = 1 2 3 4 5 6 7 8 9 ……
        A = a b a b a b a a b a b
        B =     a b a b a c b
        j =     1 2 3 4 5 6 7
     
        由于P[5]=3,因此新的j=3:
     
        i = 1 2 3 4 5 6 7 8 9 ……
        A = a b a b a b a a b a b
        B =         a b a b a c b
        j =         1 2 3 4 5 6 7
     
        这时,新的j=3仍然不能满足A[i+1]=B[j+1],此时我们再次减小j值,将j再次更新为P[3]:
     
        i = 1 2 3 4 5 6 7 8 9 ……
        A = a b a b a b a a b a b
        B =             a b a b a c b
        j =             1 2 3 4 5 6 7
     
        现在,i还是7,j已经变成1了。而此时A[8]居然仍然不等于B[j+1]。这样,j必须减小到P[1],即0:
     
        i = 1 2 3 4 5 6 7 8 9 ……
        A = a b a b a b a a b a b
        B =               a b a b a c b
        j =             0 1 2 3 4 5 6 7
     
        终于,A[8]=B[1],i变为8,j为1。事实上,有可能j到了0仍然不能满足A[i+1]=B[j+1](比如A[8]="d"时)。因此,准确的说法是,当j=0了时,我们增加i值但忽略j直到出现A[i]=B[1]为止。

        这个过程的代码很短(真的很短),我们在这里给出:
     
    j:=0;
    for i:=1 to n do
    begin
       while (j>0) and (B[j+1]<>A[i]) do j:=P[j];
       if B[j+1]=A[i] then j:=j+1;
       if j=m then
       begin
          writeln('Pattern occurs with shift ',i-m);
          j:=P[j];
       end;
    end;
     
        最后的j:=P[j]是为了让程序继续做下去,因为我们有可能找到多处匹配。
        这个程序或许比想像中的要简单,因为对于i值的不断增加,代码用的是for循环。因此,这个代码可以这样形象地理解:扫描字符串A,并更新可以匹配到B的什么位置。
     
        现在,我们还遗留了两个重要的问题:一,为什么这个程序是线性的;二,如何快速预处理P数组。
        为什么这个程序是O(n)的?其实,主要的争议在于,while循环使得执行次数出现了不确定因素。我们将用到时间复杂度的摊还分析中的主要策略,简单地说就是通过观察某一个变量或函数值的变化来对零散的、杂乱的、不规则的执行次数进行累计。KMP的时间复杂度分析可谓摊还分析的典型。我们从上述程序的j值入手。每一次执行while循环都会使j减小(但不能减成负的),而另外的改变j值的地方只有第五行。每次执行了这一行,j都只能加1;因此,整个过程中j最多加了n个1。于是,j最多只有n次减小的机会(j值减小的次数当然不能超过n,因为j永远是非负整数)。这告诉我们,while循环总共最多执行了n次。按照摊还分析的说法,平摊到每次for循环中后,一次for循环的复杂度为O(1)。整个过程显然是O(n)的。这样的分析对于后面P数组预处理的过程同样有效,同样可以得到预处理过程的复杂度为O(m)。
        预处理不需要按照P的定义写成O(m^2)甚至O(m^3)的。我们可以通过P[1],P[2],...,P[j-1]的值来获得P[j]的值。对于刚才的B="ababacb",假如我们已经求出了P[1],P[2],P[3]和P[4],看看我们应该怎么求出P[5]和P[6]。P[4]=2,那么P[5]显然等于P[4]+1,因为由P[4]可以知道,B[1,2]已经和B[3,4]相等了,现在又有B[3]=B[5],所以P[5]可以由P[4]后面加一个字符得到。P[6]也等于P[5]+1吗?显然不是,因为B[ P[5]+1 ]<>B[6]。那么,我们要考虑“退一步”了。我们考虑P[6]是否有可能由P[5]的情况所包含的子串得到,即是否P[6]=P[ P[5] ]+1。这里想不通的话可以仔细看一下:
     
            1 2 3 4 5 6 7
        B = a b a b a c b
        P = 0 0 1 2 3 ?
     
        P[5]=3是因为B[1..3]和B[3..5]都是"aba";而P[3]=1则告诉我们,B[1]和B[5]都是"a"。既然P[6]不能由P[5]得到,或许可以由P[3]得到(如果B[2]恰好和B[6]相等的话,P[6]就等于P[3]+1了)。显然,P[6]也不能通过P[3]得到,因为B[2]<>B[6]。事实上,这样一直推到P[1]也不行,最后,我们得到,P[6]=0。
        怎么这个预处理过程跟前面的KMP主程序这么像呢?其实,KMP的预处理本身就是一个B串“自我匹配”的过程。它的代码和上面的代码神似:
     
    P[1]:=0;
    j:=0;
    for i:=2 to m do
    begin
       while (j>0) and (P[j+1]<>P[i]) do j:=P[j];
       if P[j+1]=P[i] then j:=j+1;
       P[i]:=j;
    end;
     
        最后补充一点:由于KMP算法只预处理B串,因此这种算法很适合这样的问题:给定一个B串和一群不同的A串,问B是哪些A串的子串。
     
        串匹配是一个很有研究价值的问题。事实上,我们还有后缀树,自动机等很多方法,这些算法都巧妙地运用了预处理,从而可以在线性的时间里解决字符串的匹配。我们以后来说。
     
        昨天发现一个特别晕的事,知道怎么去掉BitComet的广告吗?把界面语言设成英文就行了。
        还有,金山词霸和Dr.eye都可以去自杀了,Babylon素王道。
     
    Matrix67原创
    转贴请注明出处
    November 22

    这几天过得有些颓废

        小时候家离交行最近,以我妈的名义办了一张太平洋卡,后来卡长期不用,搞丢了。前段时间没事干了的时候,拿自己的身份证去搞了一张,当天晚上就后悔了:太平洋卡在重庆还不支持网上支付。今天我的IBM赠送的Norton已经过期了半个月了,我不得不为了网上续订升级服务,换家银行再办一张卡。网上查了一下,阳光卡看起来最阳光,就到光大去搞了张卡。回家整了N久,装了N多光大的IE插件,得到的结论是:(在我看来)光大的貌似也不支持网上支付,虽然银联网上支付系统声称它支持。最后,我不得不放弃了,又颓废了一次:搞了NAV 2005续订日期延长破解补丁,省了80块钱。破解得貌似不是很好,号称能延长150年但我这里只延长到了今年年底。明天把阳光销了再去试试工商行的。网上支付还是有用的,除了Norton的续订号,有些东西你也不得不在网上买,比如跳蛋或者充气娃娃。

        每年的这个时候过得都有点阴暗。前几天MSN上收到了半打人的告别,有几只牛(水牛也算牛)也打算回归高考了。感觉很悲哀,自己也会莫名地伤感起来。OI的变数太大了,我本以为很有希望的人都考栽了,倒是平时不太注意的人突然来了个RP++。最后仍然是几家欢乐几家愁,只是谁乐谁愁很出乎意料。论坛上发愁的比较多,乐的都偷着乐去了。昨天晚上和MM吵架,还好是用短信吵的,内容(或者说起因)大概也和NOIp有关。后来我怕她了,让。男的和女的吵架时男的要让着点,毕竟女的有一些麻烦事,比如每个月心情都会周期性地烦躁一次。

        再说点别的。那天从CSC出来,听到一首很好听的歌,一听就喜欢上了。回去一查,韩雪,竹林风。我下了韩雪这个人(名字不错)的第一张专辑,真的很好听。很喜欢《飘雪》和《想起》。CSC还喜欢放《爱我就自首》,也有点意思。CSC很有商业头脑,每个学校旁都立一个,就是冲着学生来的,放学时间赚得盆满钵满。十周年了,难得啊,我初恋才四周年呢(不算小学不懂事乱来的那种)。现在生活很没规律(或者说很有规律),下午3:00起床(大家上午想找我的就不用费心了),去CSC吃饭,顺便外面逛逛,晚上总有美剧可以看,然后当夜猫子搞到个凌晨四五点。下午的时候去CSC很有情调,那时为了节电要关灯,往里面走很幽暗。经常会看到有人阴在角落里接吻的。

        明天开始,不浪费时间了,写点关于OI的东西,或者继续搞电子书,或者泡MM。有点出去打工的欲望。
    November 16

    祝福所有人

    这段时间相当忙,因此没有更新我的Space
    但从统计上来看,仍然有相当数量的人在访问
    在这里感谢你们的支持,祝你们NOIp 2006成功
    等NOIp过去了,这里将继续更新
    October 18

    最长公共上升子序列的另一个O(mn)的算法

        我在这个帖子里说过nlogn求最长上升子序列的方法:
        http://www.oibh.org/bbs/viewthread.php?tid=10682
        下面引用我自己的发言:
        f表示长度为i的上升子序列最后一个数最小是多少。显然数组f是单增的。
        读到一个新的数x后,找到某个i使得x>f[i]且x<=f[i+1],于是用x去更新f[i+1];特别地,如果所有的f[i]都小于x,则增加f的长度。
        最后看f数组有多长就行了。
        由于f单增,所以查找i时可以用二分查找,因此时间复杂度为O(nlogn)。
        举个例子,假如序列为 3 2 8 6 7 4 5 7 3,则f数组的变化过程如下:
        3
        2
        2 8
        2 6
        2 6 7
        2 4 7
        2 4 5
        2 4 5 7
        2 3 5 7
        最后,f的长度达到4,因此答案为4。
        注意,最后的f数组不一定是最长上升子序列的一个方案。
        这里要说的这个算法利用了nlogn的最长上升子序列(LIS)的技巧:用f[k]表示长度为k的上升子序列最后一个数最小是多少。
        在最长公共上升子序列中,令f[i,j][k]表示A串前i个数字,B串前j个数字,长度为k的公共上升子序列中,最后一个数最小是多少。
     
        当A[i]=B[j]时,像nlogn的最长上升子序列一样把A[i]插入到f[i-1,j]中,这需要线性的时间扫一遍f[i,j];
        当A[i]<>B[j]时,我们需要合并f[i-1,j]和f[i,j-1],使得对于每个k满足f[i,j][k]:=min{ f[i-1,j][k],f[i,j-1][k] }。这需要线性的时间扫一边f[i-1,j]和f[i,j-1]并取k相同时的较小值。
        最后输出f[n,m]的长度(使f[n,m][k]有意义的最大的k)。
        这样的复杂度是三方的,我们需要优化。
     
        考虑A[i]=B[j]的情况。当i固定时,随着j的增加,插入的位置一定也在后移,因为同样是插入的A[i],但j的增加(B串长度的增加)使得f[i,j]更优,因此可以更新的值就更靠后。于是,对于每个i,我们可以按照k的顺序扫描f[i-1,j][k] 并在A[i]可以插入f[i-1][j]的k位置时增加j,从而预处理所有A[i]=B[j]时A[i]应该插入的位置。
        再考虑A[i]<>B[j]的情况。从定义看,f[i-1,j-1]和f[i-1,j]只有一个地方不一样,因为多一个数最多只能造成一个k的值变小;同样地,f[i-1,j-1]和f[i,j-1]也只有一个地方不一样。因此,f[i-1,j]和f[i,j-1]最多只有两个k所对应的值不相同,且当有两个不同的值时,总是f[i-1,j]中的某个值较小,f[i,j-1]中的某个值较小。这给我们优化的余地。在每次处理完f[i,j]时,我们可以记录一个值x[i,j]表示f[i,j][k]与f[i-1,j][k]中值不一样的k是多少,在A[i]=B[j]时直接赋值为插入的位置,在A[i]<>B[j]时待后文说明。以后合并时,先让f[i,j]:=f[i-1,j](由于此时的f[i-1,j]已经没有别的用处了,因此可以用滚动数组记录,直接令f[i-1,j]是f[i,j],避免实际的赋值操作),然后将新的f[i,j]中的,使f[i,j-1][k]比f[i-1, j][k]小的k所对应值更新。这个k是多少呢?显然应该是x[i,j-1]。这样的操作同时可以确定x[i,j]=x[i,j-1]。
        这样,复杂度就达到了平方。
     
        附参考的资料(原来从这篇论文里学到的,不知道有没有此类的中文资料,估计没有才在这里写了一个,感兴趣的话可以下载附件仔细研究)
        
    http://matrix67.51.net/2005_IPL_LCIS.pdf
     
    Matrix67原创
    转载请注明出处
    October 07

    无题

        终于感到做一件大事很难。搞几次模拟赛还不算大事,比起搞模拟赛的举办平台差远了。此时我终于理解了VVS当初的难处。还记得Vijos才发展起来时,各种评论的声音都有,可想当时VVS发展这个OJ的艰难。最后Vijos走到了今天,经历的是近一年的风风雨雨。
        这次模拟赛的争议相当大。首先让我不满的是Vijos上出现了许多赛中讨论的帖子,答案都快讨论出来了。再后来出现了一个很XX的帖子,回了20多个;帖子本来是冲着我来的,结果后来基本上和Super Master干起来了。有时想一想,这也算是Vijos文化的一部分吧,毕竟Vijos也是从混乱中走过来的。不同的网络交流平台有不同的文化,比如Vijos的讨论就明显与论坛不一样,而且这里面也产生了相当多的专用词汇和“内部笑话”。现在Vijos已经比较稳定了,该是维护纪律、创造特色文化的时候了。而这要比以往任何一项技术性操作复杂得多。
        令人欣慰的是OIBH上大多数人反应并没有参加讨论。有一个第二题随机化过4个点的人感动得我痛哭流涕。在OIBH上听到的支持声更多。
     
        这次模拟赛的题的确很猥琐。
        交题时VVS就拿了一段第一题的样例说这个很猥琐。幸兄预言情书这玩意儿将在OIBH掀起一次十分败坏人品的讨论。这次模拟赛的主要争议都在第一题。
        第二题这样的估计以前也从来没有过。很多人敢想不敢做,很多人想都不敢想。Vijos上有人建议给出一个不同排法导致不同结果的例子,某人回帖说难道都是一样,一人再回说要真是这样就好了。
        第三题完全是原创,一点参考都没有(最多算一个上次xg的第一题)。编这个题目时想了很久,题目叙述改了很多次。
        许多人反应第四题在某某地方出现过。这是很正常的,因为这是一个经典问题。不过我还是要算原创(dd_engi说我厚颜~~),至少数据是我自己出的,网名和手机号都编进去了。这个游戏确实很好玩,我的Palm还在时上课就玩这个,里面有500个Puzzle,好像做到接近200时Palm就崩了。我评讲这个题时为了说明题意并且演示题解里给出的搜索优化方法,投影到大屏幕上玩了一会儿;满以为大家都会去做做这个被我设成难度5的题,谁知后来居然全开始玩这个游戏了。
     
        搞个模拟赛确实不简单啊,自己校对了十几次。估计我是属于那种完美主义者了吧,不能容忍任何一点小错误。完美主义者并不一定是好事。
        有人问我NOIp前还搞不。我想应该还会吧,至少还有一次。
     
        Lost第三季来了,本来要和尚猫一起看的,结果小猫不要我了。看我在你的Space上发你在KFC的照片。
        24第六季的预告片
    http://www.24trailer.com/,倒计时还有17天才能看。这个新花样还玩得不错,让期待中的人有了更迫切的盼头。明年1月14和15,4小时的Premiere。等啊等啊等。
    September 29

    IOCCC近几年的获奖作品

        想起在网上找找这个是因为lakeblur给我发过这样一个C代码:
     
    #include <stdio.h>
    main(t,_,a)
    char *a;
    {
    return!0<t?t<3?main(-79,-13,a+main(-87,1-_,main(-86,0,a+1)+a)):
    1,t<_?main(t+1,_,a):3,main(-94,-27+t,a)&&t==2?_<13?
    main(2,_+1,"%s %d %d\n"):9:16:t<0?t<-72?main(_,t,
    "@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#\
    ;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l \
    q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# \
    ){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw' \
    iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c \
    ;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')#\
    }'+}##(!!/")
      :t<-50?_==*a?putchar(31[a]):main(-65,_,a+1):main((*a=='/')+t,_,a+1)
        :0<t?main(2,2,"%s"):*a=='/'||main(0,main(-61,*a,
    "!ek;dc
    i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry"),a+1);
    }

     
        程序编译运行后不可思议地打印出一长段超过源代码长度的文字,而这些字串竟然根本没有在源代码中出现过。我知道C代码可以写得很怪,而且看这个程序估计还用了不少递归;但从没有想过还有如此荒唐的源代码,看上去基本上就是乱码。刚才我搜索到,这段代码是IOCCC的一个获奖作品。
     
        IOCCC即International Obfuscated C Code Contest,比谁的C代码写得最乱最读不懂。
        这个比赛已经举办了17年了,下面是近几年的一些获奖作品。
        你可以在http://www.au.ioccc.org/years.html看到更多,但很多需要在Linux环境下编译运行。比较有趣的又能够在windows环境下运行都已经在下面了。
        我们假设你编译后的文件名都是abc.exe。
     

    编译后在dos下输入
    abc "ash nazg durhbatuluhk, ash nazg gimbatul, ash nazg thrakatuluhk, agh burzhumh-ishi krimpatul." >abc.pgm
    然后用图片编辑器查看abc.pgm
     
                                      #include\
                                      <stdio.h>
                         #include                <stdlib.h>
                         #include                <string.h>
                        #define w "Hk~HdA=Jk|Jk~LSyL[{M[wMcxNksNss:"
                       #define r"
    Ht@H|@=HdJHtJHdYHtY:HtFHtF=JDBIl"\
                      "DJTEJDFIlMIlM:HdMHdM=I|KIlMJTOJDOIlWITY:8Y"
                     #define S"
    IT@I\\@=HdHHtGH|KILJJDIJDH:H|KID"\
                    "K=HdQHtPH|TIDRJDRJDQ:JC?JK?=JDRJLRI|UItU:8T"
                   #define _(i,j)L[i=2*T[j,O[i=O[j-R[j,T[i=2*\
                  R[j-5*T[j+4*O[j-L[j,R[i=3*T[j-R[j-3*O[j+L[j,
                 #define t"IS?I\\@=HdGHtGIDJILIJDIItHJTFJDF:8J"
        #define y                  yy(4),yy(5),                yy(6),yy(7)
      #define yy(              i)R[i]=T[i],T[i ]            =O[i],O[i]=L [i]
    #define Y _(0          ], 4] )_ (1 ], 5] )_ (2      ], 6] )_ (3 ], 7] )_=1
    #define v(i)(      (( R[ i ] * _ + T [ i ]) * _ + O [ i ]) * _ + L [ i ]) *2
    double b = 32  ,l ,k ,o ,B ,_ ; int Q , s , V , R [8 ], T[ 8] ,O [8 ], L[ 8] ;
    #define q( Q,R ) R= *X ++ % 64 *8 ,R |= *X /8 &7 ,Q=*X++%8,Q=Q*64+*X++%64-256,
    # define  p      "G\\QG\\P=GLPGTPGdMGdNGtOGlOG"   "dSGdRGDPGLPG\\LG\\LHtGHtH:"
    #  define W         "Hs?H{?=HdGH|FI\\II\\GJlHJ"    "lFL\\DLTCMlAM\\@Ns}Nk|:8G"
    # define   U           "EDGEDH=EtCElDH{~H|AJk}"       "Jk?LSzL[|M[wMcxNksNst:"
    #  define u                  "
    Hs?H|@=HdFHtEI"             "\\HI\\FJLHJTD:8H"
    char  *   x                   ,*X , ( * i )[               640],z[3]="4_",
    *Z = "4,8O4.8O4G" r U "4M"u S"4R"u t"
    4S8CHdDH|E=HtAIDAIt@IlAJTCJDCIlKI\\K:8K"U
     "4TDdWDdW=D\\UD\\VF\\FFdHGtCGtEIDBIDDIlBIdDJT@JLC:8D"t"4UGDNG\\L=GDJGLKHL\
    FHLGHtEHtE:"p"4ZFDTFLT=G|EGlHITBH|DIlDIdE:HtMH|M=JDBJLDKLAKDALDFKtFKdMK\
    \\LJTOJ\\NJTMJTM:8M4aGtFGlG=G|HG|H:G\\IG\\J=G|IG|I:GdKGlL=G|JG|J:4b"W
    S"4d"W t t"4g"r w"4iGlIGlK=G|JG|J:4kHl@Ht@=HdDHtCHdPH|P:HdDHdD=It\
    BIlDJTEJDFIdNI\\N:8N"w"
    4lID@IL@=HlIH|FHlPH|NHt^H|^:H|MH|N=J\\D\
    J\\GK\\OKTOKDXJtXItZI|YIlWI|V:8^4mHLGH\\G=HLVH\\V:4n" u t t
    "4p"W"
    IT@I\\@=HdHHtGIDKILIJLGJLG:JK?JK?=JDGJLGI|MJDL:8M4\
    rHt@H|@=HtDH|BJdLJTH:ITEI\\E=ILPILNNtCNlB:8N4t"W t"4u"
    p"4zI[?Il@=HlHH|HIDLILIJDII|HKDAJ|A:JtCJtC=JdLJtJL\
    THLdFNk|Nc|\
    :8K"; main (
    int C,char**        A) {for(x=A[1],i=calloc(strlen(x)+2,163840);
    C-1;C<3?Q=_=       0,(z[1]=*x++)?((*x++==104?z[1]^=32:--x), X =
    strstr(Z,z))      &&(X+=C++):(printf("P2 %d 320 4 ",V=b/2+32),
    V*=2,s=Q=0,C     =4):C<4?Q-->0?i[(int)((l+=o)+b)][(int)(k+=B)
    ]=1:_?_-=.5/    256,o=(v(2)-(l=v(0)))/(Q=16),B=(v(3)-(k=v(1)
    ))/Q:*X>60?y   ,q(L[4],L[5])q(L[6],L[7])*X-61||(++X,y,y,y),
    Y:*X>57?++X,  y,Y:*X >54?++X,b+=*X++%64*4:--C:printf("%d "
    ,i[Q][s]+i[Q ][s+1]+i[Q+1][s]+i[Q+1][s+1])&&(Q+=2)<V||(Q=
    0,s+=2)<640
    ||(C=1));}
     

    编译后在dos下输入abs > ioccc_ray.ppm,生成一个图片(等得可能有点久)
     
    X=1024; Y=768; A=3;
    J=0;K=-10;L=-7;M=1296;N=36;O=255;P=9;_=1<<15;E;S;C;D;F(b){E="1""111886:6:??AAF"
    "
    FHHMMOO55557799@@>>>BBBGGIIKK"[b]-64;C="C@=::C@@==@=:C@=:C@=:C5""31/513/5131/"
    "31/531/53"[b ]-64;S=b<22?9:0;D=2;}I(x,Y,X){Y?(X^=Y,X*X>x?(X^=Y):0,  I (x,Y/2,X
    )):(E=X);      }H(x){I(x,    _,0);}p;q(        c,x,y,z,k,l,m,a,          b){F(c
    );x-=E*M     ;y-=S*M           ;z-=C*M         ;b=x*       x/M+         y*y/M+z
    *z/M-D*D    *M;a=-x              *k/M     -y*l/M-z        *m/M;    p=((b=a*a/M-
    b)>=0?(I    (b*M,_      ,0),b    =E,      a+(a>b      ?-b:b)):     -1.0);}Z;W;o
    (c,x,y,     z,k,l,    m,a){Z=!    c?      -1:Z;c     <44?(q(c,x         ,y,z,k,
    l,m,0,0     ),(p>      0&&c!=     a&&        (p<W         ||Z<0)          )?(W=
    p,Z=c):     0,o(c+         1,    x,y,z,        k,l,          m,a)):0     ;}Q;T;
    U;u;v;w    ;n(e,f,g,            h,i,j,d,a,    b,V){o(0      ,e,f,g,h,i,j,a);d>0
    &&Z>=0? (e+=h*W/M,f+=i*W/M,g+=j*W/M,F(Z),u=e-E*M,v=f-S*M,w=g-C*M,b=(-2*u-2*v+w)
    /3,H(u*u+v*v+w*w),b/=D,b*=b,b*=200,b/=(M*M),V=Z,E!=0?(u=-u*M/E,v=-v*M/E,w=-w*M/
    E):0,E=(h*u+i*v+j*w)/M,h-=u*E/(M/2),i-=v*E/(M/2),j-=w*E/(M/2),n(e,f,g,h,i,j,d-1
    ,Z,0,0),Q/=2,T/=2,       U/=2,V=V<22?7:  (V<30?1:(V<38?2:(V<44?4:(V==44?6:3))))
    ,Q+=V&1?b:0,T                +=V&2?b        :0,U+=V    &4?b:0)     :(d==P?(g+=2
    ,j=g>0?g/8:g/     20):0,j    >0?(U=     j    *j/M,Q      =255-    250*U/M,T=255
    -150*U/M,U=255    -100    *U/M):(U    =j*j     /M,U<M           /5?(Q=255-210*U
    /M,T=255-435*U           /M,U=255    -720*      U/M):(U       -=M/5,Q=213-110*U
    /M,T=168-113*U    /       M,U=111               -85*U/M)      ),d!=P?(Q/=2,T/=2
    ,U/=2):0);Q=Q<    0?0:      Q>O?     O:          Q;T=T<0?    0:T>O?O:T;U=U<0?0:
    U>O?O:U;}R;G;B    ;t(x,y     ,a,    b){n(M*J+M    *40*(A*x   +a)/X/A-M*20,M*K,M
    *L-M*30*(A*y+b)/Y/A+M*15,0,M,0,P,  -1,0,0);R+=Q    ;G+=T;B   +=U;++a<A?t(x,y,a,
    b):(++b<A?t(x,y,0,b):0);}r(x,y){R=G=B=0;t(x,y,0,0);x<X?(printf("%c%c%c",R/A/A,G
    /A/A,B/A/A),r(x+1,y)):0;}s(y){r(0,--y?s(y),y:y);}main(){printf("P6\n%i %i\n255"
    "\n",X,Y);s(Y);}
     

    编译后输入abc 0 0 1可以画出x^2的函数图像,输入abc -1 0 0 1可以画出x^3-1的图像。你也可以试试其它的。

    #include <stdio.h>
    #include <stdlib.h>
    #include <math.h>
    #define _   ;double
    #define void   x,x
    #define case(break,default) break[O]:default[O]:
    #define switch(bool)   ;for(;x<bool;
    #define do(if,else)  inIine(else)>int##if?
    #define true   (--void++)
    #define false   (++void--)
    char*O=" <60>!?\\\n"_ doubIe[010]_ int0,int1 _ Iong=0 _ inIine(int eIse){int
    O1O=!O _ l=!O;for(;O1O<010;++O1O)l+=(O1O[doubIe]*pow(eIse,O1O));return l;}int
    main(int booI,char*eIse[]){int I=1,x=-*O;if(eIse){for(;I<010+1;I++)I[doubIe-1]
    =booI>I?atof(I[eIse]):!O switch(*O)x++)abs(inIine(x))>Iong&&(Iong=abs(inIine(x
    )));int1=Iong;main(-*O>>1,0);}else{if(booI<*O>>1){int0=int1;int1=int0-2*Iong/0
    [O]switch(5[O]))putchar(x-*O?(int0>=inIine(x)&&do(1,x)do(0,true)do(0,false)
    case(2,1)do(1,true)do(0,false)6[O]case(-3,6)do(0,false)6[O]-3[O]:do(1,false)
    case(5,4)x?booI?0:6[O]:7[O])+*O:8[O]),x++;main(++booI,0);}}}
     

    高精度开方。这个有点意思,已经发到OIBH上了。
    输入abc 01524157875019052100试试。
    你输入的数字需要有偶数位,否则自行添加前导0补足。
     
    #include <stdio.h>
    int l;int main(int o,char **O,
    int I){char c,*D=O[1];if(o>0){
    for(l=0;D[l              ];D[l
    ++]-=10){D   [l++]-=120;D[l]-=
    110;while   (!main(0,O,l))D[l]
    +=   20;   putchar((D[l]+1032)
    /20   )   ;}putchar(10);}else{
    c=o+     (D[I]+82)%10-(I>l/2)*
    (D[I-l+I]+72)/10-9;D[I]+=I<0?0
    :!(o=main(c/10,O,I-1))*((c+999
    )%10-(D[I]+92)%10);}return o;}
     

    画一个月亮

    #include <stdio.h>
    #include <math.h>
    double l;main(_,o,O){return putchar((_--+22&&_+44&&main(_,-43,_),_&&o)?(main(-43,++o,O),((l=(o+21)/sqrt(3-O*22-O*O),l*l<4&&(fabs(((time(0)-607728)%2551443)/405859.-4.7+acos(l/2))<1.57))[" #"])):10);}
     

    类似于hangman的猜单词游戏
     
    #ifndef int
    #ifdef while
    char s[234],d[56],*p=s,m='m';
    #define int typedef (*define)();\
     define O [6]={getc,putchar,(y)memmove,(y)printf,(y)n,(y)l};
    #include __FILE__
    signed short n(short bz){
     short pb=0,Md=1,ih=2,sfp=3,sjs=4,fo,u=5,scp=6,t,gq=7,oh,r=8,pcf=9,rs=10;
     char o=1,i=1,l,pc=i,b=r+o/2,_f=6,m=7,s=8,g,q,od=o*rs+4^s,js=_f/*3-m*'c',bs='g';
    return 1; }
    #y FILE c[a]+s,p[c],r[m]+u[i+4*o|f]-r[wob][wad]+s*f-!w|o,L+x     |  cut
    ;}int main(i,love_unix){*/;}int main(i,love_unix){/*;}int main(i,love_unix){*\;}|  here */
    while(FILE)for(;9-(i=0[O](f)););
    for(;32-(i=0[O](f));0&& 3[O]("-->%s<--", "gxdgbtgxsxpcctvpixktedhiedcte"));
    for(;'\n'-(i=O[0](f));)(i>='a'&&i<'z')?*
    #include __FILE__
                                      "Demonic Smiley" );}  /* <g> */
    #else
    #define while(int) short c=0;int*f=fopen(__##int##__,"r");for(i=0;i<25;i _)i[d]='A'+(13+i)%26;main:
    #define y define
    #define _ ++
    #include <stdio.h>
    #include <string.h>
    #include <time.h>
    #include __FILE__
    #endif
    #elif defined(signed)
    (p _)=(i-'a')[d]:!(i-'z')?*(p _)=32:(i>='A'&&i<='Z')&&((3&8|2)[O](d+1,d,24L),*(p _)=0[d]=i);/*
    #y FILE t,ra|js+t*gj,at[qdd]-=K,is _,qv _,veb _,ti _,ao[mqht] _*/
    if(c _<6) goto main; 5[O](
    #else
    #define signed short l(){char q='_';p=s+4*(time(NULL)%24)*2,m=(char)p+1;\
    *(p+8)=0; for(d[3]=10,d[33]=3[d]-10;d[3]<18;3[d] _) d[3][p]=q;3[d][p]=0;\
    hell:  printf("\t[%s]\n",p+10);if(!m) goto stoned;\
    froze: d[8]=(scanf("%c",&(2[d+__STDC__])),2[d+!NULL])&223;if(!(3[d+5]-'\n')) goto froze;\
    for(m=1[d]=0;d[1]<8;2[d-1] _) (p[d[1]]-d[8]||(p[3[d-2]+10]=4[d+4]))+(p[d[1]+10]-q||m _);\
    goto hell;stoned:;}
    FILE *X(FILE s){ char i,iev,jmqhu,xqht,mqh,ujek,sxydw,kdj,yjb,utou,qhre,eamy,jxxe,bt;}
    #endif
     
    September 26

    Matrix67的OI点滴(二):什么是离散化?

        如果说今年这时候OIBH问得最多的问题是二分图,那么去年这时候问得最多的算是离散化了。对于“什么是离散化”,搜索帖子你会发现有各种说法,比如“排序后处理”、“对坐标的近似处理”等。哪个是对的呢?哪个都对。关键在于,这需要一些例子和不少的讲解才能完全解释清楚。
        离散化是程序设计中一个非常常用的技巧,它可以有效的降低时间复杂度。其基本思想就是在众多可能的情况中“只考虑我需要用的值”。下面我将用三个例子说明,如何运用离散化改进一个低效的,甚至根本不可能实现的算法。
     
        《算法艺术与信息学竞赛》中的计算几何部分,黄亮举了一个经典的例子,我认为很适合用来介绍离散化思想。这个问题是UVA10173(http://acm.uva.es/p/v101/10173.html),题目意思很简单,给定平面上n个点的坐标,求能够覆盖所有这些点的最小矩形面积。这个问题难就难在,这个矩形可以倾斜放置(边不必平行于坐标轴)。
         
        这里的倾斜放置很不好处理,因为我们不知道这个矩形最终会倾斜多少度。假设我们知道这个矩形的倾角是α,那么答案就很简单了:矩形面积最小时四条边
    一定都挨着某个点。也就是说,四条边的斜率已经都知道了的话,只需要让这些边从外面不断逼近这个点集直到碰到了某个点。你不必知道这个具体应该怎么实现,只需要理解这可以通过某种方法计算出来,毕竟我们的重点在下面的过程。
        我们的算法很显然了:枚举矩形的倾角,对于每一个倾角,我们都能计算出最小的矩形面积,最后取一个最小值。
        这个算法是否是正确的呢?我们不能说它是否正确,因为它根本不可能实现。矩形的倾角是一个实数,它有无数种可能,你永远不可能枚举每一种情况。我们说,矩形的倾角是一个“连续的”变量,它是我们无法枚举这个倾角的根本原因。我们需要一种方法,把这个“连续的”变量变成一个一个的值,变成一个“离散的”变量。这个过程也就是所谓的离散化。
        我们可以证明,最小面积的矩形不但要求四条边上都有一个点,而且还要求至少一条边上有两个或两个以上的点。试想,如果每条边上都只有一个点,则我们总可以把这个矩形旋转一点使得这个矩形变“松”,从而有余地得到更小的矩形。于是我们发现,矩形的某条边的斜率必然与某两点的连线相同。如果我们计算出了所有过两点的直线的倾角,那么α的取值只有可能是这些倾角或它减去90度后的角(直线按“\”方向倾斜时)这么C(n,2)种。我们说,这个“倾角”已经被我们“离散化”了。虽然这个算法仍然有优化的余地,但此时我们已经达到了本文开头所说的目的。
     
        对于某些坐标虽然已经是整数(已经是离散的了)但范围极大的问题,我们也可以用离散化的思想缩小这个规模。最近搞模拟赛Vijos似乎火了一把,我就拿两道Vijos的题开刀。
        VOJ1056(
    http://www.vijos.cn/Problem_Show.asp?id=1056)永远是离散化的经典问题。大意是给定平面上的n个矩形(坐标为整数,矩形与矩形之间可能有重叠的部分),求其覆盖的总面积。平常的想法就是开一个与二维坐标规模相当的二维Boolean数组模拟矩形的“覆盖”(把矩形所在的位置填上True)。可惜这个想法在这里有些问题,因为这个题目中坐标范围相当大(坐标范围为-10^8到10^8之间的整数)。但我们发现,矩形的数量n<=100远远小于坐标范围。每个矩形会在横纵坐标上各“使用”两个值,100个矩形的坐标也不过用了-10^8到10^8之间的200个值。也就是说,实际有用的值其实只有这么几个。这些值将作为新的坐标值重新划分整个平面,省去中间的若干坐标值没有影响。我们可以将坐标范围“离散化”到1到200之间的数,于是一个200*200的二维数组就足够了。实现方法正如本文开头所说的“排序后处理”。对横坐标(或纵坐标)进行一次排序并映射为1到2n的整数,同时记录新坐标的每两个相邻坐标之间在离散化前实际的距离是多少。这道题同样有优化的余地。
        最后简单讲一下计算几何以外的一个运用实例(实质仍然是坐标的离散)。才考的VOJ1238(http://www.vijos.cn/Problem_Show.asp?id=1238)中,标程开了一个与时间范围一样大的数组来储存时间段的位置。这种方法在空间上来看十分危险。一旦时间取值范围再大一点,盲目的空间开销将导致Memory Limit Exceeded。我们完全可以采用离散化避免这种情况。我们对所有给出的时间坐标进行一次排序,然后同样用时间段的开始点和结束点来计算每个时刻的游戏数,只是一次性加的经验值数将乘以排序后这两个相邻时间点的实际差。这样,一个1..n的数组就足够了。
     
        离散化的应用相当广泛,以后你会看到还有很多其它的用途。
     
    Matrix67原创
    转载请注明出处
    September 19

    这几天里有意思的东东

        很多人闹着问我最近有什么好玩的东西。20多天里还是有很多有意思的东西
     
        Prison Break第二季Premiere的时候刚好把第一季看完。显然没有24精彩,同时疑问为什么PB在中国就这么火。
        新剧Justice非常好看,节奏异常快。House的第三季同样精彩,第二集相当有意思。
        前几天看了《东京审判》,难看死了,什么亮点都没有。今天看了《夜宴》,还是难看死了,节奏比2001太空漫游还慢。或许我不喜欢这类电影吧。总觉得国产电影是垃圾,除了《疯狂的石头》和《英雄》(别打我)。
        《侦探们的镇魂歌》有点意思,是剧场版中首次尝试把一些(简单的)谜团留给观众;比如,有些人看完了都还不知道那个侦探是KID变的。
     
        才发现的一些好玩的网站
        http://1tie.cn/一个免费的在线文本储存系统,相当有意思,使用它甚至不需要申请用户,直接在后面跟一个字串就行了。它的主页上有详细说明。我搞了一个:http://1tie.cn/matrix67/
        http://www-ui.is.s.u-tokyo.ac.jp/~takeo/teddy/teddy/teddy.html可以帮助你建一个三维模型草图。它的操作方式和真正意义上的草图没什么区别。比如,画一条线可以把你的模型切开,然后在切口上画一个半圆可以让你的模型在切面上长个包。JAVA的,首页上有个链接里有详细的帮助说明。
     
        本来还想写点和一中MM有关的东西,想了一下还是算了;对一中产生恐惧感了,天知道一中MM又会从哪里得知我的Space地址。

    MSN Space恢复更新

    数一数,近20天没更新了,破记录了。
    看别人都去了白市驿了,似乎自己也有点懒了。
    今天开始继续更新。
    September 01

    连连看 cheater release

    最近给我妈写了一个连连看的作弊程序,今天没事干,把它做成了发布版发布在这里。
    这个程序是用Delphi 7写的,393KB,目前只在连连看3.9版本中测试过(其它版本没试过)。
     
       
     
    程序可以在这里下载:
    August 28

    Matrix67的OI点滴(一):澄清P问题、NP问题、NPC问题的概念

        这或许是众多OIer最大的误区之一。
        你会经常看到网上出现“这怎么做,这不是NP问题吗”、“这个只有搜了,这已经被证明是NP问题了”之类的话。你要知道,大多数人此时所说的NP问题其实都是指的NPC问题。他们没有搞清楚NP问题和NPC问题的概念。NP问题并不是那种“只有搜才行”的问题,NPC问题才是。好,行了,基本上这个误解已经被澄清了。下面的内容都是在讲什么是P问题,什么是NP问题,什么是NPC问题,你如果不是很感兴趣就可以不看了。接下来你可以看到,把NP问题当成是NPC问题是一个多大的错误。
        还是先用几句话简单说明一下时间复杂度。时间复杂度并不是表示一个程序解决问题需要花多少时间,而是当问题规模扩大后,程序需要的时间长度增长得有多快。也就是说,对于高速处理数据的计算机来说,处理某一个特定数据的效率不能衡量一个程序的好坏,而应该看当这个数据的规模变大到数百倍后,程序运行时间是否还是一样,或者也跟着慢了数百倍,或者变慢了数万倍。不管数据有多大,程序处理花的时间始终是那么多的,我们就说这个程序很好,具有O(1)的时间复杂度,也称常数级复杂度;数据规模变得有多大,花的时间也跟着变得有多长,这个程序的时间复杂度就是O(n),比如找n个数中的最大值;而像冒泡排序、插入排序等,数据扩大2倍,时间变慢4倍的,属于O(n^2)的复杂度。还有一些穷举类的算法,所需时间长度成几何阶数上涨,这就是O(a^n)的指数级复杂度,甚至O(n!)的阶乘级复杂度。不会存在O(2*n^2)的复杂度,因为前面的那个“2”是系数,根本不会影响到整个程序的时间增长。同样地,O(n^3+n^2)的复杂度也就是O(n^3)的复杂度。因此,我们会说,一个O(0.01*n^3)的程序的效率比O(100*n^2)的效率低,尽管在n很小的时候,前者优于后者,但后者时间随数据规模增长得慢,最终O(n^3)的复杂度将远远超过O(n^2)。我们也说,O(n^100)的复杂度小于O(1.01^n)的复杂度。
        容易看出,前面的几类复杂度被分为两种级别,其中后者的复杂度无论如何都远远大于前者:一种是O(1),O(log(n)),O(n^a)等,我们把它叫做多项式级的复杂度,因为它的规模n出现在底数的位置;另一种是O(a^n)和O(n!)型复杂度,它是非多项式级的,其复杂度计算机往往不能承受。当我们在解决一个问题时,我们选择的算法通常都需要是多项式级的复杂度,非多项式级的复杂度需要的时间太多,往往会超时,除非是数据规模非常小。
        自然地,人们会想到一个问题:会不会所有的问题都可以找到复杂度为多项式级的算法呢?很遗憾,答案是否定的。有些问题甚至根本不可能找到一个正确的算法来,这称之为“不可解问题”(Undecidable Decision Problem)。The Halting Problem就是一个著名的不可解问题,在我的MSN Space上有过专门的介绍和证明。再比如,输出从1到n这n个数的全排列。不管你用什么方法,你的复杂度都是阶乘级,因为你总得用阶乘级的时间打印出结果来。有人说,这样的“问题”不是一个“正规”的问题,正规的问题是让程序解决一个问题,输出一个“YES”或“NO”(这被称为判定性问题),或者一个什么什么的最优值(这被称为最优化问题)。那么,根据这个定义,我也能举出一个不大可能会有多项式级算法的问题来:Hamilton回路。问题是这样的:给你一个图,问你能否找到一条经过每个顶点一次且恰好一次(不遗漏也不重复)最后又走回来的路(满足这个条件的路径叫做Hamilton回路)。这个问题现在还没有找到多项式级的算法。事实上,这个问题就是我们后面要说的NPC问题。
        下面引入P类问题的概念:如果一个问题可以找到一个能在多项式的时间里解决它的算法,那么这个问题就属于P问题。P是英文单词多项式的第一个字母。哪些问题是P类问题呢?通常NOI和NOIP不会出不属于P类问题的题目。我们常见到的一些信息奥赛的题目都是P问题。道理很简单,一个用穷举换来的非多项式级时间的超时程序不会涵盖任何有价值的算法。
        接下来引入NP问题的概念。这个就有点难理解了,或者说容易理解错误。在这里强调(回到我竭力想澄清的误区上),NP问题不是非P类问题。NP问题是指可以在多项式的时间里验证一个解的问题。NP问题的另一个定义是,可以在多项式的时间里猜出一个解的问题。比方说,我RP很好,在程序中需要枚举时,我可以一猜一个准。现在某人拿到了一个求最短路径的问题,问从起点到终点是否有一条小于100个单位长度的路线。它根据数据画好了图,但怎么也算不出来,于是来问我:你看怎么选条路走得最少?我说,我RP很好,肯定能随便给你指条很短的路出来。然后我就胡乱画了几条线,说就这条吧。那人按我指的这条把权值加起来一看,嘿,神了,路径长度98,比100小。于是答案出来了,存在比100小的路径。别人会问他这题怎么做出来的,他就可以说,因为我找到了一个比100小的解。在这个题中,找一个解很困难,但验证一个解很容易。验证一个解只需要O(n)的时间复杂度,也就是说我可以花O(n)的时间把我猜的路径的长度加出来。那么,只要我RP好,猜得准,我一定能在多项式的时间里解决这个问题。我猜到的方案总是最优的,不满足题意的方案也不会来骗我去选它。这就是NP问题。当然有不是NP问题的问题,即你猜到了解但是没用,因为你不能在多项式的时间里去验证它。下面我要举的例子是一个经典的例子,它指出了一个目前还没有办法在多项式的时间里验证一个解的问题。很显然,前面所说的Hamilton回路是NP问题,因为验证一条路是否恰好经过了每一个顶点非常容易。但我要把问题换成这样:试问一个图中是否不存在Hamilton回路。这样问题就没法在多项式的时间里进行验证了,因为除非你试过所有的路,否则你不敢断定它“没有Hamilton回路”。
        之所以要定义NP问题,是因为通常只有NP问题才可能找到多项式的算法。我们不会指望一个连多项式地验证一个解都不行的问题存在一个解决它的多项式级的算法。相信读者很快明白,信息学中的号称最困难的问题——“NP问题”,实际上是在探讨NP问题与P类问题的关系。
        很显然,所有的P类问题都是NP问题。也就是说,能多项式地解决一个问题,必然能多项式地验证一个问题的解——既然正解都出来了,验证任意给定的解也只需要比较一下就可以了。关键是,人们想知道,是否所有的NP问题都是P类问题。我们可以再用集合的观点来说明。如果把所有P类问题归为一个集合P中,把所有NP问题划进另一个集合NP中,那么,显然有P属于NP。现在,所有对NP问题的研究都集中在一个问题上,即究竟是否有P=NP?通常所谓的“NP问题”,其实就一句话:证明或推翻P=NP。
        NP问题一直都是信息学的巅峰。巅峰,意即很引人注目但难以解决。在信息学研究中,这是一个耗费了很多时间和精力也没有解决的终极问题,好比物理学中的大统一和数学中的歌德巴赫猜想等。
        目前为止这个问题还“啃不动”。但是,一个总的趋势、一个大方向是有的。人们普遍认为,P=NP不成立,也就是说,多数人相信,存在至少一个不可能有多项式级复杂度的算法的NP问题。人们如此坚信P≠NP是有原因的,就是在研究NP问题的过程中找出了一类非常特殊的NP问题叫做NP-完全问题,也即所谓的NPC问题。C是英文单词“完全”的第一个字母。正是NPC问题的存在,使人们相信P≠NP。下文将花大量篇幅介绍NPC问题,你从中可以体会到NPC问题使P=NP变得多么不可思议。
        为了说明NPC问题,我们先引入一个概念——约化(Reducibility,有的资料上叫“归约”)。
        简单地说,一个问题A可以约化为问题B的含义即是,可以用问题B的解法解决问题A,或者说,问题A可以“变成”问题B。《算法导论》上举了这么一个例子。比如说,现在有两个问题:求解一个一元一次方程和求解一个一元二次方程。那么我们说,前者可以约化为后者,意即知道如何解一个一元二次方程那么一定能解出一元一次方程。我们可以写出两个程序分别对应两个问题,那么我们能找到一个“规则”,按照这个规则把解一元一次方程程序的输入数据变一下,用在解一元二次方程的程序上,两个程序总能得到一样的结果。这个规则即是:两个方程的对应项系数不变,一元二次方程的二次项系数为0。按照这个规则把前一个问题转换成后一个问题,两个问题就等价了。同样地,我们可以说,Hamilton回路可以约化为TSP问题(Travelling Salesman Problem,旅行商问题):在Hamilton回路问题中,两点相连即这两点距离为0,两点不直接相连则令其距离为1,于是问题转化为在TSP问题中,是否存在一条长为0的路径。Hamilton回路存在当且仅当TSP问题中存在长为0的回路。
        “问题A可约化为问题B”有一个重要的直观意义:B的时间复杂度高于或者等于A的时间复杂度。也就是说,问题A不比问题B难。这很容易理解。既然问题A能用问题B来解决,倘若B的时间复杂度比A的时间复杂度还低了,那A的算法就可以改进为B的算法,两者的时间复杂度还是相同。正如解一元二次方程比解一元一次方程难,因为解决前者的方法可以用来解决后者。
        很显然,约化具有一项重要的性质:约化具有传递性。如果问题A可约化为问题B,问题B可约化为问题C,则问题A一定可约化为问题C。这个道理非常简单,就不必阐述了。
        现在再来说一下约化的标准概念就不难理解了:如果能找到这样一个变化法则,对任意一个程序A的输入,都能按这个法则变换成程序B的输入,使两程序的输出相同,那么我们说,问题A可约化为问题B。
        当然,我们所说的“可约化”是指的可“多项式地”约化(Polynomial-time Reducible),即变换输入的方法是能在多项式的时间里完成的。约化的过程只有用多项式的时间完成才有意义。
        好了,从约化的定义中我们看到,一个问题约化为另一个问题,时间复杂度增加了,问题的应用范围也增大了。通过对某些问题的不断约化,我们能够不断寻找复杂度更高,但应用范围更广的算法来代替复杂度虽然低,但只能用于很小的一类问题的算法。再回想前面讲的P和NP问题,联想起约化的传递性,自然地,我们会想问,如果不断地约化上去,不断找到能“通吃”若干小NP问题的一个稍复杂的大NP问题,那么最后是否有可能找到一个时间复杂度最高,并且能“通吃”所有的NP问题的这样一个超级NP问题?答案居然是肯定的。也就是说,存在这样一个NP问题,所有的NP问题都可以约化成它。换句话说,只要解决了这个问题,那么所有的NP问题都解决了。这种问题的存在难以置信,并且更加不可思议的是,这种问题不只一个,它有很多个,它是一类问题。这一类问题就是传说中的NPC问题,也就是NP-完全问题。NPC问题的出现使整个NP问题的研究得到了飞跃式的发展。我们有理由相信,NPC问题是最复杂的问题。再次回到全文开头,我们可以看到,人们想表达一个问题不存在多项式的高效算法时应该说它“属于NPC问题”。此时,我的目的终于达到了,我已经把NP问题和NPC问题区别开了。到此为止,本文已经写了近5000字了,我佩服你还能看到这里来,同时也佩服一下自己能写到这里来。
        NPC问题的定义非常简单。同时满足下面两个条件的问题就是NPC问题。首先,它得是一个NP问题;然后,所有的NP问题都可以约化到它。证明一个问题是NPC问题也很简单。先证明它至少是一个NP问题,再证明其中一个已知的NPC问题能约化到它(由约化的传递性,则NPC问题定义的第二条也得以满足;至于第一个NPC问题是怎么来的,下文将介绍),这样就可以说它是NPC问题了。
        既然所有的NP问题都能约化成NPC问题,那么只要任意一个NPC问题找到了一个多项式的算法,那么所有的NP问题都能用这个算法解决了,NP也就等于P了。因此,给NPC找一个多项式算法太不可思议了。因此,前文才说,“正是NPC问题的存在,使人们相信P≠NP”。我们可以就此直观地理解,NPC问题目前没有多项式的有效算法,只能用指数级甚至阶乘级复杂度的搜索。
        顺便讲一下NP-Hard问题。NP-Hard问题是这样一种问题,它满足NPC问题定义的第二条但不一定要满足第一条(就是说,NP-Hard问题要比NPC问题的范围广)。NP-Hard问题同样难以找到多项式的算法,但它不列入我们的研究范围,因为它不一定是NP问题。即使NPC问题发现了多项式级的算法,NP-Hard问题有可能仍然无法得到多项式级的算法。事实上,由于NP-Hard放宽了限定条件,它将有可能比所有的NPC问题的时间复杂度更高从而更难以解决。
        不要以为NPC问题是一纸空谈。NPC问题是存在的。确实有这么一个非常具体的问题属于NPC问题。下文即将介绍它。
        下文即将介绍逻辑电路问题。这是第一个NPC问题。其它的NPC问题都是由这个问题约化而来的。因此,逻辑电路问题是NPC类问题的“鼻祖”。
        逻辑电路问题是指的这样一个问题:给定一个逻辑电路,问是否存在一种输入使输出为True。
        什么叫做逻辑电路呢?一个逻辑电路由若干个输入,一个输出,若干“逻辑门”和密密麻麻的线组成。看下面一例,不需要解释你马上就明白了。
      ┌───┐
      │ 输入1├─→┐    ┌──┐
      └───┘    └─→┤    │
                         │ OR ├→─┐
      ┌───┐    ┌─→┤    │    │    ┌──┐
      │ 输入2├─→┤    └──┘    └─→┤    │
      └───┘    │               ┌─→┤AND ├──→输出
                   └────────┘ ┌→┤    │
      ┌───┐    ┌──┐            │  └──┘
      │ 输入3├─→┤ NOT├─→────-┘
      └───┘    └──┘
        这是个较简单的逻辑电路,当输入1、输入2、输入3分别为True、True、False或False、True、False时,输出为True。
        有输出无论如何都不可能为True的逻辑电路吗?有。下面就是一个简单的例子。
      ┌───┐
      │输入1 ├→─┐    ┌──┐
      └───┘    └─→┤    │
                         │AND ├─→┐
                   ┌─→┤    │    │
                   │    └──┘    │  ┌──┐
                   │               └→┤    │
      ┌───┐    │                   │AND ├─→输出
      │输入2 ├→─┤  ┌──┐      ┌→┤    │
      └───┘    └→┤NOT ├→──┘  └──┘
                       └──┘
        上面这个逻辑电路中,无论输入是什么,输出都是False。我们就说,这个逻辑电路不存在使输出为True的一组输入。
        回到上文,给定一个逻辑电路,问是否存在一种输入使输出为True,这即逻辑电路问题。
        逻辑电路问题属于NPC问题。这是有严格证明的。它显然属于NP问题,并且可以直接证明所有的NP问题都可以约化到它(不要以为NP问题有无穷多个将给证明造成不可逾越的困难)。证明过程相当复杂,其大概意思是说任意一个NP问题的输入和输出都可以转换成逻辑电路的输入和输出(想想计算机内部也不过是一些0和1的运算),因此对于一个NP问题来说,问题转化为了求出满足结果为True的一个输入(即一个可行解)。
        有了第一个NPC问题后,一大堆NPC问题就出现了,因为再证明一个新的NPC问题只需要将一个已知的NPC问题约化到它就行了。后来,Hamilton回路成了NPC问题,TSP问题也成了NPC问题。现在被证明是NPC问题的有很多,任何一个找到了多项式算法的话所有的NP问题都可以完美解决了。因此说,正是因为NPC问题的存在,P=NP变得难以置信。P=NP问题还有许多有趣的东西,有待大家自己进一步的挖掘。攀登这个信息学的巅峰是我们这一代的终极目标。现在我们需要做的,至少是不要把概念弄混淆了。
     
    Matrix67原创
    转载请注明出处

    Matrix67的OI点滴(序篇):我为什么要写这个?

        信息学竞赛与其它学科的竞赛相比有其特殊性:教师在里面起的作用不大,主要靠自己通过各种渠道获取信息。我每天都会收到很多OIer发来的消息,他们迫切希望知道很多OI知识。但是,资源是有限的,即使在网络中也是。过于专业化的东西在中文网络上搜索起来并非易事。并且,OIer们所找到的东西并不一定完全可靠。不少人学习OI都是抱着一两本奥赛书或者在OIBH中搜索,但殊不知这些地方的很多东西也都不一定完全正确。两年前,我也是一个什么都不知道的OIer。我也曾经在书店、在网络上苦苦地搜索过。因此,我知道现在OIer需要什么。我知道哪些东西OIer找不到,哪些东西普遍存在误解。我所写的东西都是我能想到的网上不太容易找到或者存在误区的问题。我想到需要写什么我就写什么,这些内容没有顺序。
        现在的OI资源存在一个问题:太过于数学化、符号化。在我看来,OI的这些问题不应该是数学化的东西,不应该用数学语言去描述。OI考的是创新能力,考的是形象思维。因此,我写的这些东西最大的特点在于形象化。我决不会扔下一大堆数学式子,而是着重表达出我的形象化理解。我竭力把一个问题说清楚,让即使没有学过OI,甚至没有学过数学的人也能看懂。
        我的OI生涯算是基本结束了,但OI事业并未结束。我要做的事还有很多。我打算在这一年的时间里留下更多的资料分享给今后的OIer。我不会去想这些东西被编印成册,我只是想让更多的人能从中学到东西。OI应该在互联网中生存,而互联网的基本精神是共享。在此,我只有一个要求,这些东西转载时请注明出处。说这话是有原因的,我已经发现了这个MSN Space里有些东西被没有标注来源地转帖出去了。
    August 24

    无题

        不知道有没有人想,Matrix67到哪里去了,怎么MSN Space也不更新了……
        没有时间更新MSN Space。这10天帮我们年级搞OI去了,有点忙。讲了几天课,组织了几次考试。
        所有这几天的资料(几个课件和三次考试的题目、标程和数据)都可以在这里下载:
        内部资料,已加密。密码是一个小女生的名字,所有字母大写。
     
        上次的模拟赛搞得算是非常成功,虽然出了若干满分。不少人回帖说题出的不错,我很高兴。有人反映说题的难度适中,但梯度不够。我自己认为我的“梯度”主要不是体现在题上,而是不同规模的数据上。比如,第一题虽然不好想,但50%的数据怎么都能过吧。毕竟OI不是OJ,不要求AC,能对多少对多少。
        我的计划是再搞两次类似的模拟赛,一次在国庆节(预计在10月3日),另一次在NOIP前一个星期。下面这两次的题和这次的题目差不多,也是注重考大家的算法设计,一旦算法想到了,程序可以在十几行内完成。
     
        然后……这个MSN Space貌似更新起来已经没有多大意义了,这里的主要访客即将奔向据说有陶然居和温泉的地方。为了提高我更新的积极性,麻烦那些通过我MSN、Vijos签名、个人主页等方式来到这里的OIer在下面留个言,申请MSN也不麻烦,还能(并且我也鼓励大家)写一个自己的Space记录每天的收获和有趣的事情。
        近段时间经常收到加QQ好友的邀请,在这里我也说一下:我的QQ是188932899,但很少上(一般别指望能看到我上线);现在主要是在MSN上活动。我的MSN是matrix67@tom.com,非常欢迎交友。
     
        关于上次的日志说的东西……我会做的,从明天开始写。
    August 10

    说一下模拟赛和MSN Space更新内容的计划

        反正这段时间没有事干,决定帮大家冲刺NOIP。
        这次模拟赛是在NOIP2006前的第一次模拟赛,题目全部原创,难度很小,着重考察算法设计能力。这次模拟赛之后计划至少还有两次类似的模拟赛,难度将逐渐增大。
        预计MSN Space接下来要更新的五个内容是:
        1. 澄清P问题、NP问题、NPC问题的概念;
        2. 什么是离散化;
        3. KMP字符串匹配算法;
        4. König定理的证明;
        5. 生成函数(在函数构造、运算和求解递推关系中的具体应用)。
        买了本本了,IBM,好舒服。