NOIP2021 题解

T1:两人轮流报数,规定所有含有 7 7 7 的数字及其倍数都不能报出,每次询问给出上一个报的数,求下一个报的数是多少(或判断上一个报的数不合法)。
询问次数 T ≤ 2 × 1 0 5 T \leq 2 \times10^5 T2×105 ,数字范围 n ≤ 1 0 7 n \leq 10^7 n107
题解:直接模拟题意即可。
从小到大处理出所有含有 7 7 7 的数,这可以通过一个递推关系来实现:设 f ( n ) f(n) f(n) 表示数字 n n n 中是否含有 7 7 7 ,则有 f ( n ) = f ( n / 10 )   ∣   ( n % 10 = = 7 ) f(n) = f(n / 10)\ |\ (n \% 10 == 7) f(n)=f(n/10)  (n%10==7) 。也就是说把 n n n 拆成个位和前面若干位来考虑即可。
然后从小到大枚举每个含有 7 7 7 的数并枚举它的倍数,把枚举到的数标记为不合法即可。作为优化,对于一个本身含有 7 7 7 ,但其某个因子中也含有 7 7 7 的数,可以跳过对于它的倍数的枚举。
这有点类似于埃氏筛的思想,不过含有 7 7 7 的数的密度并没有质数那样好的 O ( n / log ⁡ n ) O(n / \log n) O(n/logn) 的性质,但无论如何复杂度也是不超过 O ( n log ⁡ n ) O(n \log n) O(nlogn) 级别,加上常数优秀,实测只需要大约 1.6 × 1 0 7 1.6 \times 10^7 1.6×107 次筛即可。
最后处理询问时,可以直接记录每个数的下一个合法的数是多少,从大到小扫一遍所有的数即可。查询是单次 O ( 1 ) O(1) O(1) 的。
一个小细节: n = 9999998 n=9999998 n=9999998 时的答案为 10000001 10000001 10000001 ,所以刚好筛到 1 0 7 10^7 107 是不行的。

T2:给定数组 v v v ,下标从 0 0 0 m m m 。对于一个大小为 n n n ,元素为 0 ∼ m 0\thicksim m 0m 的数组 a a a 来说,它的价值是 ∏ i = 1 n v a i \prod_{i=1}^{n} v_{a_i} i=1nvai。再设一个参数 x = ∑ i = 1 n 2 a i x=\sum_{i=1}^{n} 2^{a_i} x=i=1n2ai ,只有当 x x x 的二进制中 1 1 1 的个数不超过 k k k 时,才认为这个数组 a a a 是合法的。给定 n , m , k n,m,k n,m,k 和数组 v v v ,求所有合法的 a a a 数组的价值之和。
题解:考虑从小到大填所有的数,因为这样一来每填完一种数之后,都可以把 x x x 对应位的值算出来,可能产生的进位只会影响到高位。
f [ i ] [ j ] [ l ] [ p ] f[i][j][l][p] f[i][j][l][p] 表示当前要填数字 i i i ,之前已经填了 j j j 个数,目前生成的 x x x 0 ∼ i − 1 0\thicksim i-1 0i1位中已有 l l l 1 1 1 ,且低位向第 i i i 位进位的数量为 p p p
初值为 f [ 0 ] [ 0 ] [ 0 ] [ 0 ] = 1 f[0][0][0][0] = 1 f[0][0][0][0]=1 ,转移时枚举第 i i i 位填多少个 1 1 1 (设为 q q q 个),乘一个转移系数 c [ n − j ] [ q ] × v i q c[n - j][q] \times v_i^q c[nj][q]×viq ,转移到 f [ i + 1 ] [ j + q ] [ l + ( p + q ) % 2 ] [ ( p + q ) / 2 ] f[i + 1][j + q][l + (p + q) \% 2][(p + q) / 2] f[i+1][j+q][l+(p+q)%2][(p+q)/2] 。其中 c [ n − j ] [ q ] c[n-j][q] c[nj][q] 为组合数。
也就是说,枚举有 q q q 个位置填 i i i 之后,从目前为空的 n − j n-j nj 个位置中选出 q q q 个的方案数是 c [ n − j ] [ q ] c[n-j][q] c[nj][q] ,根据题意这 q q q i i i 产生的价值贡献是 v i q v_i^q viq ;填完这 q q q i i i 后,加上之前的进位, x x x 的第 i i i 位现在有 p + q p+q p+q 1 1 1 ,最后这一位是否为 1 1 1 就取决于 ( p + q ) % 2 (p+q)\%2 (p+q)%2 的值,同时要向高一位产生 ( p + q ) / 2 (p+q)/2 (p+q)/2 个进位。
最后的dp状态是 f [ m + 1 ] [ n ] [ l ] [ p ] f[m+1][n][l][p] f[m+1][n][l][p],其中 l l l 0 0 0 枚举至 k k k p p p 0 0 0 枚举至 n n n 。别忘了 l l l 只是代表前 0 ∼ m 0 \thicksim m 0m 位的 1 1 1 的个数,还要加上数字 p p p 本身的 1 1 1 的个数才是真正的 x x x 中的 1 1 1 的个数。设 p o p c n t ( p ) popcnt(p) popcnt(p) p p p 的二进制中 1 1 1 的个数,最后将所有满足 l + p o p c n t ( p ) ≤ k l+popcnt(p) \leq k l+popcnt(p)k 的dp值 f [ m + 1 ] [ n ] [ l ] [ p ] f[m+1][n][l][p] f[m+1][n][l][p] 求和即可。
状态数 O ( n 3 m ) O(n^3m) O(n3m) ,转移 O ( n ) O(n) O(n) ,总复杂度 O ( n 4 m ) O(n^4m) O(n4m)

