MAIGO的同济题解2

Welcome to
Tongji Online Judge Solutions by Maigo Akisame Volume 2

我在TJU的所有AC程序及本题解均可在purety.jp/akisame/oi/TJU.rar下载。

purety.jp/akisame/oi/TJU.rar下载。
<script type="text/javascript"> for i=0 to 9 document.write "<th>题号<th>状态" next num=1100 '1100 prob "ac",1 prob "ac",1 prob "ac",0 prob "ac",1 prob "ac",0 prob "ac",0 prob "ac",0 prob "ac",1 prob "ac",0 prob "ac",0 '1110 prob "ac",0 prob "ac",0 prob "ac",0 prob "ac",1 prob "ac",0 prob "ac",0 prob "&nbsp",0 prob "ac",1 prob "ac",0 prob "ac",1 '1120 prob "&nbsp",0 prob "ac",1 prob "ac",1 prob "&nbsp",0 prob "ac",0 prob "ac",0 prob "ac",0 prob "ac",0 prob "ac",0 prob "ac",0 '1130 prob "ac",0 prob "ac",0 prob "ac",0 prob "ac",1 prob "ac",0 prob "ac",0 prob "ac",1 prob "ac",1 prob "ac",1 prob "ac",0 '1140 prob "ac",1 prob "ac",1 prob "ac",1 prob "ac",1 prob "ac",1 prob "ac",1 prob "ac",1 prob "ac",1 prob "ac",1 prob "ac",0 '1150 prob "ac",0 prob "ac",1 prob "ac",0 prob "ac",0 prob "ac",0 prob "ac",1 prob "ac",1 prob "ac",1 prob "ac",0 prob "ac",1 '1160 prob "ac",0 prob "ac",0 prob "ac",0 prob "ac",1 prob "ac",1 prob "ac",1 prob "ac",1 prob "ac",1 prob "ac",0 prob "ac",1 '1170 prob "ac",1 prob "ac",1 prob "ac",0 prob "ac",0 prob "ac",1 prob "ac",1 prob "ac",1 prob "ac",1 prob "&nbsp",0 prob "&nbsp",0 '1180 prob "ac",0 prob "ac",0 prob "ac",0 prob "ac",0 prob "ac",0 prob "ac",0 prob "ac",1 prob "ac",0 prob "ac",0 prob "ac",0 '1190 prob "ac",1 prob "ac",0 prob "&nbsp",0 prob "ac",0 prob "ac",0 prob "&nbsp",0 prob "ac",1 prob "ac",0 prob "ac",0 prob "ac",1 </script>
题号 状态 题号 状态 题号 状态 题号 状态 题号 状态 题号 状态 题号 状态 题号 状态 题号 状态 题号 状态
1100 ac 1101 ac 1102 ac 1103 ac 1104 ac 1105 ac 1106 ac 1107 ac 1108 ac 1109 ac
1110 ac 1111 ac 1112 ac 1113 ac 1114 ac 1115 ac 1116   1117 ac 1118 ac 1119 ac
1120   1121 ac 1122 ac 1123   1124 ac 1125 ac 1126 ac 1127 ac 1128 ac 1129 ac
1130 ac 1131 ac 1132 ac 1133 ac 1134 ac 1135 ac 1136 ac 1137 ac 1138 ac 1139 ac
1140 ac 1141 ac 1142 ac 1143 ac 1144 ac 1145 ac 1146 ac 1147 ac 1148 ac 1149 ac
1150 ac 1151 ac 1152 ac 1153 ac 1154 ac 1155 ac 1156 ac 1157 ac 1158 ac 1159 ac
1160 ac 1161 ac 1162 ac 1163 ac 1164 ac 1165 ac 1166 ac 1167 ac 1168 ac 1169 ac
1170 ac 1171 ac 1172 ac 1173 ac 1174 ac 1175 ac 1176 ac 1177 ac 1178   1179  
1180 ac 1181 ac 1182 ac 1183 ac 1184 ac 1185 ac 1186 ac 1187 ac 1188 ac 1189 ac
1190 ac 1191 ac 1192   1193 ac 1194 ac 1195   1196 ac 1197 ac 1198 ac 1199 ac
<script type="text/javascript"> detail 1100,"勇闯黄金十二宫……白羊宫" </script>

