2021CCPC网络赛(重赛)题解

自己还是太菜了,五个小时一个题也没磕出来,还是队友牛逼!...

Primality Test

先看这个题,可能一上去就被(frac{f(x)+f(f(x))}{2})向下取整吓住了,但仔细想想,(f(x))(f(f(x)))不是相邻的质数吗?那么除2,向下取整,落点一定在两者之间,那么一定是合数。当然除了2,3的特例,这种特判下即可。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
int main()
{
	int T;cin>>T;
	while(T--)
	{
		ll x;cin>>x;
		if(x==1) cout<<"YES"<<endl;
		else cout<<"NO"<<endl;	
	}
	return 0; 
} 

Nun Heh Heh Aaaaaaaaaaa

为什么这么简单的题,我当初看了那么长的时间都没看出来。
发现这个题的主要难点在于以当前i(c[i]='h')为结尾的字串为nunhehheh的方案数。我们大胆的设状态,仔细考虑dp所代表的的集合,以及进行转移。设f[i][j]表示前i位,其中第i位匹配到nunhehheh的第j位的方案数。那么若c[i]=s[j],我们考虑枚举上一个字符也就是s[j-1]出现的位置,f[i][j]=f[k][j-1].其中c[k]==s[j-1].但这种方法显然是O(n^2)的。考虑转移时,其实f[k][j-1],只要是j-1即可,我们大可以用一个数组g[j]表示前i个字符中以某个点为j结尾的方案数。这样的话转移时就是O(1)的。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e5+10,P=998244353;
int T,n,f[N][12],cnt[N],g[12];
char c[N];
string s; 
inline ll power(ll x,ll y)
{
	ll ans=1;
	while(y)
	{
		if(y&1) ans=ans*x%P;
		y>>=1;
		x=x*x%P;
	}
	return ans%P;
}

inline void clear()
{
	for(int i=0;i<=n+1;++i) 
	{
		cnt[i]=0;
		for(int j=0;j<=10;++j) f[i][j]=0;	
	}
	memset(g,0,sizeof(g));
}

