CF2229 (Codeforces Round 1100, Div. 1 + Div. 2) 补题笔记

· · 个人记录

Codeforces Round 1100

完蛋感冒了,这么晚才写完,谢罪。

前情提要:本场 EFGHI 题均为 DP。

upd log:

A

自评:红。

简要题意

给定长度为 n 的数组 a,每次操作可以选一个 x,然后对于所有 a_i 使其增加 \text{sgn}(x-a_i),求使所有 a_i 都相同的最小操作次数。

数据范围:多测,t\le 100,n,a_i\le 10^3

做法

显然选多个 x 是不优的,因为这唯一的影响是可能导致某些数浪费两次操作。

对于同一个 x 答案就是和它相差最大的那个差,于是联系初中学习的数轴相关知识(其实原题面甚至已经帮你联系了)得知选最大和最小值的中点最优,即答案为 \lceil\frac{mx-mn}{2}\rceil(其中 mx 是最大值,mn 是最小值)。

单组复杂度 O(n)

B

自评:橙。

简要题意

给定长度为 n 的数组 a,b,对于每个 i,你可以(不必须)交换 a_i,b_i,求操作后 \displaystyle\max_{i=1}^n\{a_i\}+\sum_{j=1}^nb_j 的最大值。

数据范围:多测,t\le 10^4,\sum n\le 10^5

做法

既然 a 中只能贡献一个,我们先尽可能让大的贡献,即让 a_i\le b_i。为了方便,以下不妨直接设 a_i\le b_i。这样刚才方案的答案就是 \displaystyle\max_{i=1}^n\{a_i\}+\sum_{j=1}^nb_j

由于 a 只能贡献一个,所以换多个则一定有些位置是纯亏的,所以只需要考虑换一个的情况,设交换了 a_x,b_x。一种可能是 a 的最大值没变,这种情况同样是纯亏的;否则有 b_x>\max\{a_i\}\ge a_x,则 b_x 顶掉了原来 \max\{a_i\} 的贡献,a_x 顶掉了原来 b_x 的贡献,算下来就是 a_x 顶了 \max\{a_i\} 的贡献,而 a_x\le \max\{a_i\},故还是不优的。

综上,操作后的 a_i'a_i,b_i 的较小值最优。

单组复杂度 O(n)

C

自评:红(C1)橙(C2)。

简要题意

给定长为 n 的非零数组 a,你可以进行不超过 n 次操作,每次可以选一个正数 a_i,并将 a_1a_i 全部取相反数。求构造一种方案,使得操作后 \sum a_i 最小(C1)/ 最大(C2)。

数据范围:多测,t\le 10^4,\sum n\le 2\times 10^5

做法

首先 C1 可以倒序操作每个正数(注意后面操作对前面是有影响的,这里正数指的是当前状态下的正数而非原始数组中的正数)然后就全变成负的了。

然后 C2 延续 C1 的思路,可以选恰好一个正数(显然选多个是浪费操作)并以将它变成负的为代价,把它之前的全变成正的。通过计算前缀和及前缀绝对值和可以求出若选择该正数答案是多少,取最大的并延续 C1 的构造方法即可,只不过最后要记得操作一次选定的值。

单组复杂度 O(n)

D

自评:绿。

前情提要:这里有非常逆天的做法,建议看看。

简要题意

给定长度为 n 的数组 a,b,每次操作可以选择一个 i,并将 a_i,a_{i+1} 替换为一个 a_i,a_{i+1},b_i,b_{i+1} 中的第二小,b_i,b_{i+1} 替换为一个 a_i,a_{i+1},b_i,b_{i+1} 中的第三小。这样重复 n-1 次,ab 中就都只剩下一个元素了,求这两个元素的最小值最大可以是多少。

数据范围:多测,t\le 10^4,\sum n\le 10^5

做法一

高一补过十来场 CF,这个想法在这之间好像也不是第一次出现了,可以当个模型记忆。

求最小值最大且有单调性,考虑二分答案。check 时将 <mid 的数变为 0\ge mid 的数变为 1,如果最终能剩下 1 就返回 true

