完成了NOIP2000-2008的全部题解,发上来供大家参考吧,如有谬误还请多多指教。
BYVoid原创程序及题解打包下载(37 KB)
==2009.3.26== 格式重新整理,另外附上 NOIP2008 双栈排序 twostack 题解
==2009.6.30== 添加 NOIP2008 传纸条 费用流建模
目录
NOIP2000
进制转换
和正基底进制数转化方法类似,使用短除法进制转换。
例如题中写的-15的-2进制
-2|-15 +—–…1 -2|8 +—–…0 -2|-4 +—–…0 -2|2 +—–…0 -2|-1 +—–…1 -2|1 +—–…1 0
倒序把每次余数写下,就是110001。所以结果是-15=110001(base-2)。
但要注意的是在编程中,使用整除的问题。无论是C++还是Pascal,-15 整除 -2结果为7,-15 与 -2 求模结果为-1。但我们显然不能用负数作为余数,所以要满足,商 * 除数 <= 被除数。
即结论如下:
- 当 a / b * b <=a 时,a 整除 b 结果为 a / b。
- 当 a / b * b >a 时,a 整除 b 结果为 a / b + 1。
乘积最大
题中所述的是长度为N的数字串中添加K个乘号,即把前N个数字分为W份乘积的最大值,W=K+1。
动态规划
状态
- sum[i,j] 表示从第i位到第j位组成的数字
- F[i,j] 表示把前i个数字分为j份乘积的最大值
状态转移方程
- F[i,j]=max{F[k,j-1]*sum[k+1,i]} (1<=k<=i-1)
边界条件
- F[x,1]=sum[1,x]
目标结果
- F[N,W]
时间复杂度
- O(K*N^2)
单词接龙
这是一道搜索题,可以转化为图论,求有向图无环最长路径。由于每个单词最多可以用两次,为方便起见,先把每个单词复制一遍,看做两个相同的单词。
把能接龙的单词a与b之间建立一条边(a,b),边权为单词b的长度-a,b重合部分的长度。把起始字母看做一个节点,建立起始字母能接到的单词a的边,边权为a的长度。最后从起始点开始搜索最长路径即可。
由于有向图无环最长路径是NP的,最坏情况下时间复杂度为O(N^N)。这具体取决与图的疏密程度,如果使用邻接表存储,在图不是十分密集的时候效果很好。该题测试数据中没有很强大的,所以这样就可以过了。
有两点要注意,题上说的很不明确,容易造成误解。这两点是我在看过数据后发现的。
- 所谓“两部分不能存在包含关系”,是指两部分接龙后不能长度不增加。例如ababab和ababab,可以接成ababababab,但不能为ababab。
- 接龙时,如果两个单词接龙后的单词有多种情况,要尽可能保证接龙后的单词最长。例如cabab和ababc,一定要接成cabababc,为而不是cababc。
方格取数
数据不大,搜索两条路径即可。
把每个有数字的方格看做一个顶点,该顶点与其右下的所有顶点连接一条有向边。然后从左上角的点搜索到右下角顶点的点权最长路径。搜索到一条路径以后要再搜索一遍其余节点,找出另一条最长路径。每次保留两个阶段的和的最大值。
最后的结果就是两次路径和最长的路径的长度。
最坏情况下时间复杂度为O(2^(N^2)),不过数据中没有能构造到这样时间复杂度的。
NOIP2001
一元三次方程求解
穷举就行了,精度只有0.01,范围-100至100。
要注意精度问题,不要直接判断式子等于0,而要判断式子的绝对值在(-0.01,0.01)之间,避免漏解。
数的划分
动态规划
状态
- F[i,j] 表示数i分为j部分的方案个数
状态转移方程
- F[i,j]=F[i-1,j-1]+F[i-j,j]
边界条件
- F[x,1]=1
- F[i,j]=0 (j>i)
目标结果
- F[N,K]
状态转移方程可以理解为:
每种拆分方案中,最小的数为w,按照w的不同,我们可以把拆分方案分成2类:
- w=1,我们把1除去,则剩余部分正好是i-1拆分成j-1部分,一共有F[i-1,j-1])个;
- w>1,所有的数都>1,我们把所有的数-1,则正好是i-j拆分成j部分,一共有F[i-j,j]个。
根据加法原理,得出以上方程。
统计单词个数
动态规划
以0为第一个字符存储字符串。
状态
- F[i,j]为前i个字符,分为j份的最大单词个数
- cnt[i,j]为从第i到第j的字母中单词最大个数
状态转移方程
- F[i,j]=max{ F[k,j-1] + cnt[k+1,i] } (0<=k<=i-1)
边界条件
- F[x,1]=cnt[0,x]
目标结果
- F[L,K] L为主串长度-1,即最后一位
时间复杂度
- O(K*P^2)
Car的旅行路线
明显的最短路径问题,顶点数最多只有400个。Floyed,Dijkstra,SPFA,用什么最短路算法都行。但这道题麻烦的地方不在最短路,而在构图上。
首先是已知矩形三个顶点求第四个,利用对称性可以求出。首先要找出已知三个顶点组成的三角形的直角顶点,可以用向量的数量积为0,可以以用 勾股定理的逆定理。如果(x3,y3)为已知三个顶点组成的三角形的直角顶点,根据中心对称性,要求的点(x4,y4)可以表示为(x1+x2- x3,y1+y2-y3),依此类推。
找出每个矩形的所有定点后,下面就是连接边,只要细心就不会漏。最后再算一下最短路就行了。
由于这是一个完全图,所以时间复杂度为
- Floyed O(N^3)
- Dijkstra O(N^2)
- SPFA O(N^2)
其中算法常数最小的是SPFA,我用SPFA做了这道题,尽管Floyed就可以了。
NOIP2002
均分纸牌
贪心法,一次扫描即可。
首先算出纸牌的平均数,我们希望通过尽可能少的移动,来使所有纸牌达到平均值。
从第一张纸牌开始扫描,把它[多出平均值的数量]向右移,[多出平均值的数量]有可能是负数。然后扫描第二张,一直到N 1张为止。扫描的过程中如果发现这张纸牌数值正好是平均值,那么直接跳过即可。
例如样例,平均值为10。
9 8 17 6
第1次移动,第1张牌移 1
10 7 17 6
第2次移动,第2张牌移 3
10 10 14 6
第3次移动,第3张牌移4
10 10 10 10
结果为移动了3次。
字串变换
广度优先搜索即可,没有必要双向搜索。
关键是状态的判重,我用的是ELFash,配合Treap平衡树,每次判断时间都是O(logK^N),根据数据规模最大也就是25次。
平衡树中存储字符串的Hash码,由ELFHash算法生成。如果没有重复的就加入搜索队列,否则直接淘汰。
无解有两种情况,一个是根本无法变换出目标串,另一个是在10步变换以内没有求出目标串。
自由落体
这是一道十分基础的题,不需要什么算法,只是需要细心。
显然小车接到的球一定是相邻几个,只需考虑两端即可。注意小车接球时还在移动,可以接了一个球以后移动一下再接到一个。
误差小于等于0.00001就行了。
矩形覆盖
离散化加搜索,在NOIP的确是一道难题。
首先把所有点分别以横坐标和纵坐标排序,生成两组序列。
首先从[0,500]整个区域中,分别按横向和纵向扫描若干个点,以一个矩形覆盖。然后在剩余的区域内再次分别按横向和纵向扫描若干个点,以一个矩形覆盖……直到把K个矩形覆盖完为止。
找到面积最小的方案。
NOIP2003
神经网络
这道题描述上很冗长,容易使人混淆,多次读题才能发现其意义。
根据题意,我们要在这个网络上递推。题目上给了一个递归公式,但我们可以把它改为正向的递推。
即 C[j]=Sum{W[i,j]*C[i]}-U[j] 以i的变化为阶段递推即可。
为了尽可能高效,我使用了邻接表+链队列。
题上有些地方描述的不清晰,需要来澄清。
- “神经元 i 最初状态”即为C[i]的初始值。
- 公式只对非输入层点有效。换句话说就是输入层的阈值根本没用,不用让C[i]减去U[i]。
- “仅输出最后状态非零的输出层神经元状态”,实际上是要求输出C[i]>0的i和C[i]。
侦探推理
看似简单的一道题,但很一次难拿到满分。
显然需要枚举所有解,输出唯一解,无解则输出Impossible,多解则输出Cannot Determine。
枚举谁说的是真话,是个愚蠢的策略。这样最多会有C(M,N)种可能,运行要超时不说,程序写起来还十分麻烦。由于只存在两个事实,即谁是凶手和今天是星期几。我们枚举这两个事实即可,仅有M*7种可能。
输入格式是个陷阱,很容易出错。为避免错误发生,原则就是字符串完全匹配,区分大小写。在读入的过程中,我们可以直接去掉“废话”,即与事实无关的话。
在判断谁说的是真话,谁说的是假话的时候,会出现一个人说的话前后矛盾,可能有悖论的产生,这时要直接跳过这种情况。
有人可能会不说一句话!对于这种情况,这些人是算成说真话的人呢?还是算成说假话的人?很显然,有多种可能。所以对于输入中给出的“始终说谎的人数N”,只要满足(说假话的人<=N<=说假话的人+不说话的人),即可认定为一个解。
加分二叉树
动态规划
状态
- F[i,j] 为中序遍历中第i到第j个节点所组成的子树的最大加分
- root[i,j] 为中序遍历中第i到第j个节点所组成的子树的根
状态转移方程
- F[a,b]=max{ F[a,k-1]*F[k+1,b]+F[k,k] }
- root[a,b]=使F[a,b]最大的k的值 (a<=k<=b)
边界条件
- F[i,i]=P[i] 第i个节点的分数
- F[i,j]=1 (i>j)
目标结果
- 最大加分为 F[1,N]
前序遍历要在root状态中递归得出。从a=1,b=N开始递归,a,b子树中根为root[a,b]。输出root[a,b]并递归进入(a,root[a,b]-1)和(root[a,b]+1,b)
传染病控制
搜索一棵树,从根节点开始,枚举剪掉每一条边,把剩余的节点合并为一个节点,继续回溯搜索它。返回时要把合并了的节点再拆开来。继续枚举另一条边。
这样的话,可能会超时的。所以要加上两个显而易见的优化。
优化一:当搜索过程中发现当前的感染人数已经大于已知最小感染人数,继续搜索是无意义的,要回溯。
优化二:如果遍历到一颗树的节点全部都是叶节点,不用枚举剪哪个,这时新感染人数一定为叶节点数-1。否则在枚举剪断的时候,无需剪叶节点。
优化三:启发式搜索。很明显,一棵子树的节点数越多,这棵树就越“危险”,所以优先枚举剪掉这棵子树,会减少很多搜索量。
NOIP2004
津津的储蓄计划
再简单不过的题了。边读边算就行了,相信学过编程的都能做这道题。
有个要注意的地方,就是年末取得存款的时候,最好不要乘以1.2,而是先除以5再乘以6。这样可以避免由于两次隐式转换产生的误差。这个误差对于C++尤为显著。
合并果子
由题意可知,显然是每次合并两堆数目最小的果子,合并N-1次以后消耗体力最少。
因为越早合并的果子堆,合并的次数就越多。我们把数目大的果子堆留到尽量靠后合并,才能保证尽量合并少,这样消耗体力才能最少。
我们每次合并时,都要从所有的果子中找到数目最少的两堆。由于N<=10000,显然每次O(N^2)的排序是不行的。要用到最小堆或者平衡树了。
我用的是Treap树,时间复杂度为O(N*logN)。
注意这道题不是石子归并!
合唱队形
动态规划。这道题实际上是求最长谷状序列,可以转化为最长上升、下降序列问题。方法为枚举中间长的最高的人,在其左边求最长上升序列,在其右边求最长下降序列。注意这两个序列要以中间这个人为公共点。
所以设置的两个状态分别为以第i个数为结尾的最长上升序列和以第i个数为开头的最长下降序列,这样可以保证两个序列可以连接。
状态
- F[i] 以第i个数为结尾的最长上升序列
- G[i] 以第i个数为开头的最长下降序列
- H[i] 第i个数的值
状态转移方程
- F[i]=max{ F[j] + 1 } (1<=j<=i-1 and H[j]<H[i])
- G[i]=max{ G[j] + 1 } (i+1<=j<=N and H[j]<H[i])
边界条件
- F[i]=1 (H[j]均大于等于H[i] 1<=j<=i-1 )
- G[i]=1 (H[j]均大于等于H[i] i+1<=j<=N )
目标结果
- N-最长谷状序列长度。
- max { N-(F[k]+G[k]+1) } (1<=k<=N)
虫食算
这是NOIP2004的唯一一道难题,而且是很难的题。单纯暴力搜索,超时是无疑的,加上许多强剪枝也很难通过。于是我用了解方程组的方法。
从算式中可以发现,有N位,每位都是两个未知数相加得到第三个未知数。正好是N元一次线性方程组。由于存在进位问题,枚举除最后一位以外的每位的是否进位,可以列出2^(N-1)个不同的方程组,解每个方程组即可得出解。 解方程组使用高斯消元算法,时间复杂度为O(N^3)。但枚举多达2^(N-1)个,这样还是会超时。
深入分析发现,枚举每位是否进位,仅仅影响方程的常数项。于是可以在枚举进位之前,设每个方程的常数项为d1,d2,d3,…,dn。解方程组,把未知数x1,x2,x3,…,xn用d1,d2,d3,…,dn表示,高斯消元的时候要把常数项以多项式的方式计算。
这是我们在还是枚举每位是否进位,算出每个方程的常数项,即d1,d2,d3,…,dn,代入x1,x2,x3,…,xn再判断即可。
这种方式只用计算一次方程组,虽然时间复杂度不变,但算法常数小了许多,对付N=26没有问题了。
NOIP2005
谁拿了最多奖学金
读入每行数据,进行多条件判断,算出每个人的奖学金,保留总和与最大值。
很简单的题,只要细心就不会错。
用C或C++语言读入数据十分方便,一条语句即可。但是Pascal就麻烦了。
过河
动态规划,需要状态压缩。
状态
- F[i] 跳到距离i处碰到的最少的石子数。
状态转移方程
- F[i]=min{ F[i-k] } + F[i] (S<=k<=T i-k>=0)
边界条件
- F[i]=1 i处有石子
目标结果
- min{ F[k] } (L+1<=k<=L+T-1)
状态压缩
由于L最大为10^9,直接线性递推会超时。发现石子数很少,这意味着路径上有许多很长的空白段。我们可以把长度大于ST的空白段都缩小到ST,这样最长只有10090了。
篝火晚会
首先把这个圈看做一个图,每个同学看做一个顶点。因为要形成环,所以每个顶点的度必须为2。如果存在度数不为2的顶点,那么整个图无法构成一个环,即“无论怎么调整都不能符合每个同学的愿望”输出-1。 如果是一个环,那么就遍历图,生成以第1个顶点为开头的序列。现在就要求出最小移动的代价。
在理解题意所述的调整方式中,要注意实际上就是把M个在错误位置上的人移动到正确的位置上,代价为M。一次下令移动即可。
假如生成的目标序列为1,5,3,2,4。我们现在就需要比较它和初始状态1,2,3,4,5,看有几个人离开了原来的位置。但这个序列实 际代表的是一个环,而且方向正反有两种,就需要把初始序列正反分别转N次,和得到的序列比较,看其中最少有几个位置上的人编号不相同,就得到了我们要求的 最小代价。
上述方法有大量冗余。但可以发现转来转去不管怎么转,任意两个人之间的相对位置关系在这过程中都不会变。于是想到做差,如果差小于0则加上N。
1 5 3 2 4 - 1 2 3 4 5 ———— 0 3 0 3 4
这表示序列1,5,3,2,4不转动时1,3两个人在原来的位置上,转动3个位置后5和2两个人在原来的位置上,转动4个位置后只有4一个人会在原 来的位置上。这就是说,1,5,3,2,4与1,2,3,4,5在旋转后最多有2个位置上的人编号相同,即最少有3个位置上的人编号不相同。同理要反转再 求一次。
记录每个不同的差值的个数,取其最大值P,即不调换次数最大的。结果N-P就是调换次数最小的。
等价表达式
这道题很显然不是让写多项式展开的程序的。只需要代入几次,判断与题干中表达式等价的表达式即可。
注意,只有加法、减法、乘法、乘幂四种运算,没有除法!这意味着计算表达式的时候不用考虑出现小数的问题,数据类型要用8字节整型(即Pascal中的int64,C++中的long long)。因为数据中有2004^9之类的大数,幸好还不用高精度。
对于表达式计算,我们可以现将题目中的表达式转化为后缀表达式,代入数值,并且对后缀表达式求值。当然也可以双堆栈计算表达式。多带入几次不同的数值,以避免表达式恰好相等而并不等价的情况。
NOIP2006
能量项链
动态规划,属于石子归并、合并沙堆等一类的题目。
首先把每颗珠子用一个结构存储,.L为头标记的值,.R为尾标记的值。由于项链是一个环,把环拆成两条首尾相接的链。
例如每颗珠子表示为
B[1] (2,3) B[2] (3,5) B[3] (5,10) B[4] (10,2) B[5] (2,3) B[6] (3,5) B[7] (5,10)
状态设定
- F[i,j]表示从第i颗珠子到第j颗珠子,合并一共所得的能量的最小值。
- B[i]结构为每颗珠子的头尾标记值。
状态转移方程
- F[i,j]=Max{ F[i,k] + F[k+1,j] + B[i].L * B[k].R * B[j].R } (i<=k<=j-1)
边界条件
- F[i,i]=0 (1<=i<=2*N-1)
目标结果
- Max{ F[k,k+N-1] } (1<=k<=N)
金明的预算方案
背包问题的衍生问题,动态规划。
首先把附件物品分离出来,归为主件所有。于是每次决策成了5种
- 不购买
- 购买主件
- 购买主件 + 附件 1
- 购买主件 + 附件 2
- 购买主件 + 附件 1 + 附件 2
状态设定
- F[i,j] 前i个主件,花费为j元的时候,重要程度与价格乘积的总和的最大值
- V[i] 第i个主件的价格
- W[i] 第i个主件的重要程度
状态转移方程
- F[i,j] =
- Max
- {
- F[ i - 1 , j ], (不购买)
- F[ i - 1 , j - V[i] ] + V[i] * W[i], (只购买主件)
- F[ i - 1 , j - V[i] - V[i的附件1] ] + V[i] * W[i] + V[i的附件1] * W[i的附件1], (购买主件+附件1)
- F[ i - 1 , j - V[i] - V[i的附件2] ] + V[i] * W[i] + V[i的附件2] * W[i的附件2], (购买主件+附件2)
- F[ i - 1 , j - V[i] - V[i的附件1] - V[i的附件2] ] + V[i] * W[i] + V[i的附件1] * W[i的附件1] + V[i的附件2] * W[i的附件2], (购买主件+附件1+附件2)
- }
边界条件
- F[0,k]=0 (0<=k<=N)
目标结果
- F[M,N]
作业调度方案
做这道题关键在于理解题意。这道题让我想起了流水作业调度的Johnson算法,虽然与这道题无关。
提示几点
- 每个工件的每个工序对应着不同的机器。
- 一个工件必须按照加工顺序加工。
- 把每个机器看成一个时间轴,每个时间对应着加工一个工件,或者为空闲状态。
- 题中的算法是给定的贪心策略,不需要构造,只要模拟。
按照给定的顺序,一件一件得往时间轴上插入,每个工件插入的位置必须在前面的工序都完成以后的时间断插入。每次插入扫描一遍时间轴数组,找到最前面一个。
其实题中所给的图已经一目了然了。理解题意以后,会发现这是NOIP2006最简单的题。
2^k进制数
较难的递推问题,需要不错的数学知识基础才能顺利的解决问题。
2^k进制实质上就是2进制数,分为长度为k的段。
- 每位的最大值 M = 2^k-1
- 最大的位数 N = w/k +1
- 最高位的最大值 H = (2^(W Mod K))-1
题中的“另一角度解释”其实已经给了我们解题的思路。设F[i,j]为位数为i,最高位为j的满足条件的2^k进制数的个数。
如题中解释
- 2 位数:高位为 1 F[2,1]=6 (即 12,13,14,15,16,17)。 高位为2,F[2,2]=5 , … ,F[2,6]=1(即 67)。
- 3 位数:高位是 1,F[3,1],第 2 位为2,有F[2,2]的5 个(即123,124,125,126,127),第 2 位为 3 ,F[2,3]4 个, … ,第 2 位为 6,F[2,6] 个(即 167)。F[3,1]=5+4+ … +1=15个。
我们可以发现,F[i,j]总是由F[i-1,k]推出,确切来说递推方程是Sum{ F[i-1,k] } (j+1<=k<=M)。
最终结果要分为两种情况的总和。
- 最高位大于等于1
- 最高位为0
由于状态很多,使用滚动数组可以大幅度节约空间。计算时要用要用高精度。
状态设定
- 每位的最大值 M = 2^k-1
- 最大的位数 N = w/k +1
- 最高位的最大值 H = (2^(W Mod K))-1
- F[i,j]为 位数为i,最高位为j的满足条件的2^k进制数的个数
递推方程
- F[i,j]=Sum{ F[i-1,k] } (j+1<=k<=M)
边界条件
- F[1,k]=1 (0<=k<=M)
目标结果
- Ans = Sum{ F[N,i] } + Sum{ F[j,0] } (1<=i<=H 3<=j<=N)
NOIP2007
统计数字
基本上可以说是送分题了吧。解决这道题的方法太多了,基本思想就是排序。快排,堆排,只要O(nlogn)都可以,没有可以卡快排的数据。也可以挂链哈希。当然平衡树也行。我写了Treap。
字符串的展开
这是个简单的题,但不是完全的送分题,要胆大心细。多读几遍题,掌握每一个细节。
题中陷阱很多,例如字符串长度可能很长,最好一边读一遍输出,对于Pascal尤其不要用string存起来。注意减号的问题,注意有可能有些减号不是起上述作用的,例如字符串开头就是一个减号,或者好几个减号在一起。
矩阵取数游戏
动态规划。由于每行是互不影响的,可以把每行孤立开看,对每行分别进行一次动态规划计算。结果可能很大,需要高精度计算。为避免计算高精度乘法,可以在初始化时把每个数的2^k递推算出。
状态设定
- F[i,j] 为当前行取前i个数和后j个数的最大得分。
- V[i,j] 为当前行第i个数乘以2^j的值。
状态转移方程
- F[i,j]=max{ F[i-1,j] + V[i,p] , F[i,j-1] + V[M-j+1,p] } p=i+j(表示当前是第几次取数)
- V[i,j]=V[i,j-1]+V[i,j-1]
边界条件
- V[i,1]=当前行第i个数的值
- F[0,0]=0
每行结果
- Max{ F[i,M-i] } (0<=i<=M)
最终结果
- 每行结果的总和
高精度计算时要缩位优化,否则是很容易超时的。任何可以常数优化的地方都不要放过,因为数据是比较强的。
树网的核
图论题,其实并不难,但可能理解起来有障碍。
首先,一个图可能有多个直径,但只用研究其中任意一个即可。所以要先找一条直径。
根据直径的定义,可以这样找直径。
- 在图中任意找一个顶点A。
- 以A为源点,搜索距离A最远的点B。O(N)
- 以B为源点,搜索距离B最远的点C。O(N)
下面就是在直径上枚举核。不难发现,核的长度越大,期望的最小偏心距就越小。方法如下
- 在直径上枚举顶点A。O(N)
- 找到直径上距离A不超过S的最远顶点B。O(1)
- 求AB的偏心距。O(N)
按照定义找核的偏心距,即枚举核上每个顶点A距离最远点B,且B不在核上的距离|AB|。搜索即可。要把核上的顶点从图中暂时删掉,避免找到的B在核上。时间复杂度为O(N)。
最终时间复杂度为O(N^2)。
上次修改时间 2017-02-03