Prob 1100: 勇闯黄金十二宫……白羊宫 回页首
  四件圣衣分别计算,相加即可。把一件圣衣上所有洞的破损程度值分成和尽可能接近的两组,则和较大的一组的和就是修补这件圣衣所需的最少时间。这个时间的求法可参看 1017 石子归并问题。<script type="text/javascript"> detail 1101,"勇闯黄金十二宫……金牛宫" </script>

Prob 1101: 勇闯黄金十二宫……金牛宫 回页首
  根据定义,一个数n是牛数的充要条件是它有4个或更多的质因数。因此将它分解质因数即可。注意可能有一个因数可能大于sqrt(n)。<script type="text/javascript"> detail 1103,"勇闯黄金十二宫……巨蟹宫" </script>

Prob 1103: 勇闯黄金十二宫……巨蟹宫 回页首
  这道题与 1084是极为类似的。同样是搜索每个质因数的指数,用同样的公式计算约数个数,用同样的不等式剪枝。此题与 1084相比较简单的一点是,虽然在试乘的过程中数值会超出longint甚至cardinal的范围,但不必使用高精度,用int64即可。<script type="text/javascript"> detail 1107,"勇闯黄金十二宫……天蝎宫" </script>

Prob 1107: 勇闯黄金十二宫……天蝎宫 回页首
   气死人的题:题目中说n<=10,但测试数据中的n没有超过6的!!!所以,朴素搜索即可,不必加任何剪枝。<script type="text/javascript"> detail 1113,"反约瑟夫" </script>

Prob 1113: 反约瑟夫 回页首
  用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了。<script type="text/javascript"> detail 1117,"分子量计算" </script>

Prob 1117: 分子量计算 回页首
  一点窍门:在整个式子左边加一个'(',倒着递归计算比较方便。<script type="text/javascript"> detail 1119,"范式" </script>

Prob 1119: 范式 回页首
  这道题显然要用构造法。首先我们要知道哪些数的结果是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)=a 2+b 2+c 2-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恒成立。
<script type="text/javascript"> detail 1121,"Random的问题" </script>

Prob 1121: Random的问题 回页首
  const即可。不要忽略b=0的情况。<script type="text/javascript"> detail 1122,"乐队" </script>

Prob 1122: 乐队 回页首
  见加强版 1146。<script type="text/javascript"> detail 1133,"难题" </script>

Prob 1133: 难题 回页首
  真是一道难题,要用到 棋盘多项式。另外,我把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时,这个复杂度在时间上是完全可以承受的,在空间上,若给每个棋盘编一个号,使用哈希表存储棋盘多项式,也没有问题。

  本题使用了高精度、二进制编号、哈希表等多种算法和数据结构,对编程能力也是一个极好的锻炼。<script type="text/javascript"> detail 1136,"回家" </script>


Prob 1136: 回家 回页首
  把边当作点,点当作边,Dijkstra即可。<script type="text/javascript"> detail 1137,"赌场的规矩" </script>

Prob 1137: 赌场的规矩 回页首
  很明显,这是一道图论题。首先构造一个图:每一个洞作为一个顶点,在某两个洞之间交换了几次,就在代表这两个洞的顶点之间连几条边。把最后可能藏有小球的洞对应的顶点叫做 可达的顶点,其它顶点叫做 不可达顶点。显然,可达顶点只存在于包含顶点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  
 不存在不可达的顶点  
else if 顶点1在某个环上 then  
 不存在不可达的顶点 //因为顶点1到环上的顶点必存在经过3个或更多顶点的路径,顶点1到环外的顶点总可以先走一遍环
