The 2021 CCPC Guilin Onsite (XXII Open Cup, Grand Prix of EDG)

发布时间 2023-10-21 15:11:31作者: 空気力学の詩

Preface

昨天下午16:30~21:30刚打完CCPC2021的广州,今天早上九点又开始打这场桂林,压力拉满了属于是

这场比起昨天那场良心太多了,开场还挺顺(虽然因为写Dijkstra偷懒TLE了四发),但开题啥的都是见一个会一个

中期虽然有点卡但因为祁神会了几何所以没有空机,然后再点完外卖后我突然顿悟把BK过了

然后后面就是对着祁神的F大眼看了两小时,最后上去三倍长了数组就直接过了,看来以后写旋转卡壳都三倍长好了

徐神经典暴肝字符串J,但最后过于繁琐还是没写出来,感觉我真得去加强下字符串了不然后期一点用没有就在摆烂


A. A Hero Named Magnus

签到,输出\(2x-1\)

#include<cstdio>
using namespace std;
long long t,x;
int main()
{
	//freopen("A.in","r",stdin); freopen("A.out","w",stdout);
	for (scanf("%lld",&t);t;--t) scanf("%lld",&x),printf("%lld\n",2LL*x-1);
	return 0;
}

B. A Plus B Problem

刚开始执迷于想真正把和维护起来然后一直做不来,后面仔细一想其实完全没必要真的把值算出来

考虑一个位置\(i\)的和\(c_i\)可以写成\(c_i=a_i+b_i+[a_j+b_j\ge 10]\),其中\(j>i\)且为第一个\(a_j+b_j\ne 9\)的位置

因此我们可以想到把所有\(a_i+b_i\ne 9\)的数都用一个set维护起来,每次询问要求某个位置的值时就直接查后继即可

然后考虑修改后有多少个数会有影响,首先如果修改后的数和原来相同就直接输出\(0\),否则答案至少为\(2\)

考虑什么时候会连带着之前的数产生变化,只有原来要进位,现在不进位/原来不进位,现在要进位两种情况

然后手玩一下就是找到当前位置的前驱,中间所有\(a_i+b_i=9\)的位置都会一起变化,但要注意前驱不存在的情况

总复杂度\(O(q\log n)\)

#include<cstdio>
#include<iostream>
#include<set>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5;
int n,q,a[N],b[N],r,c,d; char sa[N],sb[N]; set <int> s;
inline int getval(CI pos)
{
	auto it=s.upper_bound(pos);
	if (it==s.end()) return a[pos]+b[pos];
	return a[pos]+b[pos]+(a[*it]+b[*it]>=10);
}
inline int getchg(CI pos)
{
	auto it=s.lower_bound(pos);
	if (it==s.begin()) return 1+pos;
	return 2+pos-*(--it);
}
int main()
{
	//freopen("B.in","r",stdin); freopen("B.out","w",stdout);
	RI i; for (scanf("%d%d%s%s",&n,&q,sa+1,sb+1),i=1;i<=n;++i)
	{
		a[i]=sa[i]-'0'; b[i]=sb[i]-'0';
		if (a[i]+b[i]!=9) s.insert(i);
	}
	for (i=1;i<=q;++i)
	{
		scanf("%d%d%d",&r,&c,&d); int pre=getval(c);
		if (r==1)
		{
			if (a[c]==d) { printf("%d 0\n",pre%10); continue; }
			if (a[c]+b[c]!=9) s.erase(c);
			if (a[c]=d,a[c]+b[c]!=9) s.insert(c);
		} else
		{
			if (b[c]==d) { printf("%d 0\n",pre%10); continue; }
			if (a[c]+b[c]!=9) s.erase(c);
			if (b[c]=d,a[c]+b[c]!=9) s.insert(c);
		}
		int cur=getval(c); if (pre>=10)
		{
			if (cur>=10) { printf("%d 2\n",cur%10); continue; }
			printf("%d %d\n",cur%10,getchg(c));
		} else
		{
			if (cur<10) { printf("%d 2\n",cur%10); continue; }
			printf("%d %d\n",cur%10,getchg(c));
		}
	}
	return 0;
}

C. AC Automaton

放AK题,弃!


D. Assumption is All You Need

