第九届中国大学生程序设计竞赛总决赛(CCPC Final 2023)
Preface
入土了快三个月突然诈尸了
之前停更的主要原因还是因为去年的 ICPC 昆明再打一银后,再加上发生了许多事情,导致整个人完全没有继续打算竞的动力了遂直接一摆到底了
但不管怎么说下个月的 CCPC Final 还是要参加的,估计大概率是个人算竞生涯的最后一舞了,因此近期会低频率地 VP 一下来保持下那可怜的手感
这场去年的决赛由于是我们学校承办的,当时作为志愿者参加了,因此或多或少对题目都有了些了解,导致相比正式参赛选手有了不小的 BUFF
最后 6 题低罚时还有金尾,只能说全是队友秒了 AC 的功劳,和我关系不大
A. Add One 2
题都没看是啥,我在 J 题坐牢的时候队友就会了,好像是个差分+贪心来着,后面发现这题现场才过了 10 个队左右,大为震惊
#include <bits/stdc++.h>
using llsi = long long signed int;
constexpr int MN = 1'000'000'000;
void work() {
int n; std::cin >> n;
std::vector<int> c(n + 2);
c[0] = c[n + 1] = MN;
llsi c_sum = MN;
for(int i = 1; i <= n; ++i) {
std::cin >> c[i];
c_sum += c[i];
}
n += 1;
for(int i = n; i >= 1; --i)
c[i] -= c[i - 1];
llsi neg = 0, cost = 0;
for(int i = 1; i <= n; ++i) if(c[i] < 0) neg -= c[i];
std::vector<int> pre(n + 2), nxt(n + 2), erased(n + 2, 0);
for(int i = 1; i <= n + 1; ++i) pre[i] = i - 1;
for(int i = 0; i <= n; ++i) nxt[i] = i + 1;
auto erase = [&](int a) {
nxt[pre[a]] = nxt[a];
pre[nxt[a]] = pre[a];
erased[a] = 1;
};
using node = std::pair<int, int>;
std::priority_queue<node, std::vector<node>, std::greater<node>> pq;
auto update = [&](int i) {
if(erased[i] || i == 0 || i == n + 1) return ;
if(c[i] >= 0) return ;
if(nxt[i] == n + 1) return ;
if(c[nxt[i]] <= 0) return ;
pq.push({ nxt[i] - i, i });
};
for(int i = 1; i <= n; ++i) if(c[i] == 0) erase(i);
for(int i = 1; i <= n; ++i) update(i);
while(neg > MN) {
assert(pq.size());
auto [l, p] = pq.top(); pq.pop();
if(erased[p]) continue;
if(nxt[p] - p != l) continue;
int nx = nxt[p];
int upt = std::min({llsi(-c[p]), llsi(c[nx]), neg - MN});
cost += llsi(l) * upt;
neg -= upt, c[p] += upt, c[nx] -= upt;
int d = pre[p], e = nxt[nx];
if(c[p] == 0) erase(p); if(c[nx] == 0) erase(nx);
update(d), update(p), update(nx), update(e);
}
std::cout << c_sum + cost - MN << char(10);
return ;
}
int main() {
std::ios::sync_with_stdio(false);
int T; std::cin >> T; while(T--) work();
return 0;
}
C. Colorful Graph 2
这就是场外剧透的力量,那个 BFS 染色的方法只能说空想确实难想到,但见过了就直接秒了
#include <bits/stdc++.h>
int main() {
int T; std::cin >> T; while(T--) {
int n, m; std::cin >> n >> m;
std::vector<std::vector<int>> out(n);
std::vector<int> dis(n, 0x7f7f7f7f);
for(int i = 1; i < n; ++i) out[i].push_back(i - 1), out[i - 1].push_back(i);
out[0].push_back(n - 1); out[n - 1].push_back(0);
while(m--) {
int f, t;
std::cin >> f >> t;
out[f].push_back(t); out[t].push_back(f);
}
dis[0] = 0;
std::queue<int> q;
q.push(0);
while(q.size()) {
int hd = q.front(); q.pop();
for(auto out: out[hd]) if(dis[out] > dis[hd] + 1) {
dis[out] = dis[hd] + 1;
q.push(out);
}
}
for(int i = 0; i < n; ++i) std::cout << (dis[i] % 2 == 0 ? 'R' : 'B');
std::cout << char(10);
}
return 0;
}
D. Min or Max
签到,由于题意等价于可以删去任意位置上的数,因此只要判 \(\{b\}\) 是不是 \(\{a\}\) 的子序列即可
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
int t,n,m,a[N],b[N];
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%d%d",&n,&m);
for (RI i=1;i<=n;++i) scanf("%d",&a[i]);
for (RI i=1;i<=m;++i) scanf("%d",&b[i]);
bool flag=1; for (RI i=1,j=1;i<=m;++i)
{
while (j<=n&&a[j]!=b[i]) ++j;
if (j>n) { flag=0; break; } else ++j;
}
puts(flag?"Yes":"No");
}
return 0;
}
F. Whose Land?
这题赛事基本想出做法了,就是先离线用扫描线处理右端点,然后加入一个点的影响可以用 BFS 序来做到最多影响 \(O(k)\) 级别的区间,然后就是用数据结构乱搞搞就行了
代码懒得写了,估计太久没写码量题也写不动了
G. China Convex Polygon Contest
读懂题意后会发现其实就是给出了若干个区间 \([l_i,r_i)\),在这个区间内 \(x\) 时间交题的话贡献为 \(r_i-x\)
不难发现把 \(\{b\}\) 从小到大排序的做题顺序一定最优,求出前缀和后就得到了若干个题目完成的节点 \(s_j\)
对于每个时间节点可以选择两种操作:做完立刻提交;或者留到后面某个区间的左端点时刻再提交
不难发现这是个很经典的带悔贪心的模型,考虑从后往前考虑每个区间 \([l_i,r_i)\),并用一个大根堆来维护反悔选项
对于该区间内的所有节点 \(s_j\),除了最靠前的那个显然都是选择往后移动最优,直接取出堆顶即可
对于最靠前的那个节点,比较往后移动和保留二者的贡献,若选择保留则把 \(s_j-l_i\) 加在堆顶的值上,以表示前面的移动选项可以通过放在 \(l_i\) 并移动 \(s_j\) 来得到更优的策略
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<vector>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
int t,n,m,a[N];
signed main()
{
for (scanf("%lld",&t);t;--t)
{
scanf("%lld%lld",&n,&m); vector <int> b(n);
for (RI i=1;i<=n;++i) scanf("%d",&a[i]);
for (RI i=0;i<n;++i) scanf("%d",&b[i]);
a[0]=0; a[n+1]=m; sort(b.begin(),b.end());
for (RI i=1;i<n;++i) b[i]+=b[i-1];
// for (auto x:b) printf("%lld ",x); putchar('\n');
priority_queue <int> hp; int ans=0;
while (!b.empty()&&b.back()>m) b.pop_back();
for (RI i=n;i>=0;--i)
{
int cnt=0,mn=m;
while (!b.empty()&&b.back()>=a[i])
{
++cnt; mn=b.back(); b.pop_back();
}
if (cnt==0)
{
hp.push(a[i+1]-a[i]); continue;
}
int token=min((int)hp.size(),cnt-1);
for (RI j=1;j<=token;++j) ans+=hp.top(),hp.pop();
int x=a[i+1]-mn,y=(hp.empty()?0:hp.top());
// printf("i = %lld, x = %lld, y = %lld\n",i,x,y);
if (x>=y)
{
ans+=x;
if (hp.empty()) hp.push(mn-a[i]);
else hp.pop(),hp.push(y+mn-a[i]);
} else
{
ans+=y; hp.pop();
hp.push(a[i+1]-a[i]);
}
// printf("ans = %lld\n",ans);
}
printf("%lld\n",ans);
}
return 0;
}
H. The Game
稍作思考会发现本质就是看最后时刻剩下两个元素时会不会相同即可
以此推出先手的策略就是拿出现次数最多的元素;后手的策略就是拿出现次数最少的元素
推一下式子会发现最后就是判断出现的数的种类数和 \(n\) 的大小关系
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=2000005;
int t,n,x,vis[N];
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%d",&n);
for (RI i=1;i<=2*n;++i) vis[i]=0;
for (RI i=1;i<=2*n;++i) scanf("%d",&x),vis[x]=1;
int cnt=0; for (RI i=1;i<=2*n;++i) cnt+=vis[i];
puts(cnt<=n?"Qingyu":"Kevin");
}
return 0;
}
J. DFS Order 5
写完第一版 WA 了后手玩了 1h 没找到反例,最后还是靠队友 fix 了个 Corner Case 才过
考虑对于序列中相邻的两个元素 \(x,y\),要检验能否在 \(x\) 后面接上 \(y\),分类讨论下:
- 如果 \(y\) 是之前任意出现过的元素的祖先,则一定无解;
- 如果 \(y\) 是 \(x\) 的儿子,则直接接上即可;
- 如果 \(\operatorname{LCA}(x,y)\) 不是 \(y\) 的父亲,则一定无解,因为此时相当于会跳过没出现的元素;
经过检验后,剩下的情况只有\(\operatorname{LCA}(x,y)\) 是 \(y\) 的父亲的情形,此时我们需要从 \(x\) 开始向上跳逐个检验:
- 若该点之前没在 DFS 序中出现过,则通过检验;
- 否则该点的子树内的所有点都必须在 DFS 序中出现过;
知道到达 \(\operatorname{LCA}(x,y)\) 为止,用树状数组很容易动态维护子树内点数
#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
int n,q,x,y,vis[N],idx,fa[N],L[N],R[N]; vector <int> v[N];
inline void DFS(CI now=1,CI anc=0)
{
L[now]=++idx; fa[now]=anc;
for (auto to:v[now]) if (to!=anc) DFS(to,now);
R[now]=idx;
}
class Tree_Array
{
private:
int bit[N];
public:
#define lowbit(x) (x&-x)
inline void add(RI x,CI y)
{
for (;x<=n;x+=lowbit(x)) bit[x]+=y;
}
inline int get(RI x,int ret=0)
{
for (;x;x-=lowbit(x)) ret+=bit[x]; return ret;
}
#undef lowbit
}BIT;
int main()
{
scanf("%d%d",&n,&q);
for (RI i=1;i<n;++i)
scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
for (DFS();q;--q)
{
int m; scanf("%d",&m); bool flag=1;
vector <int> vec(m),rcv;
for (RI i=0;i<m;++i) scanf("%d",&vec[i]),++vis[vec[i]];
for (RI i=0;i<m;++i) if (vis[vec[i]]>1) { flag=0; break; }
if (!flag)
{
puts("No");
for (RI i=0;i<m;++i) vis[vec[i]]=0;
continue;
}
if (vis[1]&&vec[0]!=1)
{
puts("No");
for (RI i=0;i<m;++i) vis[vec[i]]=0;
continue;
}
for (RI i=0;i<m;++i) vis[vec[i]]=0;
rcv.push_back(vec[0]); vis[vec[0]]=1; BIT.add(L[vec[0]],1);
for (RI i=0;i+1<m&&flag;++i)
{
int x=vec[i],y=vec[i+1];
if (BIT.get(R[y])-BIT.get(L[y]-1)!=0) { flag=0; break; }
if (fa[y]==x) { rcv.push_back(y); vis[y]=1; BIT.add(L[y],1); continue; }
if (L[y]<=L[x]&&L[x]<=R[y]) { flag=0; break; }
if (L[fa[y]]<=L[x]&&L[x]<=R[fa[y]])
{
// printf("check x=%d\n",x);
for (;x!=fa[y];x=fa[x])
{
if (!vis[x]) break;
if (BIT.get(R[x])-BIT.get(L[x]-1)!=R[x]-L[x]+1) { flag=0; break; }
}
if (flag) rcv.push_back(y),vis[y]=1,BIT.add(L[y],1);
} else { flag=0; break; }
}
for (auto x:rcv) vis[x]=0,BIT.add(L[x],-1);
puts(flag?"Yes":"No");
}
return 0;
}
Postscript
久违地写了会代码发现手速啥的好像还没有退化太多,或许是个好迹象

浙公网安备 33010602011771号