T3:给定序列 a a a ,保证序列单调不降,每次可以选择一个下标 i ( 1 < i < n ) i(1\lt i \lt n) i(1<i<n) ,将 a i a_i ai 变成 a i − 1 + a i + 1 − a i a_{i-1} + a_{i+1} - a_i ai1+ai+1ai。问若干次操作之后整个序列的方差最小是多少。
题解:考虑这个操作的本质。由于序列不降,可以考虑差分。
设相邻三个数分别为 a , a + b , a + b + c a,a+b,a+b+c a,a+b,a+b+c ,差分后为 b , c b,c b,c ,对中间的数操作之后就变成 a + ( a + b + c ) − ( a + b ) = a + c a+(a+b+c)-(a+b)=a+c a+(a+b+c)(a+b)=a+c,换句话说序列变成 a , a + c , a + b + c a,a+c,a+b+c a,a+c,a+b+c , 差分后为 c , b c,b c,b 。也就是说操作的本质就是交换相邻两个差分值。
又由于对序列整体加一个值之后方差不变,因此我们直接看这些差分值即可。
由于要使得方差尽可能小,我们当然希望中间的数尽可能密一些。而相邻两个数的差的种类和数量已经固定了,唯一能变的就是安排顺序的问题了,我们自然会想让比较小的那些差值尽可能往中间放,也就是说差分数组很有可能呈现中间小两边大的性质。实际上我们可以严格证明:最优方案中差分数组一定是单谷的,也就是说先递减后递增。
由于空间有限这里就不证了。
此时我们可以这样玩:强行钦定差分数组中最小的那个数对应差分前为 0 0 0 ,然后从小到大枚举差分数组,每次可以选择将这个差分值放在当前的最左边或是最右边。在左边和右边各记录一个差分值的和 s l , s r s_l,s_r sl,sr ,则来了一个 差分值 b i b_i bi 之后,如果放在右边,就意味着 a a a 数组中多了一个数 s r + b i s_r+b_i sr+bi ,同时 s r + = b i s_r += b_i sr+=bi ;放在左边同理。
这样我们已经能设计一个 O ( 2 n ) O(2^n) O(2n) 的暴力了,下一步就是把它改成dp。
f [ l ] [ r ] [ s ] f[l][r][s] f[l][r][s] 表示目前的 s l = l , s r = r s_l=l,s_r=r sl=l,sr=r,所有已有的 a i a_i ai 和为 s s s 的答案。注意这里我们可以采用方差的另一种计算方式: D = ( ∑ i = 1 n a i 2 ) / n − ( a ′ ) 2 D=(\sum_{i=1}^n a_i^2)/n - (a')^2 D=(i=1nai2)/n(a)2 ,其中的 a ′ a' a 为平均值。这样dp状态里只需要存最小的 ∑ a i 2 \sum a_i^2 ai2 即可。另外,对于给定的 l l l r r r ,我们其实能唯一算出来目前放到第几个数了,因此不需要额外记录一维。
注意到差分值中所有的 0 0 0 我们都是可以不管的,而非 0 0 0 的差分值之和不超过 m = max ⁡ { a i } m=\max\{a_i\} m=max{ai} 且都是正的,因此数量也不超过 m m m 。因此数据范围里给的诸如 n = 10000 n=10000 n=10000 之类的完全是唬人,由于 s s s 最大是 O ( m 2 ) O(m^2) O(m2) ,上述dp的总复杂度为 O ( m 4 ) O(m^4) O(m4)
这还差一点,不过我们还有接下来的奇妙处理(warning:以下做法非官方题解,官方做法是严格 O ( n m 2 ) O(nm^2) O(nm2) 复杂度的,以下只是我和出题人放飞自我的脑洞):
想象一下 s s s 在累加的过程中有正有负,最终的最优答案不太可能偏离 0 0 0 太远。因此我们可以人为给 s s s 设定一个小于 m 2 m^2 m2 的上界,比如常数倍的 m m m 之类的。实际上,我证明了在最优答案中 s s s 不会超过 O ( m 1.5 ) O(m^{1.5}) O(m1.5) ,而且这个上界非常松,极有可能实际上最多是 O ( m ) O(m) O(m) 的。
那问题来了:我不知道这个界设定到多少才合适,万一开小了wa,开大了T咋办?还有一种玄学做法如下:
从小到大枚举最后的和 s s s (显然正负的情况是对称的,因此从 0 0 0 开始往上枚举即可)。然后在dp时不再记录 s s s 这一维,计算方差的方式也变回 ( ∑ ( a i − a ′ ) 2 ) / n (\sum (a_i - a')^2) / n ((aia)2)/n ,dp数组里存当前最小的 ∑ ( n a i − s ) 2 \sum (na_i-s)^2 (nais)2
你可能发现了问题:万一这个 s s s 不是最后这些 a i a_i ai 真正的和,这么算出来的方差不就错了吗?神奇之处就在这儿了:如果我们把方差看作一个“关于平均值的函数”,也就是在已知 a 1 , . . . , a n a_1,...,a_n a1,...,an的情况下,定义函数 f ( x ) = ( ∑ i = 1 n ( a i − x ) 2 ) / n f(x)=(\sum_{i=1}^n (a_i-x)^2)/n f(x)=(i=1n(aix)2)/n ,则得到一个关于 x x x 的二次函数,而不难验证它在 x x x 恰好等于这些 a i a_i ai 的平均值时取最小值,最小值就是这些 a i a_i ai 的方差。因此,错误的平均值只会让算出来的“方差”更大,因此把所有枚举的 s s s 都跑一遍之后,胜出的就一定是最优解中“正确”的 s s s 对应的答案了。
这样的好处在于,我们可以从小到大枚举 s s s 之后加上卡时,这样就不用担心上界开得不合理的问题了。经与出题人沟通,这样的做法根本卡不掉。
什么叫乱搞党的胜利啊(战术后仰)

T4:给定 n ∗ m n*m nm 的棋盘,棋盘上有 4 4 4 种类型的边,分别代表不可通行、只能走一步、只能一直沿一个方向往前走和可以任意走。棋子有两种颜色和等级,棋子间可以吃子,规定只能吃颜色不同且等级不高于自己的棋子,且吃完子后不能继续向前走。同时规定每次走子时经过的边类型必须相同。初始棋盘是空的,有 q q q 次操作,每次往棋盘上放一个棋子,问这个棋子能走到多少个格子。
题解:先在不考虑吃子的情况下看一个棋子能走到哪些空格子。此时可以把之前已经放上的棋子当作障碍,那么对于第一类边,只需要考虑上下左右 4 4 4 个格子;对于第二类边,只需要考虑沿着上下左右 4 4 4 个方向延伸出去的一条线段;对于第三类边,只需要考虑第三类边形成的连通块。
我们发现放一个棋子相当于删掉图中的一个点,删点维护图的连通性这件事太蛋疼了,于是我们不妨把问题反过来:假设一开始所有的棋子都是在棋盘上的,我们要每次删去一个棋子,删之前问它能走到多少个格子。
显然加点维护连通性是相对容易的,在不需要维护特殊信息的情况下只需要并查集就能搞定;即使需要维护集合,我们也有启发式合并、线段树合并等一堆合并集合的数据结构和算法。这为我们进一步分析题目提供了很好的技术支持。
首先,对于一类边,只有有限个点的情况总是好处理的:在别的情况都处理完之后,只需要暴力查询一下这几个点是否能被以其他方式走到即可。用并查集自然就可以做到。
对于二类边,我们可以在每个点上维护它向 4 4 4 个方向最远能沿着二类边走到哪。这件事也可以用并查集来维护,就是连续一段横向或纵向的二类边串起来的点分别用并查集维护起来,并查集中再顺手维护一个集合的编号最小/最大的点总是容易的。
对于三类边,我们好像也可以直接拿并查集维护所有点在三类边下的连通性,维护每个连通块的大小即可。
做完了吗?没有,最麻烦之处在于:如果一个点通过二类边和三类边都能走到,怎么去重?
此时要注意到一个性质:我们可以让二类边串起来的一排点的编号是连续的。如果我们把点按照横坐标第一关键字、纵坐标第二关键字排序的话,那么横向的二类边连通块对应编号连续的集合;反之,如果纵坐标第一关键字、横坐标第二关键字,编号连续的就是纵向的二类边连通块了。
所以,如果我们能在一个三类边连通块里查询编号位于某个区间内的点的数量,就可以实现去重了。
回过头来,我们发现简单地用并查集维护三类边的连通块似乎是行不通的,因为去重操作意味着我们还需要实打实地把每个连通块中的点记录下来。这就要用到我们先前提到的集合合并了:我们对每个三类边连通块开两个集合,分别存储其中的点按照两种编号方式的编号。合并连通块时,将两个集合对应合并,查询时在集合中区间查询即可。这里推荐使用线段树合并,因为复杂度为 1 1 1 log ⁡ \log log 且线段树天生支持区间查询。
最后还要处理吃子的情况。我们发现能通过一、二类边吃到的子每个方向上最多一个,因此也留到最后暴力处理即可;而通过三类边能吃到的子可能很多,在当前这个三类边连通块里,如果某个点又向外连了一条三类边而且恰好遇到了一个棋子,它就要被纳入考虑。
具体而言,我们要在每个三类边连通块上同时绑定与其直接通过三类边相邻的棋子集合,当然肯定要分黑白两色维护;合并连通块时,需要把两个集合分别合并,同时注意一个棋子可能同时在两个三类边连通块的集合中,因此还要去重(这里推荐先离散化使得每个棋子的等级均不同,以便于去重)。查询时,只需查询与当前棋子颜色相反的集合中,等级不超过它自身的棋子有多少个即可,这相当于一个前缀查询操作。显然这也是线段树合并就能解决的任务。对于一、二类边的特判,只需将涉及到的棋子在线段树中查询一下是否存在即可。
总结:倒序操作+合并连通块+维护集合,支持合并、区间查询+线段树合并,总复杂度 O ( ( n m + q ) log ⁡ ( n m + q ) ) O((nm+q) \log (nm+q)) O((nm+q)log(nm+q))
std码长大约 6 K B 6KB 6KB 多的样子,能在场上写出来调过的人请深受我一拜orz。实际上,如果不去写正解的话,至少前 32 32 32 分是可以直接模拟+bfs简单通过的,中间“没有三类边”的部分可以如上述题解所述用并查集维护二类边连通块,最后 n , m ≤ 1000 , q ≤ 2000 n,m\leq 1000,q\leq 2000 n,m1000,q2000 的部分只需要用并查集维护三类边连通块的大小,而一、二类边以及可能的吃子均不超过 O ( n + m + q ) O(n+m+q) O(n+m+q) 级别,可以枚举+暴力判断,复杂度 O ( n m + q ( n + m + q ) ) O(nm+q(n+m+q)) O(nm+q(n+m+q))(不过据我所知场上真正写了这档的人好像很少的样子qwq)。如上至少 56 56 56 分是可以不用写大数据结构即可实现的。有人说T4部分分是乱给的,他可不是乱给的啊

全场总结:个人感觉整体难度相比NOIP2020和CSP-S2021来说均略有下降,而且难度分布更合理了,既有T1这种小清新送分,也有T2这种仔细想想能做出来的dp和T3这种乱搞思维题,最后T4的数据结构当然也是常规防AK,而且好像也没防住操作了。唯一的不足可能是数学相关的稍微多了点?以及T4码量太大确实没啥办法,不过看在压轴题的份上也就……还好吧。
一不小心写了5000多字,好家伙我平时写论文的时候怎么没见这么个积极性呢qwq

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值