在本题中 a_i,b_i 的“地位”是相同的,故每个 i 上只有三种情况:(0,0)(0,1)(1,1)(0,1) 和别的合并时,别的不受影响,故可以忽略 (0,1)。然后应该是 (1,1)(0,0) 合成 (0,1),最后只要能剩下 (1,1) 就算成功。于是为了省一些 (1,1) 我们发现 (0,0)(0,0) 合成完还是 (0,0),即忽略 (0,1) 后连续的 (0,0) 可以视为一个。这样算下来 (0,0) 的个数能严格小于 (1,1) 的个数就可以留下 1 了。

checkO(n) 的,故单组复杂度是 O(n\log n) 的。

做法二

Solution from @akane646, proof by myself.

断言 每次取 s_2+s_3 最小的对一定最优。

::::info[证明]{open}

由于这里操作的过程是一个类似链表删除的结构,所以直接像本场 B 题或 F 题那样直接交换来证是不太容易的。所以我们先考虑容易交换的情况,即 n=3

此时我还是不太会证。但是我们是 OIer,所以我们可以花五分钟央求我们温柔美丽可爱强大的 C++ 姐姐帮我们证:

:::success[Code]{open}

//Please ignore those headfiles.I write them just because
//DEV-C++ won't support completion if i use <bits/stdc++.h>
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<ctime>
#include<cmath>
#include<cctype>
#include<string>
#include<algorithm>
#include<vector>
#include<map>
#include<unordered_map>
#include<set>
#include<queue>
#define ll long long
#define ull unsigned long long
#define lf double
#define ld long double
#define LL __int128
#define ui unsigned int
using namespace std;
ll a[]={1,2,3,4,5,6};
ll sec(vector<ll> tmp){
    sort(tmp.begin(),tmp.end());
    return tmp[1];
}
ll thi(vector<ll> tmp){
    sort(tmp.begin(),tmp.end());
    return tmp[2];
}
int main(){
    ios::sync_with_stdio(0);
    for(int i=0;i<720;i++){
        ll l=thi({sec({a[0],a[1],a[2],a[3]}),thi({a[0],a[1],a[2],a[3]}),a[4],a[5]}),r=thi({sec({a[4],a[5],a[2],a[3]}),thi({a[4],a[5],a[2],a[3]}),a[0],a[1]});
        if(l==r){
            continue;
        }
        if(!((l<r)^(sec({a[0],a[1],a[2],a[3]})+thi({a[0],a[1],a[2],a[3]})<=sec({a[4],a[5],a[2],a[3]})+thi({a[4],a[5],a[2],a[3]})))){
            for(int j=0;j<6;j++){
                cout<<a[j]<<' ';
            }
        }
        next_permutation(a,a+6);
    }
    return 0;
}

:::

她什么也没说,看来是默许了,于是在 n=3 时以上结论成立。

于是对于 n>3,对于不符合我们条件的方案,我们可以利用以上证明进行邻项交换,将其变为符合我们条件的方案。

::::

维护当前每个位置的前一个和后一个,并使用 multiset 维护各对 (i,i+1)s_2+s_3 即可做到 O(n\log n)

E

自评:下位蓝。

简要题意

给定 n 个点的无根树。你有一个初始为空的集合 s,并将要进行 n-1 次以下操作:将编号最大的叶子的编号加入 s,并任选另外一个叶子将其删除。求最终得到的 s 可能有多少种,对 998244353 取模。

数据范围:多测,t\le 10^4,\sum n\le 2\times 10^5

做法

以下“u 的子树”不含 u 本身,原因您看完就懂了。

考虑一个非叶子 u 什么时候能变成叶子。

若将其视为根,则显然是其除某一个儿子及其后代以外所有后代都被删除后。而这个过程中被删掉的点都会有一段时间变成叶子。

于是问题在于,如果某个点变成叶子时刚好变成了当时最大的叶子,那该点不能直接删,只能先把一个更大的变成叶子再删。这样一来,这个“更大的”就也要被加入 s 中。

因此 u 首先得大于 u 除某一个儿子及其后代以外所有后代的最大值才可能被加入 s。由于 u\neq n 时一定存在一个儿子满足其或者其子树内包含 n,故除去的那个儿子只能是该儿子,于是为了方便我们直接将 n 作为根(并特判掉 n 本身就是叶子的情况),这样删的范围就是 u 的子树。

