早期 NOIP 部分题目解析
早期 NOIP 部分题目解析
NOIP 1996 - 2000
P1012 [NOIP 1998 提高组] 拼数
对于两个数,如果它们的位数相同,那么我们需要将它们从高到低按位比较,比较结果较大的那个就尽量往前放。那么如果两个数的位数不同,我们可以考虑将这两个数拼起来,然后比较两种拼法哪个数较大,把较大的那个往前扔即可。
P2196 [NOIP 1996 提高组] 挖地雷
首先这道题肯定能用记忆化搜索。
如果我们用 DP 的话,状态转移非常好推,但是这道题要求我们求出最优解方案明细,这怎么办呢?因为 DP 的状态可以抽象成一个 DAG,所以解决方法是,对于每次成功更新的转移,记录该次转移的前驱状态;然后,对于最优解所对应的状态,依次回溯前驱状态,并输出目前正在回溯的状态值即可。
形式化地,因为状态转移方程是
于是,我们可以将方程展开写,当
时,对于前驱数组 \(pre\),记录
即可。
P1004 [NOIP 2000 提高组] 方格取数
十分十分经典的一个题。这道题的精髓在状态定义:因为第一次走的路径和第二次走的路径都会影响你的决策,所以把它们都融入状态定义……虽然四维的数组很吓人,但这道题的数据范围仅为⑨,所以肯定是够开的。
这启示我们,先看数据范围再想解法,没准你非常暴力的解法就是正解。
这道题还有优化,这里口胡一下。
如果我们沿用之前的四维数组定义 \(f_{i,j,k,l}\),那么因为我们每次只能向下或向右移动,也就是将横坐标或纵坐标加一,因此两趟的横纵坐标总增量是不变的,也就是,对于所有合法的状态,\(i+j=k+l\) 恒成立。
因此可以定义,\(f_{i,j,k}\) 表示目前已经走了 \(i\) 步,\(j\) 为第一次路径的横坐标,\(k\) 为第二次路径的横坐标,目前的最优解。答案就是 \(f_{2n,n,n}\) 了。
P1019 [NOIP 2000 提高组] 单词接龙
之前在《搜索》一文中已写过。这题是搜索板子,非常适合练手,可以多打几遍。
NOIP 2001 - 2005
P1090 [NOIP 2004 提高组] 合并果子
你一看就想到 P1880 [NOI1995] 石子合并,但是这道题和石子合并题意不太一样,导致算法不同。
在这道题中,你可以选择任意两堆果子进行合并。
每次的选择都是任意的,这启示我们,可能每次都要取所有果子中最大或者最小的两堆进行合并。容易感知到,最优策略是:不断取最小的两堆合并成较大的一堆。手模即可。至于证明嘛,我也不会
这道题启示我们,一定要看好题意,题意的细微差别可能导致算法选择的直接改变。
P1043 [NOIP 2003 普及组] 数字游戏
先要断环为链。我原来的解法不对,这里口胡一下更好的解法
我们发现,断环为链之后,所有的 DP 都可以在一条链上完成,于是这道题就变为了线性 DP。定义 \(f_{i,j}\) 表示前 \(i\) 个数,被分割成 \(j\) 个部分所能获得的最值。
显然,对于前 \(j\) 个部分,我们可以考虑前 \(j-1\) 个部分,以及第 \(j\) 个部分的情况。我们可以枚举第 \(j-1\) 个部分和第 \(j\) 个部分的断点 \(k\),于是,转移变为——
其中 \(sum\) 是模 \(10\) 意义下的区间数字之和。
NOIP 2006 - 2010
P1058 [NOIP 2008 普及组] 立体图
虽然是一道大模拟,但是还得有策略地模拟,不能硬写,不然会吃亏。
如果从下往上按层渲染立方体,就不用考虑覆盖的问题。
P1064 [NOIP 2006 提高组] 金明的预算方案
这五年里的 NOIP 爆了很多典,出了一堆板子题,这就是一个有依赖背包的板子。
因为每个主件最多只有 2 个附件,所以完全可以枚举这 2 个附件的子集进行背包。
这个提示我们,如果数据范围非常非常小,可以枚举子集求解问题
P1072 [NOIP 2009 提高组] Hankson 的趣味题
犹记得,当时打完 CCPC 回家做的这道题。
题意:已知 \((x,a_0)=a_1\),且 \([x,b_0]=b_1\),求满足条件的 \(x\) 有多少个。
注意到数据范围非常大,暴力找到这样的 \(x\) 显然会超时。
我们开始推导。我们知道,若整数 \(a\)、\(b\) 满足关系 \((a,b)=d\),那么必有 \((\frac ad,\frac bd)=1\)。
根据上述结论,因为
所以
又注意到
由此,因为
所以
那么等式两边同时除以等式右边,得
即
联立,可知
又因为题中确保 \(a_0|a_1\) 且 \(b_1|b_0\),所以需要找到这样的 \(x\),使得 \(x\) 既是 \(a_1\) 的整数倍,也是 \(b_1\) 的因子。
所以我们只需要筛出 \(b_1\) 的所有因子,判断它是否为 \(a_1\) 的整数倍,且满足上面的两个式子即可累积答案。
P1099 [NOIP 2007 提高组] 树网的核
题面说的很神秘,但是这道题题面大多数说的东西都没啥用,简单来说,题意就是:给你一棵无根树(即没指明根节点的树形数据结构,也就是边数 = 点数 - 1 的图),让你找出这棵树的直径(直径可能有多条,但这些直径的中点是唯一的……这个结论一点用没有,但是还挺好玩的,可以记一下,万一以后用到了呢),称这些直径的一部分连续路径叫做树网的核,除核上点外,其他点到核上点的最小距离称为点关于核的偏心距。现在给定一棵树,让你求出所有偏心距的最小值。
这道题做法非常之多,而且都很优秀,这里说一下朴素 Floyd 做法。
数据范围 300,所以可以用 \(O(n^3)\) 过掉,考虑 Floyd 跑出树上任意两点间的路径。以下记路径 \((u,v)\) 的权值和为 \(dis_{u,v}\),那么可以发现,点 \(k\) 关于该路径的偏心距就是
接着,可以证明,若路径存在不位于直径上的部分,这条路径对应的偏心距一定不会比全部位于直径上的路径的偏心距的最小值更小。可以有一个非常不严谨的感性理解:直径已经囊括了最长的路径,剩下节点到直径的偏心距一定比较小;非直径路径所囊括的权值和一定较小,剩下节点到该路径的偏心距一定较大。
然后根据上面的思路模拟就行。
P1514 [NOIP 2010 提高组] 引水入城
搜索领域大神,让我 recall 到创新人才 T1 pedestrian 那道题……
题意:有一个 \(n\times m\) 的网格,每个格子有海拔高度,只能在第一行建立蓄水站,水只能从高海拔流向低海拔(四个方向)。问:能否让最后一行所有城市都有水?如果不能,输出不能覆盖的城市数量,如果能,输出最少需要多少蓄水站。
我们注意到,如果问题有解,那么每个蓄水站在最后一行所覆盖的范围,总是最后一行的一段连续区间。下面考虑证明,这里使用反证法。
考虑如果存在一个蓄水站 B,使得它在最后一行所覆盖的区间不连续(简单地,我们认为它只能覆盖一个蓄水站,记为 b);那么由于问题有解,则必定存在另一个蓄水站 C,使得它在最后一行能够覆盖某个除 b 之外的蓄水站(记为 c)。这里,我们认为,B.x < C.x 而 b.x > c.x。那么,c 对应的流径,必定和 b 的流径存在交点。因为流径上的任何点都是单调递减的,所以 B 和 C 的流径都能流到 c 点,也就不需要蓄水站 C 了,所以不存在这样的 C,命题得证。
如果问题有解,那么我们需要用最少的区间覆盖整个最后一行。可以定义两个 DP 数组,其中 l[x][y] 表示从点 \((x,y)\) 出发能覆盖的最后一行最左位置,而 r[x][y] 表示从点 \((x,y)\) 出发能覆盖的最后一行最右位置。于是我们考虑贪心策略:对于最后一行,从最左边开始,找到所有左边界小于等于当前左边界的区间中,右边界最大的区间,然后选择这个区间,重复,直到覆盖整个区间。
NOIP 2011 - 2015
P2661 [NOIP 2015 提高组] 信息传递
我们考虑将传递方向看作一个单向边来建图。如果这个图中存在一个长度为 \(k\) 的环,那么经过 \(k\) 轮操作之后,这个环上的所有节点必将接收到它们自己所发出的信息。因为游戏一旦结束就不能继续,所以你需要找到这个图中最小的环,并输出它的长度。
具体操作就是搜索。以每个点为起点进行 DFS,求出每个点的 \(dfn\) 序,如果有环,那么某个点一定被搜了两次,环的长度就是 \(dfn_{第一次}-dfn_{第二次}+1\)。
P1941 [NOIP 2014 提高组] 飞扬的小鸟
其实背包 DP 可以理解成:源于某种决策选或不选,或仅能选一次,或能选多次,所引发的动态规划问题。
选或不选,也就是说,这个物品你要不要,如果要,决策就来源于 \(f_{j-w_i}\),如果不要,决策不变。
选一次就是 0-1 背包,能选多次就是分组或完全背包。
这道题就是这样,“下降”这个事件在一个单位时间内只能发生一次,就是 0-1 背包;“点击屏幕而上升”这个时间在一个单位时间内可以发生多次(题意已表明),就是完全背包。
所以我们已经完成了背包建模,接下来可以定义 \(f_{i,j}\) 表示飞到第 \(i\) 列,高度为 \(j\) 时,所需的最少步数。注意这里的坐标定义并非传统笛卡尔坐标系。这样定义方便一点。
如果是“下降”,那么转移方程就是
其中,\(fall_i\) 表示在第 \(i\) 列往下掉的格数。值得注意的是,这里的上一步决策来源于 \(i-1\) 这一维。而我们对照 0-1 背包的无滚动数组优化写法,发现这里也是转移自 \(i-1\) 这一维。
如果是“上升”,那么转移方程就是
因为一步可以点击多次,所以决策可能来源于当前这一列点击次数不够的决策,也可能来源于上一列点击次数不够的决策。即,要么从上一列点击一次过来,要么从本列再点一次过来。
注意要特判边界 \(m\) 的情况,此时转移方程会有所转变。
至于管道的问题,可以这样解决:维护一个管道的计数器,如果目前的列恰好等于计数器指向的那个管道的列,那么将管道覆盖的 \(f\) 数组值全部设为无穷大,代表这些格子永远不能被转移。然后判断这一列中,那些不被管道覆盖的格子是否被转移到(即 \(f\) 数组是否有值),如果没被转移到,说明撞到了这个管道,输出目前计数器的值 - 1,代表飞过了多少个管道。
最后找到第 \(n\) 列里最小的数组值即可。
P1966 [NOIP 2013 提高组] 火柴排队
这道题涉及到一个重要的数学不等式:排序不等式。
排序不等式表明:如果有两个数列 \(\{a_i\}\) 和 \(\{b_i\}\),且这两个数列都是单调递增或单调递减的,那么它们对应项乘积之和在顺序排列时达到最大值,而在逆序排列时达到最小值。具体来说,若 \(\forall a_i,a_i\geq a_{i+1}\) 且 \(\forall b_i,b_i\geq b_{i+1}\),则有:
对于任意的排列 \(σ\),当且仅当至少有一个数列为常数列或两个数列完全相同时,等号成立。
通俗点说,顺序 >= 乱序 >= 反序
这道题要求 \(\sum(a_i-b_i)^2\) 的最小值,而
这里 \(\sum(a_i^2+b_i^2)\) 是个定值。因为 \(2\sum(a_ib_i)\) 这一项前面有个负号,所以我们只需求 \(2\sum(a_ib_i)\) 的最大值就能求出原式的最小值了。
这里就要用到排序不等式了,把这两个序列都排成顺序的即可。
题里问你最小交换次数,也就是求逆序对的数量,用归并写一下就行。
P1083 [NOIP 2012 提高组] 借教室
这个答案的单调性很容易发现:所考虑的订单越多,答案就越接近不合法。所以定义 \(\text{check}(x)\) 表示:仅考虑前 \(x\) 个订单的方案是否合法。
如果 \(\text{check}(m)=1\),那么所有订单在一起的方案是合法的,输出 0 即可。
否则,二分找到最小的 \(x\),使得方案不合法;然后输出二分到的答案 - 1 即可。
P2679 [NOIP 2015 提高组] 子串
好题,非常有代表性,详见《DP 复习》一文。
NOIP 2016 - 2021
P3958 [NOIP 2017 提高组] 奶酪
奶酪的孔洞具有连通性,进而想到使用并查集。只需要写一个小模拟,判断两球是否相切,然后用并查集判断顶端和底端是否连通即可。
事实上,如果两个球球心之间的距离小于或等于它俩半径之和,则说明这两个球交上了
P2822 [NOIP 2016 提高组] 组合数问题
重要结论:
-
\(\tbinom{i-1}{j-1}+\tbinom{i-1}{j}=\tbinom{i}{j}\),这很容易证明,在此不赘述。
-
如果 \(k|a\),而且 \(k|b\),那么 \(k|a+b\)。这也显然。
现在我们开始研究这道题。
容易发现,\(k|\tbinom{i}{j}\) 等价于 \(k|\tbinom{i-1}{j-1}\) 且 \(k|\tbinom{i-1}{j}\)。
所以我们用数组 \(f_{i,j}\) 表示 \(\tbinom{i}{j}\mod k\) 的值,当它为 \(0\) 时说明可以整除,就可以累积到贡献里。
在枚举 \(i\)、\(j\) 的过程中,总的程序时间复杂度是 \(O(Tnm)\) 的,会爆最后一个点。
发现我们可以用前缀和维护答案,然后就做完了。
P7113 [NOIP2020] 排水系统
对于每个源点,模拟排水的过程,用 DFS 依照题意模拟即可。分数相加的部分不是很好写,注意一下,这里应用了 \(\frac ab+\frac cd=\frac{ad+bc}{bd}\)。
P7960 [NOIP2021] 报数
你说这是筛素数吧,倒也不是,但是很像埃氏筛的思想。
就是对于每个不合法的数,它们的倍数也不合法,给它们打上标记就行。
其实没必要优化到线性筛,埃氏筛的时间复杂度就能过。
P5020 [NOIP 2018 提高组] 货币系统
重要的性质:如果使用前 i 种货币能够凑到它后面的货币面值,那么这个被凑到的货币面值是无用的,可以废弃。
所以你从小到大排序这些货币面值,看前面的面值能不能凑出后面的面值,如果能就把计数器自减。
P5022 [NOIP 2018 提高组] 旅行
题意就是说,给你一棵树或者是基环树,输出它字典序最小的遍历顺序。
树的情况很好做,每次都走最小的点就行。
至于基环树的情况,我们枚举每一条边,然后删掉它。继续从点 1 开始遍历,再从所有遍历的结果中取字典序最小的情况。
P2827 [NOIP 2016 提高组] 蚯蚓
由于我们每次操作都要找到最长的蚯蚓,所以不难想到堆。但是这题如果用堆的话,时间复杂度是 \(O(m log n)\) 的,在这个数据范围下会爆炸,所以考虑其他方法。
由于每秒都切的是最长的蚯蚓,可以发现,每次操作中,所选要切的蚯蚓长度一定单调不减,进而,被切出来的左段和右段也一定单调不减。
因此,所选被切的蚯蚓长度本身具有单调性,而且我们在每轮操作中,对且仅对这一个最长的蚯蚓做文章,所以就没必要用堆再次维护它的单调性,而可以用队列直接做,让复杂度降到 \(O(m)\) 的。
我们使用三个队列,分别存储:1. 没被切的蚯蚓,2. 被切的蚯蚓左段,3. 被切的蚯蚓右段。每次取出三个队列中最长的一只蚯蚓,将其切断,所得到的左右部分分别放入 2 号、3 号队列,重复 \(m\) 次即可求出结果。
每次除了被切的蚯蚓,其他所有的蚯蚓都在变长。而被切的蚯蚓在这一秒钟不变长,因此我们只需要在取蚯蚓的时候将它的长度加上 \((i-1)\times q\),放回时减去 \(i\times q\) 的长度即可。
P3953 [NOIP 2017 提高组] 逛公园
首先跑一遍 Dijkstra 最短路,求出每个节点的 \(dis\) 值。
定义 \(dp_{u,k}\) 表示走到 \(u\),花费为 \(dis_u+k\) 时,走过路径的方案数。
显然,这个转移应当是倒着的,即:考虑一个指向 \(u\) 的点 \(v\),如果走到 \(v\),花费为 \(dis_v+pre\_k\) 时,其状态应为 \(dp_{v,pre\_k}\),然后将它累加到 \(dp_{u,k}\) 的贡献中。注意,这里 \(v\) 是 \(u\) 的前驱节点,而并非子节点;这启示我们,建图的时候应当建两个图,一个建正边,一个建反边。
那么这个 \(pre_k\) 应该等于什么呢?因为 \(v\) 是指向 \(u\) 的,所以当转移的时候,我们有:\(dis_u+k = dis_v+pre\_k+g_{u,v}\),进而可以得到 \(pre\_k=dis_u-dis_v+k-g_{u,v}\)。
然后我们对于这个图,在上面记忆化搜索式的 DP 即可。

浙公网安备 33010602011771号