矗立在眼前的巨大高墙,换个角度想就会成为一扇大门。|

2021hych

园龄:2年7个月粉丝:2关注:2

VP比赛补题报告之“Codeforces Round 766 (Div. 2)”

比赛链接

Codeforces Round 766 (Div. 2)

VP成绩

image

比赛经过

A 题一眼看过去,手捏几组小数据规律就出来了,证明极水,7 min 就无伤 AC 掉了。

B 题以为是个纯数学题,推式子浪费我 20 ~ 30 min。后面仔细想了想最优位置的分布情况,发现枚举即可,跳出圈子写代码,第 44 min 切掉了。(显然是慢了!)

C 题一眼小学奥数,巧妙借助唯一的偶素数 2 即可,思路 5 min 就出来了,但代码实现上花了很多时间,理论上是写复杂了,导致用了 27 分钟才把代码实现完。运气不错的是一次就无伤 AC 掉了,第 76 min 切掉了。

D 题看了看数据范围。先尝试了线性 DP,发现转移的复杂度降不下来(甚至不如暴力),弃掉这个做法了,此时因为没及时跳出来,花了 5 ~ 10 min。后面才发现了值域的范围,然后就类似于以前做过的一道二进制的题目,那道大概是求数列里任意两个数进行某种位运算的最大值(改成几种不同的方案就类似这题了),然后就开始枚举值域来判断了,简单推导就发现判断复杂度可以降成 log 级别的,代码很好写,第 104 min 无伤 AC 掉了。

E 题刚看完题就只剩 10 min。剩下的时间觉得是个二维线性 DP,但时间复杂度会炸裂,然后不知道咋搞,罚坐到 120 min。比赛结束(是的,F 题都没开题)。

赛后补题+分析

A. Not Shading

简要/形式化题意

一个 nm 列的 01 矩阵 {ai,j}。对于每个 ai,j=1 可进行两种操作:

1.对于所有 1kn,让 ak,j=1
2.对于所有 1km,让 ai,k=1

问:至少几次操作后使得对于给定的 rc,满足 ar,c=1

题解

(以下为严格证明,考场上手捏小数据更快)

情况 1:原始矩阵中 ar,c=1,答案为 0

情况 2:原始矩阵中不存在 ai,j 使得 ai,j=1,答案为 1

情况 3:存在 1kn,使得 ak,c=1 或存在 1km,使得 ar,k=1,答案为 1

otherwise: 倒序思考。若最终 ar,c=1。则在进行最后一步操作之前,必然存在 1kn,使得 ak,c=1(称为 A 局面) 或存在 1km,使得 ar,k=1(称为 B 局面)。在倒数第二步任意选取一个 ak1,k2=1 执行 1 操作,此时因为 1rn,所以 ar,k2=1,于是 B 局面成立,由情况 3,答案为 2。在倒数第二步任意选取一个 ak1,k2=1 执行 2 操作,此时因为 1cm,所以 ak1,c=1,于是 A 局面成立,由情况 3,答案为 2。综上,答案为 2

时间复杂度:O(nm)

AC code

#include<bits/stdc++.h>
using namespace std;
const int N=60;
int T,n,m,r,c,cnt;
char ch[N][N]; 
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>T;
	while(T--) {
		cin>>n>>m>>r>>c;
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++) cin>>ch[i][j];
		if(ch[r][c]=='B') {
			cout<<0<<endl;
			continue;
		}
		cnt=0;
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++) cnt+=(ch[i][j]=='B');
		if(!cnt) {
			cout<<-1<<endl;
			continue;
		}
		cnt=0;
		for(int i=1;i<=n;i++) cnt+=(ch[i][c]=='B');
		for(int j=1;j<=m;j++) cnt+=(ch[r][j]=='B');
		if(!cnt) {
			cout<<2<<endl;
			continue;
		}
		cout<<1<<endl;
	}
	return 0;
}

B. Not Sitting

简要/形式化题意

n×m 的全 0 矩阵 {Ai,j} 中,记 d(a,b,c,d)=|ac|+|bd|。甲和乙博弈。

第一轮:甲在矩阵中选取 k 个点对 (i,j),让 Ai,j=1

第二轮:乙在矩阵中选取 1 个点对 (a,b),满足 Aa,b=0

第三轮:甲在矩阵中选取 1 个点对 (c,d)