else if 与顶点1关联的边全部为单边 then  
 有且只有顶点1不可达 //因为一旦进行了与顶点1关联的操作,小球离开了1号洞,它就不可能再回来了
else if 与顶点1关联的边中有一组重边(设这组重边连接的另一顶点为a) then  
 if 除重边(1,a)以外还有重边与顶点a关联 or 顶点a在某个环上 then  
  不存在不可达的顶点 //顶点1到顶点a存在这样一条路径:先走重边(1,a)中的一条,再走a的另一组重边或环;顶点1到自身存在这样一条路径:先走重边(1,a)中的一条,再走a的另一组或环,再走重边(1,a)中的另一条;顶点1到与顶点1相连的其它任一顶点b存在路径1-a-1-b:以上路径经过的顶点数均>=3
 else  
  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  
 不存在不可达的顶点 //顶点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完全是小菜一碟了。<script type="text/javascript"> detail 1138,"多项式乘法" </script>


Prob 1138: 多项式乘法 回页首
  依次把每项相乘,降幂排序,合并同类项即可。
  数据很弱,我是把每个式子的最大项数定为1000的。如果定为10000,则会MLE;)
  至于输入中的多余空格的问题,我是宁信其有不信其无。也就多加那么几条语句而已:)<script type="text/javascript"> detail 1140,"烷烃命名之Step 1" </script>

Prob 1140: 烷烃命名之Step 1 回页首
  显然这个题是求一棵树中的最长路。以顶点a为根的子树中的最长路长度=max{顶点a的所有子树中最长路长度最大值(最长路不经过a的情况),顶点a的所有子树深度的最大值与次大值之和+1(最长路经过a的情况)}。注意到根结点有4棵子树,其余的C有各有3棵子树,H无子树,于是可用递归轻松解决问题。<script type="text/javascript"> detail 1141,"统计一氯代烷同分异构体数目" </script>

Prob 1141: 统计一氯代烷同分异构体数目 回页首
  还是DP。把连有Cl原子的C原子看作树根,那么它有3棵子树。设f[n]表示一氯n烷的同分异构体数目,则有状态转移方程

其中H为重组合的符号,表示从n个元素中可重复地取出m个的组合数,(用隔板原理想一下就会明白)。状态转移方程的第三项当且仅当n mod 3=1时存在。
  本题的结果不超过qword范围,因此不必使用高精度。<script type="text/javascript"> detail 1142,"虫食算" </script>


Prob 1142: 虫食算 回页首
  只用最简单的搜索方法即可:从右到左依次搜索每一列中的每一个字母代表的数字(这样可以充分利用进位的信息)。每假设出一个字母的值,就检查一下它左边的每一列是否可能成立。检查也很简单:如果一列的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)三个程序。<script type="text/javascript"> detail 1143,"二五语言-版本2" </script>

Prob 1143: 二五语言-版本2 回页首
  以下叙述中,“单词”均指合法单词。
  举个例子说明:若为单词转编码,如求单词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,再如当递归深度超过了所有已固定的字母时,可直接利用版本1中的DP获得结果,而不须递归下去。然而,不加这些优化的算法的复杂度已经很低了(最坏情况下为25(25个位置)*25(每个位置可能放25个字母)*252(记忆化搜索的状态总数)*5(每个状态最多有5个合法位置)=787500),这些优化都显得不太值得。<script type="text/javascript"> detail 1144,"二五语言-版本1" </script>


Prob 1144: 二五语言-版本1 回页首
  以下叙述中,“单词”均指合法单词。
  用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的字母,不合法)。
  • 若为编码转单词,则依次枚举每个字母可能的位置并累加这时产生的单词数。若某时刻累加和超过或等于了编码,则当前处理的字母应放的位置就找到了,减去最后一次累加的数值,继续处理下一个字母,直至结束。
<script type="text/javascript"> detail 1145,"敲七-版本4" </script>

