AtCoder Grand Contest 004做题总结

· · 个人记录

AtCoder Grand Contest 004

之前做的,忘了订题写总结,所以现在也没啥印象写个开头吐槽了。

题目

Divide a Cuboid

有一个由 1\times1\times1 的小正方体组成的 A\times B\times C 的大长方体,现在想把这 A\times B\times C 个小正方体染成红色或蓝色,使得以下条件成立:

问如何染色能使得红蓝正方体的个数之差最小。(2\le A,B,C\le10^9)

显然较优的分割方式是沿着大长方体的某一面把原长方体分成两半,一半红一半蓝。如果 A,B,C 其中有一个为偶数,则存在一种方法把长方体恰分成两个大小相同的长方体,答案为 0。否则答案为割的那一面正方体的个数,即 \min\{AB,BC,AC\}。时间复杂度 \mathcal{O}(1)

#include <cstdio>
#include <algorithm>
int main()
{
    long long a, b, c; scanf("%lld%lld%lld", &a, &b, &c);
    if (!(a & 1) || !(b & 1) || !(c & 1))
    { printf("0\n"); return 0; }
    printf("%lld\n", std::min(std::min(a * c, a * b), b * c));
    return 0;
}

B - Colorful Slimes

现在一共有 n 种颜色的史莱姆。初始时一个史莱姆都没有,现在想通过以下两种操作得到全部的 n 中史莱姆:

求得到全部史莱姆的最小时间。(2\le n\le2\times10^3,1\le a_i,x\le10^9)

注意到题目的难点在于施法的处理,但观察到 n 比较小,所以考虑枚举施法的次数。假设当前枚举到施法 k 次,则得到颜色为 i 的史莱姆需要获得颜色为 i,i-1,\cdot\cdot\cdot,i-k 的史莱姆其中一个(注意这里和下文中所有涉及到下标减法的都是在模意义下的),对应抓史莱姆时已经施法过的次数为 k,k-1,\cdot\cdot\cdot,0。所以如果想获得第 i 种颜色,除去施法的时间还需要 \min_{j=i-k}^i\{a_j\} 的时间。记 b_{i,k}=\min_{j=i-k}^i\{a_j\},则最终答案即为 k\times x+\sum_{i=1}^n b_{i,k}。直接枚举 k,i 并对于每个 i 暴力计算 b 的时间复杂度为 \mathcal{O}(n^3),不足以通过本题。不过有个很显然的优化,b_{i,k}=\min(b_{i,k-1},a_{i,k}),这样就可以每次 \mathcal{O}(1) 算出 b 了,时间复杂度 \mathcal{O}(n^2)

#include <cstdio>
#include <algorithm>
const int N = 2e3 + 10;
int a[N], b[N]; long long ans;
int main()
{
    int n, x; scanf("%d%d", &n, &x);
    for (int i = 0; i < n; ++i)
        scanf("%d", &a[i]), 
        b[i] = a[i], ans += a[i];
    long long ret;
    for (int k = 1; k <= n; ++k)
    {
        ret = 0;
        for (int i = 0; i < n; ++i)
        {
            b[i] = std::min(b[i], a[(i - k + n) % n]);
            ret += b[i];
        }
        ret += 1ll * k * x;
        ans = std::min(ans, ret);
    }
    printf("%lld\n", ans);
    return 0;
}

C - AND Grid

有两个 n\times m 的网格图。第一个被人涂上了若干 四连通 的红色格子,第二个被另一个人涂上了若干 四连通 的蓝色格子。现在把这两个网格图按同一方向重叠,如果格子上既有蓝色也有红色,则显示为紫色。现在给出所有显示紫色的格子,保证网格图的边缘不为紫色,构造出一组可能的原网格图。(3\le n,m\le500)

考虑构建一组模板网格图,然后对于不同的询问我们都在这组模板上修改得到答案。这组模板应该满足以下条件:

比如,这两张图就符合条件:

对于任意询问上的某个紫色结点,在模板图上该补充颜色的位置补充颜色即可。时间复杂度 \mathcal{O}(nm)

