怎样做一个 清华online judgee

一个基于Online Judge的程序设计类课程教学辅助系统,online judge,华为online ju..
扫扫二维码,随身浏览文档
手机或平板扫扫即可继续访问
一个基于Online Judge的程序设计类课程教学辅助系统
举报该文档为侵权文档。
举报该文档含有违规或不良信息。
反馈该文档无法正常浏览。
举报该文档为重复文档。
推荐理由:
将文档分享至:
分享完整地址
文档地址:
粘贴到BBS或博客
flash地址:
支持嵌入FLASH地址的网站使用
html代码:
&embed src='/DocinViewer--144.swf' width='100%' height='600' type=application/x-shockwave-flash ALLOWFULLSCREEN='true' ALLOWSCRIPTACCESS='always'&&/embed&
450px*300px480px*400px650px*490px
支持嵌入HTML代码的网站使用
您的内容已经提交成功
您所提交的内容需要审核后才能发布,请您等待!
3秒自动关闭窗口您所在位置: &
&nbsp&&nbsp&nbsp&&nbsp
毕业论文:Online Judge 在线实验系统设计.doc43页
本文档一共被下载:
次 ,您可免费全文在线阅读后下载本文档
文档加载中...广告还剩秒
需要金币:100 &&
你可能关注的文档:
··········
··········
ABSTRACT II
第1章 绪论 1
1.1 课题背景 1
1.2 国内外研究现状 2
1.3 本课题研究的意义 3
第2章 系统开发工具与主要技术简介 4
2.1 系统开发工具 4
2.2 Online Judge在线实验系统开发主要技术 4
第3章 系统分析与数据库设计 10
3.1需求分析 10
3.2 实验系统评判功能 11
3.3 数据库设计 13
第4章 系统整体设计 17
4.1 系统功能框架 17
4.2 界面布局设计 19
第5章 系统详细设计 21
5.1 前台设计 21
5.2 后台设计 27
第6章 编译运行与系统测试 32
6.1安装命令 32
6.2系统运行 33
6.3系统测试 34
参考文献 40
第1章 绪 论
1.1课题背景
Online Judge Online Judge系统(简称OJ)是一个在线的判题系统。用户可以在线提交程序多种程序(如C、C++、Pascal)源代码,系统对源代码进行编译和执行,并通过预先设计的测试数据来检验程序源代码的正确性。
一个用户提交的程序在Online Judge系统下执行时将受到比较严格的限制,包括运行时间限制,内存使用限制和安全限制等。用户程序执行的结果将被Online Judge系统捕捉并保存,然后再转交给一个裁判程序。该裁判程序或者比较用户程序的输出数据和标准输出样例的差别,或者检验用户程序的输出数据是否满足一定的逻辑条件。最后系统返回给用户一个状态:通过(Accepted,AC)、答案错误 Wrong Answer,WA 、超时 Time Limit Exceed,TLE 、超过输出限制(Output Limit Exceed,OLE 、超内存(Memory Limit Exceed,MLE)、运行时错误(Runtime Error,RE)、格式错误(Presentation Error,PE 、或是无法编译(Compile Error,CE),并返回程序使用的内存、运行时间等信息。   
Online Judge系统最初使用于ACM-ICPC国际大学生程序设计竞赛和OI信息学奥林匹克竞赛中的自动判题和排名。现广泛
正在加载中,请稍后...OnlineJudge 离线题库采集
过段时间要把以前的OJ换掉,我负责VirtualJudge的部分。需要用C与PHP写一个Linux下的VJudge。在此之前,将以前写给自己学弟学妹用的OJ离线题库的采集程序改进了.
&&&&过段时间要把以前的换掉,我负责VirtualJudge的部分。需要用C与PHP写一个Linux下的VJudge。&&&&在此之前,将以前写给自己学弟学妹用的离线题库的采集程序改进了一下。支持国内一些知名高校的OJ,为之后VJudge的开发练练手,熟悉下各个OJ的结构,免去以后再在LINUX上进行一些繁琐的测试。&&&&题目的采集没有使用任何OJ的API,直接采取从页面采集数据并处理的方式。下载HTTP文件使用的是WinINet函数集,用起来比CURL还方便。正则表达式使用的ATL库里的regex。题目的储存使用的文件进行储存。别看一个OJ就有几千道题,文件结构访问的速度却是相当地快。因为抓取出来的题目有大量的标签,先要全部去掉非常地困难(一些题目有不同的格式与链接标签,甚至有一些题目是关于标记语言的题目)。因为我现在做的是离线的题库,于是将这些标签保留写来,利用HTML的方式显示我采集的题目。&&&&目前做了六个OJ:&&&&1、bnu(北京师范大学OJ):这个OJ我觉得是国内用户体验做得最好的一个OJ,自己OJ的题目很丰富(含有较多不错的中文),VOJ的功能也完美地融合在自己的OJ中。&&&&2、hdu(杭州电子科技大学OJ):拥有国内最强判题机群,国内各大算法竞赛都选在杭电OJ举行。&&&&3、neu(成都东软学院OJ):很多人可能都不知道这OJ,也是国内少数登录需要验证码的奇葩OJ,以至于VOJ想支持它都困难,简单题爆多,欢迎来撸。我母校嘛,必须有,虽然我亚洲区牌都没拿到个,学校最高也就个铜。补习班性质的比赛嘛,二本学校的学生都不想把耍的时间花在觉得苦逼的事情上,我能拿着塑料坚持那么久也是NB了。该OJ即将在今年更换新的系统,判题内核、数据管理、界面、以及VOJ都由我们的老成员完成。希望以后学校的骚年能取得好成绩--wchrt。&&&&4、poj(北京大学OJ):搞ICPC的骚年都知道,国内最为知名的OJ之一。&&&&5、vijos(高效信息学在线评测系OJ):里面全是中文题哦,小心有大量的小学生,分分钟虐爆你的小学生。&&&&6、zoj(浙江大学OJ):国内起步最早的OJ之一,但我基本没怎么用过这个OJ。&&&&下面根据各oj分别列出了获取题目内容与标题的正则表达式:&&&&&&&&ojnum=0;
cbox-&AddString("bnu");
ojurl[ojnum]="/bnuoj/problem_show.php?pid=";
ojgetstr[ojnum].allcontent="{&div&id=\"showproblem\"&(.|\n)*?&div&id=\"one_content_base\"&}";
ojgetstr[ojnum].title="&div&id=\"showproblem\.*?&h1.*?&{.*?}&/h1&";
ojgetstr[ojnum].iscode=
ojgetstr[ojnum].getnum=-1;
ojgetstr[ojnum].pnostart=1000;
cbox-&AddString("hdu");
ojurl[ojnum]="http://acm./showproblem.php?pid=";
ojgetstr[ojnum].allcontent="{&h1(.|\n)*?Note&/a&}";
ojgetstr[ojnum].title="&h1.*?&{.*?}&/h1&";
ojgetstr[ojnum].iscode=
ojgetstr[ojnum].getnum=-1;
ojgetstr[ojnum].pnostart=1000;
cbox-&AddString("neu");
ojurl[ojnum]="http://acm./JudgeOnline/problem.php?id=";
ojgetstr[ojnum].allcontent="{&div&id=main(.|\n)*?Sample&Output(.|\n)*?BBS(.|\n)*?&/a&}";
ojgetstr[ojnum].title="&title.*?&{.*?}&/title&";
//目前不需要把内容信息分开,就不正则详细的题目内容
/*ojgetstr[ojnum].tim="Time&Limit:{.*?}&nbsp";
ojgetstr[ojnum].mem="Memory&Limit:{.*?}&br&";
ojgetstr[ojnum].des="&Description&/h2&.*?&div.*?&{.*?}&/div&";
ojgetstr[ojnum].input="&Input&/h2&.*?&div.*?&{.*?&/div&.*?}&/div&";
ojgetstr[ojnum].output="&Output&/h2&.*?&div.*?&{.*?}&/div&";
ojgetstr[ojnum].sinput="&Sample&Input&/h2&.*?{&pre&(.|\n)*?&/pre&}";
ojgetstr[ojnum].soutput="&Sample&Output&/h2&.*?{&pre&(.|\n)*?&/pre&}";*/
ojgetstr[ojnum].iscode=
ojgetstr[ojnum].getnum=-1;
ojgetstr[ojnum].pnostart=1000;
cbox-&AddString("poj");
ojurl[ojnum]="http://poj.org/problem?id=";
ojgetstr[ojnum].allcontent="{&div&class=\"ptt\"(.|\n)*?Discuss&/a&}";
ojgetstr[ojnum].title="&div&class=\"ptt\".*?&{.*?}&/div&";
ojgetstr[ojnum].iscode=
ojgetstr[ojnum].getnum=49;//poj最多只允许短时间访问49道题
ojgetstr[ojnum].pnostart=1000;
cbox-&AddString("vijos");
ojurl[ojnum]="https://vijos.org/p/";
ojgetstr[ojnum].allcontent="{&div&class=\"pcontent\"&(.|\n)*}&h4";
ojgetstr[ojnum].title="&div&class=\"content\"&.*?&/span&{.*?}&div";
ojgetstr[ojnum].iscode=
ojgetstr[ojnum].getnum=-1;
ojgetstr[ojnum].pnostart=1000;
cbox-&AddString("zoj");
ojurl[ojnum]="http://acm./onlinejudge/showProblem.do?problemCode=";
ojgetstr[ojnum].allcontent="{&div&id=\"content_body\"&(.|\n)*&/div&}";
ojgetstr[ojnum].title="&div&id=\"content_body\"&.*?&.*?&{.*?}&/span&";
ojgetstr[ojnum].iscode=
ojgetstr[ojnum].getnum=-1;
ojgetstr[ojnum].pnostart=1000;
ojnum++;因为不同的OJ网站所使用的编码格式不同,我使用的是ansi,所以要把一些使用utf-8的网页转换为ansi,下面是转换的代码:static&void&&UTF8toANSI(CString&&strUTF8)
UINT&nLen&=&MultiByteToWideChar(CP_UTF8,NULL,strUTF8,-1,NULL,NULL);
WCHAR&*wszBuffer&=&new&WCHAR[nLen+1];
nLen&=&MultiByteToWideChar(CP_UTF8,NULL,strUTF8,-1,wszBuffer,nLen);
wszBuffer[nLen]&=&0;
nLen&=&WideCharToMultiByte(936,NULL,wszBuffer,-1,NULL,NULL,NULL,NULL);
CHAR&*szBuffer&=&new&CHAR[nLen+1];
nLen&=&WideCharToMultiByte(936,NULL,wszBuffer,-1,szBuffer,nLen,NULL,NULL);
szBuffer[nLen]&=&0;
strUTF8&=&szB
delete&[]szB
delete&[]wszB
}&&&&使用WinINet函数集的CInternetSession与CHttpFile下载HTTP文件,那hdu作为例子hdu的1001题的地址是:"http://acm./showproblem.php?pid=1001" 这里的是hdu的题目PHP文件,我们需要以GET的方式请求pid=1001的数据。对于hdu的其他题目,我们只需把pid的值设置成其他的值即可。
你最喜欢的Tongji Online Judge Solutions
  四件圣衣分别计算,相加即可。把一件圣衣上所有洞的破损程度值分成和尽可能接近的两组,则和较大的一组的和就是修补这件圣衣所需的最少时间。这个时间的求法可参看问题。
  根据定义,一个数n是牛数的充要条件是它有4个或更多的质因数。因此将它分解质因数即可。注意可能有一个因数可能大于sqrt(n)。
  如果A的斧子在B手里,B的斧子在C手里……X的斧子在A手里,那么称A,B,C……X为一个循环。如果某个人的斧子恰在自己手里,那么他自己构成一个循环。循环中的人数称为循环的长度。
  先来看第一个问题:最少交换次数。长度为1和2的循环的最少交换次数显然分别为0和1。在一个长度为n的循环中,可以进行一次交换使其中一个人拿到自己的斧子,这样相当于把此循环分解成了两个循环,一个长度为1,另一个长度为n-1。由数学归纳法易知,长度为n的循环的最少交换次数为n-1。因此,总的最少交换次数等于总人数减去循环数。
  再看第二个问题:最短交换时间。长度为1和2的循环的最短交换时间同样显然分别为0和1。对于一个长度为n(n>2)的循环(其中的人按顺序编号为1..n),可以构造这样一种交换方法:
第1个时间单位,第n个人不动,第i(1&i&n/2)个人与第n-i个人交换斧子;
第2个时间单位,第i(1&i&(n+1)/2)个人与第n+1-i个人交换斧子。
  所以最短交换时间为2。因此,若最大循环长度为1,则总的最短交换时间为0;若最大循环长度为2,则总的最短交换时间为1;否则为2。
  这道题与是极为类似的。同样是搜索每个质因数的指数,用同样的公式计算约数个数,用同样的不等式剪枝。此题与相比较简单的一点是,虽然在试乘的过程中数值会超出longint甚至cardinal的范围,但不必使用高精度,用int64即可。
  气死人的题:题目中说n<=10,但测试数据中的n没有超过6的!!!所以,朴素搜索即可,不必加任何剪枝。
  用F[n]表示n个人围一圈时,最后剩下的人的编号。显然F[1]=1。当我们知道F[i-1]时,我们可以算出F[i]:首先数过去M个人,然后剩下的人相当于在玩i-1个人的约瑟夫游戏,所以最后剩下的人在这i-1个人中的次序应该是F[i-1]。因此F[i]=(M+F[i-1]) mod i,当结果为0时修正为i。
  有了这个复杂度为O(n)式子,我们便可以从小到大枚举M了。
  一点窍门:在整个式子左边加一个'(',倒着递归计算比较方便。
  这道题显然要用构造法。首先我们要知道哪些数的结果是Yes,最好能证明,还需要知道结果是No的数反例如何构造。
  做个实验就会发现,对大于等于4的偶数,反例遍地都是,而大于等于7的奇数,反例不多,但是存在。
  先看简单的情况,即偶数。设n为偶数,则Fan(n)为n组n-1个括号相乘相加。我们只需使第一组括号乘积为负,其它组括号乘积全为0即可。于是很容易想出反例:2 3 3...(共n-1个3)。
  再看奇数。仍用上面的思路,但这次要构造一组负积就困难一些了。设n为奇数。令A1=2,则A2至An中,需要有奇数个数大于2,奇数个数小于2。如果小于2的只有1个,假设为A2=1,那么第2组括号将为正值而不为0。于是我们只好令A2=A3=A4=1,而A5至An全部等于3。同样,大于2的数也不能只有一个,因此n至少等于7。
  那么Yes的情况就只能有2、3、5了。下面试着证明。为书写方便,不用A1、A2等表示每个数,而用a、b等表示。
  Fan(2)=(a-b)+(b-a)=0>=0是显然的。Fan(3)=(a-b)(a-c)+(b-a)(b-c)+(c-a)(c-b)=a2+b2+c2-ab-bc-ca=[(a-b)2+(b-c)2+(c-a)2]/2也容易证明。下面给出较难的n=5时的证明:
假设a<=b<=c<=d<=e。令A=(a-b)(a-c)(a-d)(a-e), B、C、D、E的定义类似。
显然A,C,E>=0,B,D<=0.
比较|A|和|B|:
因为|a-b|=|b-a|,|a-c|>=|b-c|,|a-d|>=|b-d|,|a-e|>=|b-e|
所以|A|>=|B|
所以A+B>=0
同理D+E>=0
又因为C>=0,所以A+B+C+D+E>=0,即Fan(5)>=0恒成立。
  const即可。不要忽略b=0的情况。
  见加强版。
  真是一道难题,要用到棋盘多项式。另外,我把1的个数的上限定为20的时候,RE216了,定为25才AC。
  什么是棋盘多项式?
  棋盘多项式是一个关于x的多项式,式中n次项的系数表示在棋盘上放置n个棋子,且无任意两个棋子同行或同列(称“互不冲突”)的方法数。例如在棋盘上,放置0个棋子且互不冲突的方法数为1,放置1个棋子且互不冲突的方法数为3,放置2个棋子且互不冲突的方法数为1,放置3个棋子且互不冲突的方法数为0,那么棋盘的棋盘多项式就是x2+3x+1,记作R()=x2+3x+1。
  怎样计算棋盘多项式?
  显然对于空棋盘,R( )=1。对非空棋盘A,任意选定一个格子g,设从A中去掉g及所有与g同行或同列的格子后剩余的棋盘为A1,从A中仅去掉g后剩余的棋盘为A2,则R(A)=xR(A1)+R(A2)。
  例如:R()
  =xR()+R() (此步选定的是左上方的格子)
  =x(xR( )+R( ))+xR( )+R()
  =x(x+1)+x+x+1
  =x2+3x+1
  棋盘多项式要怎么用?
  想象一个n*n的棋盘,输入中的1是棋盘的禁区。设禁区的棋盘多项式的i次项的系数为fi。在整个棋盘上放置n个棋子,至少有i个落入禁区的方法数为fi*(n-1)!。由容斥原理,在整个棋盘上放置n个棋子,无一落入禁区的方法数为。因此计算出禁区的棋盘多项式,问题就解决了。当然,需要用高精度。
  复杂度问题!
  在计算有n个格子禁区的棋盘多项式的过程中,总共要计算多少个棋盘的棋盘多项式呢?感觉是2n个。这个复杂度无论是时间上还是空间上都是无法承受的。怎么办呢?其实什么也不用办,因为实际上计算的棋盘数不会超过2n/2+1。如果计算棋盘多项式时,每次都选定最上面一行的最左面一格,则这个上限会在棋盘上每列都有至少2个格子,且列与列之间没有格子同行,且对于任意两列a、b,a的最高格总是高于b的次高格时达到。假设棋盘共有m列,则在需计算的棋盘中,若第x列的最高格已经不存在了,则第1至x-1列的最高格必然也不存在了。故第1列的最高格存在的棋盘只有1(即20)个,第1列的最高格不存在而第2列的最高格存在的棋盘有21个(因为第1列除去最高格后的部分有存在与不存在两种可能),前2列的最高格不存在而第3列的最高格存在的棋盘有22个(前2列除去最高格后的部分都可能存在或不存在)……m列的最高格都不存在的棋盘有2m个。也就是说,棋盘总数的上限大约为2m+1。因为对这种棋盘有m<=n/2,故实际计算的棋盘数不会超过2n/2+1。在n<=25时,这个复杂度在时间上是完全可以承受的,在空间上,若给每个棋盘编一个号,使用哈希表存储棋盘多项式,也没有问题。
  本题使用了高精度、二进制编号、哈希表等多种算法和数据结构,对编程能力也是一个极好的锻炼。
  把边当作点,点当作边,Dijkstra即可。
  很明显,这是一道图论题。首先构造一个图:每一个洞作为一个顶点,在某两个洞之间交换了几次,就在代表这两个洞的顶点之间连几条边。把最后可能藏有小球的洞对应的顶点叫做可达的顶点,其它顶点叫做不可达顶点。显然,可达顶点只存在于包含顶点1的连通分支中。以下的讨论都是针对包含1号顶点的连通分支。
  现在提出两个结论:
  结论1:若顶点1到顶点x有一条经过3个或3个以上顶点的路径,则顶点x是可达的。
  证明:如果依次进行路径上的边所代表的操作,则显然小球最后会在x号洞里。我们只需让不在这条路径上的边(以下称作“多余边”)代表的操作全部成为空操作即可。
  而这一点是很容易做到的。以路径上有且只有3个顶点的情况为例。把这3个顶点分别记作A、B、C。当小球在A洞里时,可以进行B、C之间多余的边(如果有的话)所代表的操作,这时,这些操作全部为空操作。同理,当小球在B洞里时,可以使A、C之间的多余操作成为空操作;当小球在C洞里时,可以使A、B之间的多余操作成为空操作。与A相连但不与B、C相连的多余边所代表的操作可以在小球不在A洞时进行,只与B或C相连的多余边可同理处理。与A、B、C均不相连的多余边,则可以在任意时刻处理掉。证毕。
  结论2:不可达的顶点至多只有一个。
  证明:由结论1易知,与顶点1的距离(到顶点1的最短路长度)大于1的所有顶点皆可达。对于顶点1本身和与顶点1直接相连的顶点,分以下几种情况讨论:
if 顶点1为孤立顶点 then&nbsp
 不存在不可达的顶点&nbsp
else if 顶点1在某个环上 then&nbsp
 不存在不可达的顶点//因为顶点1到环上的顶点必存在经过3个或更多顶点的路径,顶点1到环外的顶点总可以先走一遍环
else if 与顶点1关联的边全部为单边 then&nbsp
 有且只有顶点1不可达//因为一旦进行了与顶点1关联的操作,小球离开了1号洞,它就不可能再回来了
else if 与顶点1关联的边中有一组重边(设这组重边连接的另一顶点为a) then&nbsp
 if 除重边(1,a)以外还有重边与顶点a关联 or 顶点a在某个环上 then&nbsp
  不存在不可达的顶点//顶点1到顶点a存在这样一条路径:先走重边(1,a)中的一条,再走a的另一组重边或环;顶点1到自身存在这样一条路径:先走重边(1,a)中的一条,再走a的另一组或环,再走重边(1,a)中的另一条;顶点1到与顶点1相连的其它任一顶点b存在路径1-a-1-b:以上路径经过的顶点数均>=3
 else&nbsp
  if (1,a)为奇重边 then 有且只有顶点1不可达 else 有且只有顶点a不可达//顶点1到与顶点1相连的其它任一顶点b存在路径1-a-1-b;重边(1,a)中的所有边无法成为空操作,因为如果想让重边(1,a)中的某一条成为空操作,小球必须离开顶点1和顶点a,而走了就回不来了
else if 与顶点1关联的边中有超过一组的重边(设顶点1与顶点a、b均以重边相连) then&nbsp
 不存在不可达的顶点//顶点1到自身存在路径1-a-1-b-1;顶点1到顶点a存在路径1-b-1-a;顶点1到顶点b存在路径1-a-1-b;顶点1到与顶点1相连的其它任一顶点c存在路径1-a-1-c:以上路径经过的顶点数均>=3
  有了上面两个结论以及关于不可达顶点的详细分析,剩下的工作就是判断顶点1和与顶点1直接相连的顶点是否在环上了。这一点也不难。为了找到含顶点1的连通分支,我们需要以顶点1为根做一次“灌水法”。实现时用BFS,并记录从顶点1到每个顶点的路径上的第2、3个顶点(记作r1、r2)。若在BFS的过程中碰到一条连接两个已访问的顶点的边,则比较这两个顶点的r1和r2。若r1不同,则顶点1在环上,否则若r2不同,则相同的r1在环上。
  这种算法的核心只是一次灌水法,复杂度为O(n2),题目所给的数据范围n<=20完全是小菜一碟了。
  依次把每项相乘,降幂排序,合并同类项即可。
  数据很弱,我是把每个式子的最大项数定为1000的。如果定为10000,则会MLE;)
  至于输入中的多余空格的问题,我是宁信其有不信其无。也就多加那么几条语句而已:)
  显然这个题是求一棵树中的最长路。以顶点a为根的子树中的最长路长度=max{顶点a的所有子树中最长路长度最大值(最长路不经过a的情况),顶点a的所有子树深度的最大值与次大值之和+1(最长路经过a的情况)}。注意到根结点有4棵子树,其余的C有各有3棵子树,H无子树,于是可用递归轻松解决问题。
  还是DP。把连有Cl原子的C原子看作树根,那么它有3棵子树。设f[n]表示一氯n烷的同分异构体数目,则有状态转移方程其中H为重组合的符号,表示从n个元素中可重复地取出m个的组合数,(用隔板原理想一下就会明白)。状态转移方程的第三项当且仅当n mod 3=1时存在。
  本题的结果不超过qword范围,因此不必使用高精度。
  只用最简单的搜索方法即可:从右到左依次搜索每一列中的每一个字母代表的数字(这样可以充分利用进位的信息)。每假设出一个字母的值,就检查一下它左边的每一列是否可能成立。检查也很简单:如果一列的3个字母的值都已确定,那么判断一下它在进位和不进位两种情况下是否有可能成立;如果仅确定了两个字母,那么计算一下剩下的那个字母在进位和不进位两种情况下分别应取的值,如果这两个值都已被别的字母占用,则剪枝;在检查最左一列时,如果发现它向“第0位”进位了,也可以剪枝。
  为了避免试探字母取值时屡次碰到已用过的字母,除了用布尔数组used记录每个数字是否用过(检查时要用)外,还可以建立一个链表,其中存储当前尚未用过的数字。这样会在一定程度上提高程序的速度。
  但奇怪的是,对于某一个字母,按怎样的顺序试探它的值对程序效率有很大的影响。从2004年11月NOIP到2005年4月,我一直是按从0到n-1的升序试探的,最快的程序通过全部10个数据也需要2s左右的时间。借鉴了一下静默守护的程序,发现他是按0,n-1,1,n-2,2,n-3...的顺序试探的,速度比我的快得多。我又编了两个程序,分别用n-1到0的降序和随机序进行试探,发现降序的程序比静默守护的还快,随机序稍慢。下面是某电脑上我编的三个程序的运行时间比较:
