牛客小白月赛100(A,B,C,D,E,F三元环计数)

比赛链接

官方讲解

这场比较简单,ABC都很签到,D是个不太裸需要预处理的 B F S BFS BFS 搜索,E是调和级数暴力枚举,F是三元环计数。三元环考的比较少,没见过可能会偏难。


A ACM中的A题

思路:

就是枚举每个边变成原来的两倍,然后看看两短边之和是否大于第三边即可。

不能只给最短边乘 2 2 2,比如 1 4 8 这组数据,也不能只给第二短边乘 2 2 2,比如 2 3 4 这组数据。不过可以不给最长边乘 2 2 2,因为最长边乘 2 2 2 后,边的长度的顺序没有改变,而原本两短边之和却更不容易大于第三边了。

code:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long ll;

ll a[3];
bool check(){
	ll b[3];
	memcpy(b,a,sizeof(b));
	b[0]*=2;
	sort(b,b+3);
	if(b[0]+b[1]>b[2])return true;
	memcpy(b,a,sizeof(b));
	b[1]*=2;
	sort(b,b+3);
	if(b[0]+b[1]>b[2])return true;
	memcpy(b,a,sizeof(b));
	b[2]*=2;
	sort(b,b+3);
	if(b[0]+b[1]>b[2])return true;
	else return false;
}

int main(){
	cin>>a[0]>>a[1]>>a[2];
	sort(a,a+3);
	if(check())cout<<"Yes";
	else cout<<"No";
	return 0;
} 

B ACM中的C题

思路:

我们交换位置只有两种策略,一种是一个已交换的数和一个未交换的数交换,另一种是两个未交换的数交换。显然第二种策略可以让未交换的数量减少的更快,所以从贪心的角度出发,我们每次选择两个未交换的数交换,剩最后一个的时候才用第一种策略。

这样交换的话,交换次数就是 ⌈ n 2 ⌉ \left\lceil\dfrac n2\right\rceil 2n 了。不过需要注意 n = 1 n=1 n=1 的话这个数是不可能交换到其他位置的,这时候无解。

code:

#include <iostream>
#include <cstdio>
using namespace std;

int n;

int main(){
	cin>>n;
	for(int i=1,t;i<=n;i++)
		cin>>t;
	if(n==1)cout<<-1<<endl;
	else cout<<(n+1)/2;
	return 0;
}

C ACM中的M题

思路:

多了一个只能同类别之间的数可以互换,和上一题其实没什么太大差别,我们对每一个类别分别用上题的策略来换即可。如果某个类别只有一个数,就说明这个数没法换到其他位置,这时候就无解了。

code:

#include <iostream>
#include <cstdio>
#include <map>
using namespace std;

int n;
map<int,int> mp;

int calc(){
	int ans=0;
	for(auto [x,t]:mp)
		if(t==1)return -1;
		else ans+=(t+1)/2;
	return ans;
}

int main(){
	cin>>n;
	for(int i=1,t;i<=n;i++)cin>>t;
	for(int j=1,t;j<=n;j++){
		cin>>t;
		mp[t]++;
	}
	cout<<calc();
	return 0;
}

D ACM中的AC题

思路:

通过人数比 E E E 少一半,大伙都不喜欢傻逼搜索啊(摊手)。

我们先两人对称走,其中一个人走到终点后,留下另一个人,另一个人再单独走出终点。所以这题一个比较直观的思路就是跑两次 B F S BFS BFS,把前面两人对称走和一个人单独走分别 B F S BFS BFS 一下就行了。

不过这样比较难写。还有就是前一个 B F S BFS BFS 找到的最短路径 如果再加上后一个 B F S BFS BFS 的答案 就不一定是最小的了,所以我们前一个 B F S BFS BFS 不能只看找到的第一个答案,而是需要遍历到所有情况,这样的话第二段的 B F S BFS BFS 就要跑好几次,有可能会超时。