Prob 1145: 敲七-版本4 回页首
  把与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的个数)。
<script type="text/javascript"> detail 1146,"乐队(加强版)" </script>

Prob 1146: 乐队(加强版) 回页首
   首先把一盘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),绝不会超时。<script type="text/javascript"> detail 1147,"战略游戏-版本2" </script>


Prob 1147: 战略游戏-版本2 回页首
  同 版本1一样,本题也可以用树型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]<need[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]}之和。
  设根结点为root,则min{need[root,1],need[root,2]}就是答案。<script type="text/javascript"> detail 1148,"救护伤员" </script>

Prob 1148: 救护伤员 回页首
  虽然数据范围小得搜索都能过,但我还是要讲一下求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]都没有变化。也就是说,它原来属于(或不属于)相等子图,现在仍属于(或不属于)相等子图。
  • 一端在交错树中,另一端不在交错树中的边(i,j),它的A[i]+B[j]的值有所减小。也就说,它原来不属于相等子图,现在可能进入了相等子图,因而使相等子图得到了扩大。
  现在的问题就是求d值了。为了使A[i]+B[j]>=w[i,j]始终成立,且至少有一条边进入相等子图,d应该等于min{A[i]+B[j]-w[i,j]|X i在交错树中,Y i不在交错树中}。<script type="text/javascript"> detail 1151,"拼凑春联" </script>

Prob 1151: 拼凑春联 回页首
  按每相邻两个字母是否相同可以把长度为7的春联分为2 7-1=64类,设第i类有count[i]句,则答案为所有的C(count[i],2)之和。<script type="text/javascript"> detail 1155,"拯救Angel行动" </script>

Prob 1155: 拯救Angel行动 回页首
  这个题是典型的分层图BFS。在最坏情况下,因为钥匙有10种,故可以用0至1023这1024个数对应已取得的钥匙的1024种状态,把图分成1024层。这个图并不大,只有15*15*1024=230400个结点。因为从一个结点出发只有4种走法,所以完全不必担心TLE。<script type="text/javascript"> detail 1156,"传球问题" </script>

Prob 1156: 传球问题 回页首
  不要受“高二数学改编”误导而想排列组合,这个题要用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取最大值2 31-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)的算法。<script type="text/javascript"> detail 1157,"愚蠢的教官" </script>

Prob 1157: 愚蠢的教官 回页首
  在以下的叙述中,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)。<script type="text/javascript"> detail 1159,"半数集" </script>

Prob 1159: 半数集 回页首
  本题我采用的是DFS算法。判重的问题我是这样解决的:对每一个数串,按某种规则将它划分成若干个数。如果这些数恰好是搜索到这一个数串时栈中的数,那么就把答案加1,否则就不加。这样就可以保证相同的数串只算一个。这个规则就是:从后往前划分,使每个数都尽可能大。当然,每个数的开头都不能是0。
  举个例子:假设输入的数是99,那么按照规则,数串123499的划分应该是12 34 99。搜到这个数串时,如果栈中的数恰好就是12和34,那么答案加1;如果栈中的数是1 2 34或者1 2 3 4,则不加1。<script type="text/javascript"> detail 1163,"炼金术" </script>

Prob 1163: 炼金术 回页首
   题目叙述有一点毛病:可以不把金子炼成别的金属而直接送出境。
  算法嘛,就是用Dijkstra求点1到其它各点的最短路,再用Dijkstra求其它各点到点1的最短路,答案便是每个点的两个最短路长度与单价的一半的和的最小值。不说“即可”了,因为这个Dijkstra要用堆来优化,这个编程挺费事。另外,两个Dijkstra可以一起做,这样当某个点的两个最短路长度都已经求出来时,可以立即更新答案。如果某个Dijkstra中堆顶元素的最短路长度已经超过了答案,这个Dijkstra就可以停止了。<script type="text/javascript"> detail 1164,"猴子分桃" </script>

