"蔚来杯"2022牛客暑期多校训练营3

  A B C D E F G H I J
赛时过题 O   O     O   O   O
赛后补题       待补            

赛后总结:

好耶!做出了5题,55名!!校内第二!芜湖起飞!

今天把所有力所能及的题全做出来了,还做出来了F题,非常棒!今后也要和这次一样!

除非是过题人数极少的题,否则赛场上每道题至少要有2个人看过!这点要牢记!

今天这场比较顺风,看一题切一题,之后可能没这么好的运气但也要做出每一道有能力做出来的题!

比赛规划:0h~0.5h 看4题+做签到,0.5h~2h再看4题,同时结合榜单开题,2h~5h开难题但不要三个人同时看一道题。

赛时排名:

6题末尾:33名

7题末尾:12名


C Concatenation

题目难度:check-in

题目大意:N个数字字符串(可有前导0),重新排列并拼接起来输出,使得输出的数字最小。

数据范围:1<=N<=2e6,∑|si|<=2e7

解题分析:由于输出的字符串长度固定,故数字最小=字典序最小。

故直接按照a+b<b+a进行sort即可,需要注意cmp函数中的传参需要加引用否则会T。

实际上线性做法的标答很复杂,但由于这题时限比较松(4s)没有把sort卡死,所以就成了简单题hhh

参考代码:

查看代码

#include<iostream>
#include<cstdio>
#include<string>
#include<algorithm>
#define For(i,a,b) for(int i=a;i<=b;i++)
const int N=2e6+1000;
std::string s[N];
const int cmp(const std::string&a,const std::string&b)
{
	return a+b<b+a;
}
int main()
{
    std::ios::sync_with_stdio(false);
	std::cin.tie(0);
	int n;std::cin>>n;
	For(i,1,n) std::cin>>s[i];
	std::sort(s+1,s+1+n,cmp);
	For(i,1,n) std::cout<<s[i];
	return 0;
} 

A Ancestor

题目难度:easy

题目大意:给出两棵编号1-n的树A、B,A、B树上每个节点均有一个权值,给出k个关键点的编号x1…xk,问有多少种方案使得去掉恰好一个关键点使得剩余k-1个关键点在树A上LCA的权值大于树B上LCA的权值。 

数据范围:2<=K<=N<=1e5

解题思路:用线段树做区间查询RMQ (Range Minimum/Maximum Query)来求LCA(Least Common Ancestor),然后再用set维护删点/加点时的最左端和左右端。

标答:预处理出关键点序列的在树A B上的前缀LCA和后缀LCA,枚举去掉的关键节点并使用前后缀LCA算出剩余节点的LCA比较权值即可。 

参考代码:

查看代码
 #include<iostream>
#include<vector>
#include<set>
#include<cstdio>
#define For(i,a,b) for(int i=a;i<=b;i++)
const int N=1e5+1000;
std::vector<std::pair<int,int> > dep_val[2];
std::vector<int> To[2][N];
int L[2][N],R[2][N],depth[2][N],v[2][N],x[N];
void dfs(int opt,int now)
{
	dep_val[opt].push_back(std::make_pair(depth[opt][now],v[opt][now]));
	L[opt][now]=dep_val[opt].size();
	For(i,0,(int)To[opt][now].size()-1) 
	{
		depth[opt][To[opt][now][i]]=depth[opt][now]+1;
		dfs(opt,To[opt][now][i]);
		dep_val[opt].push_back(std::make_pair(depth[opt][now],v[opt][now]));
	}
	R[opt][now]=dep_val[opt].size();
}
std::pair<int,int> min[2][4*2*N];
void build(int root,int l,int r,int opt)
{
	if (l==r) 
	{
		min[opt][root]=dep_val[opt][l-1];
		return ;
	}
	int mid=(l+r)>>1;
	build(root<<1,l,mid,opt);build(root<<1|1,mid+1,r,opt);
	min[opt][root]=std::min(min[opt][root<<1],min[opt][root<<1|1]);
}
std::pair<int,int> query(int root,int l,int r,int a,int b,int opt)
{
	if (l==a&&r==b) return min[opt][root];
	int mid=(l+r)>>1;
	if (b<=mid) return query(root<<1,l,mid,a,b,opt);
	else
	{
		if (a>mid) return query(root<<1|1,mid+1,r,a,b,opt);
		else return std::min(query(root<<1,l,mid,a,mid,opt),query(root<<1|1,mid+1,r,mid+1,b,opt));
	}
}
std::set<int> minL[2], maxR[2];
int main()
{
	int n,k;scanf("%d%d",&n,&k);
	For(i,1,k) scanf("%d",&x[i]);
	For(i,1,n) scanf("%d",&v[0][i]);
	For(i,2,n)
	{
		int x;scanf("%d",&x);
		To[0][x].push_back(i);
	}
	For(i,1,n) scanf("%d",&v[1][i]);
	For(i,2,n)
	{
		int x;scanf("%d",&x);
		To[1][x].push_back(i);
	}
	dfs(0,1);dfs(1,1);
	For(i,0,1) build(1,1,dep_val[i].size(),i);
	For(i,0,1) For(j,1,k) minL[i].insert(L[i][x[j]]),maxR[i].insert(R[i][x[j]]);
	int ans=0;
	For(j,1,k) 
	{
		For(i,0,1) minL[i].erase(minL[i].find(L[i][x[j]])),maxR[i].erase(maxR[i].find(R[i][x[j]]));
		int va=query(1,1,dep_val[0].size(),*minL[0].begin(),*maxR[0].rbegin(),0).second;
		int vb=query(1,1,dep_val[1].size(),*minL[1].begin(),*maxR[1].rbegin(),1).second;
		if (va>vb) ans++;
		For(i,0,1) minL[i].insert(L[i][x[j]]),maxR[i].insert(R[i][x[j]]);
	}
	printf("%d\n",ans);
	return 0;
}

