Codeforces 题解合集

· · 算法·理论

CF1809 题解

CF1809E

1. 题意

有两个容积分别为 a 单位和 b 单位的容器。有一个长度为 n 的操作序列,每次操作将一个容器中的水向另一个倒 v_i 单位,若有一个容器为空或满则中断。问对于所有可能的初始水量,求操作后 A 容器每一种水量的可能性。

2. 分析

首先不难注意到一点,两个容器的总水量不变。
这个也算是一个典型套路,记住就好。
我们假装将两个容器底部接通。
那么一次体积为 v 的挪水操作就相当于 v 次体积为 1 的挪水操作,除非某一个容器满了或空了。
我们可以认为,在数轴上 -a 和 b 分别有两个点,称为“容积上限点”。
而原点是 “分隔点”。
若干体积水可以理解为一条线段,挪水则是线段的左右滑动。但是:

考虑何种线段可以合并处理。例如,两个分别是左 0 右 5 和左 1 右 4 的线段,进行左到右的 1 的挪水后重合。因此同长度的线段可以统一考虑。
即一系列的相邻两个左端点最初差 1 的同长度的线段在数轴上进行滑动。假设左端点初始为 \{a_k\},那么操作之后的左端点必然为 \{a_x+r,a_x+r,\dots,a_x+r,a_{x+1}+r,a_{x+2}+r,\dots,a_y+r,a_y+r,\dots,a_y+r\},即左边一段重叠,中间两两差 1,右边一段重叠。其中 r 为挪水量的总和。

若当前状态符合这个定义,因为操作被拆分成了若干左 1 或右 1。
当左 1 时,若若干线段受 -a 限制。
则它们的左侧不可能有更多线段(否则超出 -a 限制),此时右侧的线段因为平移量只有 1 所以全部不受限。即左端进行了额外一格的吞并。
同理可以证明右 1 的 b 限制和左右的原点限制。

也就是只要算出左右的重叠长度即可。

3. 代码

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm> 
#define int long long
#define maxn 10005
#define maxv 1005

using namespace std;

int _, n, a, b, v[maxn], ans[maxv][maxv];
void work() {
    int sum = 0;
    cin >> n >> a >> b;
    for(int i = 1; i <= n; i++) cin >> v[i], sum += v[i];
    for(int w = 0; w <= a + b; w++) { //枚举线段长度
        int l = max(a - w, 0ll), r = min(a + b - w, a); //因为显然数组不能访问负的位置,所以将左右中的限制和线段都向右平移 $a$。
        int L = l, R = r;//初始状态的左端点上下界
        for(int i = 1; i <= n; i++) {
            r += v[i];
            l += v[i];
            r = min(r, R);
            l = min(l, R);
            r = max(r, L); 
            l = max(l, L);
        }
        if(l == r) {
            for(int i = L; i <= R; i++) {
                ans[a - i][i + w - a] = a - l;
            }
            continue;
        } 
        for(int i = L; i < l - sum; i++) {
            ans[a - i][i + w - a] = a - l;
        }
        for(int i = l - sum; i < r - sum; i++) {
            ans[a - i][i + w - a] = a - (i + sum);
        }
        for(int i = r - sum; i <= R; i++) {
            ans[a - i][i + w - a] = a - r;
        }
                //l-sum 和 r-sum 分别是当前合并后左端点上下界的线段在平移前的左端点值。因为这个表是要根据初始值绘制的。
    }
    for(int i = 0; i <= a; i++) {
        for(int j = 0; j <= b; j++) {
            cout << ans[i][j] << ' ';
        }cout << endl;
    }
}

signed main() {
    ios_base::sync_with_stdio(false);
    _ = 1;
    while(_--) work();
}

CF1867 题解

CF1867D

1. 题意

一个无边图,每次操作选择 k 个点,将它们原有出边删去后顺次连接成环。现给出每个点的出边终点编号,问这张图是否可以通过有限次操作得到。

2. 分析

每条边在不超过一个环上。

证明:
若一个点不停走向后继能回到自身,它在环上。这样走到后继构成的路径就是这个环。
而对于后继唯一的节点构成的图,这样的走法不超过一个。因此每个节点至多在一个环上。而一条边必然与它的起点在相同的环上,则每条边所在环唯一。

