2022CCPC桂林补题题解(AMCEGJ六题)

2022CCPC桂林补题题解(AMCEGJ六题)

总览与前言

VP URL:Dashboard - 2022 China Collegiate Programming Contest (CCPC) Guilin Site - Codeforces

1

在这里插入图片描述

A. Lily(签到)

题目分析:所有不在 L 旁的字符替换为 C 即可,实现很容易就不贴代码了。

M. Youth Finale(签到,逆序对)

题目大意:给定初始数组,有两种操作:1. 可以将最前面的元素移至最后;2. 将整个数组翻转。给出冒泡排序的代码,要求每次操作后,输出当前数组进行冒泡排序的swap次数。

题目分析

  • 首先我们需要数一下逆序对,使用归并排序或者树状数组即可。
  • 随后,我们可以发现:当进行 Shift 操作时,对逆序对的影响只和这个数是谁有关系:
    • 𝑖 𝑖 i 从头移动到尾时,有 𝑖 − 1 𝑖 − 1 i1 ( 𝑖 , 𝑗 ) (𝑖, 𝑗) (i,j) 𝑗 < 𝑖 𝑗 < 𝑖 j<i 关系减少了,而有 𝑛 − 𝑖 𝑛 − 𝑖 ni ( 𝑖 , 𝑗 ) (𝑖, 𝑗) (i,j) 𝑗 > 𝑖 𝑗 > 𝑖 j>i 增加了。因此我们维护需要 Shift 谁即可完成操作。
  • Reverse 操作会改变 Shift 的方向,
    • 同时将 a n s ans ans 改为 ( n 2 ) − a n s \begin{pmatrix} n\\2 \end{pmatrix} − ans (n2)ans,因为 一共有 ( n 2 ) \begin{pmatrix} n\\2 \end{pmatrix} (n2) 个关系,原先所有逆序对变为顺序对,顺序对变为逆序对。
  • 因此,仅需数一次逆序对,回答询问是 𝑂 ( 1 ) 𝑂(1) O(1) 的,总复杂度 𝑂 ( 𝑛 l o g 𝑛 ) 𝑂(𝑛 log𝑛) O(nlogn)

主要代码

void solve(){
    cin >> n >> m;
    for(int i = 0; i < n; i++) cin >> p[i];
    for(int i = 1; i <= m; i++){
        char c; cin >> c;
        op[i] = c == 'R';
    }
    BIT bt(n);
    ll ans = 0;
    for(int i = n - 1; ~i; i--){
        ans += bt.query(p[i] - 1);
        bt.add(p[i], 1);
    }
    cout << ans << '\n';
    ll up = (ll)n * (n - 1) / 2;
    int nowid = 0, nxt = 1;
    for(int i = 1; i <= m; i++){
        if(op[i]){
            ans = up - ans;
            nxt *= -1;
            nowid += nxt;
            nowid = (nowid + n) % n;
        } else {
            ans -= p[nowid] - 1;
            ans += n - p[nowid];
            nowid += nxt;
            nowid = (nowid + n) % n;
        }
        cout << ans % 10;
    }
    cout << '\n';
}

C. Array Concatenation(思维)

题目大意:给定一个数组 b = { b 1 , b 2 , . . . , b n } b = \{ b_1 , b_2 , . . . , b_n \} b={b1,b2,...,bn} 进行 m m m 次以下操作之一(每次操作完 n n n 变为原来两倍):

  1. b : = { b 1 , b 2 , . . . , b n , b 1 , b 2 , . . . , b n } b := \{ b_1 , b_2 , . . . , b_n, b_1, b_2,...,b_n \} b:={b1,b2,...,bn,b1,b2,...,bn}
  2. b : = { b 1 , b 2 , . . . , b n , b n , b n − 1 , . . . , b 1 } b := \{ b_1 , b_2 , . . . , b_n, b_n, b_{n - 1},...,b_1 \} b:={b1,b2,...,bn,bn,bn1,...,b1}