而在将 u 加入 s 的过程中,如果其子树中存在可能成为最大叶子的点,设这些点中最大的为 v(显然,根据可能成为最大叶子的点的条件,它一定也是 u 子树内的最大值),那我们总是需要在编号区间 (v,u) 内找一个点加入 s 以确保 v 能被顺利删除,无论我们是否真的将 v 加入了 s——反之,u 将不能被加入 s,因为为了删除 v,一定要选一个 (v,n] 的点,若不选 (v,u) 内的点,则只能选 (u,n] 内的点了。

于是我们得到了一个思路:设 dp_i 表示只考虑 [1,i] 内的点,且将 i 加入 s 的方案数。设 ji 子树内的最大值,则转移为 dp_i=\displaystyle\sum_{k=j+1}^{i-1}dp_k。这是个区间求和,故可以用前缀和优化做到 O(n)

好心的出题人在最后一组样例中提醒了您只是这样并不够,因为有些点(该样例中的 2,3)即使满足条件也不可能先于 n 被删除。所以 n 的转移应该是所有不可能先于 n 被删除的 dp 值之和。这要求计入答案的点大于 n 的子树中,除了包含该点的儿子及其子树外所有点的编号。因此,这些点一定在 n 的儿子中,子树内包含 n-1(或者其本身即为 n-1)的那个儿子及其子树内。求出 n 子树内其他点的最大值并遍历这个儿子的子树即可得到答案。

单组复杂度 O(n)

F

自评:下位紫\~中位紫。(好像评高了?)

在本文中,你将看到评论区中的多种做法,包括几乎稳过的随机化做法

简要题意

给定长度为 n 的数组 a。你有一个长度为 k、初始全为 0 的数组 b。你可以将 a 中的元素任意排序,然后按你所排的顺序,将每个 a_i 加到当前 b 中任一最小值上。求操作后 b 中最大值最大可以是多少。

数据范围:多测,t\le 10^4,n\le 18,\sum 2^n\le 2^{18}

做法〇

Solution from @RockyYue.

这位大佬暴搜剪枝获得了 62 ms 的好成绩,感兴趣的可以看看,写在这里仅作膜拜用。

做法一

The same solution as the official editoral.