试探顺序运行时间
升序1.970s~1.987s
降序0.011s~0.016s
随机序1.026s~1.042s
  现在我无法从理论上说明逆序和随机序哪个更优。我感觉,既然总搜索量是一定的,影响出解速度的因素只是解在搜索树中的位置,也就是说,无论哪种试探顺序,出解时间的期望都是一样的,只是本题的测试数据可能恰好适于降序试探。专门用来对付降序的测试数据也是可能存在的。所以,我比较提倡随机序。在压缩包中,我提供了升序(1142up.pas)、降序(1142down.pas)和随机序(1142rnd.pas)三个程序。
  以下叙述中,“单词”均指合法单词。
  举个例子说明:若为单词转编码,如求单词ACF……的编码,则设一累加器,先累加以AB开头的单词的个数,再累加以ACB开头的单词的个数(这个数为0,但若已知6个字母的位置,B拐到了第2行,则可能不为0),再累加以ACD开头的单词的个数,再累加以ACE开头的单词的个数……最后加1即得答案。若为编码转单词,如求第n个单词,同样设一累加器s,先累加以AB开头的单词的个数,若s>=n了,说明第二个字母就是B,否则继续累加以AC开头的单词的个数……直到s>=n,这样第二个字母就确定了。将最后一次累加的数减去,用类似的方法确定第三、第四……个字母,直至结束。
  现在的问题是:如何求出以某给定序列开头的单词的个数?这个问题是用记忆化搜索解决的。用f[a,b,c,d,e](5>=a>=b>=c>=d>=e>=0)表示把前a+b+c+d+e个字母填入第1行的前a个格,第2行的前b个格……第5行的前e个格,且已经确定位置的字母各就各位时可能的单词数,那么f[0,0,0,0,0]就表示以给定序列开头的单词数。下面以求以AC开头的单词数为例说明递归求f数组的方法:
第一层递归安置字母A。因其位置已固定,故f[0,0,0,0,0]=f[1,0,0,0,0],进入第二层递归计算f[1,0,0,0,0]。
第二层递归安置字母B。B的位置尚未固定,于是枚举所有合法位置(合法位置指左、上方均已填有字母的位置,认为第0行与第0列均已填满。此例中为12、21),分别进入第三层递归计算f[2,0,0,0,0](这个值等于0,下面会讨论其原因)与f[1,1,0,0,0]。f[1,0,0,0,0]即等于这二者之和。
第三层递归安置字母C。这层递归的过程与第一层递归类似。更深层递归的过程与第二层递归类似。若在某一次递归中,需要计算的f值已经算出,则不必再递归下去,直接退出即可。
  因为每次计算单词个数时给定的序列都不同,所以每次都必须从头递归。
  程序的实现用了一点小技巧。上文中说,B的合法位置有两个,分别是12和21。但实际上,12这个位置已经被字母C占据,只是在第二次递归时程序并不知道这一点。请看程序第26行中的这一条件:len[x[c]]+1=y[c]。如果某个位置已固定的字母的位置已被别的字母占据,那么这个条件就为假,从而求出的单词数就成了0。当然,可以在递归之前把已被占据的位置做上标记,但因为需要搜索的状态总数本来就不多(只有252种),做标记不会显著提高时间效率,反而会增加编程复杂度。
  除上段所说的以外,还有几点可以优化的地方,如以ACB开头的单词数可不经搜索直接得到0,再如当递归深度超过了所有已固定的字母时,可直接利用中的DP获得结果,而不须递归下去。然而,不加这些优化的算法的复杂度已经很低了(最坏情况下为25(25个位置)*25(每个位置可能放25个字母)*252(记忆化搜索的状态总数)*5(每个状态最多有5个合法位置)=787500),这些优化都显得不太值得。
  以下叙述中,“单词”均指合法单词。
  用count[a,b,c,d,e](5>=a>=b>=c>=d>=e>=0)表示把前a+b+c+d+e个字母填入第1行的前a个格,第2行的前b个格……第5行的前e个格后,剩下字母的填法数。count数组可用DP计算,其思路为:如果某一行已填的字母数少于上一行已填的字母数(可认为第0行填了5个字母),那么第a+b+c+d+e+1个字母就可以填到这一行的下一个位置。具体实现可参考程序中的calcount过程。
  有了这个数组,解决问题就简单了:
若为单词转编码,则依次处理每个字母,累加把它放在给定单词中它所在位置以前的所有合法位置(指左、上方均已填有字母的位置,认为第0行与第0列均已填满)可构造出的单词数,最后把结果加1。比如,若A,B,C三个字母分别放在11,21,31三个位置,则在处理B时,累加将其放在位置12时的单词数(即count[2,0,0,0,0]);处理c时也累加将其放在位置12时的单词数(即count[2,1,0,0,0]),但不能累加把它放在位置22时的单词数(因为如果这样位置12的字母将大于位置22的字母,不合法)。
若为编码转单词,则依次枚举每个字母可能的位置并累加这时产生的单词数。若某时刻累加和超过或等于了编码,则当前处理的字母应放的位置就找到了,减去最后一次累加的数值,继续处理下一个字母,直至结束。
  把与7有关的n位数分成两种:
不含数字7,但是7的倍数的n位数。用rem[d,r]表示不含数字7且除以7除r的d位数的个数。易写出如下伪代码:
rem[0,0]:=1;for i:=1 to 6 do rem[0,i]:=0;
for d:=1 to n do
for i:=0 to 6 do
for j:=0 to 9 except 7 do //j表示第d位数
inc(rem[d,(i*10+j) mod 7],rem[d-1,i]);
  不含数字7,但是7的倍数n位数的个数就是rem[n,0]。
含数字7的n位数。显然这一类数的总数为(其中i表示数字7的个数)。
  首先把一盘CD装不下的歌全部踢掉!!!
  剩下的工作就是DP了。用f[i,j]表示在前i首歌中选出j首在CD上占的最小时间。这里说的时间包括除最后一盘外的CD上浪费的时间。f[i,j]=min{f[i-1,j],sum(f[i-1,j-1],第i首歌的长度)}。这里的sum的含义是:如果最后一盘CD剩下的时间正好可以放下第i首歌,那么直接相加即可,否则要再加上最后一盘CD剩下的时间(这些时间已被浪费了)。找一个最大的j使f[n,j]<=t*m,这个j就是答案。
  f数组可做成滚动数组。这个算法的复杂度为O(n2),绝不会超时。
  同一样,本题也可以用树型DP来解。但本题的状态设计比版本1要复杂一些。具体来说,有以下三个状态:
need[n,0]:表示以n为根的子树中,除n以外的结点全被了望到,而只有n本身没有被了望到所需的最少士兵数;
need[n,1]:表示以n为根的子树中的全部结点都被了望到,但n结点上没有士兵时所需的最少士兵数;
need[n,2]:表示以n为根的子树中的全部结点都被了望到,且n结点上有士兵时所需的最少士兵数。
  每个结点三个need值的计算,还要分叶子结点与非叶子结点来讨论。