Prob 1164: 猴子分桃 回页首
  用a k表示第k个猴子走后剩余的桃子数。我们要求的就是a 0和a n的最小值。由题意可列出递推式:
a k+1=(a k-1)*(n-1)/n……①
  这个递推式的形式类似等比数列的递推式,但多一个-1。为了将它整理成标准的等比数列的形式,我们设
b k=a k+x……②
让{b k}成等比数列:
b k+1=b k*(n-1)/n……③
而x是未知数。下面想办法求x:
  把②式代入③式,得
a k+1+x=(a k+x)*(n-1)/n
a k+1*n=a k*(n-1)-x……④
  同时把①式整理成如下形式:
a k+1*n=a k*(n-1)-(n-1)……⑤
  比较④⑤两式,得
x=n-1
  回过头来我们再关注数列{b k}。显然
b n=b 0*[(n-1)/n] n
因此
b 0 min=n n,b n min=(n-1) n
代入②式即求得答案:
a 0 min=n n-(n-1),b n min=(n-1) n-(n-1)
<script type="text/javascript"> detail 1165,"切蛋糕" </script>

Prob 1165: 切蛋糕 回页首
  先讨论比较简单的直线的情况。若直线没有切着蛋糕,则答案显然。下面只讨论直线切着蛋糕的情况。观察一下右面这个图,图中直线的上方有2块蛋糕,而下方有3块。每一块的外边(这个“边”不念轻声)都有一个特征:左转,比如下方较大的那一块,它的外边是A-4-5-F,是左转的。每一条内边也有一个共同特征:右转。还以下方较大的那一块为例,它的内边是E-12-13-B,是右转的。恰好,每一块蛋糕都有且仅有一个外边,所以我们只需统计一下在直线上下方各有多少段左转的边界,第一问就已经解决了。
  这一结论也适用于圆外蛋糕数目的统计,但对于圆内就不适用了,因为圆内的每块蛋糕没有“外边”、“内边”的概念,它的边数不固定,每条边的旋转方向也不固定。比如左面那个图,圆内较大的一块竟有3条边,且左边不转,下边和右边都是右转。然而很不幸,题目问的恰恰就是圆内的块数。我们必须另辟蹊径。
  刚才讨论直线的情况时,我们没有充分利用内边的信息。其实,内边的条数与蛋糕被切成的块数也是有直接关系的。如果一块蛋糕有n个内边,那么它的切口就有n+1个,如下方较大的一块有1个内边,它的两个切口是AB和EF。而与这两个切口相连的上方的块一定不同!这暗示我们,直线一边的内边的数目,与 另一边的蛋糕块数有某种联系!我们从原始的情况来想。首先想象一块蛋糕被直线一分为二。然后,蛋糕的上方收缩,整个蛋糕成为“凹”字形,直线恰好经过它的两条“腿”。这时,直线下方多了一条内边,上边多了一块蛋糕。于是我们发现了,直线上方的蛋糕块数等于下方内边数加1,下方的蛋糕块数等于上方内边数加1。这个算法也适用于求圆内的蛋糕块数,因为圆外蛋糕的内边同样具有“右转”的特征。还是看一下左边的图,由于圆外上方的那一块有一条内边(点8附近),故圆内的蛋糕块数为2。
  剩下的问题就是判断线段是否与直线相交、线段是否进入圆或从圆中出来的几何问题了。相信这些你都会的:)<script type="text/javascript"> detail 1166,"背包问题" </script>

