[正睿集训2021] 杂题再讲

你没看错就是 2021 年的题,他就是诈尸了。

Bank Security Unification

题目描述

点此看题

给定长度为 \(n\) 的数列,希望您从中选出一个子序列,使得相邻两项按位与之和最大。

\(2\leq n\leq 10^6,a_i\leq 10^{12}\)

解法

首先不难想到一个 \(dp\),设 \(dp[i]\) 表示考虑前 \(i\) 个数,子序列的结尾是 \(a_i\) 的最大权值。

考虑这样一个 \(\tt observation\):如果 \(a_j\)\(a_k\) 的最高位相同,并且 \(j<k\)那么从 \(k\) 转移一定比从 \(j\) 转移更优,这是因为 \(a_i\and a_k+a_k\and a_j\geq a_i\and a_j\)

这说明了对于每个数位我们只需要保留离 \(i\) 最近的一个数,把他们存下来暴力转移时间复杂度 \(O(n\log n)\)

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 1000005;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,a[M],p[M],dp[M];
signed main()
{
	n=read();
	for(int i=1;i<=n;i++)
	{
		a[i]=read();dp[i]=dp[i-1];
		for(int j=0;j<40;j++) if(p[j])
			dp[i]=max(dp[i],dp[p[j]]+(a[i]&a[p[j]]));
		for(int j=0;j<40;j++)
			if(a[i]&(1ll<<j)) p[j]=i;
	}
	printf("%lld\n",dp[n]);
}

黎明前的巧克力

题目描述

点此看题

解法

选出两个集合其实有点复杂,我们考虑略微把问题转化一下(我竟然做出了这步转化!):找出所有异或和为 \(0\) 的集合 \(S\),每个集合的贡献是 \(2^{|S|}\),求出贡献和。

显然可以套集合幂级数,考虑最后答案的集合幂级数是:

\[\prod_{i=1}^n (2x^{\{a_i\}}+1) \]

考虑复杂度瓶颈在对于每个 \(a_i\) 都要单独 \(\tt fwt\) 一遍,但是考虑到集合幂级数的形式是单一的,我们可以考虑模拟正变换的过程,对于每个位置,\(1\) 的贡献一定是 \(1\)\(2x^{\{a_i\}}\) 的贡献是 \(2/-2\)

那么考虑最后乘起来结果的每一位一定是形如 \((-1)^k\cdot 3^{n-k}\),我们只要知道 \(k\) 就能知道这一位上的取值,考虑求出 \(\tt fwt\) 正变换的和 \(f_i\),那么 \(-k+3(n-k)=f_i\)\(k=\frac{3n-f_i}{4}\)

又因为正变换的和等于和的正变换,所以只需要做一次正变换,最后再 \(\tt fwt\) 回去即可。答案就是 \(x^{\empty}\) 这一位的系数,因为要除去都是空集的情况所以要减 \(1\),时间复杂度 \(O(n\log n)\)

总结

若干个简单的集合幂级数相乘可以考虑模拟 \(\tt fwt\)

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 2000005;
const int MOD = 998244353;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,inv2,inv4,f[M],pw[M];
int qkpow(int a,int b)
{
	int r=1;
	while(b>0)
	{
		if(b&1) r=r*a%MOD;
		a=a*a%MOD;
		b>>=1;
	}
	return r;
}
void fwt(int *a,int n,int op)
{
	for(int i=1;i<n;i<<=1)
		for(int p=i<<1,j=0;j<n;j+=p)
			for(int k=0;k<i;k++)
			{
				int x=a[j+k],y=a[i+j+k];
				a[j+k]=(x+y)%MOD;
				a[i+j+k]=(x-y+MOD)%MOD;
				if(op==-1)
				{
					a[j+k]=a[j+k]*inv2%MOD;
					a[i+j+k]=a[i+j+k]*inv2%MOD;
				}
			} 
}
signed main()
{
	n=read();m=1<<20;pw[0]=1;
	inv2=(MOD+1)/2;inv4=qkpow(4,MOD-2);
	for(int i=1;i<=n;i++) f[0]++,f[read()]+=2;
	for(int i=1;i<=n;i++) pw[i]=pw[i-1]*3%MOD;
	fwt(f,m,1);
	for(int i=0;i<m;i++)
	{
		int x=f[i],k=(3*n-x+MOD)%MOD*inv4%MOD;
		f[i]=(k&1)?(MOD-pw[n-k]):pw[n-k];
	}
	fwt(f,m,-1);
	printf("%lld\n",(f[0]+MOD-1)%MOD);
}