甲以 d(a,b,c,d) 最大为最优,乙以 d(a,b,c,d) 最小为最优。甲乙均采用最优决策。

求对于所有整数 k,其中 0kn×m1,博弈的结果 d(a,b,c,d) 的值。

题解

最后一轮的决策显然比较好讨论,因为第二轮已经确定。那么对于第三轮,为使 d(a,b,c,d) 尽可能大,即 |ac|+|bd| 尽可能大,则 c 取极值(1n),d 取极值(1m)。因而得到第三轮只有四种可能的最优决策:(1,1)(1,m)(n,1)(n,m),从而 d(a,b,c,d)=max{a+b2,a1+mb,na+b1,na+mb}

由特殊到一般,考虑 k=0 时,这时由刚刚的结论可得对于一个确定 (a,b),其博弈结果可以 O(1) 确定,那么只要 O(nm) 的暴力求出 min1an,1bm{max{a+b2,a1+mb,na+b1,na+mb}},就得到了最优博弈结果,记这个最优决策为 (a,b)

k=1 时,第一轮为了让第二轮选不到 (a,b),必然会让 Aa,b=1,从而第二轮的最优决策博弈结果为 min1an,1bm,(a,b)(a,b){max{a+b2,a1+mb,na+b1,na+mb}},可以发现其实就是次小值。

以此类推,记矩阵 {Bi,j},其中 Bi,j=max{i+j2,i1+mj,ni+j1,ni+mj},则最优博弈结果为 {Bi,j} 中的第 (k+1) 小。原问题转化为将 {Bi,j} 内的数据从小到大排序输出即可,优先队列即可 O(nmlognm) 解决,桶排序可做到 O(nm)

AC code

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10; 
int T,n,m;
int cnt;
priority_queue<int>q;
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>T;
	while(T--) {
		cin>>n>>m;
		cnt=0;
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++) {
				int num=0;
				num=max(num,i+j-2);
				num=max(num,i-1+m-j);
				num=max(num,n-i+j-1);
				num=max(num,n-i+m-j);
				q.push(-num);
			}
		while(!q.empty()) {
			cout<<-q.top()<<" ";
			q.pop();
		}
		cout<<endl;
	}
	return 0;
}

C. Not Assigning

简要/形式化题意

给定一个无边权无根树,请为每条边赋边权,使得所有边数不大于 2 的树链中,边权和为质数。

题解

对于所有边数为 1 的树链,显然,此边权一定是质数,因此树上的所有边的边权均为质数。

对于所有边数为 2 的树链,需要满足两条边权相加仍然是质数,即质数加质数仍为质数。如果是两个奇(偶)质数相加必然会得到一个大于 2 的偶数(合数,舍去)。那么必然一个是偶素数 2,另一个是奇质数 p,满足 p+2P

对于一个度数为 3 的点 k,设与其相邻的三个点为 a,b,c。则 akb(树链 1),akc(树链 2),bkc(树链 3),三条树链必然都只包含一个边权 2。如果 ak 的边权为 2,则在树链 1 中,kb 不为 2,在树链 2 中,kc 不为 2,那么与树链 3 的约束条件矛盾。同理可证其他情形均不成立。当度数大于 3 时,由于包含了度数为 3 这个子集,故也是无解。综上可得,有解的充要条件为树的形态是一条链。

那么一个简单的构造方案是从度数为 1 的点开始遍历整条链,按 2,p,2,p 的顺序给边赋边权。由于是构造一组可行解,这里就取 p=3 了。

