CUGBACM21 级组队训练(四)题解

出题思路

  • 没有很水的签到,中档题为主
  • 思维题为主,穿插一些算法知识/思维
  • 两个码农题练一下码力

A. 座位安排

把每对朋友的要求看成一条边,那么每个点的度数最多为 2,说明每个连通分量要么是环要么是链。是链的话每个要求都能满足,是环的话除非整个图是一个大环,否则必然有一条边不能满足,那么舍弃掉钱最少的条件。

B. 左撇子与右撇子

非常简单的 dp,令 dpi,jdp_{i, j} 为考虑了前 ii 个问题,且左撇子与右撇子数量之差为 jj 时所需要采访的最少的老师个数。

我们遍历每个问题,对于每个问题,我们要么只采访左撇子,要么只采访右撇子,要么左撇子右撇子各采访一个。具体状态转移可以看代码~

C. 打扫道路

一种比较简单的判断一条边 (u,v)(u, v) 是否在 aabb 的最短路上的方法是检查 aauu 的最短距离加 bbvv 的最短距离(也可能反过来)加 (u,v)(u, v) 边的长度是否是 aabb 的最短距离,也就是检查:

min(disau+disbv,disav+disbu)+w=disab\min(disa_u + disb_v, disa_v + disb_u) + w = disa_b

还有一种做法是利用从 aa 开始的最短路 DAG(K 题题解中有介绍),将其中的边反转后后从 bb 出发,所有经过的边就是在 aabb 的最短路上的边。

D. 厕所管理员

首先把所有人按 deadline 归类,然后从小到大遍历 deadline,我们维护到当前时间点,两种坑位能让多少人上完厕所,假设当前的 deadline 为 tt,上一个 deadline 为 prevprev,那么我们能多让 (tprev)(s1)(t - prev) \cdot (s - 1) 的不需要厕纸的人上完厕所,能多让 tprevt - prev 需要厕纸的人上完厕所。

对于当前 deadline,我们先让不需要厕纸的人尽量去没有厕纸的坑位,如果剩下的不需要厕纸的人(如果有)加上需要厕纸的人大于有厕纸的坑位能让人上完厕所是数量,答案就是 No,否则我们继续处理下一个 deadline。

E. 这一样吗?

模拟题,没什么思维难度。一种比较好想的做法可能是先预处理出配对的括号,每一对括号就是一个子树,然后跑一遍 dfs。下面给出一个只遍历一次的实现。

一个比较容易错的输入是两棵树都只有一个节点,且编号不一样,如果你给 11061-10^6 每个节点都开了一个邻接表,那么你将判断不出来 No。一种解决方法是把根节点连到节点 0 上。

F. 电影之夜

如果我们把依赖关系(ixii\to x_i)看成是图的话,这种每个点出度为 1 的图被称为函数图(functional graph)。图的结构由一个环以及一些挂在环上的树构成。对于环上的人,他们要么都去要么都不去,所以我们可以把他们看成一个点,这样图就变成了一个有向的树。我们在树上 dp,定义 dpudp_u 为:邀请 uu 且同时让 uu 的子树里的人的要求都满足的邀请方式的数量,那么状态转移为:

dpu=vchild(u)dpv+1dp_u = \prod_{v \in \operatorname{child}(u)} dp_v + 1

如果怕 dfs 找环写错的话也可以利用强连通分量来缩点。

G. 送礼物

不妨考虑按 aia_i 的大小遍历礼物,假设送给小明礼物 ii,那么送给小红任意 ii 之前的礼物都不会使小明嫉妒。同时我们还要不能使小红嫉妒,所以我们要知道在前 ii 个礼物中有多少个 jj 使得 bjbib_j \ge b_i,我们可以将 bb 离散化之后用树状数组维护。

更本质地说,这个题是二维偏序问题,二维偏序问题通常先按其中一个维度排序然后用树状数组解决。感兴趣的同学可以自行了解。

H. 循环排序

如果 aa 中有重复的元素的话,不失一般性地,假设有两个 1,首先我们可以利用一次操作将两个 1 放到 1 和 2 的位置。然后我们可以利用这两个 1 来交换任意两个位置:假设我们要交换 i,ji, j (i<ji < j),我们只需要应用操作 (2,i,j)(2, i, j)(1,2,i)(1, 2, i) 即可交换 i,ji, j

如果 aa 中没有重复的元素,说明 aa 是一个长为 nn 的排列。我们不妨从逆元的角度入手,题目中的操作本质上就是先交换 ai,aja_i, a_j,再交换 ai,aka_i, a_k,由于一次交换操作会使整个序列的逆元个数的奇偶性改变(想想为什么),所以循环操作不会使整个序列的逆元个数的奇偶性改变,所以不妨大胆猜想当且仅当整个序列的逆元的个数为偶数个时,我们可以通过循环操作排序。下面给出当逆元个数为偶数个时可以通过循环操作排序的证明:

考虑将 i=1n2i = 1\dots n - 2 依次放到第 ii 个位置,对于每一个 ii 我们可以用一次循环操作在不破坏之前排好序的元素的情况下把 ii 放到位置 ii。在排好前 n2n - 2 个元素之后,由于我们没有改变逆序对的奇偶性,所以最后剩下的两个元素也一定是有序的。

更本质的,我们也可以从排列(permutation)的角度来看这个问题,定义一个排列的奇偶性为它的逆序对数量的奇偶性。三循环可以用排列来表示,所有偶排列都可以表示为三循环排列的复合。想深入了解的话可以看这篇英文博客(不用全看,挑自己感兴趣的即可)。

I. 送礼物 2

本题做法不唯一,下面介绍 dp 做法:

不难想到一种 dp 状态 dpi,sum,sizedp_{i, sum, size},代表前 ii 个数中,是否存在和为 sumsum 且大小为 sizesize 的子集。本来想卡掉这个做法的,和易老师以及伍老师讨论之后还是放弃了,下面是优化后的做法:

dpi,sumdp_{i, sum} 为前 ii 个数中,和为 sumsum 的子集的大小的集合。例如,假设a=1,2,3a = 1, 2, 3,因为 sum({1,2})=3,sum({3})=3\operatorname{sum}(\{1, 2\}) = 3, \operatorname{sum}(\{3\}) = 3,那么 dp3,3={1,2}dp_{3, 3} = \{1, 2\}。那么状态转移是显然的:

dpi,sum+aidpi,sum+ai{s+1sdpi1,sum}dp_{i, sum + a_i} \coloneqq dp_{i, sum + a_i} \cup \{ s + 1 | s \in dp_{i - 1, sum}\}

如果我们用二进制表示集合的话,这个状态转移可以被非常容易的写成

dp[i][sum + a[i]] |= dp[i - 1][sum] << 1;

这个优化技巧在找哈密顿路径算法中也有应用。

J. 寻宝

不难看出这个是 bfs(只是状态有点复杂),由于数据范围比较小,所以我们可以直接记录当前在第几个单词的第几个位置。由于不能经过同一个位置两次,也就是说在一行里只能向左或者向右,所以我们还要记录当前的方向。所以我们 bfs 的状态就是 dis[row][col][idx][pos][dir],代表到达 (row,col)(row, col) 这个位置,当前字母是第 idxidx 个单词的第 pospos 位置,且当前方向是 dirdir 的最短路径。具体实现细节请看代码。

K. 旅行记录

首先我们跑一遍最短路,对于每个点,如果到这个点的最短距离在里程记录里出现过,我们就标记这个点。我们要找的就是一条经过 dd 个标记过的点到达点 nn 的路径,并且路径是最短路。

考虑从点 1 起始的所有最短路上的所有边所构成的子图,如果我们将这些边定向(从离点 1 近的点指向离点 1 远的点),我们将会得到一个 DAG。这个 DAG 上从点 1 到点 nn 的任意路径都是一条最短路。由于是 DAG,我们可以按拓扑序 dp,令 dpudp_u 为到达 uu 时经过的最多标记的点的数量,令 multiumulti_u 是否有多条这样的路径。状态转移的代码:

for (int v : g[u]) { // g 是上面提到的 DAG
    if (dp[u] + marked[v] > dp[v]) {
        dp[v] = dp[u] + seen[v];
        multi[v] = multi[u];
        prev[v] = u;
    } else if (dp[u] + seen[v] == dp[v]) {
        multi[v] = 1;
    }
}

L. 彩票

枚举被涂黑两次的格子的个数,假设有 ii 个格子被涂黑了两次。首先我们先选择第 pp 个球的编号,它可以是我们填的 2n2n 个数中的任意一个。前 p1p - 1 个球中,有 n1+in - 1 + i 个数是我们填的,我们先选被涂黑两次的格子,有 (n1i)\binom{n - 1}{i} 种选法,在剩下每个只涂黑一次的格子中我们选一个数,有 2n1i2^{n - 1 - i} 中选法。前 p1p - 1 个球中没出现在纸上的有 (m2np1(n1+i))\binom{m - 2 n}{p - 1 - (n - 1 + i)}。最后前 p1p - 1 个球有 (p1)!(p - 1)! 种顺序,第 pp 个球后面的 mpm - p 个球有 (mp)!(m - p)!种顺序。所以答案为:

(i=0pn2n(n1i)2n1i(m2np1(n1+i))(p1)!(mp)!)÷m!\left(\sum_{i = 0}^{p - n} 2 \cdot n \cdot \binom{n - 1}{i} \cdot 2^{n - 1 - i} \cdot \binom{m - 2 n}{p - 1 - (n - 1 + i)} \cdot (p - 1)! \cdot (m - p)!\right) \div m!

M. 连通分量计数

考虑树形 dp,设 dpi,j,kdp_{i, j, k} 为选择 ii 的子树中的节点的方式,使得导出子图中有 jj 个连通分量,kk 代表节点 ii 是否被选择。转移比较暴力,我们直接看 dfs 的代码:

auto dfs(int u, int p) -> vector<vector<mint>> {
    vector dp(2, vector<mint>(2));
    dp[0][0] = 1;
    dp[1][1] = 1;
 
    for (auto v : g[u]) {
        if (v == p) continue;
 
        auto res = dfs(v, u);
 
        // 每次将 v 子树的结果加入当前结果中,所以新的结果最多有 dp.size() + res.size() 
        // 个连通分量
        vector ndp(dp.size() + res.size() - 1, vector<mint>(2, 0));
        for (int i = 0; i < (int)dp.size(); i++) {
            for (int j = 0; j < (int)res.size(); j++) {
                // 不选 u 的时候,子树中的连通分量互不干扰,
                // 所以连通分量的个数为 i + j
                ndp[i + j][0] += dp[i][0] * (res[j][0] + res[j][1]);
                // 选 u 的时候,如果 v 不选,连通分量的个数也是直接相加
                ndp[i + j][1] += dp[i][1] * res[j][0];
                // 如果选 u 并且选 v 的话,会有两个连通分量相连,
                // 所以总的连通分量个数为 i + j - 1
                if (i + j > 0) {
                    ndp[i + j - 1][1] += dp[i][1] * res[j][1];
                }
            }
        }
        swap(dp, ndp);
    }
 
    return dp;
};

整个 dfs 乍一看是 O(n3)O(n^3) 的,如果我们像上面那样只转移到当前处理过子树的大小之和,整个过程其实是 O(n2)O(n^2) 的(证明略,我也不会)。