Grafting

题目描述

点此看题

解法

无根树问题可以优先考虑定根,这已经是我第三次写下这句话了,可是我还是没有用好它。

那么本题如何定根了,考虑到每个点只能被操作一次,可以把第一次操作的点当成根定下来。这里可以 \(O(n^2)\) 地枚举第一个点和它的操作,然后在原树和目标树都以它为根建树。

有根之后操作就简单许多了,首先考虑必要条件,如果某个点在两棵树中父亲不相同,那么它是必须要操作的,并且由于每个点只能操作一次其他点是一定不能操作的,答案下界就出来了。

答案下界出来了还要判断不合法,两棵树中如果儿子不需要操作但是父亲需要操作就一定不合法。此外关于操作顺序,在原树中儿子一定先于父亲操作,在目标树中父亲一定先于儿子操作,那么我们把图建出来跑拓扑,如果出现环就不合法,否则拓扑序就是我们的操作顺序。

时间复杂度 \(O(Tn^3)\),时刻注意清空的问题。

#include <cstdio>
#include <vector>
#include <iostream>
#include <queue>
using namespace std;
const int M = 55;
const int inf = 0x3f3f3f3f;
#define pb push_back 
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int T,n,fl,ans,da[M],fa[M],fb[M],in[M],op[M];
vector<int> A[M],B[M],g[M];
void dfs(int u,int fa,vector<int> *G,int *p)
{
	p[u]=fa;
	for(int v:G[u]) if(v^fa) dfs(v,u,G,p);
}
int topo()
{
	int res=0,cnt=0;queue<int> q;
	for(int i=1;i<=n;i++)
	{
		res+=(op[i]=fa[i]!=fb[i]);
		in[i]=0;g[i].clear();
	}
	for(int i=1;i<=n;i++)
		if((!op[i] && op[fa[i]]) || (!op[i] && op[fb[i]]))
			return inf;
	for(int i=1;i<=n;i++) if(op[i])
	{
		if(op[fa[i]]) g[i].pb(fa[i]),in[fa[i]]++;
		if(op[fb[i]]) g[fb[i]].pb(i),in[i]++;
	}
	for(int i=1;i<=n;i++)
		if(!in[i]) q.push(i);
	while(!q.empty())
	{
		int u=q.front();q.pop();cnt++;
		for(int v:g[u]) if(!--in[v]) q.push(v);
	}
	return cnt<n?inf:res;
}
void work()
{
	n=read();ans=inf;fl=1;
	for(int i=1;i<=n;i++)
	{
		da[i]=fa[i]=fb[i]=0;
		A[i].clear();B[i].clear();
	}
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read();
		A[u].pb(v);A[v].pb(u);
		da[u]++;da[v]++;
	}
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read();
		B[u].pb(v);B[v].pb(u);
	}
	dfs(1,0,A,fa);dfs(1,0,B,fb);
	for(int i=1;i<=n;i++) if(fa[i]!=fb[i]) {fl=0;break;}
	if(fl) {puts("0");return ;}
	for(int i=1;i<=n;i++) if(da[i]==1)
	{
		dfs(i,0,B,fb);
		for(int j=1;j<=n;j++) if(i!=j)
		{
			dfs(j,0,A,fa);fa[j]=i;fa[i]=0;
			ans=min(ans,topo()+1);
		}
	}
	printf("%d\n",ans>n?-1:ans);
}
signed main()
{
	T=read();
	while(T--) work();
}

Construction of a tree

题目描述

点此看题

解法

考虑必要条件,对于任意集族 \(\mathcal S\),记 \(f(\mathcal S)\) 表示集族 \(\mathcal S\) 包含的点集大小,那么有解的必要条件是:\(\forall \mathcal S,f(\mathcal S)\leq |\mathcal S|-1\),如果不满足该条件显然一定会构成环。