考虑到我们只需要找到最少的步数,而不需要知道中间过程,所以第二段我们只需要知道某个位置到出口最短的距离是多少,这可以直接从所有终点 B F S BFS BFS 到整个地图预处理出来。第一个 B F S BFS BFS 比较好写,就是一个简单的 B F S BFS BFS,不过走到下一个位置的时候需要推出对称位置,再把限制条件再写一遍(比如不能出界,不能走到陷阱,不能走到已经走过的点)。

推出对称点用的是中点坐标公式,假设两个点坐标分别为 ( x 1 , y 1 ) ( x 2 , y 2 ) (x_1,y_1)(x_2,y_2) (x1,y1)(x2,y2),中点坐标为 ( x , y ) (x,y) (x,y),那么就有 x = x 1 + x 2 2 , y = y 1 + y 2 2 x=\dfrac{x_1+x_2}2,y=\dfrac{y_1+y_2}2 x=2x1+x2,y=2y1+y2。而对于中心对称来说,原点和对称点就是上面的两点,对称中心点就是中点,知道中点坐标 ( x , y ) (x,y) (x,y),知道原点坐标 ( x 1 , y 1 ) (x_1,y_1) (x1,y1),那么对称点坐标就是 ( x 2 , y 2 ) = ( 2 x − x 1 , 2 y − y 1 ) (x_2,y_2)=(2x-x_1,2y-y_1) (x2,y2)=(2xx1,2yy1)。我是这样想的:两点坐标加起来就是两倍中点坐标,所以两倍中点减去一个点就是另一个点。

code:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <queue>
#include <cstring>
#include <vector>
#include <array>
using namespace std;
const int maxn=2e3+5;
const int inf=1e9;

int n,m,sx,sy;
int fx[]={1,-1,0,0},fy[]={0,0,1,-1};
string mp[maxn];
int dis[maxn][maxn];

void init(){
	vector<vector<bool> > vis(n+1,vector<bool>(m+1,false));
	memset(dis,0x3f,sizeof(dis));
	queue<array<int,3> > q;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			if(mp[i][j]=='@'){
				q.push({i,j,0});
				vis[i][j]=true;
			}
	
	while(!q.empty()){
		auto [ux,uy,d]=q.front();
		q.pop();
		dis[ux][uy]=d;
		for(int i=0,x,y;i<4;i++){
			x=ux+fx[i];y=uy+fy[i];
			if(x<1 || x>n || y<1 || y>m)continue;
			if(mp[x][y]=='#')continue;
			if(vis[x][y])continue;
			q.push({x,y,d+1});
			vis[x][y]=true;
		}
	}
}
int bfs(int sx,int sy){
	int ans=inf;
	vector<vector<bool> > vis(n+1,vector<bool>(m+1,false));
	queue<array<int,3> > q;
	q.push({sx,sy,0});
	vis[sx][sy]=true;
	while(!q.empty()){
		auto [ux,uy,d]=q.front();
		q.pop();
		if(mp[ux][uy]=='@'){
			ans=min(ans,d+dis[2*sx-ux][2*sy-uy]);
		}
		for(int i=0,x,y,rx,ry;i<4;i++){
			x=ux+fx[i];y=uy+fy[i];
			rx=sx*2-x;ry=sy*2-y;
			if(x<1 || x>n || y<1 || y>m)continue;
			if(rx<1 || rx>n || ry<1 || ry>m)continue;
			if(mp[x][y]=='#' || mp[rx][ry]=='#')continue;
			if(vis[x][y])continue;
			q.push({x,y,d+1});
			vis[x][y]=true;
		}
	}
	return ans==inf?-1:ans;
}

int main(){
	cin>>n>>m>>sx>>sy;
	for(int i=1;i<=n;i++){
		cin>>mp[i];
		mp[i]=" "+mp[i];
	}
	init();
	cout<<bfs(sx,sy);
	return 0;
}

E ACM中的CM题

思路1(三分):

其实能不能算正解我也不清楚,总感觉很悬,非常粗糙的做法。