AC code

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int T,n,u,v,ans[N];
int deg[N],flag[N];
map<pair<int,int>,int>Map;
vector<int>G[N];
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>T;
	while(T--) {
		memset(deg,0,sizeof(deg));
		memset(flag,0,sizeof(flag));
		cin>>n;
		for(int i=1;i<=n;i++) G[i].clear();
		for(int i=1;i<n;i++) {
			cin>>u>>v;
			if(u>v) swap(u,v);
			Map[make_pair(u,v)]=i;
			G[u].push_back(v);
			G[v].push_back(u);
			deg[u]++;
			deg[v]++;
		}
		bool f=false;
		for(int i=1;i<=n;i++)
			if(deg[i]>=3) {
				cout<<-1<<endl;
				f=true;
				break;
			}
		if(f) continue;
		int st;
		for(int i=1;i<=n;i++) 
			if(deg[i]==1) {
				st=i;
				break;
			}
		int cnt=0,pre;
		while(cnt<=n-1) {
			cnt++;
			pre=st;
			flag[pre]=1;
			for(int i=0;i<G[pre].size();i++) {
				int now=G[pre][i];
				if(flag[now]) continue;
				st=now;
			}
			if(cnt&1) ans[Map[make_pair(min(st,pre),max(st,pre))]]=2;
			else ans[Map[make_pair(min(st,pre),max(st,pre))]]=3;
		}
		for(int i=1;i<n;i++) cout<<ans[i]<<" ";
		cout<<endl;
		for(int i=1;i<=n;i++) 
			for(int j=0;j<G[i].size();j++)
				Map[make_pair(min(i,G[i][j]),max(i,G[i][j]))]=0;
	}
	return 0;
}

D. Not Adding

简要/形式化题意

给定长度为 n 的序列 aa 的数据两两不同。每次操作可选择一个数对 (ai,aj),把 gcd{ai,aj} 加到序列 a 的末尾,前提是 gcd{ai,aj} 不在加之前的 a 数组中。问:最多能进行几次操作。

题解

题意很迷人,稍加转化可知,原问题是在问 a 数组中所有子序列的 gcd 能形成几个不同的且不属于原始 a 数组的数。显然这个值域范围给了很好的提示。我们反向考虑,枚举值域内的数 p,判断其是否可以形成。

首先,可以形成 p 的数必然是 p 的倍数。筛出所有的原始 a 数组中 p 的倍数,设这个集合为 S。对于集合 S 的子集 S,它所有数的 gcd 必然不小于 S 中所有数字的 gcd。然而他们都不会小于 p(因为 p 是他们的公约数)。因此考虑全集 Sgcd 是否等于 p 即可,如果不是,显然他的子集也不可能,如果是,则 p 可以被形成。

那么瓶颈就在于如何高效的筛出原始 a 数组中 p 的倍数,由于值域不大,直接记录一个桶即可,这样枚举倍数是 log 级别的。总体时间复杂度为 O(VlogV)

AC code

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,a[N],b[N],ans;
int gcd(int a,int b) {
	return b?gcd(b,a%b):a;
} 
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++) {
		cin>>a[i];
		b[a[i]]=1;
	}
	for(int i=1;i<N;i++) {
		int cnt=0,ex;
		if(b[i]) continue;
		for(int j=2;i*j<N;j++) 
			if(b[i*j]) {
				cnt++;
				if(cnt==1) ex=i*j;
				else ex=gcd(ex,i*j);
			}
		if(ex==i) ans++;
	}
	cout<<ans;
	return 0;
}

E. Not Escaping

简要/形式化题意

给定一个 n×m 的矩阵,长度为 n 的数组 xk 个有序五元组 (a,b,c,d,h),满足 a<c。选择一条从 (1,1)(n,m) 的路径,开始时,代价为 0。该路径上,对于从 (a,b) 直接到 (c,d),若 ac,则存在 h 使得 (a,b,c,d,h) 出现在有序五元组集合中,代价减 h。若 a=cbd 无限制,代价加 |bd|×xa。问路径结束后代价的最小值。

题解

显然可以看出是一道二维线性 DP 题。记 dpi,j 表示从 (1,1)(i,j) 的合法路径的最小代价。

对于同行的,把绝对值拆掉:当 b>d 时,代价加 (bd)×xa,当 b<d 时,代价加 (db)×xa,两者的最小值即为答案,故从左到右和从右到左两次转移即可,转移方程如下:

dpi,j=min1j<j{dpi,j,dpi,j+(jj)×xi}

dpi,j=minj<jm{dpi,j,dpi,j+(jj)×xi}

优化:对于决策点 j1j2 满足 j1<j2<j。如果 j1j 的最优决策点。则

dpi,j1+(jj1)×xi=dpi,j1+(j2j1)×xi+(jj2)×xi<dpi,j2+(jj2)×xi

得出:dpi,j1+(j2j1)×xi<dpi,j2。与 dpi,j2 的最优性质矛盾。因此可能作为最优决策点的只有 j 左侧第一个点和 j 右侧第一个点,改写之后为。

dpi,j=min{dpi,j,dpi,j1+xi}