Prob 1166: 背包问题 回页首
  把能取得的若干连续重量用区间存储,比如重量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,则总重量数为2 n(包括0),在最坏情况下,这些重量都不连续,就需要2 n个区间。但在最大规模随机数据的情况下,实验得知区间数一般不超过200000个,因此题目所给的内存是足够的。
  以上算法还有一点值得优化。这道题不能使用 1017题那样的朴素算法,原因就在于重量数太多,而区间恰恰解决了这一矛盾。为了让区间的优势发挥得更充分,我们在执行上述算法之前先对所有物体的重量进行一次排序,然后从小到大依次处理。这样,在计算过程中,重量就会最大程度地向轻的方向聚集,区间也会最大程度地合并,时间、空间需求都大大降低:未经优化的算法用时6.9s,而优化后的算法仅用2.3s;实验发现,在输入最大规模的随机数据时,优化后的算法在运行过程中区间的最大数目仅为几十至几百个,远远小于优化前的十万个左右。但是,当物体个数比较少而重量又很分散的情况下,区间数接近物体数的指数函数,如物体数为18,最大重量为100000的情况下,最大区间数仍可超过100000,因此建议仍把最大区间数定在200000左右。<script type="text/javascript"> detail 1167,"城墙防御" </script>

Prob 1167: 城墙防御 回页首
  这道题的算法是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]表示这两列高度相同的情况。
  算法不难,但编程上需要在细节上下功夫,尽可能地优化常数系数。下面我将对照程序,说明怎样尽可能缩短运行时间:
  1. a[i]、b[i]这两个值需要经常用到,因此需对其进行预处理。注意到在状态转移方程中,是加a[i]+b[i]还是加b[i]与m的奇偶有关,于是用a[true,i]表示原来的a[i]+b[i],用a[false,i]表示原来的b[i],这样在j循环中就可以减少一次判断了。
  2. 显然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函数的调用次数。
  3. DP算法的一个特点是它计算的信息量很大,而算出来的信息往往有一部分是没有用的。如右图,整个矩形表示本题中DP计算的总信息量,而其中只有蓝色部分是有用的,这部分还占不上总信息量的一半。因此有必要仅计算蓝色部分所代表的信息,这就是j循环前计算的k、l的用途。这一操作可以显著提高程序效率。
  4. 继续观察状态转移方程。方程中有求较小值这一步,用程序中的方法做而不另外设一个临时变量,既可以省一个变量,在某些情况下还可以省去一次赋值。这样做导致了j循环必须用downto而不能用to,请仔细思考一下为什么。
  5. 再介绍一个好东西:{$R-},即关闭range check。在保证程序不会发生数组越界等错误时,range check是既多余又耗时的。加上这一编译开关后,程序的运行时间缩短了一半以上,提交后用2.3s AC(虽然这个速度仍不是很快)。P.S.其实还有一个编译开关{$Q-},即关闭overflow check,也能提高程序速度。
<script type="text/javascript"> detail 1169,"广告印刷" </script>

Prob 1169: 广告印刷 回页首
  显然当广告面积最大时,要有一个建筑物被刷满。枚举这个建筑物的编号,然后寻找把这个建筑物刷满的广告向左、向右最远分别能延伸到什么位置。
  用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)时遇到了困难,才发现还有个常数系数在捣鬼。<script type="text/javascript"> detail 1170,"一进制计数" </script>


Prob 1170: 一进制计数 回页首
  用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(n 3/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<i,故i也无用。
  这种改进算法避免了检验一个数是否是另一个数的约数,只用了2s多就AC。<script type="text/javascript"> detail 1171,"最大奖励" </script>

Prob 1171: 最大奖励 回页首
  呼呼,思路比程序还长,有些地方还显得很突兀,不知怎么能想到。我问了21天,终于搞懂了。谢谢 Wanght_1的整理。

  此题的DP用逆推。至于为什么用逆推,下文会讲。用F[i]表示第i题以后的题的最大总得分(i就是阶段)。状态转移方程为:
    F[n]=0(显然)
    F[i]=max{F[j]+W[i,j]}(i<j<=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]<B[i,k2] <=> g(k1,k2)>i。

  现在说一下为什么用逆推而不是顺推。观察上面推导过程中两个红色的*i。括号外的这个系数是取决于一段开头的位置的。如果用逆推,这个系数对应的是阶段,因此上面的推导能够进行。如果用顺推,这个系数将对应于决策,而我们现在讨论的就是不同决策的优劣,这个系数不同将使推导遇到相当大的困难。

  维护一个决策队列q,使qj递减,g(qj,qj+1)也递减。(谁知道怎么想到的呢?)
  依次计算F[n-1]至F[0]。下面考虑计算F[i]时的情形。

  1. 目前决策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)<=i,则B[i,a]>=B[i,b],决策a不劣于b。
    • 若g(a,b)>i,则g(b,c)>i,B[i,b]<B[i,c],决策c优于b。
    无论哪种情况,b要么不是最优决策,要么是最优决策但不唯一。因此可不断删除队列中倒数第二个决策,直至队列中决策不足三个或g(a,b)>g(b,c)为止。
  2. 现在阶段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的系数。总之,若某一段的分值非正,则在这一段前划分的决策一定不是最优决策。