int main()
{
	//freopen("1.in","r",stdin);
	scanf("%d",&T);
	s="2nunhehheh";
	while(T--)
	{
		scanf("%s",c+1);
		n=strlen(c+1);
		clear();
		for(int i=n;i>=1;--i) 
		{
			cnt[i]=cnt[i+1];
			if(c[i]=='a') cnt[i]++;
		}
		ll ans=0;
		for(int i=1;i<=n;++i)
		{
			for(int j=9;j>=1;--j)//匹配nunhehheh的每一位。 
			{
				if(c[i]==s[j])
				{
					f[i][j]=g[j-1];
					if(j==1) f[i][j]=1;
					g[j]=(g[j]+f[i][j])%P;
					if(j==9) ans=(ans+(ll)f[i][j]*(power(2,cnt[i])-1)%P)%P;
				}
			}
		}
		printf("%lld
",ans);
	}
	return 0;
}

Monopoly

哎呀,这个题,感觉自己当时的思路已经很接近了,但是还是输给了对于分类讨论的复杂性的恐惧,加上当时没有一个完整的思路进行支撑,就弃疗了。
首先我们设(S)为整个序列的和,(s_i)为第i位的前缀和,那么可以有这个式子(s_i+kS=x(k>=0,1leq ileq n)),其中只有(S,x)已知,我们做适当的调整,(kS=x-s_i),这样的话(x-s_i)必须是(S)的正整数(以及0)的倍数。那么可以想到(x,s_i)(S)同余,也就是说余数相同。这样的话,我们可以对(s_i)根据对(S)的余数进行分类,每次询问的x只在模数相同的一类中找。接下来考虑最小化(i+k*n)的值,这个时候我们可以讨论S>0,那么为了满足倍数的关系,必须满足(s_ileq x),同时为了k足够小,我们需要找到的(s_i)足够大,(你把这个过程放在数轴上想)。也就是小于等于x的最大值。这不是二分吗?我们再在每一类中进行排序,直接二分查找即可。接下来考虑S<0的情况,我们可以将序列中的每个数取反,再将x取反即可。注意当S=0时,系统会报错,单独讨论这种情况。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e5+10; 
int T,n,m,num;
map<ll,int>mp;//map表示序列号。 
map<ll,int>id[N];//余数为i的数字为j的最小的i。 
vector<ll>ve[N];
ll s[N];

inline void clear()
{
	mp.clear();num=0;
	for(int i=0;i<=n;++i) 
	{
		s[i]=0;
		id[i].clear();	
		ve[i].clear();
	}
}

inline void solve()
{
	mp[0]=0;
	for(int i=1;i<=n;++i)
	{
		if(mp.find(s[i])==mp.end()) 
			mp[s[i]]=i;
	}
	for(int i=1;i<=m;++i)
	{
		ll x;scanf("%lld",&x);
		if(mp.find(x)==mp.end()) puts("-1");
		else printf("%d
",mp[x]);
	}
}

int main()
{
//	freopen("1.in","r",stdin);
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d%d",&n,&m);
		clear();
		for(int i=1;i<=n;++i)
		{
			ll x;scanf("%lld",&x);
			s[i]=s[i-1]+x;
		}
		if(s[n]==0) {solve();continue;}
		int op=1;if(s[n]<0) op=-1;
		ll S=s[n]*op;
		for(int i=0;i<=n;++i)
		{
			s[i]*=op;
			ll ps=(s[i]%S+S)%S;
			if(mp.find(ps)==mp.end()) mp[ps]=++num;
			if(id[mp[ps]].find(s[i])==id[mp[ps]].end()) 
			{
				id[mp[ps]][s[i]]=i;
				ve[mp[ps]].push_back(s[i]);
			}
		}
		for(int i=1;i<=num;++i) sort(ve[i].begin(),ve[i].end());
		for(int i=1;i<=m;++i)
		{
			ll x;scanf("%lld",&x);
			x*=op;
			if(x==0) {puts("0");continue;}
			ll ps=(x%S+S)%S;
			if(mp.find(ps)==mp.end()||ve[mp[ps]][0]>x) {puts("-1");continue;}
			int j=mp[ps];
			int k=upper_bound(ve[j].begin(),ve[j].end(),x)-ve[j].begin()-1;
			ll so=ve[j][k];
			ll ans=id[j][so]+((x-so)/S)*n;
			printf("%lld
",ans);
		}
	}
	return 0;
}

Jumping Monkey

这个题当初还是队友做出来的,自己主要是思路就没想到那一块去。由于每次从一个点出发一直跳,跳的最多的点的个数,想想其实问你的就是从这个点出发能跳到哪些点去,因为这些点我们可以按点权从小到大排序依次跳就完事了。当初困在了DP的思维上,想不出来好的转移方式。后来看了看题解大大的做法,其实是图论的知识吧,也不对就是思维的问题吧。我们考虑先将所有的点从小到大排序,依次考虑每个点能否到达点i,这样点i一定比之前的点权大,只要联通即可。也就是重新建图加边的问题。考虑当前是一个空白的图,我们依次将每个点加进去,考虑哪些点能到达当前这个点的话,就是连通块的问题,若当前点能够连到某个连通块,则这个连通块内的所有点都能到达这个点,那么这些点的答案都加1.然后将这个点及其连到的点合并成一个新的连通块即可。考虑整个需要我们维护的操作就是加上点,连通块整个的值加1,合并连通块。对于连通块的做法,我只会并查集,其实并查集就是维护连通块是否联通的问题,顺带记录一些信息。接下来就是这个题的精妙之处,我们发现,我们每次都是将一个连通块的值都加1,我们可以将加进去的点i当做根节点,这样的话那些连通块的深度就加1,符合我们的要求,我们只需要将点i向原本这个连通块的根节点连边即可。但这样不就打乱了原本的土的结构了吗?其实我们没必要维护原本的土的结构,我们还需要在一个点i加进去之后,那些连通块的点都可以加1就行。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int T,n,f[N],a[N],b[N],d[N];
vector<int>son[N]; 
vector<int>bian[N];
map<pair<int,int>,bool>mp;

inline void clear()
{
	mp.clear();
	for(int i=1;i<=n;++i) 
	{
		son[i].clear();
		bian[i].clear();
		f[i]=i;b[i]=i;	
		d[i]=0;	
	}
}

inline bool cmp(int x,int y) {return a[x]<a[y];}

inline int getf(int x) {return f[x]==x?x:f[x]=getf(f[x]);}

inline void dfs(int x)
{
	for(auto y:bian[x])
	{
		d[y]=d[x]+1;
		dfs(y);
	}
}

int main()
{
//	freopen("1.in","r",stdin);
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d",&n);
		clear();
		for(int i=1;i<n;++i)
		{
			int x,y;scanf("%d%d",&x,&y);
			son[x].push_back(y);
			son[y].push_back(x);
		}
		for(int i=1;i<=n;++i) scanf("%d",&a[i]);
		sort(b+1,b+n+1,cmp);
		for(int i=1;i<=n;++i)//依次加入每一个点。
		{
			int x=b[i];
			for(auto y:son[x])//遍历i的每一个出边
			{
				if(a[y]>a[x]) continue;
				int t=getf(y);
				if(mp.find({x,t})==mp.end())
				{
					mp[{x,t}]=1;
					bian[x].push_back(t);
					f[t]=x;
				}
			} 
		}
		d[b[n]]=1;
		dfs(b[n]);
		for(int i=1;i<=n;++i) printf("%d
",d[i]);
	}
	return 0;
} 

Public Transport System

这个题当初也是想了好久没出来,显然因为如果(a_i>a_{i-1})的话我们走当前的边权的值就为(a_i-b_i),这就使得我们必须记录当前点的上一条边是哪一条,想了想,其实所有的状态数也不多,即m个状态,毕竟只有m条边,每条边对应一个状态。但这个记录就只能用map实现,如果再跑dijkstra总的复杂度为O(mlognlogm),m的范围为(1.2 imes10^6),这算出来,(6 imes10^9),好吧,我觉得出题人肯定就是专门卡这个暴力的....
瞅瞅题解大大的做法,原来是要重新建图,又是建图的问题,这种题不是只在网络流中考察吗?好吧,这次确实拓了眼界。首先先将两种边分开,因为两种边权的图,毕竟没有单边权的方便。考虑边权(a_i)的边,我们没什么限制条件,但对于边权为(a_i-b_i)的边,我们必须满足一定条件,也就是前一条边的(a_i)必须小于当前的(a_i)。我们可以发现由于一个之前的边对应的可走的边权为(a_i-b_i)的边的(a_i)是在一个区间的,所以这就给我们的优化带来了可能。(不是我说的,是题解说的。)具体的,我们可以这样做:首先设d为当前点x的出度,我们将x拆分成d+1个点,((x_0,x_1,x_2,...,x_d)),让他们分别管理这些(a_i-b_i)出边,其中所有边权为(a_i)的边由(x_0)管理。我们将x所有边权为(a_i-b_i)的出边按照(a_i)的从大到小排序,接下来将他们依次交给(x_i)管理(也就是(x_i)连排过序后为i的边)。我们接下来i从0到d-1,从(x_{i+1})(x_{i})连边权为0的边。这样由于提前排过序,能从小的(a_i)(a_i-b_i)的特殊边,一定能从大的(a_i)(a_i-b_i)的特殊边。之后我们只需要根据每个入边找到相对应能走的最大的(x_i),满足它所管理的边的(a_i)大于他的入边即可。
具体的如下图:
image
image
这样,一共的点为n+m,一共的边为2*m,即使m是(1.2 imes 10^6)的数据量,我们跑dijkstra也完全没有问题。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=4e5+10;
int du[N],n,m,l[N],link[N],tot,vis[N];
ll dis[N];
//l[i]记录每个点拆出来的d+1个点的第一个点的编号。 
vector<int>son[N];//记录每个点,对应的边的编号。 
struct bian{int x,y,A,B;}b[N];
struct wy{int y,v,next;}a[N<<1];
priority_queue<pair<ll,int> >q;

inline void add(int x,int y,int v)
{
	a[++tot].y=y;a[tot].v=v;a[tot].next=link[x];link[x]=tot;
}

inline void clear()
{
	tot=0;
    for(int i=1;i<=n+m;++i) link[i]=0;
    for(int i=1;i<=n;++i) son[i].clear(),du[i]=0;
}

inline bool cmp(int x,int y)
{
	return b[x].A>b[y].A;
}

inline int find(int x,int A)//有一个ai的入边应该连向点x中的哪个点
{
	if(!du[x]||A>=b[son[x][0]].A) return l[x];//返回x0的情况。
	int L=0,R=du[x]-1;
	while(L<R)//在x的边中查找>A的最小的边。 
	{
		int mid=L+R+1>>1;
		if(b[son[x][mid]].A>A) L=mid;
		else R=mid-1;
	}
	return l[x]+L+1;
} 

inline void dijkstra()
{
	while(q.size()) q.pop();
	for(int i=1;i<=n+m;++i)
	{
		dis[i]=1e18;
		vis[i]=0;
	}
	dis[l[1]]=0;q.push({0,l[1]});
	while(!q.empty())
	{
		int x=q.top().second;q.pop();
		if(vis[x]) continue;
		vis[x]=1;
		for(int i=link[x];i;i=a[i].next)
		{
			int y=a[i].y;
			if(dis[x]+a[i].v<dis[y])
			{
				dis[y]=dis[x]+a[i].v;
				q.push({-dis[y],y});
			}
		}
	}
}

int main()
{
//	freopen("1.in","r",stdin);
	int T;scanf("%d",&T);
	while(T--)
	{
		scanf("%d%d",&n,&m);
		clear();
		for(int i=1;i<=m;++i)
		{
			scanf("%d%d%d%d",&b[i].x,&b[i].y,&b[i].A,&b[i].B);
			son[b[i].x].push_back(i);
			du[b[i].x]++;
		} 
		for(int i=1;i<=n;++i)
		{
			if(son[i].size()) 
				sort(son[i].begin(),son[i].end(),cmp);
		}
		int num=0;
		for(int i=1;i<=n;++i)  //给每个点分配编号且处理内部的边。
		{
			l[i]=++num; //l[i] - l[i]+du[i]是这个点所有拆出来的点的编号。 
			for(int j=du[i];j>=1;--j) add(l[i]+j,l[i]+j-1,0);
			num+=du[i];
		} 
		for(int i=1;i<=n;++i)//处理点i的所有边
		{
			int js=son[i].size();
			for(int j=0;j<js;++j)//枚举i的所有出边 
			{
				int id=son[i][j];
				int y=find(b[id].y,b[id].A);
				add(l[i],y,b[id].A);//先处理边权为ai的。 
				add(l[i]+j+1,y,b[id].A-b[id].B);
			}
		} 
		dijkstra();
		for(int i=1;i<=n;++i) 
		{
			if(dis[l[i]]==1e18) dis[l[i]]=-1;
			printf("%lld",dis[l[i]]);	
			if(i!=n) printf(" "); 
		}
		printf("
");
	}
	return 0;
}
原文地址:https://www.cnblogs.com/gcfer/p/15398628.html