Educational Codeforces Round 69 (Rated for Div. 2) 解题报告

Nickel_Angel

2019-07-25 13:56:29

Personal

[题目总览请点击这里(๑¯∀¯๑)](http://codeforces.com/contest/1197) **由于作者太弱了QAQ,部分题目参考了其他 OIer 的题解,并放上了原题解的链接** *** ### A [DIY Wooden Ladder](http://codeforces.com/contest/1197/problem/A) #### Description 我们定义 $k$ 阶楼梯是由满足下述条件的 $k + 2$ 块木板组成的: $1.$ 由两块长度至少为 $k + 1$ 的木板组成楼梯的底部。 $2.$ 剩下 $k$ 块木板长度至少为 $1$ 的木板组成阶梯。 现在有 $T$ 次询问,每次询问给你 $n$ 个木板和它们的长度,其中第 $i$ 个木板的长度为 $a_i$ 。 问用这些木板最多能组成几阶楼梯? #### Solution 不难发现,组成楼梯的阶数是由木板中长度的次大值决定的。 故只需找出这个次大值,就知道最多能组成几阶楼梯。但注意有可能出现木板不够用的情况,故最终答案为 $min(max_{sec} - 1,n - 2)$ (其中 $max_{sec}$ 为次大值)。 #### Code ```cpp #include <cstdio> #include <cstring> #include <cctype> #include <iostream> #include <algorithm>   using namespace std;   namespace IO {   char ch; bool f;   template<typename T> inline void input(T &x) { x = 0; ch = getchar(); f = false; while (!isdigit(ch)) { f |= (ch == '-'); ch = getchar(); } while (isdigit(ch)) { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); } x = f ? -x : x; }   }   using namespace IO;   namespace Primary {   const int maxn = 1e5 + 10; int T, n, max1, max2, x;   void main() { input(T); while (T--) { max1 = 0, max2 = 0; input(n); for (int i = 1; i <= n; ++i) { input(x); if (x > max1) { max2 = max1; max1 = x; } else if (x > max2)//注意,本题找到是非严格的次大值,即次大值和最大值可以相等 max2 = x; } printf("%d\n", min(max2 - 1, n - 2)); } } }   int main() { Primary::main(); return 0; } ``` *** ### B [Pillars](http://codeforces.com/contest/1197/problem/B) #### Description 给定 $n$ 个柱子,柱子的编号从 $1$ 到 $n$ 。 每个柱子上有一个盘子,其中编号为 $i$ 的柱子上的盘子的半径为 $a_i$,各个盘子的半径互不相同。 你每次可以将柱子 $i$ 上的盘子移动至柱子 $j$ 的条件是: $1.$ $i,j$ 两个柱子必须相邻。 $2.$ 柱子 $i$ 上仅有一个盘子。 $3.$ 要么柱子 $j$ 上没有盘子,要么柱子 $j$ 上的盘子半径大于柱子 $i$ 上的盘子半径。 现在问能否把所有盘子均移动到同一柱子上。 #### Solution 考虑一种策略:我们每次将半径次大的盘子移动至半径最大的盘子上,由于半径次大的盘子移动后不会对其他盘子的移动造成影响,故我们可以视为将这个盘子删除,在找到当前的次大值,重复这个步骤即可。 注意到如果 $i,j$ 两个柱子不相邻,但它们之间的柱子上没有盘子,这和它们相邻是没有区别的。故可以用链表模拟这个过程即可。 ```cpp #include <cstdio> #include <cstring> #include <cctype> #include <iostream> #include <algorithm>   using namespace std;   namespace IO {   char ch; bool f;   template<typename T> inline void input(T &x) { x = 0; ch = getchar(); f = false; while (!isdigit(ch)) { f |= (ch == '-'); ch = getchar(); } while (isdigit(ch)) { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); } x = f ? -x : x; }   }   using namespace IO;   namespace Primary {   const int maxn = 2e5 + 10; int n, a[maxn], pre[maxn], next[maxn], pos[maxn];   inline void del(int x) { next[pre[x]] = next[x]; pre[next[x]] = pre[x]; }   void main() { scanf("%d", &n); a[0] = -1; for (int i = 1; i <= n; ++i) { scanf("%d", a + i); pre[i] = i - 1; next[i] = i + 1; pos[a[i]] = i; } next[n] = 0;//防止越界 for (int i = n; i > 1; --i) { if (a[pre[pos[i]]] == i - 1 || a[next[pos[i]]] == i - 1) del(pos[i]);//链表的删除操作 else { puts("NO");//若不能将当前的次大值移动至最大值,则一定无解 return; } } puts("YES"); } }   int main() { Primary::main(); return 0; } ``` *** ### C [Array Splitting](http://codeforces.com/contest/1197/problem/C) #### Description 有一个长度为 $n$ 的单调不减的序列,其中第 $i$ 个元素为 $a_i$ 。 现在你需将该序列分成互不相交且非空的 $k$ 段,设第 $j$ 段中的最大值为 $max(j)$,最小值为 $min(j)$,则该段的代价为 $max(j) - min(j)$。现在你需要使分成这 $k$ 段的总代价尽可能小,问最小总代价。 #### Solution 由于这个序列单调不减,设一段 $j$ 的起始位置为 $start_j$,末尾位置为 $end_j$,则对于这段显然有 $max(j) = a_{end_j}$,$min(j) = a_{start_j}$,故这段的代价为 $a_{end_j} - a_{start_j}$ 。 则这 $k$ 段的总代价为: $$\sum_{i = 1}^{k} a_{end_i} - a_{start_i}$$ 据题意可知,第一段的起始位置一定是 $1$,最后一段的末尾位置一定是 $n$ 。故 $k$ 段的总代价可重写为: $$a_n - a_1 + \sum_{i = 1}^{k - 1} a_{end_i} - a_{start_{i + 1}}$$ 不难发现 $a_1,a_n$ 是确定的,只需考虑后面的和式的计算,而中间各段之间是相邻的,即对于相邻的两段 $i,i + 1$,均有 $end_i + 1 = start_{i + 1}$,故上式可以写成: $$a_n - a_1 + \sum_{i = 1}^{k - 1} a_{end_i} - a_{end_i + 1} \text{(注意 $end_i + 1$ 的 $ +1$ 不在下标 $i$ 中)}$$ 故我们只需考虑序列中 $a_i - a_{i + 1}$的前 $k - 1$ 小的值即可。不妨将其依次算出,并从小到大排序,取前 $k - 1$ 个即是最优解。 #### Code ```cpp #include <cstdio> #include <cstring> #include <iostream> #include <algorithm>   using namespace std;   namespace Primary {   const int maxn = 3e5 + 10; int n, k, a[maxn], d[maxn]; long long ans = 0;   void main() { scanf("%d%d", &n, &k); for (int i = 1; i <= n; ++i) { scanf("%d", a + i); d[i - 1] = a[i - 1] - a[i]; } sort(d + 1, d + n + 1); ans = a[n] - a[1];//记得统计a[n],a[1]的贡献 for (int i = 1; i < k; ++i) ans += d[i]; printf("%I64d", ans); }   }   int main() { Primary::main(); return 0; } ``` *** ### D [Yet Another Subarray Problem](http://codeforces.com/contest/1197/problem/D) #### Description 给定一个长度为 $n$ 的序列 $a_i$ 和两个正整数 $m,k$。 你可以选择其中的一段子序列 $a_l,a_{l + 1},\cdots,a_{r - 1},a_r$。 这段子序列的代价为 $\sum_{i = l}^{r} a_i - k \lceil \frac{r - l + 1} {m} \rceil$(特殊的,对于一个空的序列,其代价为 $0$)。 对于给定序列,你需选出一个子序列,使其代价尽可能大,求最大代价。 #### Solution (参考了[这篇题解](https://www.luogu.org/blog/Hakuryu/solution-cf1197d)(by Mital)的思路) 先应用一下前缀和,原式为: $$sum[r] - sum[l - 1] - k\lceil \frac{r - l + 1} {m} \rceil$$ 设 $x = l - 1$,故原式: $$sum[r] - sum[x] - k\lceil \frac{r - x} {m} \rceil$$ 而 $r = \lfloor \frac {r} {m} \rfloor m+ r\,mod\,m$,$x = \lfloor \frac x m \rfloor m+ x\,mod\,m$,故原式: $$sum[r] - sum[x] - k\lceil \frac{\lfloor \frac {r} {m} \rfloor m+ r\,mod\,m - \lfloor \frac x m \rfloor m+ x\,mod\,m} {m} \rceil$$ 即: $$sum[r] - sum[x] - k \lceil \lfloor \frac r m \rfloor - \lfloor \frac x m \rfloor + \frac {r\,mod\,m + x\,mod\,m} {m} \rceil$$ 考虑到 $\lfloor \frac{r} {m} \rfloor$,$\lfloor \frac {x} {m} \rfloor$ 为整数,故可直接提出,得到: $$sum[r] - k \lfloor \frac {r} {m} \rfloor - (sum[x] - k \lfloor \frac {x} {m} \rfloor) - k \lceil \frac {r\,mod\,m - x\,mod\,m} {m} \rceil$$ 设 $f(x) = sum[x] - k \lfloor \frac {x} {m} \rfloor$,则: $$f(r) - f(x) - k \lceil \frac {r\,mod\,m - x\,mod\,m} {m} \rceil$$ 将式子化至这里,我们发现可以考虑枚举子序列右端点 $r$,这样 $f(r)$ 和 $r\,mod\,m$ 就固定了,发现 $m$ 很小,故可以维护对于模 $m$ 剩余系中的 $i\,(i \in [0,m - 1])$ 维护最小的 $f(x)$,使 $x$ 与 $i$ 在模 $m$ 意义下同余,记作 $f_{ min}(i)$,每次枚举 $r$ 时遍历一遍 $f_{min}$ 更新答案,后用 $f(r)$ 更新 $f_{min}(r\,mod\,m)$ 即可。 ```cpp #include <cstdio> #include <cstring> #include <cmath> #include <algorithm> #include <iostream>   using namespace std;   namespace Primary {   const long long inf = 1e14; const int maxn = 3e5 + 10; long long n, m, k, a[maxn]; long long sum[maxn], f[maxn], f_min[maxn];   void main() { scanf("%I64d%I64d%I64d", &n, &m, &k); sum[0] = 0; for (int i = 1; i <= n; ++i) { scanf("%I64d", a + i); sum[i] = sum[i - 1] + a[i]; f[i] = sum[i] - k * ((long long)i / m); } for (int i = 1; i < m; ++i) f_min[i] = inf; f_min[0] = 0; long long res, ans = 0, tmp; for (int i = 1; i <= n; ++i)//枚举右端点 { res = -inf; for (int j = 0; j < m; ++j) { tmp = ceil(1.0 * ((i % m) - j) / m); res = max(res, f[i] - f_min[j] - k * tmp);//更新当前答案 } f_min[i % m] = min(f_min[i % m], f[i]);//用f[i]更新f_min[i % m] ans = max(ans, res); } printf("%I64d", ans); }   }   int main() { Primary::main(); return 0; } ``` *** ### E [Culture Code](http://codeforces.com/contest/1197/problem/E) #### Description 有一些俄罗斯套娃,第 $i$ 个套娃的外部体积为 $out_i$,内部空心体积为 $in_i$,显然有 $in_i < out_i$。 你想将一些套娃放入包中,但包中没有太多的空闲空间,但幸运的是你可以将它们套起来带走。具体的,若 $out_i \leq in_j$ 你可以将套娃 $i$ 套在 $j$ 中。 现在你套了一些套娃,设这些套娃的编号为 $i_1,i_2,\cdots,i_k$,则剩余空间为: $$in_{i_1} + (in_{i_2} - out_{i_1}) + \cdots + (in_{i_k} - out_{i_{k - 1}})$$ 设所有方案中剩余空间最小的方案为 $m$,你需要求出剩余空间等于 $m$ 且套娃数量尽可能多的方案数是多少。由于答案可能很大,需对 $10^9 + 7$ 取模。 #### Solution (参考了[这篇题解](https://www.luogu.org/blog/Ynoi/solution-cf1197e)(by 树链剖分)的思路) 本题为 $dp$ 题。 先将所有套娃按 $out_i$ 从小到大排序。定义 $f[i]$ 表示最外面使用第 $i$ 个套娃,剩余的最小空间,$g[i]$ 表示最小空间的方案数,则: $$f[i] = min(f[j] + in[i] - out[j])\,(in[i] \geq out[j])$$ $$g[i] = \sum g[j]\,(in[i] \geq out[j],f[i] = f[j] + in[i] - out[j])$$ 设 $in_{max}$ 为所有 $in$ 中的最大值,$out_{min}$ 为所有 $out$ 中的最小值。 由于要保证剩余空间最小,故 $\forall i,in[i] < out_{min}$ 都恒有 $g[i] = 1$ 。 设 $ans_f$ 为满足条件的最小剩余空间,由于要确保该套娃数量尽可能多,即装到装不下为止,故有:$ans_f = min(f[i])\,(out[i] \geq in_{max})$ 。 故答案为:$ans = \sum g[i]\,(out_i \geq in_{max}, f[i] = ans_f)$ 时间复杂度为 $O(n^2)$,显然无法通过 $2 \times 10^5$ 的数据,故考虑优化: 由于 $f[i]$ 是由 $f[j] - out[j] + in[i]\,(in[i] \geq out[j])$ 转移过来的,且 $f[j] - out[j]$ 的值一定是 $j \in[1, i - 1]$ 中最小的,故可以先二分找到一个最大的 $j$ 使得 $in[i] \geq out[j]$,由于 $out_i$ 是从小到大排序过的,故对于 $k \in [1,j]$,均符合 $in[i] \geq out[k]$ 的要求,故我们可以维护一个 $f[i] - out[i]$ 前缀最小值 $pre_{min}[i]$ 以及最小值的方案数之和 $sum[i]$,就能在 $O(\log n)$ 的时间内完成一次转移。 由于需转移 $n$ 次,故总时间复杂度为 $O(n \log n)$ 。 #### Code ```cpp #include <cstdio> #include <cstring> #include <algorithm> #include <iostream>   using namespace std;   namespace Primary {   const int p = 1e9 + 7; const int maxn = 2e5 + 10; int n, in_max = 0, out_min = p, out[maxn], g[maxn], f[maxn]; int pre_min[maxn], sum[maxn], ans_f = p;   struct doll { int in, out; bool operator < (const doll &rhs) const { if (out < rhs.out || out > rhs.out) return out < rhs.out; return in < rhs.in; } }e[maxn];   void main() { scanf("%d", &n); for (int i = 1; i <= n; ++i) { scanf("%d%d", &e[i].out, &e[i].in); in_max = max(in_max, e[i].in); out_min = min(out_min, e[i].out); } sort(e + 1, e + n + 1); for (int i = 1; i <= n; ++i) { out[i] = e[i].out;//将排完序的 out 复制出来,便于用 upper_bound 二分 if (e[i].in < out_min) g[i] = 1; } for (int i = 1, pos; i <= n; ++i) { pos = upper_bound(out + 1, out + n + 1, e[i].in) - out - 1; f[i] = ((long long)pre_min[pos] + e[i].in) % p; if (!g[i]) g[i] = sum[pos]; if (f[i] - e[i].out == pre_min[i - 1] && i != 1) sum[i] = ((long long)sum[i - 1] + g[i]) % p;//若当前方案就为最优方案,直接求和 else if (f[i] - e[i].out < pre_min[i - 1] || i == 1) sum[i] = g[i];//若当前方案更优,则记录当前方案的方案数 else sum[i] = sum[i - 1];//否则直接继承目前最优方案的方案数 pre_min[i] = min(pre_min[i - 1], f[i] - e[i].out); if (e[i].out > in_max) ans_f = min(ans_f, f[i]); } int ans = 0; for (int i = 1; i <= n; ++i) { if (e[i].out > in_max && f[i] == ans_f) { ans = ((long long)ans + g[i]) % p; } } printf("%d", ans); }   }   int main() { Primary::main(); return 0; } ``` *** ### F [Coloring Game](http://codeforces.com/contest/1197/problem/F) ~~由于涉及到了博弈论,线性代数的知识,作者太弱了,先咕咕咕~~