发现这东西很像 \(\tt Hall\) 定理,我们先把点和集合跑个二分图匹配,如果不存在完美匹配则一定无解,如果存在完美匹配那么可以保证:\(\forall \mathcal S,f(\mathcal S)\leq |\mathcal S|\)

然后我们拿必要条件来构造(注意并不是 \(\tt Hall\) 定理的条件,所以后面还会出现无解的情况),找到未匹配的点,记他为 \(rt\),那么从 \(rt\) 开始 \(dfs\),搜索与点 \(u\) 邻接的集合,然后找到集合对应的匹配点 \(v\),那么 \((u,v)\in E\),但是问题来了,如果搜索中途终止是否能说明无解?

考虑搜索中途终止的条件,被访问过的点的个数等于被访问过的集合个数\(+1\),考虑没被访问过集族 \(\mathcal T\) 一定满足:\(f(\mathcal T)\geq |\mathcal T|\),由于这东西是必要条件所以搜索终止就意味着无解。

总结

第一步思考很重要,我称之为类 \(\tt Hall\) 模型:找必要条件的堆砌。

可以逐步逼近我们想要的条件,并不需要一步到位。

#include <cstdio>
#include <iostream>
#include <queue>
using namespace std;
const int M = 200005;
const int inf = 0x3f3f3f3f;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,S,T,tot,f[M],cur[M],d[M],vis[M],ea[M],eb[M];
struct edge{int v,c,next;}e[M<<3];
void add(int u,int v,int c)
{
	e[++tot]=edge{v,c,f[u]},f[u]=tot;
	e[++tot]=edge{u,0,f[v]},f[v]=tot;
}
int bfs()
{
	queue<int> q;q.push(S);d[S]=1;
	for(int i=1;i<=T;i++) d[i]=0;
	while(!q.empty())
	{
		int u=q.front();q.pop();
		if(u==T) return 1;
		for(int i=f[u];i;i=e[i].next)
		{
			int v=e[i].v;
			if(!d[v] && e[i].c>0)
			{
				d[v]=d[u]+1;
				q.push(v);
			}
		}
	}
	return 0;
}
int dfs(int u,int ept)
{
	if(u==T) return ept;
	int tmp=0,flow=0;
	for(int i=f[u];i;i=e[i].next)
	{
		int v=e[i].v;
		if(d[v]==d[u]+1 && e[i].c>0)
		{
			tmp=dfs(v,min(ept,e[i].c));
			if(!tmp) continue;
			ept-=tmp;flow+=tmp;
			e[i].c-=tmp;e[i^1].c+=tmp;
			if(!ept) break;
		}
	}
	return flow;
}
void work(int u)
{
	for(int i=f[u];i;i=e[i].next)
	{
		int v=e[i].v,o=v-n;
		if(v==0 || vis[v]) continue;
		vis[v]=1;
		for(int j=f[v];j;j=e[j].next)
			if(e[j].c && e[j].v!=T)
			{
				m++;ea[o]=u;eb[o]=e[j].v;
				work(e[j].v);
			}
	}
}
signed main()
{
	n=read();tot=1;T=n<<1;
	for(int i=1;i<n;i++)
	{
		int k=read();add(i+n,T,1);
		while(k--) add(read(),i+n,1);
	}
	for(int i=1;i<=n;i++) add(S,i,1);
	int ans=0,rt=0;
	while(bfs())
	{
		for(int i=S;i<=T;i++) cur[i]=f[i];
		ans+=dfs(S,inf);
	}
	if(ans<n-1) {puts("-1");return 0;}
	for(int i=f[0];i;i=e[i].next)
		if(e[i].c) rt=e[i].v;
	work(rt);
	if(m!=n-1) {puts("-1");return 0;}
	for(int i=1;i<n;i++) printf("%d %d\n",ea[i],eb[i]);
}

Candy Piles

题目描述

点此看题

解法

首先有一个 \(\tt observation\):如果我们确定了操作 \(1,2\) 的次数,那么剩下具体每堆糖果都是可以确定的。这说明我们可以设计一个 \(O(n^2)\)\(dp\),设 \(f(i,j)\) 表示进行了 \(i\) 次一操作,进行了 \(j\) 次二操作,先手必胜还是先手必败。