可以想到我们一定先提高能力,准备好了之后再排雷,不然你先排雷再提高能力,总次数没有减少,排雷的范围还小,不划算。

我们把能力提高到 m m m,然后算最少的排雷次数,把提高能力和排雷次数加起来就是总共花费的时间。

这个算最少排雷次数可以 O ( n ) O(n) O(n) 来算,也可以 O ( n log ⁡ n ) O(n\log n) O(nlogn)

O ( n ) O(n) O(n) 写法就是从小到大枚举有雷的位置,假设上次排雷的范围是 [ l , r ] [l,r] [l,r],如果这个雷在范围内就跳过(因为在范围内就说明这个雷被排过了),如果不在范围内,就以它的位置为左端点 l l l,开始新一轮的排雷,数一下中间重置了几次 l l l 就是排雷次数。

O ( n log ⁡ n ) O(n\log n) O(nlogn) 的写法是用 s e t set set,把雷的位置存入,每次以未被排过的位置最小的雷开始排,用 u p p e r b o u n d upper_bound upperbound 找到下一个未被排过的雷,跳到最后,跳的次数就是排雷次数。这个方法的时间复杂度准确来说应该是 排雷次数乘以 log ⁡ n \log n logn

我们把能力提高到 m m m m m m 看成自变量,总共花费的时间看成是因变量,这个函数就是一个类似 “单谷” 的函数,因为我们能力设的太小或者太大都会导致花费的时间大大增加,只有处在中间的某个位置的时候花费时间才少。可以想到用三分来解决。

不过这不是一个单纯的单谷函数,而是一个大致形状类似单谷,在局部上可能会“抖动”,那么我们可以把范围缩小到一定程度,然后在一个小范围内暴力地计算答案。

code:

#include <cstdio>
#include <iostream>
#include <set>
#include <algorithm>
using namespace std;
const int maxn=2e5+5;

int n,a[maxn];

int check(int m){//当排雷能力变为m时的花费时间
	int ans=m;
	for(int i=1,l,r=0;i<=n;i++){
		if(r<a[i]){
			l=a[i];
			r=l+m;
			ans++;
		}
	}
	return ans;
}

int main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	sort(a+1,a+n+1);
	
	int l=0,r=2e5,lm,rm;
	while(l+1000<r){
		lm=l*2/3+r/3;
		rm=l/3+r*2/3;
		if(check(lm)<check(rm))r=rm;
		else l=lm;
//		cout<<l<<" "<<r<<" "<<check(lm)<<" "<<check(rm)<<endl;
	}
	int ans=1e9;
	for(int i=l;i<=r;i++)
		ans=min(ans,check(i));
	cout<<ans<<endl;
	return 0;
}

思路2(调和级数):

上面说过 O ( n log ⁡ n ) O(n\log n) O(nlogn) 的写法的时间复杂度准确来说应该是 排雷次数乘以 log ⁡ n \log n logn。而当 m m m 比较大的时候,排雷次数就会变少,最坏情况下就是 2 e 5 2e5 2e5 的位置里每个位置都有雷,那么时间复杂度就是 20000 m ∗ log ⁡ n \dfrac{20000}{m}*\log n m20000logn

如果我们暴力地来枚举 m m m,时间复杂度一共是 200000 1 ∗ log ⁡ n + 200000 2 ∗ log ⁡ n + ⋯ + 200000 200000 ∗ log ⁡ n = 200000 log ⁡ n ∗ ( 1 1 + 1 2 + ⋯ + 1 200000 ) \dfrac{200000}1*\log n+\dfrac{200000}2*\log n+\dots+\dfrac{200000}{200000}*\log n=200000\log n*(\dfrac{1}1+\dfrac12+\dots+\dfrac{1}{200000}) 1200000logn+2200000logn++200000200000logn=200000logn(11+21++2000001),括号内的数是调和级数,其和约等于 ln ⁡ 200000 ≈ log ⁡ n \ln 200000\approx\log n ln200000logn,因此时间复杂度变成 = n log ⁡ 2 n =n\log^2 n =nlog2n

