2022/4/30 五一集训#1

比赛链接:此处

A.石头剪刀布

  • 虽然看出了是并查集,但由于太菜了没有看出来暴力成分……;
  • 暴力枚举每一个孩子为法官时,如果与该孩子无关的条件逻辑自洽了,那么这个人就可能为法官;
  • 如果合法的法官数为 \(0\),那么这一局不可能;如果合法的法官数大于 \(1\),那么说明有许多人作为法官都是可能的,所以法官无法确认;当且仅当合法法官数为 \(1\)时,才能确定法官;
  • 法官确定的行数为所有情况下最后一个逻辑不自洽的地方;
AC code
#include<iostream>
#include<algorithm>
#include<exception>
#include<cstring>
#include<cmath>
#include<ios>
using namespace std;

const int N=505;

int n,m,t;
int ans[N<<2],last,f[N<<2];

struct memr{
	int a,b,k;
}kid[N<<2];

int get(int x){
	if(x==f[x]) return x;
	return f[x]=get(f[x]);
}

void merge(int x,int y){
	f[get(x)]=get(y);
	return ;
}

void pre(){
	for(int i=1;i<=3*n+5;++i)
		f[i]=i;
	return ;
}

void dfs(int i){
	if(i>n) return ;
	pre();
	bool s=1;
	for(int j=1;j<=m;++j){
		if(kid[j].a==i || kid[j].b==i) continue;
		int x=kid[j].a,y=kid[j].b;
		int x_e=x+n,y_e=y+n,x_ey=x+2*n,y_ey=y+2*n;
		if(kid[j].k==0){
			if(get(x)==get(y_e) || get(x_e)==get(y)){
				s=0,last=max(last,j);break;
			}
			merge(x,y);merge(x_e,y_e);merge(x_ey,y_ey);
		}
		else if(kid[j].k==1){
			if(get(x)==get(y_e) || get(x)==get(y)){
				s=0,last=max(last,j);break;
			}
			merge(x_e,y);merge(x,y_ey);merge(x_ey,y_e);
		}
		else{
			if(get(x)==get(y_ey) || get(x)==get(y)){
				s=0,last=max(last,j);break;
			}
			merge(x_ey,y);merge(x,y_e);merge(x_e,y_ey);
		}
	}
	if(s) ans[++t]=i;
	dfs(i+1);
	return ;
}

int main(){
	while(scanf("%d%d",&n,&m)!=EOF){
		char opt;
		for(int i=1;i<=m;++i){
			cin>>kid[i].a>>opt>>kid[i].b;
			++kid[i].a,++kid[i].b;
			kid[i].k=(opt=='=')?0:((opt=='<')?-1:1);
		}
		t=last=0;
		dfs(1);
		if(t==0) puts("Impossible");
		else if(t>1)	puts("Can not determine");
		else printf("Player %d can be determined to be the judge after %d lines\n",ans[t]-1,(n==1)?0:last);
	}
	return 0;
}

B.永无乡

  • 虽然正解是线段树合并或者平衡树,但我是用分块 \(AC\) 的……;
  • 纪念一下第一道在考场上 AC 的紫题
  • 直接看这个也可、

Solution

  • 我们看到 连通 两个字,考虑使用并查集来维护这一信息。

  • 由于后面需要查询重要度排名 \(k\) 的岛屿,我们就需要考虑一下怎么较为快速的求出这一信息。

    我们看到题目:“每座岛都有自己的独一无二的重要度”,再知道 \(1\le p_i\le n\),就想到这个重要度与岛的编号是一一对应且连续的。

    这时我们就想到分块。将重要度分为 \(\sqrt n\times \sqrt n\) 个区间,每个区间记录当前连通块在这个区间里的岛屿个数。

    此时我们需要一个 \(val[\ ]\) 数组来记录每个重要度对应的岛屿编号。

    由于每次查询是在一个连通块内,所以我们需要一个 \(n\times \sqrt n\) 的数组,\(num_{i,j}\) 表示在并查集 \(get()\) 值为 \(i\) 的连通块里,第 \(j\) 个区间中有多少个岛屿。

    这时分块的状态就设置完毕了。

  • 由于分块的思想是大段维护,局部朴素,所以局部朴素部分先抛开(毕竟怎么写都是暴力),来看一下大段维护部分(查询第一个总岛屿数 \(< k\) 的区块)。

    首先考虑到时间,由于 \(q\le 3\times 10^5\),我们需要以尽可能快的时间完成查询这一任务。

    如果遍历来寻找,最坏复杂度是 \(O(\sqrt n)\),显然不太可行。这时想到二分\(O(\log _ 2 n)\)的复杂度)。

    如果二分,那么我们就需要将分块数组的定义改为 \(num_{i,j}\) 表示:在并查集 \(get()\) 值为 \(i\) 的连通块里,\(0\sim j\) 个区间中有多少个岛屿。也就是之前定义 的前缀和

    这样在查询时二分即可。

    对于局部朴素,设求得区间及以前已经含有 \(x\) 个该连通块里的岛屿,在求得区间的下一个区间里遍历,找到的第 \(k-x\)\(get()\) 值等于当前连通块编号的值即为答案。

  • 由于在 \(q\) 个操作中还包含新修桥这一操作,所以我们考虑一下合并连通块时其他数组怎么处理。

    可以想到,\(num\) 数组应该直接相加,这一操作不会破坏前缀和的性质。