ORZ祁神秒了构造

考虑从大到小依次把数放到对应的位置,不难发现每个数只能向后移动

采用如下的贪心策略,假设当前数位于\(x\),目标位置为\(y(y>x)\),则考虑找到\([x+1,y]\)中最大的数\(z\),将\(x\)\(z\)上的数交换

这样的正确性很容易感性理解,因为我们在把当前这个数归位的同时,尽量地将后面大的数向前移动以保证它们在后面的操作可以归位,同时交换次数上界也是符合要求的

总复杂度\(O(n^2)\)

#include<bits/stdc++.h>
using namespace std;
const int N = 2500;

vector<pair<int, int> > ans;
int t, n, A[N], B[N], st[N], ed[N];
int mx[N];

void myswap(int x, int y){
	ans.emplace_back(x, y);
	st[A[x]]=y, st[A[y]]=x;	
	swap(A[x], A[y]);
}
int main(){
	scanf("%d", &t);
	while (t--){
		ans.clear();
		scanf("%d", &n);
		for (int i=1; i<=n; ++i) scanf("%d", &A[i]), st[A[i]]=i;	
		for (int i=1; i<=n; ++i) scanf("%d", &B[i]), ed[B[i]]=i;
		
		bool ok=true;
		for (int i=n; i>0; --i){
			if  (st[i]>ed[i]){ok=false; break;}
			mx[ed[i]] = A[ed[i]];
			for (int j=ed[i]-1; j>=st[i]; --j) mx[j]=max(mx[j+1], A[j]);
			
			int cur=st[i];
			while (cur<ed[i]){
//				printf("cur=%d\n, ed[i]=%d", cur, ed[i]);
				int nxt = st[mx[cur+1]];
				myswap(cur, nxt);
				cur = nxt;	
			}
			A[ed[i]]=-1;
		}
		
		if (!ok) printf("-1\n");
		else{
			printf("%d\n", ans.size());
			for (auto [x, y] : ans) printf("%d %d\n", x, y);	
		}
	}
	return 0;	
}

E. Buy and Delete

首先要发现其实Bob的操作次数就是\(\{0,1,2\}\),同时为\(0\)的情况可以特判

考虑要让Bob删两轮,就是要使图中有至少一个环,那我们不妨就求出图中权值之和最小的环即可

可以枚举某条边\(u\to v\),钦定它一定在环上,那么我们直接在剩下的图上跑出\(v\to u\)的最短路,再加上这条边的边权即可

但如果直接这么做的话复杂度是\(O(m\times (n+m)\log n)\)的,有些卡常

仔细一想会发现其实可能的起点只有\(n\)个,因此可以优化成\(O(n\times (n+m)\log n)\),即可通过此题

#include<cstdio>
#include<iostream>
#include<utility>
#include<queue>
#include<random>
#include<algorithm>
#define int long long
#define RI register int
#define CI const int&
#define fi first
#define se second
using namespace std;
typedef pair <int,int> pi;
const int N=2005,M=5005,INF=1e18;
int n,m,c,id[M],tx[M],ty[M],tz[M],x[M],y[M],z[M],dis[N][N],vis[N],ext[N]; vector <pi> v[N];
signed main()
{
	//freopen("E.in","r",stdin); freopen("E.out","w",stdout);
	RI i,j; int mi=1e9; for (scanf("%lld%lld%lld",&n,&m,&c),i=1;i<=m;++i)
	scanf("%lld%lld%lld",&tx[i],&ty[i],&tz[i]),id[i]=i,mi=min(mi,tz[i]);
	if (c<mi) return puts("0"),0;
	mt19937_64 rng((std::random_device())()); shuffle(id+1,id+m+1,rng);
	for (i=1;i<=m;++i) x[id[i]]=tx[i],y[id[i]]=ty[i],z[id[i]]=tz[i];
	for (i=1;i<=m;++i)
	{
		if (!ext[y[i]])
		{
			for (j=1;j<=n;++j) v[j].clear(),dis[y[i]][j]=INF,vis[j]=0;
			for (j=1;j<=m;++j) if (i!=j) v[x[j]].push_back(pi(y[j],z[j]));
			priority_queue <pi,vector <pi>,greater <pi>> hp;
			hp.push(pi(dis[y[i]][y[i]]=0,y[i])); ext[y[i]]=1;
			while (!hp.empty())
			{
				int now=hp.top().se; hp.pop();
				if (vis[now]) continue; vis[now]=1;
				for (auto [to,w]:v[now]) if (dis[y[i]][to]>dis[y[i]][now]+w)
				hp.push(pi(dis[y[i]][to]=dis[y[i]][now]+w,to));
			}
		}
		if (dis[y[i]][x[i]]+z[i]<=c) return puts("2"),0;
	}
	return puts("1"),0;
}

