2021 ICPC Gran Premio de Mexico 2da Fecha部分题题解

前面的水题,在队友的配合下,很快就拿下了,剩下几道大毒瘤题,一直罚座三个小时,好让人自闭...但不得不说,这些题的质量是真的高!

H. Haunted House

首先看这个题,大眼一扫,觉得是某种数据结构的题,之后就把思考的重心放在了如何快速判断某个人会在哪个屋子遇到鬼上,由于鬼的活跃期会不定时的更新(指的是当一个鬼吓到一个人之后就会立即休眠,它的周期也会随即发生变化),发现这个东西真的很难维护,让人找鬼感觉做不了。...害,思路一旦僵住之后,路就走窄了...其实我们可以再看看这个题的数据范围,因为一个题的数据范围的特性往往会提示本题的解决方法。我们发现一个事情,首先每个鬼的精力是一个排列,如果按照我们刚才的想法,完全没有这个必要啊。还有发现人进来的时刻\(t\leq10^5\),为什么这个时间也给得这么小,如果在最后的算法和时间t没关系的话,大可以给个\(1e9\)啊。所以这个时候,我们就可以尝试转换下思路。维护鬼的状态,有点多,不妨我们维护人,让鬼去抓人。人的信息很简单就是进来的时间,我们对鬼进行操作。这个时候就要思考怎么让鬼抓人,如果我们总的去枚举时间的话,那么同一时刻会有多个人同时进入不同的屋子,这个貌似也很难搞。相比较于人一个一个的进入,我们不妨将所有人一起进入,只是他们的时间不同了。也就是说之前我们的想法就是他们一个一个的走全部的屋子,现在我们要让他们一起走第一个屋子,一起走第二个屋子,...这样的话,对于鬼的状态我们就能很轻松的维护。他们一起走,第一个鬼拦住一部分人,第二个鬼拦住一部分人,...直到最后一个鬼。思考发现这和题意是等价的。我们枚举每个鬼的活跃期,进行阻拦,然后,如果有人在其活跃期内,则直接更新该鬼的状态。发现每个鬼的活跃期的个数为\(\frac{m}{x}\),m表示我们枚举总的时间,注意x是个排列,那么这里的复杂度就为\(mlogm\),这里的m为总的时间,m取2e,用set再方便不过了,总的复杂度就为\(MlogMlogN\).

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m,p[N],t[N],ro[N],ts[N];
set<pair<int,int> >st;
int main()
{
//	freopen("1.in","r",stdin);
	scanf("%d%d",&n,&m);
	for(int i=0;i<n;++i) scanf("%d",&p[i]);
	for(int i=0;i<m;++i) 
	{
		scanf("%d",&t[i]);
		st.insert({t[i],i});	
	}
	for(int i=0;i<n;++i)//哪个人会被第i个鬼吓到
	{
		int cur=0;//记录当前鬼开始活跃的时间,最初是从0开始活跃的。 
		while(true)//不断枚举鬼的活跃的时间,阻拦人。 
		{
			auto it=st.lower_bound({cur-i,-1});//人走i秒到当前屋子,相当于鬼的活跃-i。
			if(it==st.end()) break;//鬼开始活跃的时间超出了所有人到达的时间。直接退出。 
			pair<int,int>x=*it;
			if(x.first<=cur+p[i]-1-i)//x到达时,鬼还在活跃期内。 当前这个人被鬼吓。 
			{
				st.erase(it);//从人群中剔除。
				ro[x.second]=i;
				ts[x.second]=x.first+i;
				cur=x.first+i+p[i]+1;
			}
			else//鬼在这个活跃期内没有吓到人。 
			{
				cur+=2*p[i];
			}
		} 
	}
	for(auto x:st) ro[x.second]=ts[x.second]=-1;
	for(int i=0;i<m;++i) printf("%d %d\n",ro[i],ts[i]);
	return 0;
}

A. Alice Birthday

这个题的题意也是很简单的,给你一个无向图,每个边都可以删或不删,问最后的连通块为i的方案数,都输出出来?(\(1\leq i\leq n\))
首先观察题目的数据范围,N最大是14,这铁定状压(啥,你说爆搜,这档次的题应该不会考吧...)
接下来的问题是怎么状压,由于本题和连通块的个数有关,并且要用到状压,我们可以想到用f[s][j]表示状态s下,连通块的个数为j的方案数。考虑怎么转移。这个题真的加深了我对DP的理解,每个状态的实质就是一堆有某个共同性质的集合。我们转移时可以考虑根据某个规则,将该集合内的所有状态分成不同的组,这样就实现了状态的转移。比如说这个题,我们对集合S而言,我们只考虑编号最小的点所在的连通块,于是我们按照编号最小的点所在的不同连通块的集合为规则,划分这个集合。思考:当前集合的任何一个合法的状态都是可以根据编号最小的点进行枚举的,且不重复,我们设t为s的子集且t包含了s中编号最小的点,那么\(f[s][j]=\sum f[s异或t][j-1]*g[t]\);其中\(g[t]\)表示当前状态为t时,为一个连通块的方案数。考虑这个怎么搞,正难则反,我们考虑当前状态下,不连通的方案数,同样的我们考虑编号最小的点所在的连通块,肯定的是这个连通块,肯定没有和其他的点全部联通,我们考虑枚举所有的子集t且包含编号最小的点,我们可以是这个连通块联通,剩下的点之间的边都可选可不选,还是按照编号最小的点所在的连通块的状态来分组。至于所有状态枚举子集的复杂度为\(O(3^n)\)此题,我怀疑14就是这么来的。...

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=20010,P=998244353;
int n,m,a[21][21],xp[N];
ll f[N][21],g[N],has[N];
int main()
{
//	freopen("1.in","r",stdin);
	scanf("%d%d",&n,&m);
	xp[0]=1;
	for(int i=1;i<=m;++i) xp[i]=xp[i-1]*2%P;
	for(int i=1;i<=m;++i)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		x--;y--;
		a[x][y]=a[y][x]=1;
	}
	for(int S=1;S<(1<<n);++S)//处理g[S]. 
	{
		for(int j=0;j<n;++j) if(S&(1<<j))
			for(int k=j+1;k<n;++k) if(S&(1<<k)) 
			{
				if(a[j][k]) has[S]++;
			}
		g[S]=xp[has[S]];//先算出总的方案数。 
		int u=S&(-S);
		for(int t=(S-1)&S;t;t=(t-1)&S)
		{
			if(t&u)//必须包含编号最小的点。 
			{
				g[S]=(g[S]-g[t]*xp[has[S^t]])%P; 
				g[S]=((g[S]+P)%P+P)%P;
			}
		}
	}
	for(int S=1;S<(1<<n);++S)
	{
		for(int j=1;j<=n;++j)
		{
			if(j==1) f[S][j]=g[S];
			else
			{
				int u=S&(-S);
				for(int t=(S-1)&S;t;t=(t-1)&S)
				{
					if(t&u)//必须包含编号最小的点。 
					{
						f[S][j]=(f[S][j]+f[S^t][j-1]*g[t])%P;
					}
				}
			}
		} 
	} 
	for(int i=1;i<=n;++i) printf("%lld\n",f[(1<<n)-1][i]);
	return 0;
} 
posted @ 2021-10-25 16:36  逆天峰  阅读(241)  评论(0编辑  收藏  举报
作者:逆天峰
出处:https://www.cnblogs.com/gcfer//