AC code
#include<bits/stdc++.h>
using namespace std;

inline int read(){
	int s=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		s=s*10+int(ch-'0');
		ch=getchar();
	}
	return s*f;
}

#define re register

const int N=1e5+10;

int n,m,q;
int rk[N],f[N],siz[N],val[N];
int dx,num[N][320];

inline int id(int x){
	return (x-1)/dx+1;
}

int get(int x){
	if(x==f[x]) return x;
	return f[x]=get(f[x]);
}

void merge(int x,int y){
	int a=get(x),b=get(y);
	siz[b]+=siz[a];
	for(re int i=1;i<=id(n);++i)
		num[b][i]+=num[a][i];
	f[a]=b;
	return ;
}

int main(){
	n=read(),m=read();
	dx=sqrt(n);
	for(re int i=1;i<=n;++i){
		rk[i]=read(),f[i]=i,siz[i]=1,val[rk[i]]=i;
		for(re int j=id(rk[i]);j<=id(n);++j)
			num[i][j]++;
	}
	int u,v;
	for(re int i=1;i<=m;++i){
		u=read(),v=read();
		merge(u,v);
	}
	q=read();
	char opt[2];
	while(q--){
		scanf("%s",opt);
		u=read(),v=read();
		if(opt[0]=='B')
			merge(u,v);
		else{
			u=get(u);
			if(siz[u]<v){
				puts("-1");
				continue;
			}
			int l=1,r=id(n);
			while(l<r){
				int mid=(l+r)>>1;
				if(num[u][mid]>=v)
					r=mid;
				else l=mid+1;
			}
			if(num[u][l]>=v)
				--l;
			v-=num[u][l];
			for(re int i=l*dx+1;i<=(l+1)*dx;++i){
				if(get(val[i])==u)	v--;
				if(!v){
					printf("%d\n",val[i]);
					break;
				}
			}
		}
	}
	return 0;
}

C.[AGC004D] Teleporter

  • 这是一道贪心然而并没有看出来
  • 首先我们猜测,当城市 \(1\) 为自环时最优,因此去掉从城市 \(1\) 出发的那条线,使整张图变成一棵以城市 \(1\) 为根节点的,题目要求变为在 \(k\) 步以内到达城市 \(1\)
  • 接下来使用贪心的思想,改动的传送阵数量最小,就意味着可以不用改动的城市尽量不改动;所以在深搜回溯时,每当一个节点距离其子树上最远的节点距离为 \(k-1\) 时,则意味着需要将这个节点连同它的子树一起直接接到城市 \(1\) 上;
AC code
#include<bits/stdc++.h>
using namespace std;

inline int read(){
	int s=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		s=s*10+int(ch-'0');
		ch=getchar();
	}
	return s*f;
}

const int N=1e5+10;

int n,k,ans=0;
int f[N];
int head[N],ver[N<<1],nxt[N<<1],tot=0;

inline void add(int x,int y){
	ver[++tot]=y,nxt[tot]=head[x],head[x]=tot;
	return ;
}

int dfs(int s,int dep){
	int fd=dep;
	for(int i=head[s];i;i=nxt[i])
		fd=max(fd,dfs(ver[i],dep+1));
	if(fd-dep==k-1){
		if(f[s]!=1) f[s]=1,++ans;
		return dep-1;
	}
	return fd;
}

int main(){
	n=read(),k=read();
	int v;
	for(int i=1;i<=n;++i){
		f[i]=read();
		if(i!=1) add(f[i],i);
	}
	dfs(1,1);
	if(f[1]!=1) ++ans;
	printf("%d",ans);
	return 0;
}
posted @ 2022-04-30 15:44  Star_LIcsAy  阅读(45)  评论(0)    收藏  举报