J Journey

题目难度:easy

题目大意:给定一个城市有N个十字路口,除了右转,直行、左转和掉头都需要等红灯,求起点<s1,s2>到终点<t1,t2>最少等几次红灯。

其中<i,j>表示当前位于十字路口i->十字路口j的路上,从<i,j>到<j,i>需要调头。

数据范围:2<=N<=5e5

解题思路:每条路看做一个点,不同路通过十字路口连权值为0/1的边。求起点到终点的最短路。

赛时情况:

一开始用map来存点,然后跑spfa,第一发T

怀疑是spfa被卡了,改成map+双端队列,第二发WA

怀疑是双端队列有问题,比如说某个点可以从一个距离为3的点a花1代价到达,也可从令一个距离为3的点b花0代价到达。如果从到a到达被加入队列,在处理b的时候就不会再更新了,答案就会偏大。

(后来想想可以在处理b的时候也更新并加入队头,之后处理由a转移的那个点时直接continue即可)。

那么就用ε-closure的方式,每次入队时把该点走0边到达的点全部加入,采用map+普通队列,第三发T

怀疑是找右转时的%4比较慢,但%4应该会被优化成&3,那么问题应该出在map。

发现一共就4*N个点把map改成根据十字路口和方向重标号,再加上普通队列,第四发A

赛后总结:

一共有4N=2e6个点,每个点有4个方向,也就是8e6条边。

如果使用map,则复杂度大约为8e6 * 25 =2e8

限时7s,不太理解为什么map会超时,也许是评测机比较卡吧,第一发、第三发罚时也确实是难以避免。

不过第二发罚时理论上应该能避开的,不过考虑到赛场上有失误也正常,害

参考代码:

查看代码
 #include<iostream>
#include<cstdio>
#include<map>
#include<queue>
#define For(i,a,b) for(int i=a;i<=b;i++)
const int N=5e5+1000;
int c[N][5],n,s1,s2,t1,t2;
int dis[N*4];
std::queue<int>Q;
int id(int x,int y)
{
	return (x-1)*4+y;
}
void Add(int u)
{
	int first=u/4+1;int second=c[first][u%4];
	For(i,0,3) if (c[second][(i-1+4)&3]==first)
	{
		if (c[second][i]==0) continue;
		int v=id(second,i);
		if (!dis[v]) 
		{
			dis[v]=dis[u];
			Q.push(v);
			Add(v);
		}
	}	
}
int main()
{
	scanf("%d",&n);For(i,1,n) For(j,0,3) scanf("%d",&c[i][j]);
	scanf("%d%d%d%d",&s1,&s2,&t1,&t2);
	For(i,0,3) if (c[s1][i]==s2) 
	{
		int now=id(s1,i);
		dis[now]=1;
		Q.push(now);
		Add(now);
	}
	
	while (!Q.empty())
	{
		int u=Q.front();Q.pop();
		int first=u/4+1;int second=c[first][u%4];
//		printf("%d %d %d\n",u,first,second);
		For(i,0,3) 
		{
			if (c[second][i]==0||c[second][(i-1+4)&3]==first) continue;
			int v=id(second,i);
			if (!dis[v]) 
			{
				dis[v]=dis[u]+1;
				Q.push(v);
				Add(v);
			}
		}
	}
	For(i,0,3) if (c[t1][i]==t2)
		printf("%d\n",dis[id(t1,i)]-1);
	return 0;
}

H Hacker

题目难度:medium

题目大意: 给出长度为n的小写字符串A和k个长度为m的小写字符串B1…Bk,B的每个位置拥有统一的权值v1…vm。

对于每个Bi求最大区间和满足该区间构成的字符串是A的子串(空区间合法)。 

数据范围:n,m,k<=1e5,m*k<=1e6

解题思路:注意到可以用双指针形式求出对每一个Bi的开始位置,最长能和A匹配多远,然后再用线段树求最大子段和。

又发现双指针中,l+1即去掉首字母,等价于在SAM上跳fail,r+1即加上为字母,等价于在SAM上跳son,那么这题就是一道板子题了。

赛时经历:非常迅速地抄了板子,结果一交WA了。原本以为是线段树维护最大子段和有问题,后来发现问题出在SAM板子抄错了。。。

以后抄板子完了以后一定要和板子一行一行核对一下!

参考代码:

查看代码
 #include<iostream>