因此,可从最后一题向前扫描,累加分值,得到一个正分值后就把累加的这一段压缩成一道题,并把累加和清零。这样压缩以后,除了第一题分值可能非正以外,其余题的分值均为正数,于是便可以使用斜率优化法了。<script type="text/javascript"> detail 1174,"第一章 胜利的逃亡" </script>

Prob 1174: 第一章 胜利的逃亡 回页首
  用Bellman-Ford求2次最小费用增广路即可。当然,第一次用Dijkstra会更快些。<script type="text/javascript"> detail 1175,"第二章 文明的复兴" </script>

Prob 1175: 第二章 文明的复兴 回页首
  首先将词典排序,然后从文本中的每个单词中提取出字母,用二分查找的方法确定这些字母组成的单词是否在词典中。用一般的方法对词典排序可能太慢,可以用一种部分桶排的方法:首先把所有单词按第一个字母排序,这样首字母相同的单词就各自聚到了一起;然后把首字母相同的单词按第二个字母排序……这样的排序方法避免了直接比较单词时,前面的字母都相同带来的冗余比较,对于此题来说是比较适合的排序算法。
  当然,建trie树的方法也是可行的。<script type="text/javascript"> detail 1176,"第三章 光荣的梦想" </script>

Prob 1176: 第三章 光荣的梦想 回页首
  若数列中有这样两个数,前面的那个数比后面的大,那么这两个数就称为一个 逆序对。下面证明最小的交换次数等于数列中逆序对的个数x。
  • x次是足够的。当数列中存在逆序对的时候,一定有一个逆序对是相邻的两个数,否则,整个数列就成了不递减的,与数列中存在逆序对矛盾。那么,每一次交换一定可以消灭一个逆序对,因此x次是足够的。
  • x次是必需的。显然,一次交换最多只能消灭一个逆序对,因此要消灭全部x个逆序对至少需要x次交换。
  现在的问题就是如何高效地求出数列中逆序对的数目。如果先把数列离散化,再用静态排序二叉树等树形数据结构计算每个数之前比它大的数有多少个,然后累加的方法,那么离散化和树形数据结构的复杂度均为O(nlogn),这个复杂度的算法被执行了两次。能不能只执行一次呢?答案是肯定的:利用归并排序。
  回忆一下归并排序的过程:将数列分为长度相等或相差1的两段,分别归并排序后,把两段看成两个队列,每次取队首元素的较小值,排成一列后就得到有序的数列。观察排序前的数列,它当中的逆序对可以分为3类:1.两个数都在前一半的;2.两个数都在后一半的;3.一个数在前一半,一个数在后一半的。前两种情况的逆序对的个数可在对那一半的归并排序中计算,对整个序列的归并排序的主程序中只需计算第3种情况。注意到归并过程中,当第二个队列的队首元素小于第一个队列的队首元素时,第二个队列的队首元素与第一个队列中剩下的元素均构成了逆序对,这时在答案上加上第一个队列中剩余元素的个数即可。只加这么一条语句,就可以在归并排序的过程中顺便求出逆序对的个数了。<script type="text/javascript"> detail 1177,"第四章 英雄的叛变" </script>