dpi,j=min{dpi,j,dpi,j+1+xi}

考虑跨行的情况,对于有序五元组 (a,b,c,d,h) 来说。

dpc,d=min{dpc,d,dpa,bh}

那么这样做的时间复杂度 O(nm) 的,无法通过此题。仔细思考一下行内的转移式子。我们发现,如果没有出现在有序五元组中的点对,他在转移之前的值为 inf(初始时,除了dp1,1=0dpi,j 都为 inf)。故不可能作为最优决策点。也就是说,实际上只有 2k 个有效点加上起始点一共 2k+2 个。我们只要对这些点进行 DP 即可。实现的时候要多一步排序,也是这个算法的瓶颈,所以时间复杂度 O(nlogk)

当然,状态的设计要改一改,记 dpi(1,1) 到第 i 个有效点的最小代价,然后维护一下编号之间的关系,实现起来略微复杂。

AC code

#include<bits/stdc++.h>
#define int long long 
#define fi first
#define se second
using namespace std;
const int N=1e5+10; 
int t,n,m,k,a,b,c,d,h,x[N]; 
vector<pair<int,int> >E1[N];
pair<int,int>F[3*N];
int dp[3*N];
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>t;
	while(t--) {
		int cnt=0;
		cin>>n>>m>>k;
		for(int i=1;i<=n;i++) cin>>x[i];
		for(int i=1;i<=n;i++) E1[i].clear();
		E1[1].push_back(make_pair(1,++cnt));
		for(int i=1;i<=k;i++) {
			cin>>a>>b>>c>>d>>h;
			E1[a].push_back(make_pair(b,++cnt));
			F[cnt]=make_pair(cnt+1,h);
			E1[c].push_back(make_pair(d,++cnt));
		}
		E1[n].push_back(make_pair(m,++cnt));
		for(int i=1;i<=cnt;i++) dp[i]=1e18;
		dp[1]=0;
		for(int i=1;i<=n;i++) {
			sort(E1[i].begin(),E1[i].end());
			int len=E1[i].size();
			for(int j=1;j<len;j++) 
				dp[E1[i][j].se]=min(dp[E1[i][j].se],dp[E1[i][j-1].se]+x[i]*(E1[i][j].fi-E1[i][j-1].fi));
			for(int j=len-2;j>=0;j--) 
				dp[E1[i][j].se]=min(dp[E1[i][j].se],dp[E1[i][j+1].se]+x[i]*(E1[i][j+1].fi-E1[i][j].fi));
			for(int j=0;j<len;j++) 
				if(dp[E1[i][j].se]!=1e18&&F[E1[i][j].se].fi) 
					dp[F[E1[i][j].se].fi]=min(dp[F[E1[i][j].se].fi],dp[E1[i][j].se]-F[E1[i][j].se].se);
		}
		if(dp[cnt]<1e18) cout<<dp[cnt]<<endl;
		else cout<<"NO ESCAPE"<<endl;
	}
	return 0;
}

F. Not Splitting

简要/形式化题意

给定一个 k×k 的网格(k 为偶数),网格上有若干张 1×2 的多米诺骨牌。选取若干条格边组成一条连续的线,将网格分成两个全等的部分。问最少需要拿走几张骨牌,才能使每张骨牌所占的两个格子均属于其中一个部分。

题解

看似无从下手,但可以发现如果从整个格子最中间的格点(注意,是格点),向相对的两边引两条关于该格点中心对称的两条线,就能将网格分成两个全等的部分。当然这个方法是充分且必要的,感性理解一下,本人不咋会证明这个抽象的东西。

总而言之,对与 (i,j) 右下角的格点,若在分割线上,则其相对于最中间格点的中心对称点,(ki,kj) 右下角的格点也必然在分割线上。

回到原问题,最少需要拿走几张骨牌,本质上就是在问,最少会穿过几张骨牌。从图的角度看待,(k+1)2 个格点,建立一个网格图,对于一张骨牌 (r1,c1)(r2,c2)(它所占的两个格子)。若 r1=r2,则 (r11,c1) 右下角的格点与 (r1,c1) 右下角的格点之间的边权加 1。若 c1=c2,则 (r1,c11) 右下角的格点与 (r1,c1) 右下角的格点之间的边权加 1

