2022CCPC桂林补题题解(AMCEGJ六题)
2022CCPC桂林补题题解(AMCEGJ六题)
文章目录
总览与前言
VP URL:Dashboard - 2022 China Collegiate Programming Contest (CCPC) Guilin Site - Codeforces
A. Lily(签到)
题目分析:所有不在 L 旁的字符替换为 C 即可,实现很容易就不贴代码了。
M. Youth Finale(签到,逆序对)
题目大意:给定初始数组,有两种操作:1. 可以将最前面的元素移至最后;2. 将整个数组翻转。给出冒泡排序的代码,要求每次操作后,输出当前数组进行冒泡排序的swap次数。
题目分析:
- 首先我们需要数一下逆序对,使用归并排序或者树状数组即可。
- 随后,我们可以发现:当进行 Shift 操作时,对逆序对的影响只和这个数是谁有关系:
- 将 𝑖 𝑖 i 从头移动到尾时,有 𝑖 − 1 𝑖 − 1 i−1 个 ( 𝑖 , 𝑗 ) (𝑖, 𝑗) (i,j), 𝑗 < 𝑖 𝑗 < 𝑖 j<i 关系减少了,而有 𝑛 − 𝑖 𝑛 − 𝑖 n−i 个 ( 𝑖 , 𝑗 ) (𝑖, 𝑗) (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 变为原来两倍):
- 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}
- 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,bn−1,...,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=1n′∑j=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∣=2∣axby−aybx∣,可以发现分子是一个扩欧式子,其中 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) (2≤n≤2×105, 1≤m≤5×105)
题目分析:看题目描述
(
u
i
,
v
i
)
(u_i, v_i)
(ui,vi) 一般很容易想到图吧。 根据所给限制建图,将
𝑝
𝑢
<
𝑝
𝑣
𝑝_𝑢 < 𝑝_𝑣
pu<pv 的限制视为一条
𝑢
→
𝑣
𝑢 → 𝑣
u→v 的单向边,题目保证会得到一个 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
pv≥pu+k,同理若
p
u
p_u
pu 未知
p
v
p_v
pv 已知则有
p
u
≤
p
v
−
k
p_u \le p_v - k
pu≤pv−k,对于这一拓扑关系我们可以通过拓扑排序+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,Rv−1)。
此时问题已然转化为:给定 k k k 个区间和 k k k 个互不相同的数,我们需要给每个数匹配一个包含它的区间,此外每个区间匹配的数还要满足一些拓扑关系。
如果暂不考虑拓扑关系的话,就是一个经典问题,存在一个贪心的做法:从小到大枚举所有数,当枚举到 𝑥 𝑥 x 时,从所有左端点 ≤ 𝑥 ≤ 𝑥 ≤x 且还没被匹配的区间中,选择右端点最小的那个匹配给 𝑥 𝑥 x,这个过程用优先队列优化。然后分析一下区间的性质:如果存在边 ( 𝑢 , 𝑣 ) (𝑢, 𝑣) (u,v),根据转移方程可得 𝐿 𝑢 + 1 ≤ 𝐿 𝑣 , 𝑅 𝑢 + 1 ≤ 𝑅 𝑣 𝐿_𝑢 + 1 ≤ 𝐿_𝑣,𝑅_𝑢 + 1 ≤ 𝑅_𝑣 Lu+1≤Lv,Ru+1≤Rv,按照上述贪心做法, [ 𝐿 𝑢 , 𝑅 𝑢 ] [𝐿_𝑢, 𝑅_𝑢] [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;
}
本文来自博客园,作者:CH-Yu,转载请注明原文链接:https://www.cnblogs.com/chuanhua-blogs/p/18852122