F. Illuminations II

小清新计算几何题,就连像我这种常年不写几何的人看完题目就知道怎么做了

首先由于凸包的性质不难发现对于内凸包的每条边,它们要么同时被照亮要么都不被照亮

那么我们不妨把这条边所在的直线与外凸包的交点求出来,则这两点之间对应的外凸包边长除以外凸包周长的值就是它被照亮的概率,最后根据期望的线性性累加起来即可

实现的时候可以维护每条边长的前缀和,然后用一个旋转卡壳维护交在外凸包的哪个点即可

复杂度\(O(n+m)\)

#include<bits/stdc++.h>
#define LD long double
#define int long long
using namespace std;

struct Pt{
	int x, y;
	Pt operator-(const Pt b)const{return Pt{x-b.x, y-b.y};}
	int crs(Pt b)const{return x*b.y-y*b.x;}	
	LD len()const{return sqrtl(x*x+y*y);}	
};
int cross(Pt p, Pt a, Pt b){return (a-p).crs(b-p);}
using VecP = vector<Pt>;

LD ins(Pt p1, Pt v1, Pt p2, Pt v2){
	return v1.len() * (1.0L*v2.crs(p1-p2) / v1.crs(v2));	
}

const int N = 2e5+5;
int n, m;
VecP tmpa, tmpb, cha, chb;
LD len[N*4];

signed main(){
	scanf("%lld%lld", &n, &m);
	tmpa.resize(n); tmpb.resize(m);
	for (int i=0; i<n; ++i) scanf("%lld%lld", &tmpa[i].x, &tmpa[i].y);
	for (int i=0; i<m; ++i) scanf("%lld%lld", &tmpb[i].x, &tmpb[i].y);
	
	for (int t=0; t<3; ++t){
		cha.insert(cha.end(), tmpa.begin(), tmpa.end());	
		chb.insert(chb.end(), tmpb.begin(), tmpb.end());	
	}
	for (int i=1; i<n; ++i) len[i]=len[i+n]=len[i+2*n]=(cha[i]-cha[i-1]).len();
	len[n]=len[n*2]=len[n*3]=(cha[0]-cha[n-1]).len();
	for (int i=1; i<=n*3; ++i) len[i]+=len[i-1];
	
	LD ans=0.0L;
	int p=0, q=0;
	for (int i=0; i<m; ++i){
		while (1){
			if (cross(chb[i], chb[(i+1)], cha[p])>=0 && cross(chb[i], chb[(i+1)], cha[(p+1)])<0) break;
			++p;
		}
		while (1){
			if (cross(chb[i], chb[(i+1)], cha[q])<=0 && cross(chb[i], chb[(i+1)], cha[(q+1)])>0) break;
			++q;
		}
		while (q<p) q+=n;
		
		LD res=len[q]-len[p];
		LD ta=ins(cha[p], (cha[(p+1)]-cha[p]), chb[i], (chb[(i+1)]-chb[i]));
		LD tb=ins(cha[q], (cha[(q+1)]-cha[q]), chb[i], (chb[(i+1)]-chb[i]));
		res-=ta; res+=tb;
//		printf("i=%d p=%d q=%d res=%.3Lf ta=%Lf tb=%Lf\n", i, p, q, res, ta, tb);
		ans += (chb[(i+1)]-chb[i]).len() * (res / len[n]);
	}
	printf("%.11Lf\n", ans);
	return 0;	
}

G. Occupy the Cities

挺Easy的一个题,开场和徐神讲了题意就被徐神秒了,本来我还打算去写贪心的说