每次操作形成一个长度为 k 的环。我们来研究一下能否通过若干操作形成一个其他长度的环。
考虑一条环上边 B\to C 及其环上前驱 A\to B,当 A\to B 形成时,B 必然在成环序列里。那么 B 的出边就会被刷新。所以任一条边的成边时间不早于其环上前驱。
可这是个环啊。我们对环用一圈这个不等式,就变成了一个等式:环上所有边成边时间相同!可是我们又注意到,一次操作只能形成长度为 k 的环。所以任意环长度必须为 k。

从一个点开始跳后继,由于每个点的出边不但是唯一而且是存在的,所以可以无限地跳下去。而图中的点有 n 个,跳 n+1 次一定跳到过某个节点多次,即每个点都能跳到一个环上。

考虑环外的边 A\to B 是如何建立的。若形如 X\to A 的边已经全部建立完成,那么选择最终状态某个环上任意 k-2 个异于A,B 的点,建立一个环即可。因为 B 的出边会在后续操作中被更正,而这次操作也不会影响 A 的入边。
而操作中 A 的前驱因为在环上,最后一次操作时会被直接更正,无需操心。因此,在环信息正确的情况下,任意环外边都是可以被建立的。

坑

所以要特判 $1$。推一下发现每个点必须自环。没别的坑了。 ### 3. 代码 ```cpp #include<iostream> #include<cstdio> #include<algorithm> #include<cstring> #include<string> #include<queue> #include<vector> using namespace std; int n, k, f[200005], don[200005], ctn[200005]; bool work() { cin >> n >> k; for(int i = 1; i <= n; i++) don[i] = 0; for(int i = 1; i <= n; i++) cin >> f[i]; if(k == 1) { for(int i = 1; i <= n; i++) if(i != f[i]) return false; return true; } for(int i = 1; i <= n; i++) { if(don[i] == 0) { int cnt = 0, r; for(r = i; !don[r]; r = f[r]) { ctn[r] = cnt; cnt++; don[r] = i; } if(don[r] != i) continue; if(cnt - ctn[r] != k) return false; } } return true; } signed main() { int _; cin >> _; while(_--) { cout << (work() ? "Yes\n" : "No\n"); } } ``` # CF1935 题解 ## CF1935A ### 1. 题意 给定一个字符串 $s$ 和一个正**偶数** $n$,你要对 $s$ 进行恰好 $n$ 次操作。每次操作形如以下两者**之一**。 1. 左右翻转 $s$。形式化地,设其长度为 $m$,则同时执行 $s_i := s_{m+1-i}$。 2. 将 $s$ 拼接上 $s$ 的左右翻转。形式化地,先令 $t=s$,然后对 $t$ 做 $1$ 操作,再 $s:=s+t$。其中 $+$ 指字符串拼接。 问能得到的字典序最小的字符串。 $2\le n\le 10^9,|s|\le 100$。 ### 2. 分析 设最初的字符串为 $s_0$,翻转后为 $s_1$。 那么可以简单归纳证明,无论操作多少次,最后字符串一定以 $s_0$ 或 $s_1$ 开头。 那么我们为了让字典序最小,可以贪心地取一个较小的前缀,并进行分类讨论。确定了开头之后,我们还希望后面的东西尽可能少。 + $s_0\le s_1$。 则答案就是 $s_0$。因为 $s_0$ 翻转偶数次还是 $s_0$。 + $s_0>s_1$。 因为 $n$ 是偶数,则 $s_0$ 翻转偶数次的答案是 $s_0$。为了达成 $s_1$ 作为开头,我们只能对它翻转 $n-1$ 次,然后做一次 $2$ 操作。答案是 $s_0+s_1$。 ### 3. 代码 ```cpp #include<iostream> #include<cstdio> #include<vector> #include<stack> #include<queue> #include<algorithm> #define maxn 800005 #define int long long using namespace std; int n; string s; void work() { cin >> n >> s; string rvs = s; reverse(rvs.begin(), rvs.end()); if(s > rvs) { //字符串的比较是比较字典序 cout << rvs + s << '\n'; } else cout << s << '\n'; } signed main() { ios::sync_with_stdio(false); cin.tie(0); int _; cin >> _; while(_--) work(); } ``` ## CF1935B ### 1. 题意 给定一个长度为 $n$ 的序列 $\{a_n\}$。问是否存在一种划分方案,把序列划分成若干个区间,使得每个数被划分到恰好一个区间,且每个区间的 $\operatorname{mex}$ 相等。若存在请给出任意一组。 对于自然数序列 $\{b_m\}$,它的 $\operatorname{mex}$ 表示其中最小的未出现的的自然数。即 $\min x\in \mathbb{N}$,使得 $\forall 1\le i\le m,b_i\neq x$。 $0\le a_i<n,2\le n\le 10^5$。 ### 2. 分析 此处采用一种经典的刻画 $\operatorname{mex}$ 的方法。 对于一个数列 \{b_m\},若其 $\operatorname{mex}$ 为 $x$,则: + $\forall 0\le i<x,\ \exists j,\ b_j=i$。即任意不超过 $x$ 的自然数都出现了 + $\forall j,\ b_j \neq x$,即 $x$ 没有出现 同时成立。 如果这个序列被划分成了至少三段,我们考虑任意两段,它们同时满足这两条性质,因此合并后也满足这两条性质,故合并后 $\operatorname{mex}$ 不变。 所以如果存在方案。则一定存在划分为 $2$ 个区间的方案。枚举断点,用 `multiset` 维护两侧的 $\operatorname{mex}$ 即可。 在实现时,我们可以根据定义即“最小没有出现自然数”优化代码,因为值域较小,可以维护要维护的集合在值域上的补集,每次查询取其中最小值即可。 又因为前缀是正向维护,后缀是反向维护,所以每一次修改之后都是上一次的超集,所以不需要考虑删数的问题。 ### 3. 代码 ```cpp #include<iostream> #include<cstdio> #include<vector> #include<stack> #include<queue> #include<set> #include<algorithm> #define maxn 200005 #define int long long using namespace std; set<int> st; int n, a[maxn], pre[maxn], suf[maxn]; // pre 记录从 1 到 i 的 mex,suf 记录从 i 到 n 的 mex void work() { cin >> n; st.clear(); //多测清空 for(int i = 0; i <= n; i++) { //一定要记得比值域多放进去一个,比如 ai 的最大值是1000,就要把 0~1001都放进去 st.insert(i); // 值域作为全集 } for(int i = 1; i <= n; i++) { cin >> a[i]; st.erase(a[i]); //集合加数等价于补集删数 pre[i] = *st.begin(); // 取最小值 } st.clear(); for(int i = 0; i <= n; i++) { st.insert(i); } for(int i = n; i >= 1; i--) { st.erase(a[i]); suf[i] = *st.begin(); } for(int i = 2; i <= n; i++) { if(suf[i] == pre[i - 1]) { //若 1~i 和 i+1~n 的 mex 相等,输出方案。 cout << "2\n1 "; cout << i - 1 << '\n' << i << ' ' << n << '\n'; return ; } } cout << "-1\n"; //否则无解 } signed main() { ios::sync_with_stdio(false); cin.tie(0); int _; cin >> _; while(_--) work(); } ``` ## CF1935C ### 1. 题意 给定两个长度为 $n$ 的序列 $\{a_n\},\{b_n\}$ 和一个正整数 $T$,求最大的 $k$,使得存在一个长度为 $k$ 的序列 $\{p_k\}$,满足 $\forall i\neq j,p_i \neq p_j$,且 $\sum\limits_{i=1}^k a_i+\sum\limits_{i=1}^{k-1} |b_i-b_{i+1}|\le T$。 $1\le n\le 2000$。 ### 2. 分析 注意到选好 $p$ 的内容之后调整顺序并不会对第一项产生影响。而按 $b$ 大小排序可以让第二项最小。 所以我们先把两个序列对应位置不变地按 $b$ 不降排序。 然后,由于已经排好序,第二项只会受选定的位置右端点与左端点 $b$ 的差有关。 所以可以枚举左端点扫描右端点。若左右端点分别为 $l,r$,则此时选定的 $a$ 的和不能超过 $T-(b_r-b_l)$。可以直接用线段树上二分做,但是不想写线段树。 考虑能否贪心。当左右端点确定时,选的一定是区间中 $a$ 最小的一段前缀。左右端点事实上可以不选,因为计算的左右端点比选中的左右端点更大只会导致条件更严格(不会算出错误答案),且枚举到正确左端点并扫到正确右端点时一定会计算这一组,除非有更优解(正确答案一定被计算)。 如果每次加入一个新的 $a$,调整 $T-(b_r-b_l)$ 的限制,当选中的数超限时就删除最大的,这样对吗? 在某一个位置选定区间中最小若干个值的时候,由于扫描右端点只会让限制更严格,所以曾经的宽松限制一定能容下这些数存在。又因为是每次删最大的,所以比它们更大的一定先被删掉。 所以贪心是正确的。 ### 3. 代码 ```cpp #include<iostream> #include<cstdio> #include<vector> #include<stack> #include<queue> #include<set> #include<algorithm> #define maxn 200005 #define int long long using namespace std; int n, l; struct node { int a, b; bool operator<(node y)const { if(b == y.b) return a > y.a; return b < y.b; } } a[maxn]; priority_queue<int> q; //可用 multiset 代之,千万不要 set void work() { cin >> n >> l; // 这里的 l 是叙述中的 T for(int i = 1; i <= n; i++) { cin >> a[i].a >> a[i].b; } int ans = 0; sort(a + 1, a + n + 1); //按 b 排序 for(int i = 1; i <= n; i++) { while(!q.empty()) q.pop(); int sum = 0; int cnt = 0; for(int j = i; j <= n; j++) { q.push(a[j].a); //扫一个新的点,把 a 暂时选上 cnt++; //统计个数 sum += a[j].a; //记录已经选择的数的和 while(sum + a[j].b - a[i].b > l && !q.empty()) { //超出限制 sum -= q.top(); q.pop(); cnt--; } ans = max(ans, cnt); // 一定要记得更新一次端点就更新一次答案 } } cout << ans << '\n'; } signed main() { ios::sync_with_stdio(false); cin.tie(0); int _; cin >> _; while(_--) work(); } ``` ## CF1935D ### 1. 题意 给你一个正整数 $c$ 和一个自然数集合 $S$(值域为 $c$),问有多少组 $0\le x\le y\le c$,满足 $x+y\notin S,y-x\notin S$。 $1\le n\le 3\times 10^5,1\le c\le 10^9