对叶子结点i:显然,need[i,0]=0,need[i,1]=maxint(maxint表示不可能),need[i,2]=1。
对非叶子结点i(用j表示它的任一个儿子):
need[i,0]的计算:因为结点i本身不可以被了望到,所以它的任何一个儿子结点上都不可以放士兵,即need[i,0]应该等于所有need[j,1]之和。若有某个need[j,1]为maxint,那么need[i,0]亦为maxint。
need[i,1]的计算:因为结点i本身没有士兵,所以它的每个儿子都必须被i的子树中的结点上的士兵了望到,而且至少一个儿子结点上有士兵。也就是说,need[i,1]应等于所有min{need[j,1],need[j,2]}之和,只是在所有儿子都满足need[j,1]&ltneed[j,2]时,需要往need[i,1]上加上最小的一个need[j,2]-need[j,1]。
need[i,2]的计算:因为结点i本身就有士兵了,所以它的儿子结点怎样都可以,即need[i,2]等于所有min{need[j,0],need[j,1],need[j,2]}之和加1。
  设根结点为root,则min{need[root,1],need[root,2]}就是答案。
  虽然数据范围小得搜索都能过,但我还是要讲一下求X、Y顶点数相等的二分图最大权匹配(也叫最佳匹配)的KM算法。
  KM算法是通过给每个顶点一个标号(叫做顶标)来把求最大权匹配的问题转化为求完备匹配的问题的。设顶点Xi的顶标为A[i],顶点Yi的顶标为B[i],顶点Xi与Yj之间的边权为w[i,j]。在算法执行过程中的任一时刻,对于任一条边(i,j),A[i]+B[j]>=w[i,j]始终成立。KM算法的正确性基于以下定理:
  若由二分图中所有满足A[i]+B[j]=w[i,j]的边(i,j)构成的子图(称做相等子图)有完备匹配,那么这个完备匹配就是二分图的最大权匹配。
  这个定理是显然的。因为对于二分图的任意一个匹配,如果它包含于相等子图,那么它的边权和等于所有顶点的顶标和;如果它有的边不包含于相等子图,那么它的边权和小于所有顶点的顶标和。所以相等子图的完备匹配一定是二分图的最大权匹配。
  初始时为了使A[i]+B[j]>=w[i,j]恒成立,令A[i]为所有与顶点Xi关联的边的最大权,B[j]=0。如果当前的相等子图没有完备匹配,就按下面的方法修改顶标以使扩大相等子图,直到相等子图具有完备匹配为止。
  我们求当前相等子图的完备匹配失败了,是因为对于某个X顶点,我们找不到一条从它出发的交错路。这时我们获得了一棵交错树,它的叶子结点全部是X顶点。现在我们把交错树中X顶点的顶标全都减小某个值d,Y顶点的顶标全都增加同一个值d,那么我们会发现:
两端都在交错树中的边(i,j),A[i]+B[j]的值没有变化。也就是说,它原来属于相等子图,现在仍属于相等子图。
两端都不在交错树中的边(i,j),A[i]和B[j]都没有变化。也就是说,它原来属于(或不属于)相等子图,现在仍属于(或不属于)相等子图。
X端不在交错树中,Y端在交错树中的边(i,j),它的A[i]+B[j]的值有所增大。它原来不属于相等子图,现在仍不属于相等子图。
X端在交错树中,Y端不在交错树中的边(i,j),它的A[i]+B[j]的值有所减小。也就说,它原来不属于相等子图,现在可能进入了相等子图,因而使相等子图得到了扩大。
  现在的问题就是求d值了。为了使A[i]+B[j]>=w[i,j]始终成立,且至少有一条边进入相等子图,d应该等于min{A[i]+B[j]-w[i,j]|Xi在交错树中,Yi不在交错树中}。
  以上就是KM算法的基本思路。但是朴素的实现方法,时间复杂度为O(n4)——需要找O(n)次增广路,每次增广最多需要修改O(n)次顶标,每次修改顶标时由于要枚举边来求d值,复杂度为O(n2)。实际上KM算法的复杂度是可以做到O(n3)的。我们给每个Y顶点一个“松弛量”函数slack,每次开始找增广路时初始化为无穷大。在寻找增广路的过程中,检查边(i,j)时,如果它不在相等子图中,则让slack[j]变成原值与A[i]+B[j]-w[i,j]的较小值。这样,在修改顶标时,取所有不在交错树中的Y顶点的slack值中的最小值作为d值即可。但还要注意一点:修改顶标后,要把所有的slack值都减去d。
  按每相邻两个字母是否相同可以把长度为7的春联分为27-1=64类,设第i类有count[i]句,则答案为所有的C(count[i],2)之和。
  这个题是典型的分层图BFS。在最坏情况下,因为钥匙有10种,故可以用0至个数对应已取得的钥匙的1024种状态,把图分成1024层。这个图并不大,只有15*15*个结点。因为从一个结点出发只有4种走法,所以完全不必担心TLE。
  不要受“高二数学改编”误导而想排列组合,这个题要用DP。用a[i]表示传了i次后球又回到甲手里的传法总数,b[i]表示传了i次后球到了乙手里的传法总数,当然,b[i]也表示传了i次后球到了丙、丁等人手里的传法总数,因为乙、丙、丁等人的地位是一样的。很容易写出状态转移方程:
a[i]=(p-1)*b[i-1](倒数第二次只能传给乙、丙等p-1个人)
b[i]=a[i-1]+(p-2)*b[i-1](倒数第二次或者传给甲,或者传给丙、丁等p-2个人)
显然a[1]=0,b[1]=1。
  但是,用这种方法递推太慢了,n取最大值231-1时显然要超时。换一种思路:假设要传i+j次,考虑传了i次以后的情况,列出如下方程:
a[i+j]=a[i]*a[j]+(p-1)*b[i]*b[j](传了i次后,球可以又回到甲手里,也可以在其他p-1个人手里)
b[i+j]=a[i]*b[j]+b[i]*a[j]+(p-2)*b[i]*b[j](传了i次后,球可以在甲手里,也可以在乙手里,也可以在其他p-2个人手里)
同样,a[1]=0,b[1]=1。
  用这组方程可以很容易设计出O(logn)的算法。
  在以下的叙述中,sum(a,b)表示从a加到b的和(a<=b,且a,b为整数)。
  我们把小于等于(n+1) div 2的号叫做小号,其余的号叫做大号。显然,小号都排在奇数位上,大号都排在偶数位上。那么,在绝对值和的计算过程中,不在端点的大号都做了两次被减数,不在端点的小号都做了两次减数,而在端点大号或小号的则只做了一次被减数或减数。而随n的奇偶性不同,最后一个数是大号还是小号也不同,也就是说最后一个数的“功能”(被减数还是减数)不同。于是我们对n的奇偶性分情况讨论。
  当n是奇数时,队首的1少做了一次减数,队尾的n div 2+1也少做了一次减数,于是绝对值和=2(sum(n div 2+2,n)-sum(1,n div 2+1))+1+n div 2+1。这个式子最终可以化简到n*(n-1) div 2。
  用last(n)表示n个人的队列中队尾的编号。当n是偶数时,队首的1少做了一次减数,队尾的last(n)少做了一次被减数,于是绝对值和=2(sum(n div 2+1,n)-sum(1,n div 2))+1-last(n)。剩下的工作就是求last(n)了。当n为奇数时,显然last(n)=n div 2+1。当n为偶数时,把所有的小号全部去掉,大号全部减去n div 2,发现现在的队列变成了n div 2个人时的队列,即last(n)=n div 2+last(n div 2)。这样一直递归下去,直到n变成奇数为止。
  算法复杂度仅为O(logn)。
  本题我采用的是DFS算法。判重的问题我是这样解决的:对每一个数串,按某种规则将它划分成若干个数。如果这些数恰好是搜索到这一个数串时栈中的数,那么就把答案加1,否则就不加。这样就可以保证相同的数串只算一个。这个规则就是:从后往前划分,使每个数都尽可能大。当然,每个数的开头都不能是0。
  举个例子:假设输入的数是99,那么按照规则,数串123499的划分应该是12 34 99。搜到这个数串时,如果栈中的数恰好就是12和34,那么答案加1;如果栈中的数是1 2 34或者1 2 3 4,则不加1。
  题目叙述有一点毛病:可以不把金子炼成别的金属而直接送出境。
  算法嘛,就是用Dijkstra求点1到其它各点的最短路,再用Dijkstra求其它各点到点1的最短路,答案便是每个点的两个最短路长度与单价的一半的和的最小值。不说“即可”了,因为这个Dijkstra要用堆来优化,这个编程挺费事。另外,两个Dijkstra可以一起做,这样当某个点的两个最短路长度都已经求出来时,可以立即更新答案。如果某个Dijkstra中堆顶元素的最短路长度已经超过了答案,这个Dijkstra就可以停止了。
  用ak表示第k个猴子走后剩余的桃子数。我们要求的就是a0和an的最小值。由题意可列出递推式:ak+1=(ak-1)*(n-1)/n……①
  这个递推式的形式类似等比数列的递推式,但多一个-1。为了将它整理成标准的等比数列的形式,我们设bk=ak+x……②让{bk}成等比数列:bk+1=bk*(n-1)/n……③而x是未知数。下面想办法求x:
  把②式代入③式,得ak+1+x=(ak+x)*(n-1)/n即ak+1*n=ak*(n-1)-x……④
  同时把①式整理成如下形式:ak+1*n=ak*(n-1)-(n-1)……⑤
  比较④⑤两式,得x=n-1
  回过头来我们再关注数列{bk}。显然bn=b0*[(n-1)/n]n因此b0 min=nn,bn min=(n-1)n代入②式即求得答案:a0 min=nn-(n-1),bn min=(n-1)n-(n-1)
  先讨论比较简单的直线的情况。若直线没有切着蛋糕,则答案显然。下面只讨论直线切着蛋糕的情况。观察一下右面这个图,图中直线的上方有2块蛋糕,而下方有3块。每一块的外边(这个“边”不念轻声)都有一个特征:左转,比如下方较大的那一块,它的外边是A-4-5-F,是左转的。每一条内边也有一个共同特征:右转。还以下方较大的那一块为例,它的内边是E-12-13-B,是右转的。恰好,每一块蛋糕都有且仅有一个外边,所以我们只需统计一下在直线上下方各有多少段左转的边界,第一问就已经解决了。
  这一结论也适用于圆外蛋糕数目的统计,但对于圆内就不适用了,因为圆内的每块蛋糕没有“外边”、“内边”的概念,它的边数不固定,每条边的旋转方向也不固定。比如左面那个图,圆内较大的一块竟有3条边,且左边不转,下边和右边都是右转。然而很不幸,题目问的恰恰就是圆内的块数。我们必须另辟蹊径。
  刚才讨论直线的情况时,我们没有充分利用内边的信息。其实,内边的条数与蛋糕被切成的块数也是有直接关系的。如果一块蛋糕有n个内边,那么它的切口就有n+1个,如下方较大的一块有1个内边,它的两个切口是AB和EF。而与这两个切口相连的上方的块一定不同!这暗示我们,直线一边的内边的数目,与另一边的蛋糕块数有某种联系!我们从原始的情况来想。首先想象一块蛋糕被直线一分为二。然后,蛋糕的上方收缩,整个蛋糕成为“凹”字形,直线恰好经过它的两条“腿”。这时,直线下方多了一条内边,上边多了一块蛋糕。于是我们发现了,直线上方的蛋糕块数等于下方内边数加1,下方的蛋糕块数等于上方内边数加1。这个算法也适用于求圆内的蛋糕块数,因为圆外蛋糕的内边同样具有“右转”的特征。还是看一下左边的图,由于圆外上方的那一块有一条内边(点8附近),故圆内的蛋糕块数为2。
  剩下的问题就是判断线段是否与直线相交、线段是否进入圆或从圆中出来的几何问题了。相信这些你都会的:)
  把能取得的若干连续重量用区间存储,比如重量3,4,5可用区间[3,6)表示。阅读下文,你将体会到用半开半闭区间与用闭区间相比的优越性。
  开始时解集中只有一个区间[0,1)。然后处理每一个物体。设当前处理的物体重量为w,那么把原来解集A中的每个区间都向右平移w得到一个集合B,合并A和B得到新的解集。合并时选用归并排序,这样可使原解集与新解集都保持有序性。当即将加入新解集的区间[c,d)与新解集最后一个区间[a,b)重叠或恰好相连(其充要条件为c<=b)时,将两区间合并为一个区间。这里体现了半开半闭区间的优越性之一:若用闭区间,则条件为c<=b+1,浪费一次加法运算。当所有物体都处理完毕后,累加每个区间中整数的数目。这里体现了半开半闭区间的另一点优越性:半开半闭区间[a,b)中的整数个数为b-a,而闭区间[a,b]中的整数个数为b-a+1,相比之下,半开半闭区间又节省了一次加法运算。因总重量不能为0,故答案为累加和减1。
  思路已经有了,在具体实现时又遇到这样一个问题:在某一时刻区间最多会有多少呢?理论上讲,若物体数为n,则总重量数为2n(包括0),在最坏情况下,这些重量都不连续,就需要2n个区间。但在最大规模随机数据的情况下,实验得知区间数一般不超过200000个,因此题目所给的内存是足够的。
  以上算法还有一点值得优化。这道题不能使用题那样的朴素算法,原因就在于重量数太多,而区间恰恰解决了这一矛盾。为了让区间的优势发挥得更充分,我们在执行上述算法之前先对所有物体的重量进行一次排序,然后从小到大依次处理。这样,在计算过程中,重量就会最大程度地向轻的方向聚集,区间也会最大程度地合并,时间、空间需求都大大降低:未经优化的算法用时6.9s,而优化后的算法仅用2.3s;实验发现,在输入最大规模的随机数据时,优化后的算法在运行过程中区间的最大数目仅为几十至几百个,远远小于优化前的十万个左右。但是,当物体个数比较少而重量又很分散的情况下,区间数接近物体数的指数函数,如物体数为18,最大重量为100000的情况下,最大区间数仍可超过100000,因此建议仍把最大区间数定在200000左右。
  这道题的算法是O(n*m)的DP。显然可以用列数作阶段。状态需要好好设计:用2*m表示最后一列属于第m个凹口(第0个凹口定义为最左边的空白),用2*m+1表示最后一列左边已经有了m个凹口,而这一列本身是凸出的。用f[n,m]表示阶段n状态m时的最大适用度和,则显然有
f[n,m]=min{f[n-1,m-1],f[n-1,m]}+a[i]+b[i],当m为奇数
f[n,m]=min{f[n-1,m-1],f[n-1,m]}+b[i],当m为偶数
  其中,a[i]表示第1行第i列土地的适用度,b[i]表示第2行第i列土地的适用度。若某个f[n,m]对应不可能情况,则其值为负无穷大。f[n-1,m-1]表示最后一列与倒数第二列高度不同的情况,f[n-1,m]表示这两列高度相同的情况。
  算法不难,但编程上需要在细节上下功夫,尽可能地优化常数系数。下面我将对照程序,说明怎样尽可能缩短运行时间:
a[i]、b[i]这两个值需要经常用到,因此需对其进行预处理。注意到在状态转移方程中,是加a[i]+b[i]还是加b[i]与m的奇偶有关,于是用a[true,i]表示原来的a[i]+b[i],用a[false,i]表示原来的b[i],这样在j循环中就可以减少一次判断了。
显然f[n,*]只与f[n-1,*]有关,因此f应做成滚动数组f[boolean,0..maxm](此处的maxm应为题目中的maxm*2+1)。因为在j循环中要屡次访问f[odd(i),*]和f[not odd(i),*],因此用b1、b2两个布尔变量暂存odd(i)和not odd(i)的值,以减少odd函数的调用次数。
DP算法的一个特点是它计算的信息量很大,而算出来的信息往往有一部分是没有用的。如右图,整个矩形表示本题中DP计算的总信息量,而其中只有蓝色部分是有用的,这部分还占不上总信息量的一半。因此有必要仅计算蓝色部分所代表的信息,这就是j循环前计算的k、l的用途。这一操作可以显著提高程序效率。
继续观察状态转移方程。方程中有求较小值这一步,用程序中的方法做而不另外设一个临时变量,既可以省一个变量,在某些情况下还可以省去一次赋值。这样做导致了j循环必须用downto而不能用to,请仔细思考一下为什么。
再介绍一个好东西:{$R-},即关闭range check。在保证程序不会发生数组越界等错误时,range check是既多余又耗时的。加上这一编译开关后,程序的运行时间缩短了一半以上,提交后用2.3s AC(虽然这个速度仍不是很快)。P.S.其实还有一个编译开关{$Q-},即关闭overflow check,也能提高程序速度。
  显然当广告面积最大时,要有一个建筑物被刷满。枚举这个建筑物的编号,然后寻找把这个建筑物刷满的广告向左、向右最远分别能延伸到什么位置。
  用l[i]、r[i]分别表示建筑物i被刷满时广告向左、向右最远能延伸到的位置。从左到右依次计算每个l[i]:初始时令l[i]=i,然后while h[i]<=h[l[i]-1] do l[i]:=l[l[i]](h[i]表示第i个建筑物的高度)。r[i]的计算方法类似,只是方向为从右向左。为计算方便可令h[0]=h[n+1]=0。答案就是所有h[i]*(r[i]-l[i]+1)中的最大值。
  由于上述的while循环类似于并查集中的路径压缩,故本算法的复杂度可近似认为是O(n),只是常数系数较大些。
  P.S.我在竞赛时,看到n的最大值是1000000,就不自觉地想O(nlogn)复杂度的算法,像静态排序二叉树,像堆,等等等等……花了两个小时,编了五个程序,换来一个TLE:(
  后来想到这个算法,5分钟编完程,2.2s AC。当时,我觉得这个算法就是O(n)的,看到用时这么长,感觉很奇怪。后来证明复杂度为O(n)时遇到了困难,才发现还有个常数系数在捣鬼。
  用unary[n]表示n的一进制表达式的最短长度。这个表达式的最后一步可以是加,也可以是乘。如果是加的话,那么几乎肯定有一个加数是1(这个我没有证明);如果是乘的话,则需要枚举n的约数i(大于1,小于等于trunc(sqrt(n)))。由此得状态转移方程:unary[n]=min{unary[n-1]+1,unary[i]+unary[n div i]}。
  下面解释一下为什么我认为“如果最后一步是加,则几乎肯定有一个加数是1”:整个unary数组的总趋势是增加的,而且越是在开头,增加得越快,较往后的位置基本在同一数值附近摆动。更为甚者,开头5个元素恰好是1,2,3,4,5,增长得不能再快了(因为unary[n+1]-unary[n]<=1)。那么unary[1]+unary[n-1]<=unary[2]+unary[n-2]<=unary[3]+unary[n-3]<=unary[4]+unary[n-4]<=unary[5]+unary[n-5],而这一串不等式中,不仅所有的<=全取等号的可能性微乎其微,而且在一般情况下,unary[5]+unary[n-5]比unary[1]+unary[n-1]要大不少。因此可近似认为,若unary[i]+unary[n-i](i<=n-i)中的i再增大,由于unary[i]的增长速度快于unary[n-i]的下降速度,这个式子的值是不会变得小于unary[1]+unary[n-1]的。实践也证明了这一点。
  但是,如果对于每一个n,从2至trunc(sqrt(n))依次检验每个数是否为n的约数,则算法复杂度为O(n3/2),运行将花费20~30秒,严重超时。于是换一种思路:给每个数开一个栈(当然也可以用队列)。假设当前处理到n,则i就在i的倍数中不小于n且最小的那一个的栈中。处理n(n>=2)时,首先令unary[n]=unary[n-1]+1,然后对于n的栈中的每一个元素f,(1)检查unary[f]+unary[n div f]是否小于当前的unary[n],若小于则更新;(2)将f从n的栈中取出,放到n+f的栈里。我的程序中,栈是用数组模拟的链表实现的。另外,每一个数i不必在开始时就放到i的栈里,可等到处理到sqr(i)时再放进sqr(i)的栈,因为在此之前,若i在n的栈里,则若n=i,显然i无用,若n>i,因为n div i&lti,故i也无用。
  这种改进算法避免了检验一个数是否是另一个数的约数,只用了2s多就AC。
  呼呼,思路比程序还长,有些地方还显得很突兀,不知怎么能想到。我问了21天,终于搞懂了。谢谢Wanght_1的整理。
  此题的DP用逆推。至于为什么用逆推,下文会讲。用F[i]表示第i题以后的题的最大总得分(i就是阶段)。状态转移方程为:
    F[n]=0(显然)
    F[i]=max{F[j]+W[i,j]}(i&ltj<=n)
  其中,W[i,j]=(S[j]-S[i])*i-T
    S[n]为前n道题的分值和。
  依次计算F[n-1]至F[0],F[0]就是答案。
  用B[i,k]表示阶段i的决策k(决策k表示第i题之后的那一段到第k题为止)的结果,则B[i,k]=F[k]+W[i,k]。
  下面寻找n>=k1>k2>i,且S[k1]>S[k2]时,B[i,k1]>=B[i,k2](决策k1不劣于k2)的充要条件:
    B[i,k1]>=B[i,k2]
   F[k1]+(S[k1]-S[i])*i-T>=F[k2]+(S[k2]-S[i])*i-T
   F[k2]-F[k1]<=(S[k1]-S[k2])*i
   (F[k2]-F[k1])/(S[k1]-S[k2])<=i
  令g(k1,k2)=(F[k2]-F[k1])/(S[k1]-S[k2]),则B[i,k1]>=B[i,k2]
g(k1,k2)<=i
  同理有B[i,k1]&ltB[i,k2]
g(k1,k2)>i。
  现在说一下为什么用逆推而不是顺推。观察上面推导过程中两个红色的*i。括号外的这个系数是取决于一段开头的位置的。如果用逆推,这个系数对应的是阶段,因此上面的推导能够进行。如果用顺推,这个系数将对应于决策,而我们现在讨论的就是不同决策的优劣,这个系数不同将使推导遇到相当大的困难。
  维护一个决策队列q,使qj递减,g(qj,qj+1)也递减。(谁知道怎么想到的呢?)
  依次计算F[n-1]至F[0]。下面考虑计算F[i]时的情形。
目前决策i+1尚未进入队列。如果i+2<=n且a[i+2]=0,那么显然i+1不是最优决策,此步不必进行。否则,将决策i+1放到队列末尾,这样保证了qj递减。但对于此时队尾的三个决策a,b,c,g(a,b)>g(b,c)不一定成立。关注一下g(a,b)<=g(b,c)的情况:
若g(a,b)=B[i,b],决策a不劣于b。
若g(a,b)>i,则g(b,c)>i,B[i,b]&ltB[i,c],决策c优于b。
无论哪种情况,b要么不是最优决策,要么是最优决策但不唯一。因此可不断删除队列中倒数第二个决策,直至队列中决策不足三个或g(a,b)>g(b,c)为止。
现在阶段i的可能决策都已经在队列中了。用a,b表示队列中的头两个决策。若g(a,b)>i,那么a不是最优决策。由于g(a,b)>i-1,因此a在以后也永远不可能成为最优决策,故可将其删除。不断删除队首决策直至g(a,b)<=i。这时由于队列中所有g(a,b)均<=i,故决策的效益是递减的,所以a为最优决策,F[i]=B[i,a]。
  由于队列在整个过程中只被遍历一次,故算法复杂度为O(n)。
  上述题解写于2005年4月。7月,我学到了一种较好地理解上述算法的方法:
  如右图,把队列中的决策画到一个坐标系中(注意,右边的点位于队首,左边的点位于队尾)。那么,阶段i的最优决策应满足的性质是:过此点作一条斜率(指绝对值,下同)为i的直线,其它所有决策应都位于此直线下方(或直线上)(记作性质①)。下面我们来看如何利用这个图来理解上述算法:
g函数的值就是两点连线的斜率;
在队尾删除的决策,在图中处于点C的位置,而点C无论何时都不会满足性质①;
在队首删除的决策,在图中处于点A的位置,由于i递减,直线的斜率会越来越小,点A以后永远不会满足性质①。
  由于算法可以利用斜率来理解,因此它被称为斜率优化法。
  由此也可以理解为什么本题使用逆推:因为斜率对应着阶段,而本题中,连续提交的一段题的系数是取决于这一段左端的位置的,左端为阶段而右端为决策,所以要用逆推。
  这种算法要求S具有单调性。那么,扩展一下,如果有某些题的分值为负怎么办呢?可以证明,若某一段(最左一段除外,记作A)的开头一部分(记作B)总分非正,则这一定不是一种最优划分。分两种情况考虑:
若A总分值非正,则把A与上一段合并,这样不仅省了一次提交(少减了T分),而且减小了A的系数;
若A的总分为正,则这一段中除去B后剩下的一段(记作C)总分仍为正,故可将B移到前一段去,这样一来减小了B的系数,二来增大了C的系数。总之,若某一段的分值非正,则在这一段前划分的决策一定不是最优决策。
因此,可从最后一题向前扫描,累加分值,得到一个正分值后就把累加的这一段压缩成一道题,并把累加和清零。这样压缩以后,除了第一题分值可能非正以外,其余题的分值均为正数,于是便可以使用斜率优化法了。
  用Bellman-Ford求2次最小费用增广路即可。当然,第一次用Dijkstra会更快些。
  首先将词典排序,然后从文本中的每个单词中提取出字母,用二分查找的方法确定这些字母组成的单词是否在词典中。用一般的方法对词典排序可能太慢,可以用一种部分桶排的方法:首先把所有单词按第一个字母排序,这样首字母相同的单词就各自聚到了一起;然后把首字母相同的单词按第二个字母排序……这样的排序方法避免了直接比较单词时,前面的字母都相同带来的冗余比较,对于此题来说是比较适合的排序算法。
  当然,建trie树的方法也是可行的。
  若数列中有这样两个数,前面的那个数比后面的大,那么这两个数就称为一个逆序对。下面证明最小的交换次数等于数列中逆序对的个数x。
x次是足够的。当数列中存在逆序对的时候,一定有一个逆序对是相邻的两个数,否则,整个数列就成了不递减的,与数列中存在逆序对矛盾。那么,每一次交换一定可以消灭一个逆序对,因此x次是足够的。
x次是必需的。显然,一次交换最多只能消灭一个逆序对,因此要消灭全部x个逆序对至少需要x次交换。
  现在的问题就是如何高效地求出数列中逆序对的数目。如果先把数列离散化,再用静态排序二叉树等树形数据结构计算每个数之前比它大的数有多少个,然后累加的方法,那么离散化和树形数据结构的复杂度均为O(nlogn),这个复杂度的算法被执行了两次。能不能只执行一次呢?答案是肯定的:利用归并排序。
  回忆一下归并排序的过程:将数列分为长度相等或相差1的两段,分别归并排序后,把两段看成两个队列,每次取队首元素的较小值,排成一列后就得到有序的数列。观察排序前的数列,它当中的逆序对可以分为3类:1.两个数都在前一半的;2.两个数都在后一半的;3.一个数在前一半,一个数在后一半的。前两种情况的逆序对的个数可在对那一半的归并排序中计算,对整个序列的归并排序的主程序中只需计算第3种情况。注意到归并过程中,当第二个队列的队首元素小于第一个队列的队首元素时,第二个队列的队首元素与第一个队列中剩下的元素均构成了逆序对,这时在答案上加上第一个队列中剩余元素的个数即可。只加这么一条语句,就可以在归并排序的过程中顺便求出逆序对的个数了。
  这道题的输入中有前导的0,请加以处理,否则会WA掉!
  下文提到一个数的“前一半”时,若这个数有奇数位,“前一半”均包括中间一位。
  给每个完美数编一个号,使得相邻的完美数的编号也相邻。编号方法如下:
若其位数为偶数,则取其前一半,在最高位前一位加1。如456654的编号为1456。
若其位数为奇数,则取其前一半,在最高位上加1。如121的编号为22,92429的编号为1024。
  由编号还原出完美数的方法如下:
若编号首位为1且第二位不为0,则说明对应的完美数为偶数位。去掉首位的1得到的就是完美数的前一半。
若编号首位大于1或前两位为10,则说明对应的完美数为奇数位。若首位大于1则在首位减1,否则在第2位减1,得到的就是完美数的前一半。
  这样子算法就很简单了:求出前一半与a相同的完美数的编号。若a比这个完美数小,则将编号减1。在编号上加上k,还原成完美数,就得到答案。
  在USACO上学到了一种既简单又优秀的“切法”,它不仅适用于正方体,同时也适用于长方体。
  这种“切法”依次计算每个长方体不与以前的长方体重叠的部分的体积,相加。计算过程是递归进行的。在计算第x个长方体时,调用cal(x-1,第x个长方体)。过程cal(m,长方体C)的内容是:从第m个长方体到第1个长方体依次判断与长方体C的相交情况,如果都不相交,则累加长方体C的体积;如果有一个长方体与长方体C相交了(不妨设是第y个),就把长方体C在第y个长方体外的部分切成若干块,对每一块D执行过程cal(y-1,长方体D)。切的方法是:如果长方体C有在第y个长方体左边的部分,就把它切下来;然后剩下的部分如果有在第y个长方体右边的部分,也把它切下来;上、下、前、后四个方向按同样方法处理。
  这种切法对于一般数据来说效率是很高的。它所遇到的最坏情况就是输入的长方体都是扁平的板状,交织成三维网络(当然,本题输入的都是正方体,没有这个问题),这种情况下此法会切出数量级为O(n3)的小长方体(感觉跟离散化了一样)。但大多数的小长方体不会再被切割了,因此这种切法的最坏时间复杂度可以近似认为是O(n3)。使用线段树可以把平均时间复杂度降至O(n2logn),但编程复杂度明显高于切法。
  BS题目把m<=400说成m<=50000吓唬人。另外,注意输入中相邻两项之间可能有多个空格。
  利用本题,我来讲一下差分约束系统。
  所谓差分约束系统,就是求关于xi的由一系列形如xi-xj>=a的不等式组成的不等式组的解。我们构建一个有向图,图中的每个结点代表一个xi。对于每个不等式xi-xj>=a,连一条从xj到xi的权为a的边。不妨设x1=0,那么令xi等于x1至xi的最长路的长度,这样就得到了一组可行解。若图中存在正权回路,即对于某些点最长路不存在,那么不等式组无解。
  为什么呢?先看有解的情况。因为若从xi到xj的有一条长为s的路,则要求xj-xi>=s。现在xj与xi的差是xi到xj的最长路的长度,那么对于从xi到xj的任一条路(设其长为s),自然有xj-xi>=s成立。再看无解的情况。设从xi到其本身有一条权为s的正权回路,则要求xi-xi>=s,即0>=s,这显然是不可能的。
  有了差分约束系统这个武器,解决本题就不难了。用Si表示01串前i位的和,规定S0=0。建一个含有N+1个结点的有向图,每个结点分别代表S0至SN。然后在图中连边:
由于每个数非0即1,故有0<=Si-Si-1<=1(1<=i<=N),即对于每个i,从Si-1向Si连一条权为0的边,从Si向Si-1连一条权为-1的边。
对任意长度为L0的子串,其中0的个数不小于A0,不大于B0,即其中1的个数不小于L0-B0,不大于L0-A0。故有L0-B0<=Si-Si-L0<=L0-A0(L0<=i<=N)。故对于每个i,从Si-L0向Si连一条权为L0-B0的边,从Si向Si-L0连一条权为A0-L0的边。
对任意长度为L1的子串,其中1的个数不小于A1,不大于B1,即A1<=Si-Si-L1<=B1(L1<=i<=N)。故对于每个i,从Si-L1向Si连一条权为A1的边,从Si向Si-L1连一条权为-B1的边。
  求S0到每个结点的最长路,就可以求出每个Si了。01串中的第i位si也就等于Si-Si-1。
  下文中,&S符号下方的i=1与上方的n均省略。
  首先,我们对均方差的公式&=sqrt(&S(xi-x)2/n)进行分析:根号显然是没有用的;对于一个特定的棋盘,n为定值,故根号下的分母也是没有用的。我们继续整理根号下的分子:
   &S(xi - x)2
  = &S(xi2 - 2xix + x2)
  = &Sxi2 - 2nx2 + nx2
  = &Sxi2 - nx2
  分析整理后的式子:对于一个特定的棋盘,n是定值,x也是定值(等于棋盘每个格子的权之和除以n)。因此,问题的目标就由均方差最小转化为平方和最小。显然这个问题可以用动态规划解决:对于每块棋盘,我们用四类处理方法:切去左边的一部分,切去右边的一部分,切去上边的一部分,切去下边的一部分。
  在动态规划的过程中需要屡次用到某块棋盘的权和,我们可以用预处理的方法使得不必每次都重新累加:设s[i,j]为棋盘(1,1)-(i,j)的权和(若i=0或j=0,则s[i,j]=0),则棋盘(u,v)-(x,y)的权和为s[x,y]-s[u-1,y]-s[x,v-1]+s[u-1,v-1]。

我要回帖

更多关于 uva.onlinejudge.org 的文章

 

随机推荐