求操作后的 b b b 数组的前缀和之和取模后的最大值。
即求
m a x { ∑ i = 1 n ′ ∑ j = 1 i b j m o d    1 0 9 + 7 } max\begin{Bmatrix}\sum_{i=1}^{n'} \sum_{j=1}^{i} b_j \mod 10^9 + 7\end{Bmatrix} max{i=1nj=1ibjmod109+7}
题目分析:注意到进行一次操作2后,后续无论进行什么操作, b b b 数组都一样。进一步发现,无论何时进行操作2后,对结果的影响都是一样的。因此,答案只有两种情况,全是操作1的,以及进行过操作2的。$O ( m ) $ 暴力求解即可,实现比较容易就不贴代码了。

E. Draw a triangle(思维,扩欧)

题目大意:给定两个整数点 A , B A,B A,B,要求找出第三个整数点 C C C,使得够出的非退化三角形面积 S Δ A B C S_{\Delta ABC} SΔABC 最小。

题目分析:考虑三角形面积公式: S Δ A B C = ∣ A B → × A C → ∣ = ∣ a x b y − a y b x ∣ 2 S_{\Delta ABC} = |\overrightarrow{AB}\times \overrightarrow{AC}| =\frac{|a_xb_y-a_yb_x|}{2} SΔABC=AB ×AC =2axbyaybx,可以发现分子是一个扩欧式子,其中 a x , a y a_x, a_y ax,ay 已知。所以最小值即为 c c c gcd ⁡ ( a x , − a y ) ∣ c \gcd(a_x,-a_y)|c gcd(ax,ay)c,即 c = gcd ⁡ ( a x , − a y ) c = \gcd(a_x,-a_y) c=gcd(ax,ay)。然后可以解出 A C → \overrightarrow{AC} AC 进而求出 C C C

主要代码

void solve(){
    ll x1, y1, x2, y2;
    cin >> x1 >> y1 >> x2 >> y2;
    if (x1 == x2){
        cout << x1 + 1 << ' ' << y1 << '\n';
        return;
    }
    if (y1 == y2){
        cout << x1 << ' ' << y1 + 1 << '\n';
        return;
    }
    ll x = x2 - x1, y = y2 - y1;  // AB向量
    ll x3, y3;
    exgcd(-y, x, x3, y3);  // (x3, y3) AC向量
    cout << x3 + x1 << ' ' <<y3 + y1 << '\n';  // 求 C;
}

G. Group Homework(半个银牌题,换根DP)

题目大意:给定一棵树,树上每个点都有一个权值,选择两条路径,计算只被这两条路径中的一条路径经过过的点的权值和的最大值。

题目分析

​ 考虑两条链的关系,我们可以证明至多交于一点。若交于两点以上,我们可以修改为更大的答案(可以看看官方题解给的图)。因此要么是两条独立的路径,要么是交于一点。对于前者,我们可以考虑拆开一条树边两边用 换根 DP 求带权直径;对于后者,我们可以枚举交点,用树形 D P DP DP 维护到叶子最长链,前四大加起来即可。时间复杂度 𝑂 ( 𝑛 ) 𝑂(𝑛) O(n);考虑到时限充裕,为了实现方便可以直接 s o r t sort sort,为 𝑂 ( 𝑛 l o g 𝑛 ) 𝑂(𝑛 log𝑛) O(nlogn),详解如下

有一个公共点时

我们可以遍历讨论公共点,以每个点为根进行搜索,搜索以该点为根节点引出的四条最大权值子链(四条子链可以组合成两条路径),权值之和就是以该点为公共点的最大路径,实现为 d f s ( ) dfs() dfs() d p [ x ] [ f a ] dp[x][fa] dp[x][fa]表示以 x x x 为根, f a fa fa 为父亲节点的最长链。

无公共点时

遍历边,假设该边为分界,将树分为两个块,寻找每个块的最大链权值,相加。实现为 d f s 2 ( ) dfs2() dfs2() d p 2 [ x ] [ f a ] dp2[x][fa] dp2[x][fa] 表示以 x x x 为根, f a fa fa 为父亲节点的子树的直径,转移就是 max(两个孩子最长链的和,所有子树直径的)。

AC代码参考

// Author: Chuanhua Yu
// Time: 2024-10-24.
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e5 + 5, M = 4e5 + 5;

int n, tot;
map<int, int> dp[N], dp2[N];
int f[N], a[N];
int head[N], ver[M], nxt[M];

inline void add(int x, int y){
    ver[++tot] = y; nxt[tot] = head[x]; head[x] = tot;
}

int dfs1(int x, int fa){
    if(dp[x][fa]) return dp[x][fa];
    int res = a[x];
    for(int i = head[x]; i; i = nxt[i]){
        int y = ver[i];
        if(y == fa) continue;
        res = max(res, dfs1(y, x) + a[x]);
    }
    return dp[x][fa] = res;
}

int dfs2(int x, int fa){
    if(dp2[x][fa]) return dp2[x][fa];
    vector<int> mx(2, 0);
    int res = a[x];
    for(int i = head[x]; i; i = nxt[i]){
        int y = ver[i];
        if(y == fa) continue;
        res = max(dfs2(y, x), res);
        int link = dp[y][x];
        if(mx[0] < link){
            swap(mx[0], mx[1]);
            mx[0] = link;
        } else if (mx[1] < link) mx[1] = link;
    }
    res = max(res, mx[0] + mx[1] + a[x]);
    return dp2[x][fa] = res;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    tot = 1;
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> a[i];
    for(int i = 1; i < n; i++){
        int x, y; cin >> x >> y;
        add(x, y);
        add(y, x);
    }
    int ans = 0;
    // 交于一点的情况
    for(int x = 1; x <= n; x++){
        vector<int> ls(4, 0);
        for(int i = head[x]; i; i = nxt[i])
            ls.emplace_back(dfs1(ver[i], x));
        auto cmp = [&](int x, int y){
            return x > y;
        }; sort(ls.begin(), ls.end(), cmp);
        ans = max(ans, ls[0] + ls[1] + ls[2] + ls[3]);
    }
    // 不交于一点的情况
    for(int x = 1; x <= n; x++)
        for(int i = head[x]; i; i = nxt[i])
            ans = max(ans, dfs2(x, ver[i]) + dfs2(ver[i], x));

    cout << ans << '\n';

    return 0;
}

J. Permutation Puzzle(银牌题,构造,拓扑排序+DP)

题目大意:给你一个长度为 n n n 的排列的一部分,和 m m m 个形如 p u i < p v i p_{u_i} < p_{v_i} pui<pvi 的限制条件,让你构造出这个排列缺失的那一部分或确定其无解。 ( 2 ≤ n ≤ 2 × 1 0 5 ,   1 ≤ m ≤ 5 × 1 0 5 ) (2\le n\le 2\times10^5,\ 1\le m\le5\times10^5) (2n2×105, 1m5×105)

题目分析看题目描述 ( u i , v i ) (u_i, v_i) (ui,vi) 一般很容易想到图吧。 根据所给限制建图,将 𝑝 𝑢 < 𝑝 𝑣 𝑝_𝑢 < 𝑝_𝑣 pu<pv 的限制视为一条 𝑢 → 𝑣 𝑢 → 𝑣 uv 的单向边,题目保证会得到一个 DAG。假设路径 ( u , v ) (u,v) (u,v) 包含 k k k 条边,那么诺 p u p_u pu 已知 p v p_v pv 未知,则 p v ≥ p u + k p_v\ge p_u + k pvpu+k,同理若 p u p_u pu 未知 p v p_v pv 已知则有 p u ≤ p v − k p_u \le p_v - k pupvk,对于这一拓扑关系我们可以通过拓扑排序+DP来快速求出所有 p i p_i pi 的上下界 [ L i , R i ] [L_i,R_i] [Li,Ri]

具体来说:

  • 先正向拓扑排序求出 𝐿 𝐿 L:对于边 ( 𝑢 , 𝑣 ) (𝑢, 𝑣) (u,v),做转移 𝐿 𝑣 = max ⁡ ( 𝐿 𝑣 , 𝐿 𝑢 + 1 ) 𝐿_𝑣 = \max(𝐿_𝑣, 𝐿_𝑢 + 1) Lv=max(Lv,Lu+1)
  • 然后反向拓扑排序求出 𝑅 𝑅 R:对于边 ( 𝑢 , 𝑣 ) (𝑢, 𝑣) (u,v),做转移 𝑅 𝑢 = min ⁡ ( 𝑅 𝑢 , 𝑅 𝑣 − 1 ) 𝑅_𝑢 = \min(𝑅_𝑢, 𝑅_𝑣 − 1) Ru=min(Ru,Rv1)

此时问题已然转化为:给定 k k k 个区间和 k k k 个互不相同的数,我们需要给每个数匹配一个包含它的区间,此外每个区间匹配的数还要满足一些拓扑关系。

如果暂不考虑拓扑关系的话,就是一个经典问题,存在一个贪心的做法:从小到大枚举所有数,当枚举到 𝑥 𝑥 x 时,从所有左端点 ≤ 𝑥 ≤ 𝑥 x 且还没被匹配的区间中,选择右端点最小的那个匹配给 𝑥 𝑥 x,这个过程用优先队列优化。然后分析一下区间的性质:如果存在边 ( 𝑢 , 𝑣 ) (𝑢, 𝑣) (u,v),根据转移方程可得 𝐿 𝑢 + 1 ≤ 𝐿 𝑣 , 𝑅 𝑢 + 1 ≤ 𝑅 𝑣 𝐿_𝑢 + 1 ≤ 𝐿_𝑣,𝑅_𝑢 + 1 ≤ 𝑅_𝑣 Lu+1LvRu+1Rv,按照上述贪心做法, [ 𝐿 𝑢 , 𝑅 𝑢 ] [𝐿_𝑢, 𝑅_𝑢] [Lu,Ru] 一定比 [ 𝐿 𝑣 , 𝑅 𝑣 ] [𝐿_𝑣, 𝑅_𝑣] [Lv,Rv] 更早被匹配到,即一定满足 𝑝 𝑢 < 𝑝 𝑣 𝑝_𝑢 < 𝑝_𝑣 pu<pv。所以直接贪心求出来的就是原问题的合法解,如果贪心无解则原问题一定无解。

总时间复杂度 𝑂 ( 𝑚 + 𝑛 l o g 𝑛 ) 𝑂(𝑚 + 𝑛 log𝑛) O(m+nlogn)

AC代码参考

// Author: Chuanhua Yu
// Time: 2024-10-25.
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
using vi = vector<int>;
const int N = 2e5 + 5, M = 1e6 + 5;

int n, m, tot;
int head[N], ver[M], nxt[M];
int p[N], L[N], R[N], degin[N], degout[N];
bool vis[N];

inline void add(int x, int y){
    ver[++tot] = y; nxt[tot] = head[x]; head[x] = tot;
}

void solve(){
    tot = 1; cin >> n >> m;
    for(int i = 1; i <= n; i++) head[i] = degin[i] = degout[i] = 0;
    memset(vis, false, (n + 1) * sizeof(bool));
    for(int i = 1; i <= n; i++){
        cin >> p[i];
        vis[p[i]] = true;
        if(p[i]) L[i] = R[i] = p[i];
        else L[i] = 1, R[i] = n;
    }
    for(int i = 0; i < m; i++){
        int x, y; cin >> x >> y;
        add(x, y); add(y, x);
        degin[y] ++; degout[x]++;
    }
    queue<int> q;
    for(int i = 1; i <= n; i++)
        if(!degin[i]) q.push(i);
    while(!q.empty()){
        int x = q.front(); q.pop();
        for(int i = head[x]; i; i = nxt[i]){
            if(i & 1) continue;
            int y = ver[i];
            L[y] = max(L[y], L[x] + 1);
            if(--degin[y] == 0) q.push(y);
        }
    }
    for(int i = 1; i <= n; i++)
        if(!degout[i]) q.push(i);
    while(!q.empty()){
        int x = q.front(); q.pop();
        for(int i = head[x]; i; i = nxt[i]){
            if((i & 1) == 0) continue;
            int y = ver[i];
            R[y] = min(R[y], R[x] - 1);
            if(--degout[y] == 0) q.push(y);
        }
    }

    vi id(n);
    iota(id.begin(),id.end(), 1);
    auto cmp = [&](int x, int y){return L[x] < L[y];};
    sort(id.begin(), id.end(), cmp);

    priority_queue<pii> pq;
    int i = 0;
    for(int now = 1; now <= n; now++){
        if(vis[now]) continue;
        vis[now] = true;
        while(i < n && L[id[i]] <= now){
            if(p[id[i]]){
                i++;
                continue;
            }
            pq.emplace(-R[id[i]], id[i]);
            i++;
        }
        if(pq.empty() || -(pq.top().first) < now){
            cout << "-1\n";
            return;
        }
        int idx = pq.top().second; pq.pop();
        p[idx] = now;
    }
    for(int j = 1; j <= n; j++)
        cout << p[j] << " \n"[j == n];
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int T;
    cin >> T;
    while(T--)
        solve();
    return 0;
}
posted @ 2024-10-25 14:17  CH-Yu  阅读(29)  评论(0)    收藏  举报  来源