2. 分析

问有多少组值域内的 x\le y 使得 x+y\in S 显然是好做的,遍历 S 并计算即可。
所以可以考虑容斥。
为了方便,可以数形结合。我们取横纵坐标均在 [0,c] 的整点。

长度为 n 的序列中,若元素两两不同,则无序二元组的个数为 \frac{n(n+1)}{2}。

3. 代码

#include<iostream>
#include<cstdio>
#include<vector>
#include<stack>
#include<queue>
#include<set>
#include<algorithm>
#define maxn 200005
#define int long long

using namespace std;


int n, c, sum, cnt0, cnt1;

void work() {
    sum = cnt0 = cnt1 = 0;
    cin >> n >> c;
    int a;
    sum = (c + 1) * (c + 2) / 2;
    for(int i = 1; i <= n; i++) {
        cin >> a;
        sum -= (a + 2) / 2; //第一部分减掉
        sum -= (c - a + 1); //第二部分减掉
        if(a & 1) cnt1++; //奇偶分开统计
        else cnt0++;
    }
    sum += cnt1 * (cnt1 + 1) / 2; //加上
    sum += cnt0 * (cnt0 + 1) / 2; //加上
    cout << sum << '\n';
}

signed main() {
    ios::sync_with_stdio(false); cin.tie(0);
    int _; cin >> _; while(_--) work();
} 