至此,原问题被转化为,求一条经过中心格点且贯穿网格图(即 (k2,k2) 右下角的格点)的最短路径,并且该路径满足最开始提到的那个结论。我们可以以中心格点为原点,利用 Dijkstra 算法同时向两边求最短路即可。时间复杂度为 O(k2logk2)

为了方便实现,(i,j) 右下角的格点哈希成 i×(k+1)+j+1 存储。本质上把网格图的格边去掉,剩下的按序编号,简化建图。

AC code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=510;
const int M=1e6+10;
int t,k,n,r1,c1,r2,c2; 
int vis[M],d[M];
map<pair<int,int>,int>Map;
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>t;
	while(t--) {
		priority_queue<pair<int,int> >q;
		cin>>n>>k;
		for(int i=1;i<=(k+1)*(k+1);i++) d[i]=1e18;
		for(int i=1;i<=(k+1)*(k+1);i++) vis[i]=0;
		Map.clear();
		for(int i=1;i<=n;i++) {
			cin>>r1>>c1>>r2>>c2;
			if(r1>r2) swap(r1,r2);
			if(c1>c2) swap(c1,c2);
			if(r1==r2) {
				Map[make_pair((r1-1)*(k+1)+c2,r1*(k+1)+c2)]++;
				Map[make_pair(r1*(k+1)+c2,(r1-1)*(k+1)+c2)]++;
			}
			if(c1==c2) {
				Map[make_pair(r1*(k+1)+c1,r1*(k+1)+c1+1)]++;
				Map[make_pair(r1*(k+1)+c1+1,r1*(k+1)+c1)]++;
			}
		}
		d[k*k/2+k+1]=0;
		q.push(make_pair(0,k*k/2+k+1));
		while(!q.empty()) {
			int x=q.top().second;
			q.pop();
			if(vis[x]) continue;
			vis[x]=1;
			vis[k*k+2*k+2-x]=1;
			if(x<=k+1||x>k*(k+1)||x%(k+1)==1||x%(k+1)==0) {
				cout<<n-d[x]<<endl;
				break;
			}
			int y,z;
			y=x-1,z=Map[make_pair(x,y)]+Map[make_pair(k*k+2*k+2-x,k*k+2*k+2-y)];
			if(d[y]>d[x]+z) {
				d[y]=d[x]+z;
				q.push(make_pair(-d[y],y));
			}
			y=x+1,z=Map[make_pair(x,y)]+Map[make_pair(k*k+2*k+2-x,k*k+2*k+2-y)];
			if(d[y]>d[x]+z) {
				d[y]=d[x]+z;
				q.push(make_pair(-d[y],y));
			}
			y=x+k+1,z=Map[make_pair(x,y)]+Map[make_pair(k*k+2*k+2-x,k*k+2*k+2-y)];
			if(d[y]>d[x]+z) {
				d[y]=d[x]+z;
				q.push(make_pair(-d[y],y));
			}
			y=x-k-1,z=Map[make_pair(x,y)]+Map[make_pair(k*k+2*k+2-x,k*k+2*k+2-y)];
			if(d[y]>d[x]+z) {
				d[y]=d[x]+z;
				q.push(make_pair(-d[y],y));
			}
		}
	}
	return 0;
}

考后反思

首先后两题没有充足的时间思考,毕竟只留了 16 min。仔细分析一下时间布局,发现耗时最多的是 B 题,不过无论是哪题,耗时的原因的都是绕弯子没绕回来。想了一大堆奇葩做法结果没有果断舍弃掉。以后要看清数据范围,和优化的瓶颈选择做法。(比如 C 题,DP 的做法在转移上显然没有优化的余地,应该果断放弃,B 题,数据范围和题目描述并没有要求对于每一个 k 独立 O(1) 算出来,及时从数学式推导中跳出来),预估在这场比赛中可以省去半小时左右的时间。

补题的时候发现,这场比赛有很多是结论题,比如 F 题,只要结论一出来,正解基本上就是呼之欲出了。但是严格证明过程却远远难于题目本身。因此,造数据找规律是极为重要的。(F 题可以多画几条分割线,找一找公共格点,然后结论就出来了)。

总而言之就是:优化瓶颈和数据范围选算法;手造大小数据找规律猜结论。

结尾

考的还行~。

本文作者:2021hych

本文链接:https://www.cnblogs.com/2021hych/p/17512674.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   2021hych  阅读(16)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起