所以我们如果用这所谓的 O ( n log ⁡ n ) O(n\log n) O(nlogn) 的写法,就可以直接暴力地算。

code:

#include <iostream>
#include <cstdio>
#include <set>
using namespace std;
const int maxn=2e5+5;

int n;
set<int> S;

int check(int m){
	int ans=m;
	set<int>::iterator it=S.begin();
	while(it!=S.end()){
		it=S.upper_bound(*it+m);
		ans++;
	}
	return ans;
}

int main(){
	cin>>n;
	for(int i=1,t;i<=n;i++){
		cin>>t;
		S.insert(t);
	}
	int ans=1e9;
	for(int i=0;i<=200000;i++)
		ans=min(ans,check(i));
	cout<<ans<<endl;
	return 0;
}

F ACM中的ACM题

思路:

三元环计数,虽然不是用来计数而是用并查集把边加入同一个集合,大体做法是差不多的。三元环计数的讲解可以看 OI wiki

首先可以想到如果有一条边连接了 u   x u\ x u x,另一条边连接了 u   y u\ y u y,如果想要这两条边同属一个集合,那就必须连接 x   y x\ y x y。也就是必须是三元环才能让环上三边同属一个集合。

我们在三元环计数的时候,其实是比较暴力地枚举三元环上的三个点三条边,只是我们是通过按顺序不重复地枚举,导致时间复杂度降为了 O ( n n ) O(n\sqrt n) O(nn )。因为我们枚举出了所有三元环的三边,所以我们用三元环计数的写法,把计数的部分改成用并查集将三边合并成同一个集合,最后看一下所有边是否被合并到了同一个集合即可。

三元环计数在 这场K题 也考过一次。

code:

#include <iostream>
#include <cstdio>
#include <array>
#include <vector>
using namespace std;
const int maxn=2e5+5;

int T,n,m;

int deg[maxn];
array<int,2> edge[maxn];

int head[maxn],ent;
struct edge{
	int v,id,nxt;
}e[maxn];
void add(int u,int v,int id){
	e[++ent]={v,id,head[u]};
	head[u]=ent;
}

int vis[maxn];

int f[maxn];
void init(){
	for(int i=1;i<=n;i++)deg[i]=0;
	for(int i=1;i<=n;i++)head[i]=0;
	ent=0;
	for(int i=1;i<=m;i++)f[i]=i;
}
int find(int x){
	return f[x]==x?x:f[x]=find(f[x]);
}
void merge(int x,int y){
	int fx=find(x),fy=find(y);
	f[fy]=fx;
}

int main(){
	cin>>T;
	while(T--){
		cin>>n>>m;
		init();
		for(int i=1;i<=m;i++){
			auto &[u,v]=edge[i];
			cin>>u>>v;
			deg[u]++;
			deg[v]++;
		}
		for(int i=1;i<=m;i++){
			auto &[u,v]=edge[i];
			if(deg[u]<deg[v] || (deg[u]==deg[v] && u>v))swap(u,v);
			add(u,v,i);
		}
		for(int u=1;u<=n;u++){
			for(int i=head[u];i;i=e[i].nxt)vis[e[i].v]=e[i].id;
			for(int i=head[u],v,id1;i;i=e[i].nxt){
				v=e[i].v;
				id1=e[i].id;
				for(int j=head[v],w,id2;j;j=e[j].nxt){
					w=e[j].v;
					id2=e[j].id;
					if(vis[w]){
						merge(vis[w],id1);
						merge(id1,id2);
					}
				}
			}
			for(int i=head[u];i;i=e[i].nxt)vis[e[i].v]=0;
		}
		int cnt=0;
		for(int i=1;i<=m;i++)
			if(f[i]==i)
				cnt++;
		puts((cnt==1)?"Yes":"No");
	}
	return 0;
}
  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值