#include <cstdio>
const int N = 600;
int mp[N][N]; char req[N][N];
int res[N][N][2]; char ans[2][N][N];
int main()
{
    int h, w; scanf("%d%d", &h, &w);
    for (int i = 1; i <= h; ++i)
        for (int j = 1; j <= w; ++j)
        {
            if (i == 1) { mp[i][j] = 1; continue; }
            if (i == h) { mp[i][j] = 0; continue; }
            mp[i][j] = (j & 1);
        } 
    for (int i = 1; i <= h; ++i)
        scanf("%s", req[i] + 1);
    for (int i = 1; i <= h; ++i)
        for (int j = 1; j <= w; ++j)
            if (req[i][j] == '#')
                res[i][j][mp[i][j] ^ 1] = res[i][j][mp[i][j]] = 1;
            else res[i][j][mp[i][j]] = 1;
    for (int i = 1; i <= h; ++i)
    {
        for (int j = 1; j <= w; ++j)
            ans[0][i][j] = res[i][j][0] ? '#' : '.',
            ans[1][i][j] = res[i][j][1] ? '#' : '.';
        ans[0][i][w + 1] = ans[1][i][w + 1] = '\0';
    }
    for (int i = 1; i <= h; ++i)
        printf("%s\n", ans[0][i] + 1);
    printf("\n");
    for (int i = 1; i <= h; ++i)
        printf("%s\n", ans[1][i] + 1);
    return 0;
}

D - Teleporter

有一张有 n 个结点的图,其中每个结点 i 都能通过传送门到达 a_i 结点,保证每个点都能通过若干次传送到达 1 号结点。现在给定一个整数 k。需要把一些 a_i 的值改变,保证从任意结点出发经过 恰好 k 次传送都能到达 1 号结点。求最少需要改变多少 a_i 的值。(2\le n\le10^5,1\le a_i\le n,1\le k\le10^9)

首先根据题目描述,我们能很轻松建立起一个图论模型。给出一张 n 个结点 n 条边的有向图,对于每个点 i 都有一条到 a_i 的有向边。注意到原题中给出的条件说明这张图是弱连通的,问题即为最少改变多少个有向边的终点能使得从任意结点出发经过恰好 k 条边都能到达 1

能猜测到的一个结论是,改变完边之后,一定有 a_1=1,即在 1 处存在自环。证明也不难,因为从 1 出发走 k 步能到达 1,说明 1 被包含在一个大小是 k 的因数的环上。假设 a_1=r\ne 1,则 r 应该也在上述环上,而 r 走恰好 k 步会回到 r。又由于任意点走 k 步会到 1,所以 r=1,假设不成立。所以 a_1=1 成立。