CF1935E

1. 题意

给定一个限制序列 \{(x_n,y_n)\},表示 x_i\le a_i\le y_i。

$1\le n,q\le 2\times 10^5,1\le l_i\le r_i\le n,0\le x_i\le y_i<2^{32}$。 ### 2. 分析 由于问位运算最大值,所以可以从高位往低位贪心。 第 $i$ 位可以为 $1$,当且仅当有至少一个数在第 $i$ 位可以为 $1$。 如果有数必须在这一位为 $1$,则直接用这个数,避免带来不必要的限制。 否则会对一个数产生一个更严格的下界。 + 如果这一位特别宽松,还额外有一个可以是 $0$ 也可以是 $1$ 的数,就可以直接把后面的位都解决。因为是连续变化,所以可以有 $1000\dots \operatorname{or} {0111\dots}=1111\dots$。 + 如果这一位不太宽松,且我们用掉了一个可 $0$ 可 $1$ 的,那么这个数从上界的下一个 $1$ 位开始重新变得可 $0$ 可 $1$。 + 如果这一位不太宽松,且我们用掉了一个一定为 $1$ 的数,不会产生额外影响。 + 如果这一位非常严格,不能达成 $1$,直接继续贪下一位。 对于数的判定,若某一位上,某个数的上下界不同,则从这一位开始这个数可 $0$ 可 $1$。否则按照上下界判断是否一定为 $1$。 至此,我们可以总结如下贪心策略: 1. 从高开始枚举每一位。 2. 看这一位在给定区间内有多少个可 $0$ 可 $1$ 的数(称为第一贡献),和多少个必须为 $1$ 的数(称为第二贡献)。第二贡献有 $\ge 1$ 个产生 $1$ 的贡献,第一贡献按个数产生贡献。 3. 总贡献 $\ge 2$ 时,把后面的位和这一位全置 $1$,结束贪心过程。 4. 总贡献 $=0$ 时,不管这一位,继续贪心。 5. 总贡献 $=1$ 时,这一位置成 $1$,若有第一贡献,将 $[下一位,进行第一贡献的数^{*}后面所有位中的最高位]$ 区间的第一贡献减少 $1$。 \*:对于每一个数和每一位,找到这之前这一位第一个可 $0$ 可 $1$ 的数。只需要找一个,因为有两个就会结束贪心。。 ### 3. 代码 ```cpp #include<iostream> #include<cstdio> #include<vector> #include<stack> #include<queue> #include<set> #include<algorithm> #define maxn 400005 #define int long long using namespace std; int n, q, l, r, cnt[maxn][32], lst[maxn][32], x[maxn], y[maxn], l2[maxn]; inline int l2c(int x) { return (x >> 16) ? l2[x >> 16] + 16 : l2[x]; } void work() { cin >> n; for(int i = 0; i <= n + 2; i++) { x[i] = y[i] = 0; for(int j = 0; j <= 30; j++) { cnt[i][j] = lst[i][j] = 0; //多测清空!!! } } for(int i = 1; i <= n; i++) { cin >> l >> r; x[i] = l, y[i] = r; for(int j = 29; j >= 0; j--) { if(i == 1) lst[i][j] = 0; lst[i + 1][j] = lst[i][j]; if((l >> j) != (r >> j)) cnt[i][j] = cnt[i - 1][j] + 1048576; //判定贡献类别,偷懒压数了,cnt的后20位为第二贡献,前面的位为第一贡献。这么写要开 long long。 else if((l >> j) & 1) cnt[i][j] = cnt[i - 1][j] + 1; //产生第二贡献 else cnt[i][j] = cnt[i - 1][j]; //没有贡献,也要做前缀和转移!! if(((l >> j) ^ (r >> j)) == 1) lst[i + 1][j] = i; //维护 * 的内容。 } } cin >> q; while(q--) { cin >> l >> r; int bs = 0, xum = 0; int q0[32] = {0}; //每一位总贡献 for(int i = 29; i >= 0; i--) { int sum = cnt[r][i] - cnt[l - 1][i]; int ctn = 0; for(int p = lst[r + 1][i]; p >= l; p = lst[p][i]) { q0[i]++; for(int t = y[p] & ((1 << i) - 1); t; t &= ((1 << l2c(t)) - 1)) { //每次取下一个 1 位。 q0[l2c(t)]++; //l2c 为自己写的取最高位函数。在每个 1 位产生贡献。若没被使用,则必然有其他数被使用,二次贡献,结束。若被使用,则一定是继续找下一个 1 } ctn++; if(ctn > 1) break; //同一位超过两次贡献,直接结束 } int s1 = sum & 1048575; //提取第二贡献 if(s1) s1 = 1; // 若有,无论多少个都是 1 贡献 q0[i] += s1; } for(int i = 29; i >= 0; i--) { if(q0[i] >= 2) { // 二次贡献全变 1 xum |= (1 << (i + 1)) - 1; } else if(q0[i] >= 1) xum |= (1 << i); //一次贡献单位变 1 } cout << xum << ' '; } cout << '\n'; } signed main() { l2[0] = -1; for(int i = 1; i <= 200000; i++) l2[i] = l2[i >> 1] + 1; ios::sync_with_stdio(false); cin.tie(0); int _; cin >> _; while(_--) work(); } ``` ## CF1935F ### 1. 题意 给定一棵树,对于每个顶点 $v$,问把它删掉后,剩余部分连成一棵树的最小代价,并给出最小代价的方案。在 $(i,j)$ 之间连边的代价是 $|i-j|$。 $5\le n\le 2\times 10^5$。 ### 2. 分析 对于一个顶点 $v$,我们设它的度数为 $d_v$。 容易发现,答案的最小值不会小于 $d_v-1$。 若 $v$ 没有被删除,则对于所有形如 $(i,i+1)$ 的边跑最小生成树一定能做到 $d_v-1$。 所以可以直接对所有形如 $(i\neq v,i+1\neq v)$ 的边和 $(v-1,v+1)$ 跑最小生成树。但这样复杂度太高了,无法接受。 通过最小生成树的思路,发现答案不会超过 $d_v$。如果两个连通块无法通过 $(i\neq v,i+1\neq v)$ 形式的边连通,则一定是 $<v$ 一个块,$>v$ 一个块。 否则找到与 $1$($v=1$ 时取 $2$)不连通的第一个点,则它前面一个和 $1$ 连通,故它和 $1$ 连通。从末尾再来一次,可以发现如果不连通一定是 $<v$ 一个块,$>v$ 一个块。 同时,此时任何一个子树的编号没有跨块。否则可以取到 $d_v-1$。 现在我们已经会了计算答案,考虑如何构造方案。 直接枚举每个点,看它左右有没有不同子树的,并查集维护。复杂度过高,无法接受。 考虑少枚举一些点。比如,仅仅枚举每个子树的最大最小编号的点。这样一定是对的吗? 从 $1$($v=1$ 时取 $2$)开始,找到第一个没有通过这样的边与之连通的。由于同一个子树的被连通了,所以这个点之前没有和它在同一个子树的。所以它是子树最小值,可以向它前一个点连边,除非 $i=v+1$。 同理倒着来一次,利用最大值证明,这样的边可以连通 $[v+1,n]$。 如果最小代价是 $d_v$,那么再连通 $v-1,v+1$ 即可。否则,不需要任何额外连边,因为左侧和右侧内部已经连通,而有至少一个子树连通了左侧和右侧。 **一些技巧** 由于这是一棵无根树,对于“每个子树”这样的表述,我们是把顶点 $v$ 当作根来看的。直接维护复杂度无法接受,可以随意钦定一个根把树“拎起来"做换根 DP。 实现上,我钦定了 $1$ 为根。代码注释中,对于一个节点,内部子树是 1 为根它拥有的子树,外部子树是 1 为根它没有,它自己为根才有的(对于 1 不存在,对于其他点包含 1)的子树。 ### 3. 代码 ```cpp #include<iostream> #include<cstdio> #include<vector> #include<queue> #include<stack> #define maxn 200005 using namespace std; int n, l[maxn], r[maxn], cnt[maxn], siz[maxn], val[maxn], rep[maxn], f[maxn], ft[maxn], vst[maxn], mx1[maxn], mx2[maxn], mn1[maxn], mn2[maxn]; int iv[maxn]; int _G_T; int hg[maxn], tg[maxn], ha[maxn], eg, eq, ea; struct edge { //存图和答案用的,单双向链表 int v, u, next; } g[maxn * 2], ans[maxn * 2]; void addg(int id, int u, int v = -1) { //图,双向,v u next(结构体中)分别为边到达的点,上一条边,下一条边 if(hg[id] != -1) g[hg[id]].u = eg; else tg[id] = eg; g[eg] = (edge){u, v, hg[id]}; hg[id] = eg++; } void adda(int id, int u, int v) { //答案,单向,v u next(结构体中)分别为答案边的 u v 和删除这个点方案的下一条边。 ans[ea] = (edge){u, v, ha[id]}; ha[id] = ea++; } struct Stack { //手写栈 int n, a[maxn]; Stack() { n = 0; } bool empty() { return n == 0; } void push(int x) { a[n++] = x; } int pop() { return a[--n]; } void clear() { n = 0; } int front() { return a[n - 1]; } } q, p; inline int find(int x) { //并查集一号,用来枚举点时合并子树 return f[x] == x ? x : f[x] = find(f[x]); } bool t(int x, int y) { //并查集一号的合并 int fx = find(x), fy = find(y); if(fx == fy) return false; f[fx] = fy; return true; } inline int frep(int x) { //并查集二号,用于判定每个点所在的子树 return rep[x] == x ? x : rep[x] = frep(rep[x]); } inline void dfs(int id, int fr = 0) { //第一次 dfs 算最小值和子树内编号最大最小值 int cnt0 = 0, cnt1 = 0; int deg = id == 1 ? 0 : 1; //钦定了 1 为根,若是根没有连向父节点的度数,否则有 1 for(int j = hg[id]; ~j; j = g[j].next) { int v = g[j].v; if(v == fr) continue; ft[v] = id; dfs(v, id); deg++; siz[id] += siz[v]; l[id] = min(l[id], l[v]); //子树编号min r[id] = max(r[id], r[v]); //max if(r[v] < id) cnt0 += siz[v]; if(l[v] > id) cnt1 += siz[v]; } cnt[id] = deg - 1; val[id] = deg - 1; if((cnt0 == id - 1 || cnt1 == n - id) && id != 1 && id != n) { //若内部子树完全占满了 id 的一侧,则需要额外代价。 val[id]++; adda(id, id - 1, id + 1); } siz[id]++; } void dfs1(int id, int fr = 0) { mn1[id] = n + 1, mn2[id] = n + 1, mx1[id] = 0, mx2[id] = 0;//换根,记录每个点子树的最小次小值 for(int j = hg[id]; ~j; j = g[j].next) { int v = g[j].v; int l0v = l[v], r0v = r[v]; if(v == fr) { //继承父节点的值,但是要扣掉自己这个子树,这就是最大最小值维护前两最的原因 l0v = l[id] == mn1[fr] ? mn2[fr] : mn1[fr]; r0v = r[id] == mx1[fr] ? mx2[fr] : mx1[fr]; l0v = min(l0v, id); r0v = max(r0v, id); } if(l0v < mn1[id]) mn2[id] = mn1[id], mn1[id] = l0v; else if(l0v < mn2[id]) mn2[id] = l0v; if(r0v > mx1[id]) mx2[id] = mx1[id], mx1[id] = r0v; else if(r0v > mx2[id]) mx2[id] = r0v; } for(int j = hg[id]; ~j; j = g[j].next) { int v = g[j].v; if(v == fr) continue; dfs1(v, id); } } int rfep(int x) { //判断是内部子树还是外部子树 int p0 = frep(x); if(iv[p0]) return p0; return 0; } void dfs2(int id, int fr = 0) { //方案记录 for(int j = hg[id]; ~j; j = g[j].next) { int v = g[j].v; if(v == fr) continue; dfs2(v, id); //先搜儿子,这样子树的判定才能生效 f[v] = v; //重置并查集一号 } for(int j = hg[id]; ~j; j = g[j].next) { int v = g[j].v; if(v == fr) continue; iv[v] = 1; // 记录儿子,这样 rfep 函数才生效 } f[0] = 0; //外部子树用0这个点 for(int j = hg[id]; ~j; j = g[j].next) { int v = g[j].v; int l0, r0; if(v != fr) l0 = l[v], r0 = r[v]; //内部子树 else { //外部子树 v = 0; l0 = l[id] == mn1[fr] ? mn2[fr] : mn1[fr]; r0 = r[id] == mx1[fr] ? mx2[fr] : mx1[fr]; } if(l0 > 1 && l0 <= n && l0 - 1 != id) {//边合法 if(t(rfep(l0 - 1), v)) {//边有效 adda(id, l0 - 1, l0);//连边 } } if(r0 >= 1 && r0 < n && r0 + 1 != id) { if(t(rfep(r0 + 1), v)) { adda(id, r0 + 1, r0); } } } for(int j = hg[id]; ~j; j = g[j].next) { int v = g[j].v; if(v == fr) continue; iv[v] = 0; //记录的儿子退回去 rep[v] = id; //认祖,使 rfep 生效 } } int _; void work() { q.clear(); cin >> n; eg = ea = 0; for(int i = 0; i <= n + 1; i++) { hg[i] = ha[i] = tg[i] = -1; //多测清空 } _G_T = 0; for(int i = 0; i <= n + 1; i++) { rep[i] = i; siz[i] = cnt[i] = 0; l[i] = i, r[i] = i; } f[0] = 0; for(int i = 1; i < n; i++) { int u, v; cin >> u >> v; addg(u, v); addg(v, u); } _G_T = 0; dfs(1); //先搜一遍 dfs1(1);//换根DP dfs2(1);//记录答案 for(int i = 1; i <= n; i++) { cout << val[i] << ' ' << cnt[i] << '\n'; int ctn = 0; for(int j = ha[i]; ~j; j = ans[j].next) { cout << ans[j].u << ' ' << ans[j].v << '\n';ctn++; } cout << '\n'; } } signed main() { ios::sync_with_stdio(false); cin.tie(0); cin >> _; while(_--) work(); } ```