Prob 1177: 第四章 英雄的叛变 回页首
   这道题的输入中有前导的0,请加以处理,否则会WA掉!

  下文提到一个数的“前一半”时,若这个数有奇数位,“前一半”均包括中间一位。
  给每个完美数编一个号,使得相邻的完美数的编号也相邻。编号方法如下:

  • 若其位数为偶数,则取其前一半,在最高位前一位加1。如456654的编号为1456。
  • 若其位数为奇数,则取其前一半,在最高位上加1。如121的编号为22,92429的编号为1024。
  由编号还原出完美数的方法如下:
  • 若编号首位为1且第二位不为0,则说明对应的完美数为偶数位。去掉首位的1得到的就是完美数的前一半。
  • 若编号首位大于1或前两位为10,则说明对应的完美数为奇数位。若首位大于1则在首位减1,否则在第2位减1,得到的就是完美数的前一半。
  这样子算法就很简单了:求出前一半与a相同的完美数的编号。若a比这个完美数小,则将编号减1。在编号上加上k,还原成完美数,就得到答案。<script type="text/javascript"> detail 1186,"卫星覆盖" </script>

Prob 1186: 卫星覆盖 回页首
  在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(n 3)的小长方体(感觉跟离散化了一样)。但大多数的小长方体不会再被切割了,因此这种切法的最坏时间复杂度可以近似认为是O(n 3)。使用线段树可以把平均时间复杂度降至O(n 2logn),但编程复杂度明显高于切法。<script type="text/javascript"> detail 1190,"个人所得税" </script>

Prob 1190: 个人所得税 回页首
  BS题目把m<=400说成m<=50000吓唬人。另外,注意输入中相邻两项之间可能有 多个空格。<script type="text/javascript"> detail 1196,"01串" </script>

Prob 1196: 01串 回页首
  利用本题,我来讲一下 差分约束系统
  所谓差分约束系统,就是求关于x i的由一系列形如x i-x j>=a的不等式组成的不等式组的解。我们构建一个有向图,图中的每个结点代表一个x i。对于每个不等式x i-x j>=a,连一条从x j到x i的权为a的边。不妨设x 1=0,那么令x i等于x 1至x i的最长路的长度,这样就得到了一组可行解。若图中存在正权回路,即对于某些点最长路不存在,那么不等式组无解。
  为什么呢?先看有解的情况。因为若从x i到x j的有一条长为s的路,则要求x j-x i>=s。现在x j与x i的差是x i到x j的最长路的长度,那么对于从x i到x j的任一条路(设其长为s),自然有x j-x i>=s成立。再看无解的情况。设从x i到其本身有一条权为s的正权回路,则要求x i-x i>=s,即0>=s,这显然是不可能的。
  有了差分约束系统这个武器,解决本题就不难了。用S i表示01串前i位的和,规定S 0=0。建一个含有N+1个结点的有向图,每个结点分别代表S 0至S N。然后在图中连边:
  • 由于每个数非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的边。
  求S 0到每个结点的最长路,就可以求出每个S i了。01串中的第i位s i也就等于S i-S i-1。<script type="text/javascript"> detail 1199,"棋盘分割" </script>

Prob 1199: 棋盘分割 回页首
  下文中, Σ符号下方的i=1与上方的n均省略。
  首先,我们对均方差的公式σ=sqrt( Σ(x i- x) 2/n)进行分析:根号显然是没有用的;对于一个特定的棋盘,n为定值,故根号下的分母也是没有用的。我们继续整理根号下的分子:
    Σ(x i - x) 2
  = Σ(x i 2 - 2x i x + x 2)
  = Σx i 2 - 2n x 2 + n x 2
  = Σx i 2 - n x 2
  分析整理后的式子:对于一个特定的棋盘,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]。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值