![](https://cdn.luogu.com.cn/upload/image_hosting/cwgpmn4l.png) 又因为 $1$ 处的自环可以消耗次数,所以原问题就变为了从任意点出发,经过 **至多** $k$ 条边都能到达 $1$。来看一个当 $k=2$ 时的可能解法(同来自 AT 原题解): ![](https://cdn.luogu.com.cn/upload/image_hosting/rcnqsd7t.png) 即把一些边改到连向 $1$ 来缩短距离。这样看,原题面又可以转化为: > 给出一棵有根树,你要删掉最少的边,使得对于每个连通块都有: > - 以根节点为根的子树深度最多为 $k$。 > - 不以根节点为根的子树深度最多为 $k-1$。 删边就是把原边改为指向 $1$,这样不以根节点为根的子树还需要一次机会走一条边到 $1$,所以最多为 $k-1$。这样就简单了,可以用 $\rm dfs$ 轻松实现。具体来讲,对于一个点 $x$,如果它有一个儿子 $y$ 满足以 $y$ 为根的子树深度为 $k-1$ 且 $x$ 不为根,就割掉 $x,y$ 之间的边,最终答案即为割掉的边数。时间复杂度 $\mathcal{O}(n)$。 ```cpp #include <cstdio> #include <cstring> #include <algorithm> const int N = 1e5 + 10; struct edge{ int v, next; }E[N]; int p[N], cnt, a, k, ans; inline void init() { memset(p, -1, sizeof (p)); cnt = 0; } inline void insert(int u, int v) { E[cnt].v = v; E[cnt].next = p[u]; p[u] = cnt++; } int dfs(int u, int fa) { int dep = 0; for (int i = p[u], v; i + 1; i = E[i].next) v = E[i].v, dep = std::max(dep, dfs(v, u)); ans += ((++dep) >= k && (fa > 1)); return dep % k; } int main() { init(); int n; scanf("%d%d%d", &n, &k, &a); ans += (a != 1); for (int i = 2; i <= n; ++i) scanf("%d", &a), insert(a, i); dfs(1, 0); printf("%d\n", ans); return 0; } ``` #### E - Salvage Robots 有一个 $n\times m$ 的网格图,其中每个格子可能有以下三种状态: - 这个格子是空的。 - 这个格子是出口,整个网格图中出口有且仅有一个。 - 这个格子里有一个机器人。 现在想通过以下操作把尽可能多的机器人送到出口: - 选择上下左右四个方向的一个,然后所有机器人沿着这个方向行进一步。如果一个机器人在到达出口前走到了边界外,它就会爆炸消失。 问最多能把多少机器人送到出口。($2\le n,m\le100$) 发现一堆机器人的状态不好描述,所以我们反向考虑,不移动机器人,而是去移动整个网格图和出口,并描述出口的移动情况。注意到描述出口移动情况的关键在于它移动的最远端。具体来讲,我们用一个四元组 $(l,r,d,u)$ 表示出口往左最多移动了 $l$ 格,往右 $r$,往下 $d$,往上 $u$。而这样的话,从上边界往下 $d$ 格,从下边界往上 $u$ 格,从左边界往右 $r$ 格,从右边界往左 $l$ 格都是已经出界过的地方,里面不可能有机器人了,有可能是爆炸了,有可能在爆炸前到出口了。当然,出口经过的地方也没有机器人了,因为它们出去了。总的来说,如下图(又来自 AT),黄色部分是出口到达的地方,红色部分是没有机器人的地方: ![](https://cdn.luogu.com.cn/upload/image_hosting/jqucrd3s.png) 换句话说,上图中所有带颜色的格子都不会有机器人,白色格子里面机器人还是原来的样子。考虑设 $f_{l,r,u,d}$ 表示当前状态用 $(l,r,u,d)$ 描述时能救出多少机器人。则转移即为尝试拓展这个黄色的部分并计算贡献。具体来讲,我们分别尝试把黄色部分往四个方向拓展一格,则新救下的机器人即为新拓展的黄色部分和原来白色部分的交集。如下图(还是来自 AT),当往下拓展一格,可以救下紫色部分的机器人,当往右拓展一格可以救下绿色部分的机器人,往其他方向拓展不能救下新机器人: ![](https://cdn.luogu.com.cn/upload/image_hosting/xpa7yai7.png) 直接 $\mathcal{O}(n^2m^2)$ 转移即可,具体细节见代码。 ```cpp #include <cstdio> #include <algorithm> const int N = 110; int f[N][N][N][N], v[N][N][2]; char s[N]; int main() { int n, m, x, y, ans = 0; scanf("%d%d", &n, &m); for (int i = 1; i <= n; ++i) { scanf("%s", s + 1); //计算列/行机器人的前缀和方便计算转移 for (int j = 1; j <= m; ++j) { if (s[j] == 'E') x = i, y = j; else { v[i][j][0] = v[i - 1][j][0] + (s[j] == 'o'); v[i][j][1] = v[i][j - 1][1] + (s[j] == 'o'); } } } //这里底下转移的时候取 min/max 其实就是计算白色部分。具体来讲就是红色边界和黄色边界取一个 min/max。 for (int l = 0; l < y; ++l) for (int r = 0; r <= m - y; ++r) for (int u = 0; u < x; ++u) for (int d = 0, s; d <= n - x; ++d) { //最终答案即为所有状态取 max ans = std::max(ans, s = f[l][r][u][d]); if (l + r < y - 1) f[l + 1][r][u][d] = std::max(f[l + 1][r][u][d], s + v[std::min(x + d, n - u)][y - l - 1][0] - v[std::max(x - u - 1, d)][y - l - 1][0]); if (l + r < m - y) f[l][r + 1][u][d] = std::max(f[l][r + 1][u][d], s + v[std::min(x + d, n - u)][y + r + 1][0] - v[std::max(x - u - 1, d)][y + r + 1][0]); if (u + d < x - 1) f[l][r][u + 1][d] = std::max(f[l][r][u + 1][d], s + v[x - u - 1][std::min(y + r, m - l)][1] - v[x - u - 1][std::max(y - l - 1, r)][1]); if (u + d < n - x) f[l][r][u][d + 1] = std::max(f[l][r][u][d + 1], s + v[x + d + 1][std::min(y + r, m - l)][1] - v[x + d + 1][std::max(y - l - 1, r)][1]); } printf("%d\n", ans); return 0; } ``` #### F - Namori 给出一张 $n$ 个点 $m$ 条边的连通无向图,初始时所有结点颜色均为白色。我们定义一次操作为: - 选择两个相邻同色结点,把它们的颜色翻转,也就是黑变白,白变黑。 问能否通过若干次操作使得原图所有结点变为黑色。($2\le n\le m,n-1\le m\le n$) 这道题看起来非常难下手,所以我们先来考虑数据范围里面比较简单的那一档 $m=n-1$。但即使在树上,原问题依然很棘手,所以我们要想到一个转化。注意到树一定是二分图,那我们把原图黑白染色后,原题即变为: > 能选择两个相邻异色结点,把它们的颜色翻转,问能否通过若干次操作使得原图从初始状态变为所有结点都颜色翻转的状态。 以样例 1 为例。原题的操作是这样: ![](https://cdn.luogu.com.cn/upload/image_hosting/1o8qshyq.png) 而经过黑白染色后的操作就变为了: ![](https://cdn.luogu.com.cn/upload/image_hosting/qpfftqit.png) 我们还可以通过棋子的模型把原题的题意进一步解释: > 现在有一棵树,一些结点上有棋子,每次操作可以把棋子推到相邻的空位上,给出初始状态和结束状态,问能否通过若干次操作使原图从初始状态到结束状态。 这看起来就可做多了,下文中我们称初始状态为 $S$,结束状态为 $T$。首先当 $S$ 和 $T$ 状态下的棋子数目不一样时,显然不可能,否则一定存在一种方案。我们来对每条边分别考虑,显然一条边会把树分为两个连通块 $G_1,G_2$,则经过这条边的棋子数量即为 $S$ 下 $G_1$ 中的棋子数量与 $T$ 下 $G_1$ 中的棋子数量之差的绝对值。因为是最终操作是反色嘛,所以这个其实就是 $G_1$ 在 $S$ 下棋子数量和空位数量之差,我们钦定 $G_1$ 是那个子树连通块,这就可以一遍 $\rm dfs$ 求出来了,时间复杂度 $\mathcal{O}(n)$。 接下来我们来考虑 $m=n$ 的情况。这种情况显然是一棵基环树,但基环树也分种类。偶环基环树是二分图,情况跟树类似,我们先来考虑,而奇环基环树不是二分图,情况有所变化。对于偶环基环树,我们依然采用跟树一样的棋子模型。考虑环上的一条边 $e_i=(u,v)$,我们设在最终按方向 $u\rightarrow v$通过这条边的棋子数量为 $x$。(如果按照 $v\rightarrow u$ 通过则会做一个 $-1$ 的贡献)考虑割掉这条边之后,原图会变成一棵树,我们可以用上文所述算法算出这一部分的贡献 $a_{i}$,所以这条边的贡献为 $|a_i+x|$。所以最终答案即为 $\sum_{i=1}^c |a_i+x|$,其中 $c$ 表示偶环的大小。显然可以通过 $a_i$ 的中位数来找到答案的最小值(当然题解说也可以三分,我不是很会三分就不介绍了)。时间复杂度 $\mathcal{O}(n)

然后是奇环基环树,这个东西不是二分图,也就是说如果我们依然对它进行黑白染色,会存在两个相邻的点颜色相同,考虑剪掉这条边,则情况又回到了树的情况。但不一样的是,我们可以对这条边相邻的两个点进行操作,可以同时放上一个棋子或同时拿掉一个棋子。这样我们就可以改变棋子数量了,但不能改变棋子数量的奇偶性,所以当 S,T 内棋子数量奇偶性不同时,显然无解。否则我们可以通过若干次操作使得 S,T 状态下棋子相同,然后按照树的方法做就好了。答案就是树的答案加上操作次数。时间复杂度 \mathcal{O}(n)

#include <cstdio>
#include <cstring>
#include <algorithm>
const int N = 1e5 + 10; int fa[N], a[N], b[N], tp;
struct edge{ int v, next; }E[N << 1]; int p[N], cnt;
inline void init() { memset(p, -1, sizeof (p)); cnt = 0; }
inline void insert(int u, int v) { E[cnt].v = v; E[cnt].next = p[u]; p[u] = cnt++; }
int getf(int x) { return x == fa[x] ? x : fa[x] = getf(fa[x]); }
void dfs(int u, int typ)
{
    a[u] = typ;
    for (int i = p[u], v; i + 1; i = E[i].next)
    {
        v = E[i].v; if (v == fa[u]) continue;
        fa[v] = u; dfs(v, -typ); a[u] += a[v];
    }
}
int main()
{
    init(); int n, m, rt = 1, rs = 1, ans = 0; scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) fa[i] = i;
    for (int i = 1, x, y; i <= m; ++i)
    {
        scanf("%d%d", &x, &y);
        if (getf(x) != getf(y)) fa[fa[x]] = fa[y];
        else { rt = x; rs = y; continue; }
        insert(x, y); insert(y, x);
    }
    fa[rt] = 0; dfs(rt, 1);
    if (n == m)
    {
        for (int i = rs; i; i = fa[i]) b[++tp] = a[i];
        if (tp & 1)
        {
            if (a[rt] & 1) return printf("-1\n"), 0;
            for (int i = rs; i; i = fa[i]) a[i] -= a[rt] >> 1;
        }
        else
        {
            if (a[rt]) return printf("-1\n"), 0;
            std::sort(b + 1, b + tp + 1);
            for (int i = rs; i; i = fa[i]) a[i] -= b[tp >> 1];
        }
    } else if (a[rt]) return printf("-1\n"), 0;
    for (int i = 1; i <= n; ++i) ans += std::abs(a[i]);
    printf("%d\n", ans); return 0;
}