> **断言** 最大值放最后一项一定最优。 比较简略的证明(不难发现忽略了大量情况,但都不是重点):我们设 $q$ 是 $a$ 中最大值。假设存在一种方案使得最后操作的值为 $p$,$p$ 加到的 $b$ 的项最终为 $u+p$,$q$ 加到的 $b$ 的项最终为 $v+q$($p,q$ 加到同一项显然无意义)。考虑交换 $p,q$ 的加入时间。显然,在本题中,**$b$ 的极差不会超过 $q$**(证明里会频繁涉及,不要忘了),故交换后的答案一定是加了 $q$ 的一项。而且,由于 $q$ 先被加入,故根据题意 $u>v$。 - 若 $v+p\ge u$,则答案变为 $u+q$,因为 $u>v$ 且 $q>p$ 所以 $u+q$ 既大于 $v+q$ 也大于 $u+p$,一定更优。 - 否则,因为 $v$ 加上 $p$ 后还是最小的,故新的答案是 $v+p+q$。 - 若原来的答案是 $v+q$,则新答案自然更大。 - 若原来的答案是 $u+p$,则因为 $v+q>u$,所以 $v+q+p>u+p$,还是更大。 而最大值会被放在放它之前 $b$ 的最小值处。故问题变为了“最小值最大”的形式,且显然答案具备单调性,所以考虑二分答案。 我们希望 $b$ 的最小值不小于我们 check 的点 $x$,就是希望所有值都 $\ge x$,而 $b$ 中的每个值又都是 $a$ 中若干个元素相加得到的,故我们猜想,直接将 $a$(除去最大值,下同)分为 $k$ 个元素和 $\ge v$ 的部分,能分出来就是对的。感性理解一下,如果当前要放在 $b_i$,则取出第 $i$ 部分中未放的最大值即可,这样即使是最后到达 $x$ 的部分也总能取到 $x$。 这样 check 函数的写法就很显然了:设 $dp_{s,i}$ 表示已经用过了 $s$,分出了 $i$ 部分时最多剩下多少,然后暴力转移即可。单组复杂度 $O(n2^n\log V)$,$V$ 是值域。 额不过这个状态是我脑抽写的,其实完全可以把 $i$ 也存到 $dp$ 值里,都是一样的。 ### 做法二 > Solution from @[phsads](https://codeforces.com/blog/entry/153948?#comment-1367018). > > 洛谷题解区同样有大佬提及。 学到了新 trick。 首先还是用做法一中的性质。 但是我们不二分。我们想要最大化最小和,故我们将全部 $2^{n-1}$ 个集合按子集和降序排序。设 $dp_s$ 表示 $s$ 至多可以被多少个已加入的集合表示出来,每次加入一个集合时,更新包含它的所有集合,直至某次更新一个 $dp$ 值时其为 $k$。于是这本质上是在枚举每个集合的子集,故单组复杂度为 $O(3^n)$,而且实际上 $n$ 还减了 $1$,可以通过。 为了防止有和我一样没见过的选手不知道复杂度怎么来的,简单写一下证明。 $$\sum_{s\in U}2^{|s|}=\sum_{i=1}^{n-1}\binom{n}{i}2^i=(1+2)^{n-1}=3^{n-1}$$ ### 做法三 > Solution from @[Nachia](https://codeforces.com/blog/entry/153948?#comment-1367191). 首先还是用做法一中的性质。 既然我们只关心最小和,那我们可以考虑降序分出每个集合,则最后一个是这种分法里最小的。这意味着,对于当前选出的一个集合 $s$,设未选的集合为 $r$,还(不算 $s$)需要分出 $k$ 个,则应该有 $$k\sum_{i\in s}a_i\ge\sum_{i\in r}a_i$$ 于是状态和做法一是一样的,不过转移的条件改成了这个。但是这样我们就不用二分了,故复杂度是优秀的 $O(n2^n)$! ### 做法四 > Solution from @[okwedook](https://codeforces.com/blog/entry/153948?#comment-1367195). 洛谷的一位大佬在他的退火题解中提到: > 由于测试数据组数很多,我们对于 $n>5$ 的数据跑模拟退火,对于 $n≤5$ 的数据直接跑暴力。 而 CF 的这位佬想了另一个办法:写一个类似爬山的东西,即每次先 shuffle,然后枚举每个对,看看交换是否更优;然后每轮并行处理所有数据以方便卡时。但是那个佬称他 FST 了,故我将枚举对的过程也改成了随机顺序。经测,这个东西过的概率还挺大的: ![](https://cdn.luogu.com.cn/upload/image_hosting/p8or900l.png) ## G 自评:中位紫(但 Clist 上是 $2961$,可能因为个人差而评得略低)。 ### 简要题意 英语小课堂:hospitable 有一个意思是“好客的”。 有 $n$ 个地点排成一排,第 $i$ 个有**好客度** $h_i$。每相邻两个地点间有道路,道路 $(i,i+1)$ 在第 $d_i$ 天建成。你从地点 $x$ 出发,每天可以移到相邻的地点(如果两地点间有道路)或者不动,然后你的**满足感**会增加你这一天结束后所在地点的**好客度**。求 $k$ 天后最大的**满足感**。 数据范围:多测,$t\le 10^4,\sum n\le 2\times 10^5,k\le 10^9$。 ### 做法 竞选最招笑选手:较快地完成了状态设计及 $O(n^2)$ DP,然后花费三小时没想到怎么优化转移。 --- 做这个题,只需要把握一个贪心的思路:**对于同一天,我不可能凭着 $h$ 大的不待,待在 $h$ 小的上**(当然这个“同一天”不太严谨,大意就是能用一天大的换一天小的就要换)。这样本题的几个 claim 就都能自然一些了。 注意到 $k$ 巨大,所以我们猜想可能的方案就是到达某个位置后在那里赖到最后。于是我们可以调整的部分就是怎么到那个位置。 首先,我们“赖在”的位置一定是 $x$ 到其方向的前缀最大值。因为一条路没修好,影响的是一整个后缀,也就是对于 $x$ 的同一个方向,更远的点不可能先到,所以如果我们选了一个不是前缀最大值的,那我们可以用更少的时间去到达一个更大的,我们不如赖在这个更大的。 而它既然是前缀最大值,那沿着刚才的思路,我在到达它之前不会到达更大的(同样,否则我为什么不留在更大的呢),于是根据贪心原则,**我们一定会尽早到达**。这个尽早的时间(设为 $arr_i$)可以考虑我就一直往这一个方向走,没路就等着的用时,可以扫一遍求出。 于是容易想到一个 DP:设 $dp_i$ 表示尽早到达 $i$ 的最大满足感。对于每个可能最终“赖在”的位置,枚举 $arr$ 比它小的所有位置 $j$ 并考虑从这些位置转移过去。根据贪心原则,我想在能尽早到达 $i$ 的基础上尽可能晚出发,所以有方程: $$dp_i=\max_{arr_j\le arr_i-\text{dis}(i,j)}\left\{dp_j+h_j\cdot(arr_i-\text{dis}(i,j))+\sum_{l\in (j,i]}h_l\right\}$$ 其中 $\text{dis}(i,j)$ 表示 $i$ 到 $j$ 的距离。为了方便,以上不妨设了 $i<j$,实际编码时应注意。 这个东西是 $O(n^2)$ 的(里面的 $\sum$ 用前缀和),可以用一些科技做到 $O(n\log n)$ 或者 $O(n)$ 等,这里不展开。 但是不想用科技的你发现这个东西有很多 $j$ 是没有意义的。事实上,因为除非要越过 $x$,否则改变方向一定是不优的(不如留在某个前缀最大值上),所以可以考虑用自己更新别人的方式,只向左右分别找第一个,这样每个点就只转移两个点,单组复杂度就是 $O(n)$ 了。 ## H 不评价。 ### 简要题意 给定长度为 $n$,字符集为 `01?` 的字符串 $s$。求通过将所有 `?` 换为 `0` 或 `1` ,后删除任意多(可能为 $0$)个 `1` 的个数为偶数($0$ 也是偶数)的子串,所能得到的不同的串的个数,对 $998244353$ 取模。 数据范围:多测,$t\le 100,\sum n\le 3000$。 ### 做法 很多大佬引入了自动机来思考本题,可是我不会啊!所以这里用官方题解的讲法来简单说说。 --- 按照常规的思考顺序,我们先考虑没有 `?` 的情况。 计数题,问的就是符合条件的情况数量,所以我们要先探究如何判断一个 $t$ 是否满足条件,然后才能计数。 这里这个环节倒是比较典。对于 $t$ 中的每个字符,我们贪心地取其第一个能匹配的位置(当然,别忘了考虑到上个匹配位置间 `1` 的个数)即可,因为这样留给后面的空间更大,而 $s$ 又已经定下,所以前移前一个位置不会导致它到后一个位置间 `1` 个数的奇偶性改变,自然不劣。 而这样思考后我们克服了计数中的一个重要难点:同一个 $t$ 可能在 $s$ 中多个位置被匹配。我们完全可以只在其第一次出现时计数:设 $dp_i$ 表示前 $i$ 个位置能匹配多少 $t$,转移时用自己更新别人,考虑往后加一个 `0` 或 `1`,于是只会更新下一个能选 `0` 的位置和下一个能选 `1` 的位置。 但是这个东西并不能直接搬到有 `?` 的情况,因为我们“选尽可能早的匹配位置”的正确性来自于 $s$ 已经定下时,前移前一个位置不会导致它到后一个位置间 `1` 个数的奇偶性改变。而如果只是视 `?` 为通配符,则不能保证这条性质成立,做法也就失去了正确性。 所以我们在匹配 `?` 时要讨论其中填入的值。由于如果要扩展刚才的做法,那 `?` 的两种填法都被算在 $dp$ 值里了,故不能用前缀 `1` 的数量来减;但后缀不存在这个问题。于是我们设 $dp_{i,j}$ 表示后缀中有偶数个 `1`(`?` 不算)时最早匹配到 $i$、后缀中有奇数个 `1` 时最早匹配到 $j$ 的 $t$ 数量,转移仍然只需考虑再加入一个 `0` 或者 `1` 的位置即可。 单组复杂度 $O(n^2)$。 ## I 不评价。 ### 简要题意 给定一棵 $n$ 个点的树及整数 $k$。若以 $x$ 为根,在树上选 $k$ 个点,则定义其中一个点的价值为其到根的路径上**选了的**点的点权和。对于所有 $x\in[1,n]$,求当以 $x$ 为根时,在树中选择**必须包含 $x$ 的** $k$ 个点的最大价值和。 数据范围:多测,$t\le 100,\sum n\le 4000$。 ### 做法 本题的 Hint0 提到一个非常重要的思想:当点的贡献不是易于转移的形式(例如树上的子树)时,应该试着尽量转化为好转移的形式。感觉我缺乏这种思想故备注一下。 --- 这么想了之后这题的转化本身倒是挺显然:到根的路径上包含 $u$ 的点显然只有 $u$ 子树内的点。 但是根不定啊,怎么办? 其实他让你求所有根那基本是得换根了,因为真得对每个根跑相同的东西那一般意义不大。~~而且解题本来也该从特殊到一般思考吧~~。 于是先假设 $1$ 为根。设 $dp_{i,j}$ 表示 $i$ 子树内选了 $j$ 个点的最大贡献。将 $u$ 合并到 $v$ 时,暴力的转移是分别枚举 $dp_u$ 和 $dp_v$ 的第二维,这个复杂度似乎是 $O(n^3)$ 的。 但它其实是 $O(n^2)$ 的。注意到对于 $u$,实际有意义的枚举范围只有已经合并了的儿子的 $sz$(子树大小),对于 $v$ 则是 $sz_v$,故把 $v$ 合并到 $u$ 上的复杂度是两者相乘。这里我们把枚举到 $sz_i$ 视为在枚举 $i$ 子树内的所有点,于是合并的过程就是在枚举点对,而这个过程中枚举的点对的 LCA 都是 $u$,因为每对点都在 $u$ 的两个不同儿子中。 然后我们额外维护一下必须选 $u$ 的最大贡献,就能求出点 $1$ 的答案了。 然后考虑换根。我们只知道一个点子树内选点的答案,所以我们自然想知道子树外选点的答案来进行合并。我们设 $f_{i,j}$ 表示 $i$ 子树外选了 $j$ 个点的最大贡献。这里有两种转移方法。 #### 方法一 我们考虑从当前点中逐一删掉儿子的贡献(至于为什么是逐一——我想有一个原因是,为了做出类似刚才的复杂度)。但是,相对于其父亲,一个儿子的 $f$ 应该合并进其父亲的其他所有儿子及其父亲本身。这导致还未删除的儿子的贡献需要被计算。 而如果我们按倒序删掉这些儿子,那么没被删掉的儿子刚好是一段前缀,而这段前缀的总贡献正是只处理了这些儿子时的 $dp$ 值!于是只要把考虑完每个儿子后的 $dp$ 值存下来,分别枚举已删除的儿子和其他未删除的儿子中选了多少,就可以得到这个儿子的 $f$ 了。 最后不要忘记算上父亲自身的贡献。 单组复杂度 $O(n^2)$。 #### 方法二 在方法一的基础上,我们想偷个懒:如果只有两个儿子,就不用这么麻烦地去维护了!因为此时转移到某个点时,贡献只包含两部分:其父亲的 $f$ 和其父亲的另一个儿子的 $dp$,最后算上其父亲自身的贡献。 于是当一个点有大于两个儿子时,我们可以添加一些辅助结点来使其变成二叉树。从官解那里盗张图,就清楚了: ![](https://codeforces.com/predownloaded/8b/16/8b16c4fb6de9cae03c6f9dbaefaf197545ea256e.png) 这样所有东西本质上和刚才的方法是一样的。显然,结点个数不会超过 $2n$,故单组复杂度不变。