直接DP,设\(f_{i,0/1}\)表示前\(i\)个位置,上个\(1\)的位置第一步选择向左/向右的最小轮次,转移稍微讨论下即可

#include <bits/stdc++.h>

int t, n;

char s[1000005];
int dp[1000005][2];

inline int calc(int fr, int to, int first) {
	if(to == fr + 1) return 0;
	int len = to - fr - 1;
	if(first >= len) return 1;
	len -= first;
	return ((len + 1) >> 1) + 1;
}

int main() {
	// freopen("1.in", "r", stdin);
	scanf("%d", &t); while(t--) {
		scanf("%d", &n);
		scanf("%s", s + 1);
		int pre = 0;
		for(int i = 1; i <= n; ++i) if(s[i] == '1') {
			if(pre == 0) {
				if(i == 1) dp[i][0] = dp[i][1] = 0;
				else
					dp[i][0] = i - 1;
					dp[i][1] = i;
			} else {
				dp[i][0] = std::min(
					std::max(dp[pre][0], calc(pre, i, 1)),
					std::max(dp[pre][1], calc(pre, i, 2))
				);
				dp[i][1] = std::min(
					std::max(dp[pre][0], calc(pre, i, 0)),
					std::max(dp[pre][1], calc(pre, i, 1))
				);
			}
			pre = i;
		}
		if(pre < n)
			printf("%d\n", std::min(
				std::max(dp[pre][0], n - pre + 1),
				std::max(dp[pre][1], n - pre)
			));
		else printf("%d\n", std::min(dp[pre][0], dp[pre][1]));
	}
	return 0;
}

H. Popcount Words

题目都没看,做不来捏


I. PTSD

签到题,我题目都没看

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N = 1e6+5;
int t, n;
char str[N];
signed main(){
	scanf("%lld", &t);
	while (t--){
		scanf("%lld%s", &n, str+1);
		int cnt=0, ans=0;
		for (int i=n; i>0; --i){
			if (cnt>0 && '1'==str[i]) --cnt, ans+=i;
			else ++cnt;
		}
		printf("%lld\n", ans);
	}
	return 0;	
}

J. Suffix Automaton

徐神写的SAM做法好像很复杂,感觉要用后缀树或者后缀数组试试


K. Tax

本来以为是个网络流之类的题,后面仔细一想发现就是个爆搜题

考虑把以\(1\)为起点到每个点的最短路求出来,则一条边\(u\to v\)可以走当且仅当\(dis_u+1=dis_v\)

不妨考虑直接DFS求解,\(n\)个点能构成的最大路径条数其实只有\(O(3^{\frac{n}{3}})\)级别的,直接一交就过了

#include<cstdio>
#include<iostream>
#include<queue>
#include<utility>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
typedef pair <int,int> pi;
const int N=55,M=N*N,INF=1e18;
int n,m,W[M],x,y,z,dis[N],c[M],ans[N]; vector <pi> v[N];
inline void BFS(CI st)
{
	for (RI i=1;i<=n;++i) dis[i]=INF;
	queue <int> q; q.push(st); dis[st]=0;
	while (!q.empty())
	{
		int now=q.front(); q.pop();
		for (auto [to,w]:v[now]) if (dis[to]>dis[now]+1)
		dis[to]=dis[now]+1,q.push(to);
	}
}
inline void DFS(CI now,CI ret=0)
{
	ans[now]=min(ans[now],ret);
	for (auto [to,w]:v[now]) if (dis[to]==dis[now]+1)
	++c[w],DFS(to,ret+c[w]*W[w]),--c[w];
}
signed main()
{
	//freopen("K.in","r",stdin); freopen("K.out","w",stdout);
	RI i,j; for (scanf("%lld%lld",&n,&m),i=1;i<=m;++i) scanf("%lld",&W[i]);
	for (i=1;i<=m;++i) scanf("%lld%lld%lld",&x,&y,&z),v[x].push_back(pi(y,z)),v[y].push_back(pi(x,z));
	for (i=1;i<=n;++i) ans[i]=INF;
	for (BFS(1),DFS(1),i=2;i<=n;++i) printf("%lld\n",ans[i]);
	return 0;
}

L. Wiring Engineering

不可做题,弃!


Postscript

这周训练量挺大的说,要被干晕了……