AtCoder 做题寄录

· · 个人记录

AtCoder 是人类智慧题的集中区,天天上 AtCoder 逛的也不是什么正经人(

AGC018D

题意:给一棵带权树,建立一个完全图使得 G_{i,j} = dis(i,j) ,求最长哈密顿路径。

看到题后,有一个直觉就是,设边 i 两边的连通块中,较小的那个的大小为 S_i ,则答案为 ans = \sum 2S_iw_i

我们如果求的是哈密顿回路,这个答案真的是对的,虽然它看上去很不靠谱。

但哈密顿路径没有那个“回”的过程,所以答案会稍微小一些。

这题要针对重心进行分类讨论,我们知道一棵树有一个或两个重心,且如果有两个重心,则这两个重心相邻。

1. 如果有两个重心

这等价于存在一条边 e 使得 S_e = n / 2e 的两个端点分别为两个重心,显然,虽然 2S_e = n ,但这条边最多被经过 n - 1 次,所以答案减去 w_e

2.如果有一个重心

重心有个性质在于:删去重心后,剩余每个联通块的子树大小都不大于 n / 2

设该重心为 G,连出去的边为 e_1,e_2,\dots,e_k,对应的联通块为 T_1,T_2,\dots,T_k

我们是没法让这 k 条边都走到最理想的次数的,否则我们开始时要在每个 T_i 外面,结束时也要在每个 T_i 外面,但不再 T_{1 \dots k} 中的点只有 G 一个。

我们考虑牺牲权值最小的那条边,这样答案就是 \sum 2S_iw_i - \min\{w_{e_1},\dots,w_{e_k}\}

因为每个|T_i| 都不超过 n/2 ,所以我们可以构造方案,使得每个p_jp_{j+1} 都不在同一个 T_i 中,这容易理解。

总的来说,这个题容易有一个直觉,即考虑边的贡献,但想出正确的分类讨论并不容易,需要对重心的性质有非常敏感的直觉。

在众多 AGC 题目中,这题的题解算比较详细的了。

AGC011C

题意:给出两张无向图 A = (V_A,E_A),B = (V_B,E_B),构造一张新的图 C ,满足 V_C = V_A \times V_B ,新图中的点 (a,b)(a',b') 有边当且仅当 aa' 有边且 bb' 有边,求C 的连通分量个数。

(原题中是 A = B 的情况)

我们先考虑 AB 均连通的情况。

容易发现,点 (a,b)(a',b') 连通,当且仅当存在一个数 l ,使得 aa' 有长度为 l 的路径,bb' 也有长度为 l 的路径。

我们很容易把路径的长度加上 2,所以我们只需考虑路径长度的奇偶性。 (这是无向图上非常重要的思想,如果在理论层面出现了“路径长度”,容易将其转化为奇偶性,然后使用二分图相关理论解决问题)

如果 AB 都是二分图,设两部分别为 S,T,那么S_A \times T_A \cup S_A \times T_B 是不连通的,因为 S_A 内部没有连边,所以只需考虑两个分量 S_A \times T_A \cup S_B \times T_BS_A \times T_B \cup S_B \times T_A.

这两个分量之间没有连边,另一方面,在二分图中,一个点到另一个点的路径的奇偶性是确定的,而这两个连通分量分别对应两种可能的奇偶性。

相反地,如果AB 有至少一个不是二分图,我们肯定能通过走一个奇环来反转奇偶性,故每一对点都有办法连通,就只有一个连通分量。

考虑 AB 不一定连通的情况,设点的数量分别为 N_A,N_B ,孤立点数量分别为 i_A,i_B ,不是二分图的连通分量数为 p_A,p_B,是二分图的连通分量数为 q_A,q_B, 则答案为

i_Ai_B + i_A(N_B-i_B)+i_B(N_A-i_A)+p_Ap_B+p_Aq_B+p_Bq_A+2q_Aq_B

这题涉及到一个重要思想,对于这种图长得比较奇怪的题,我们不要去尝试想象图的形状,而是尝试分解问题,并针对”连通性”等字眼转化为路径问题,再注意到无向图上路径长度的奇偶性,解决问题。

同样用到了这个重要思想的还有 [HNOI2019]校园旅行

AGC009C

题意:将 n 个数 s_1,\dots,s_n (s_i \lt s_{i+1}) 分成两个集合 S,T ,满足 S 中任意两数的差的绝对值大于等于 AT 中任意两数的差的绝对值大于等于 B ,求方案数。

O(n \log n)

F_{i,j,0/1} 表示 dp 到第 i 个数,上一个与元素 i 不在同一个集合的元素是 j0/1 表示 iS 或者 T 集合。

则显然有转移:

F_{i,j,1} \leftarrow F_{i-1,j,1} ,s[i] - s[i - 1] \ge B\\ F_{i,i-1,1} \leftarrow F_{i-1,j,0},s[i] - s[j] \ge B \end{cases}

对于 j = 0 的情况稍稍注意一下。

对于这种 dp 的优化,我们可以尝试把第 i 位滚动掉,并尝试用数据结构维护转移。

注意到对于第一、三种转移,如果不满足条件,我们把数组全部置为 0 即可,否则啥都不用干。

对于第二、四种转移,只有一个特殊点会被转移,同时 j 的取值也是一个区间。

所以,我们维护一颗支持全局赋 0,单点修改,区间求和的线段树即可。

如果要划分两个集合的话,这种 dp 状态的设计是比较套路的。

ARC117D

题意:给一棵无权树,要求构造一组正整数 E_1,E_2,\dots,E_n 满足对于任意 1 \le i,j \le n,都有 |E_i - E_j| \ge dis(i,j),同时让 \max(E) 最小,然后输出 E

如果我们构造好了一组 E,那么一定存在一个排列 p_1,p_2,\dots,p_n 满足 E_{p_i} \le E_{p_{i+1}}

那么 E_{p_n} - E_{p_1} \ge \sum_{i=1}^{n-1} dis(p_i,p_{i+1})

为了让上界最小,我们取等号。

问题转化为找到排列 p_1,p_2,\dots,p_n 使得 \sum_{i=1}^{n-1} dis(p_i,p_{i+1}) 最小(有趣的是,上面那道题要让他最大)

我们还是考虑回路的情况,这种时候每条边最少也要被经过两次,答案为 2(n - 1)

如果去掉dis(p_n,p_1) ,答案即为 2(n - 1) - dis(p_1,p_n)

p_1,p_n 为直径的两个端点即可。

这道题巧妙地把上界表示了出来,以至于我都不知道如何总结了。

ARC115E

给出一个数列 a_1,a_2,\dots,a_n ,求满足以下要求数列的个数:

当然这题有 sb 线段树做法,但我做这题的CF原题的时候写了一遍线段树了,不想写了。

考虑容斥,设有 k 个位置满足 x_{i-1} = x_i,其余随意的方案为 f(k),答案就是 \sum_{k=0}^n (-1)^k f(k)

考虑 dp

对于建立在容斥上的 dp 有一个套路,就是把容斥系数放进转移里面。

鉴于此设计 dp,设 F_i 表示前 i 位的方案。

F_i = \sum\limits_{j=1}^i(-1)^{i-j}F_{j-1}\min(a_j,a_{j+1},\dots,a_i)

转移的意义是考虑 a_j = a_{j+1} = \dots = a_i

因为有 \min 所以用单调栈优化转移,因为有 (-1)^{i-j} ,所以分奇偶记录前缀和即可。

ARC116E

题意:给出一棵树,要求选出 K 个点 p_1,p_2,\dots,p_k,最小化 \max_{i=1}^n\min_{j=1}^k dis(i,p_j)

看到这玩意考虑二分,设一个时间 T,问题转化为每个点都要在时间 T 内被某一个点到达。

我们称一个被选中的节点覆盖了所有与他距离小于等于 T 的节点。

考虑设计树形 dp,算出为了到达目的最少需要几个点。

我们要记录如下状态(这也是我不太能想到的地方):

如果有些选中节点能够覆盖别的子树的未覆盖节点,我们就不需要选中根了,这可以用前两个记录的信息来判断。

AGC020C

给出一个长度为 n 的数列 A_1,A_2,\dots,A_n,将其所有非空子序列的和排成一个序列 S_1,S_2,\dots,S_{2^n - 1}

求出该序列的中位数,即 S_{2^{n-1}}

1 \le n,A_i \le 2000

题解:

这题肯定不能使用常规的二分中位数来做。

S_0 = 0 表示空序列的和。

既然要求中位数,考虑把这 2^n 个序列对半分,也就是分出 2^{n-1} 个数对 (P,Q),我们不妨设 \sum P \le \sum Q

分数对也要讲究分法,我们让 P 并上 Q 等于原序列,那么 (P,Q) 也就是一个原序列的划分,这样看上去更加贴近“中位数“的有关性质。

Sum = \sum A

\sum P \le \frac{1}{2}Sum,\sum Q \ge \frac{1}{2}Sum

所以我们的这种分法,正好把这 2^n 个子序列平均分到 \frac{1}{2} Sum 的两端。

那么我们所求的就是大于等于 \frac{1}{2}Sum 的最小的能凑到的和。

使用 bitset 背包即可。

做的时候,直接证明较为困难,可行的思路是猜出结论与 \frac{1}{2} Sum 有关。

ARC064D

这题一眼望过去,用不了什么现成的科技,最朴素的思路就是,考虑每个串会被算几次。

事实上,一个串会被算多次,也意味着有两个回文串互相循环同构。

循环同构也会出现所谓“重复”的情况,这与一个串的最小整周期有关。

如果一个回文串的最小整周期为 x,那么只有 x 个串是“可能有用的”

故我们先考虑一个周期内的问题。

回文串有两种:AA^rAcA^r (A^r 表示 A 的翻转,c 表示一个字符)

对于 AA^r ,与其循环同构的回文串只有 A^rA

对于 AcA^r,没有与其循环同构的回文串。(这就是我没有发现的地方)

所以如果 x 是偶数,一个回文串会带来 \frac{x}{2} 的贡献,否则带来 x 的贡献。

接下来考虑如何算周期为 x 的回文串个数。

先可以算周期是 x 约数的回文串个数,为 K^{\lceil \frac{x}{2} \rceil},再用其约数的答案来减即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 6000,P = 1e9 + 7;
inline int qpow(int a,int b) { int res = 1;while(b) {if(b&1) res = 1ll * res * a % P;a = 1ll * a * a % P;b >>= 1;} return res;}
int factor[N],f[N],tot,n,k;
int main()
{
    scanf("%d%d",&n,&k);
    for(int i = 1;i * i <= n;i++)
    {
        if(n % i) continue;
        factor[++tot] = i;
        if(i * i != n) factor[++tot] = n / i;
    }
    sort(factor + 1,factor + tot + 1);
    int ans = 0;
    for(int i = 1;i <= tot;i++)
    {
        f[i] = qpow(k,(factor[i] + 1) >> 1);
        for(int j = 1;j < i;j++)
            if(factor[i] % factor[j] == 0) (f[i] += P - f[j]) %= P;
        ans = (ans + 1ll * ((factor[i] & 1) ? factor[i] : factor[i] / 2) * f[i] % P) % P;
    }
    cout << ans << endl;
    return 0;
}

ARC067F

容易发现最终选择的烧烤店一定是一段区间。(推到这里花了15分钟)

对于 l,r,其答案为 \sum_{i=1}^m \max_{j=l}^rB_{j,i} - (s_r - s_l)

重点在与 \sum_{i=1}^m \max_{j=l}^r B_{j,i}

如果你想着开 m 个数据结构,扫描线增量维护,必然会陷入难以统计答案的困局,因为 \sum\max 是极不好复合的。(挂在这里)

正确的做法是,考虑每个 B_{j,i} 的贡献。

每一维是独立的,而在每一维中 ,我们设 l_j 表示 j 左边第一个大于 B_{j,i} 的数,r_j 表示 j 右边第一个大于等于 B_{j,i} 的数。那么,当且仅当 L \in [l_j+1,i],R \in [i,r_j - 1]B_{j,i} 会对区间 [L,R] 的答案造成贡献。

使用二维差分把这个子矩形整体加即可。

```cpp #include <bits/stdc++.h> using namespace std; const int N = 5e3 + 5,M = 2e2 + 5; int B[N][M]; int n,m,a[N]; long long s[N]; long long dif[N][N]; int L[N],R[N]; int stk[N],top; inline void inc(int x1,int x2,int y1,int y2,int v) { dif[x1][y1] += v; dif[x1][y2 + 1] -= v; dif[x2 + 1][y1] -= v; dif[x2 + 1][y2 + 1] += v; } int main() { scanf("%d%d",&n,&m); for(int i = 1;i < n;i++) scanf("%d",&a[i]),s[i + 1] = s[i] + a[i]; for(int i = 1;i <= n;i++) for(int j = 1;j <= m;j++) scanf("%d",&B[i][j]); for(int i = 1;i <= m;i++) { top = 0; for(int j = 1;j <= n;j++) { while(top && B[stk[top]][i] <= B[j][i]) --top; L[j] = top ? stk[top] : 0; stk[++top] = j; } top = 0; for(int j = n;j >= 1;j--) { while(top && B[stk[top]][i] < B[j][i]) --top; R[j] = top ? stk[top] : n + 1; stk[++top] = j; } for(int j = 1;j <= n;j++) inc(L[j] + 1,j,j,R[j] - 1,B[j][i]); } long long ans = 0; for(int i = 1;i <= n;i++) for(int j = 1;j <= n;j++) { dif[i][j] += dif[i][j - 1] + dif[i - 1][j] - dif[i - 1][j - 1]; if(i <= j) ans = max(ans,dif[i][j] - (s[j] - s[i])); } cout << ans << endl; return 0; } ``` 本题最大的难点在于不去想数据结构,而是考虑单个 $B$ 的贡献,这样式子中的 $\max$ 实际上变成了一种约束条件,这样才能真正把每一维拆开。 ### ARC112E 是一道对题目性质分析比较透彻的 $dp$ 题。 一个数的位置,至于其最后一次操作有关,所以我们把研究对象从操作转到了数,这样才能进一步分析。 我们称每个数的最后一次操作为“有效操作”。 那么假设有 $L$ 个有效操作把数放在开头,有 $R$ 个有效操作把数放在结尾,那么 $a_{L+1},\dots,a_{n-R}$ 这一段的相对位置是一直没有改变的,因为它们没有进行过任何操作,所以,$a_{L+1},\dots,a_{n-R}$ 必须是递增的。 接下来我们要针对操作计数。 如果有了上面这个观察,就可以设出状态 $dp[i][l][r]$ 表示进行了 $i$ 次操作(有效+无效),$l,r$ 意义同上。转移如下: ```cpp dp[i + 1][l][r] += dp[i][l][r] * 2 * (l + r) // 无效操作,放在任意一次有效操作的前面。 dp[i + 1][l + 1][r] += dp[i][l][r] //有效操作 dp[i + 1][l][r + 1] += dp[i][l][r] //有效操作 ``` 这个 $dp$ 在时间线上是反向的,即从后向前填充操作序列,可见**倒流**的思想不仅能应用于数据结构,还能用于 $dp$. 我们发现 $dp$ 转移过程中只用到了 $l + r$,所以有个想法是直接记录 $dp[i][l + r]$,最后再来分配这 $l + r$ 次有效操作。 于是可以写出 $dp2[i][j]$ ,其中 $j = l + r

而原来的 dp[i][l][r] 则是现在的 dp2[i][l + r] * \binom{l + r}{l},我们用组合数规定这 l + r 次有效操作的顺序。

然后对于满足条件的 l,r 统计即可。

本题的关键在于观察到“有效操作”的特殊性,以及根据有效操作的特点设计倒流式的 dp,这种“有效操作”其实跟普通的赋值具有很相近的特点,故思想也是相似的。最后一步的优化也非常好,是对转移方程的观察与思考。