直接优化很困难,我们可以\(dp\) 形象化,也就是放在平面坐标系上。那么向右走就代表删去最大的一堆,向上走就代表所有堆都拿走一个,我们把 \(a_i\) 从大到小排序之后放上去:

uxite0.png

如上图,边界一定是必胜态,如果向上和向右都必胜则该点必败,如果向上和向右存在必败那么这个点必胜。此外我们还可以观察出一个关键结论:若不挨到边界,\(f(x,y)=f(x+1,y+1)\)

我们可以找到最大的边长为 \(i\) 且不碰到边界的正方形,那么 \(f(0,0)=f(i,i)\),我们只需要考虑向上和向右能延伸的长度即可,如果都是奇数则后手必胜,否则先手必胜。

#include <cstdio>
#include <algorithm>
using namespace std;
const int M = 100005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,a[M];
signed main()
{
	n=read();
	for(int i=1;i<=n;i++)
		a[i]=read();
	sort(a+1,a+1+n,greater<int>());
	int len=1,up=0,rt=1;//len=i-1
	for(;len<=n && len+1<=a[len+1];len++);
	up=a[len]-len+1;
	for(int i=len+1;i<=n && a[i]>=len;i++,rt++);
	if(up%2 && rt%2) puts("Second");
	else puts("First");
}

Sugigma: The Showdown

题目描述

点此看题

解法

考虑在 \(A\) 能到达的范围内(指不被 \(B\) 抓到),如果出现了在蓝树上距离 \(\geq 3\) 的红边,那么一定可以无限循环(王负剑,王负剑

如果没有出现这样的红边,考虑以 \(B\) 的起点为根建蓝树,那么 \(A\) 的活动范围一定局限在 \(B\) 的子树内,并且由于 \(A\) 不能跨过 \(B\),所以 \(B\) 的每一步都是有效的。

设点 \(u\) 在红树上离 \(x\) 的距离是 \(d\),在蓝树上离 \(y\) 的距离是 \(r\),如果 \(d>r\),说明 \(A\) 不能会走到这个点,因为在走到这个点之前就会被抓住;如果 \(d=r\) 说明 \(A\) 只要走到这个点就会被抓住;如果 \(d<r\) 就说明 \(A\) 可以走到这个点,并且可以得到 \(2r\) 的总步数。

所以最后我们在红树上 \(\tt dfs\),时刻保证访问到的点可达(\(d<r\)),然后考虑是否能无限循环,如果不能我们取最大的 \(2r\)

#include <cstdio>
#include <vector>
#include <cstdlib> 
#include <iostream>
using namespace std;
const int M = 200005;
#define pb push_back
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,x,y,ans,ok[M],ea[M],eb[M],fa[M],dep[M];
vector<int> A[M],B[M];
void dfsb(int u,int p)
{
	fa[u]=p;
	for(auto v:B[u]) if(v^fa[u])
		dep[v]=dep[u]+1,dfsb(v,u);
}
bool chk(int u,int v)
{
	if(dep[u]<dep[v]) swap(u,v);
	if(dep[u]==dep[v])
		return fa[u]==fa[v];
	if(dep[u]==dep[v]+1)
		return fa[u]==v;
	if(dep[u]==dep[v]+2)
		return fa[fa[u]]==v;
	return 0;
}
void dfsa(int u,int p,int d)
{
	if(d>=dep[u]) return ;
	if(ok[u]) {puts("-1");exit(0);}
	ans=max(ans,dep[u]<<1);
	for(int v:A[u]) if(v^p)
		dfsa(v,u,d+1);
}
signed main()
{
	n=read();x=read();y=read();
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read();
		A[u].pb(v);A[v].pb(u);
		ea[i]=u;eb[i]=v;
	}
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read();
		B[u].pb(v);B[v].pb(u);
	}
	dfsb(y,0);
	for(int i=1;i<n;i++) if(!chk(ea[i],eb[i]))
		ok[ea[i]]=ok[eb[i]]=1;
	dfsa(x,0,0);
	printf("%d\n",ans);
}
posted @ 2022-02-05 09:32  C202044zxy  阅读(325)  评论(5编辑  收藏  举报