#include<cstdio>
#define For(i,a,b) for(int i=a;i<=b;i++)
const int N=1e5+1000;
int n,m,k;long long v[N];
char A[N],B[N];
namespace SAM
{
	const int M=2*N;
	int cnt=1,last=1;
	int len[M],size[M],pa[M],son[M][30];
	void insert(int c,int v)
	{
		int now=++cnt;len[now]=v;size[now]=1;
		for(;last&&!son[last][c];last=pa[last]) son[last][c]=now;
		if (!last) pa[now]=1;
		else
		{
			int last_son=son[last][c];
			if (len[last_son]==len[last]+1) pa[now]=last_son;
			else
			{
				++cnt;
				len[cnt]=len[last]+1;
				For(i,1,26) son[cnt][i]=son[last_son][i];
				for(;son[last][c]==last_son;last=pa[last]) son[last][c]=cnt;
				pa[cnt]=pa[last_son];
				pa[last_son]=cnt;
				pa[now]=cnt;
			}
		} 
		last=now;
	}
	int go(int &now,int c)
	{
		if (son[now][c]) return now=son[now][c],1;
		return 0;
	}
	void fail(int &now,int nowlen)
	{
		if (len[pa[now]]+1==nowlen) now=pa[now];
	}
}
long long max[3][4*N],sum[4*N];
void Build(int root,int l,int r)
{
	if (l==r) 
	{
		max[1][root]=max[2][root]=max[0][root]=std::max(0ll,v[l]);
		sum[root]=v[l];
		return ;
	}
	int mid=(l+r)>>1;
	Build(root<<1,l,mid);Build(root<<1|1,mid+1,r);
	max[0][root]=std::max(std::max(max[0][root<<1],max[0][root<<1|1]),max[2][root<<1]+max[1][root<<1|1]);
	max[1][root]=std::max(max[1][root<<1],sum[root<<1]+max[1][root<<1|1]);
	max[2][root]=std::max(max[2][root<<1|1],sum[root<<1|1]+max[2][root<<1]);
	sum[root]=sum[root<<1]+sum[root<<1|1];
}
void query(int root,int l,int r,int a,int b,long long &maxt,long long &maxl,long long &maxr,long long &sumt)
{
//	printf("?? %d %d %d %d %d\n",root,l,r,a,b);
	if (l==a&&r==b) 
	{
		maxt=max[0][root];
		maxl=max[1][root];
		maxr=max[2][root];
		sumt=sum[root];
		return ;
	}
	int mid=(l+r)>>1;
	if (b<=mid) return query(root<<1,l,mid,a,b,maxt,maxl,maxr,sumt);
	else
	{
		if (a>mid) return query(root<<1|1,mid+1,r,a,b,maxt,maxl,maxr,sumt);
		else 
		{
			long long maxt1,maxt2,maxl1,maxl2,maxr1,maxr2,sumt1,sumt2;
			query(root<<1,l,mid,a,mid,maxt1,maxl1,maxr1,sumt1);
			query(root<<1|1,mid+1,r,mid+1,b,maxt2,maxl2,maxr2,sumt2);
			maxt=std::max(std::max(maxt1,maxt2),maxr1+maxl2);
			maxl=std::max(maxl1,sumt1+maxl2);
			maxr=std::max(maxr2,sumt2+maxr1);
			sumt=sumt1+sumt2;
		}
	}
}
int main()
{
	scanf("%d%d%d",&n,&m,&k);
	scanf("%s",A+1);For(i,1,n) SAM::insert(A[i]-'a'+1,i);
	For(i,1,m) scanf("%lld",&v[i]);Build(1,1,m);
	For(i,1,k)
	{
		scanf("%s",B+1);
		int now=1,r=0;long long ans=0;
		For(l,1,m)
		{
			if (r<l) now=1,r=l-1;while (r<m&&SAM::go(now,B[r+1]-'a'+1)) r++;
			if (r<l) continue;
			long long maxt=0,maxl=0,maxr=0,sumt=0;
			query(1,1,m,l,r,maxt,maxl,maxr,sumt);
			ans=std::max(ans,maxt);
			SAM::fail(now,r-l+1);
		}
		printf("%lld\n",ans);
	}
	return 0;
}

F Fief

题目难度:medium

题目大意:给定一个n个点m条边的无向图,q个询问每次询问两点x, y,求是否存在一个n的排列,使得第一个元素为x,最后一个元素为y,且排列的任意一个前缀、任意一个后缀都连通。

赛时经历:

首先如果原图是一条链且x,y是其两端那么就肯定可行。

又猜测这题和割点有关,那么就先用tarjan求出强联通分量。

一开始以为问题等价于x->y有经过所有点恰好一次的哈密顿路径,但是后来发现不用这么强的条件,每次往X(x所在连通图)新加的点b是未必是由之前的最后一个点走一步到达的,可能是更前面的点走一步到达。

然后猜测如果将新图按照强联通分量缩点后是一条链然后x,y在其两端肯定可行。

 

 


D Directed

题目难度:medium-hard

posted @   th-is  阅读(97)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~
点击右上角即可分享
微信分享提示