[省选集训2022] 模拟赛13

神必的集合

题目描述

有一个集合 \(S\),集合里的元素都是 \([0,2^n)\) 中的整数,这个集合满足 \(S\) 非空并且 \(\forall a,b\in S,a\oplus b\in S\),给出 \(m\) 条限制,每条限制形如集合中第 \(x_i\) 个数是 \(y_i\),问满足条件的集合个数,答案对 \(998244353\)

\(n\leq 60,m\leq 200\)

解法

首先考虑 \(m=0\) 怎么做,一个关键的 \(\tt observation\) 是:集合 \(S\) 和最简线性基构成双射。其中最简线性基表示,\(\forall i<j\),若 \(b_i\) 有值,那么 \(b_j\) 的第 \(i\) 位等于 \(0\)(其实找映射关系就可以发现这个结论)

那么如果第 \(i\) 位前面有 \(p_i\) 个位置有值,那么这一位的方案数是 \(2^{i-p_i}\),也就是没有值的那些位置可以任意填 \(0/1\),否则只能填 \(0\),那么可以用 \(dp\) 完成这个计数的过程,设 \(dp[i][j]\) 表示考虑到第 \(i\) 位,\(p_i+1=j\) 的方案数是多少。

现在考虑有限制的情况,第 \(x_i\) 个数是 \(y_i\) 等价于 \(<y_i\) 的数恰好有 \(x_i-1\) 个,并且 \(y_i\) 可以被线性基表示出来。为了满足第二个条件我们可以提前建出 \(y_i\) 的线性基,那么限制转化为线性基的某些位置必须填。

考虑第一个条件,首先考虑知道线性基怎么求出 \(<y\) 的个数,由于 \(y\) 已经被插入线性基了,我们按位考虑,如果第 \(i\) 位有贡献当且仅当线性基第 \(i\) 位有值并且 \(y\) 的第 \(i\) 位为 \(1\),这样把第 \(i\) 位调整成 \(0\) 之后后面有 \(2^{p_i}\) 可以任意选,所以贡献是 \(2^{p_i}\)

那么我们把 \(x_i-1\) 二进制拆分,然后把限制分解到每个位置上,最后再跑 \(dp\) 即可,时间复杂度 \(O(nm)\)

#include <cstdio>
#include <iostream>
using namespace std;
#define int long long
const int M = 205;
const int MOD = 998244353;
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,mx,ans,dp[M][M],s[M],b[M],x[M],y[M];
void add(int &x,int y) {x=(x+y)%MOD;}
signed main()
{
	freopen("set.in","r",stdin);
	freopen("set.out","w",stdout);
	n=read();m=read();
	for(int i=1;i<=m;i++)
		x[i]=read()-1,y[i]=read();
	for(int i=0;i<=n;i++) s[i]=(1ll<<n)-1;
	for(int i=1;i<=m;i++)
	{
		int x=y[i];
		for(int j=n-1;j>=0;j--)
		{
			if(!(x>>j&1)) continue;
			if(!b[j]) {b[j]=x;break;}
			x^=b[j];
		}
	}
	for(int i=1;i<=m;i++)
		for(int j=0;j<n;j++)
		{
			if(x[i]>>j&1) s[j+1]&=y[i],mx=max(mx,j+1);
			else s[j+1]&=(s[0]^y[i]);
		}
	if(!b[0]) dp[0][0]=1;
	if(s[1]&1) dp[0][1]=1;
	for(int i=1;i<n;i++)
	{
		if(b[i])
		{
			for(int j=1;j<=i+1;j++) if(s[j]>>i&1)
				add(dp[i][j],dp[i-1][j-1]);
		}
		else
		{
			for(int j=1;j<=i+1;j++) if(s[j]>>i&1)
				add(dp[i][j],(1ll<<i+1-j)%MOD*dp[i-1][j-1]);
			for(int j=0;j<=i;j++)
				add(dp[i][j],dp[i-1][j]);
		}
	}
	for(int i=mx;i<=n;i++) add(ans,dp[n-1][i]);
	printf("%lld\n",ans);
}

法阵

题目描述

\(n\) 个元素 \(\{a_1...a_n\}\)\(m\) 次询问 \([l_i,r_i]\),问满足 \(l_i\leq x<y<z\leq r_i\and y-x\leq z-y\) 的三元组 \((x,y,z)\),其最大的 \(a_x+a_y+a_z\) 是最少。

\(n,m\leq 5\cdot 10^5,a_i\leq 10^9\)

解法

首先考虑 \(m=1\) 怎么做?一个关键的 \(\tt observation\) 是:只有满足 \(a_x>a_{x+1},a_{x+2}...a_{y-1}\)\(a_y>a_{x+1},a_{x+2}...a_{y-1}\)\((x,y)\) 才是可能对答案有贡献的,并且这样的二元组只有 \(O(n)\) 对。

那么我们可以从后往前枚举 \(x\),并且维护单调栈,那么 \(y\) 只可能是单调栈被弹出的元素和栈顶的元素,那么知道 \(x,y\) 之后 \(z\) 就是后缀最大值,这样就成功解决了 \(m=1\) 的情况。

回到本题,我们把询问挂在左端点处,只需要对 \(z\) 维护线段树,就可以把 \(z\) 的范围限制在 \([l_i,r_i]\),时间复杂度 \(O(n\log n)\)

总结

找结论的一个方向是,考虑可能贡献的元素。本题因为确定 \(x,y\) 之后才能确定 \(z\),所以必须要找结论。

此外枚举的量不要局限,我一开始枚举 \(y\) 怎么都想不出来,要是枚举 \(x\) 这题可能会好做得多。

单调栈的应用不止有处理 \(\max/\min\) 的功能,还可以帮助你只考虑有贡献的信息。

#include <cstdio>
#include <vector>
#include <iostream>
using namespace std;
const int M = 500005;
const int N = M<<2;
#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;
}
void write(int x)
{
	if(x>=10) write(x/10);
	putchar(x%10+'0');
}
int n,m,tp,a[M],b[N],s[M],ans[M],tr[N],mx[N];
struct node{int x,y;};vector<node> q[M];
void build(int i,int l,int r)
{
	if(l==r) {tr[i]=b[i]=a[l]=read();return ;}
	int mid=(l+r)>>1;
	build(i<<1,l,mid);
	build(i<<1|1,mid+1,r);
	tr[i]=b[i]=max(b[i<<1],b[i<<1|1]);
}
void zxy(int i,int c)
{
	if(!c) return ;
	tr[i]=max(tr[i],b[i]+c);
	mx[i]=max(mx[i],c);
}
void upd(int i,int l,int r,int L,int R,int c)
{
	if(L>r || l>R) return ;
	if(L<=l && r<=R) {zxy(i,c);return;}
	int mid=(l+r)>>1;
	zxy(i<<1,mx[i]);zxy(i<<1|1,mx[i]);
	upd(i<<1,l,mid,L,R,c);
	upd(i<<1|1,mid+1,r,L,R,c);
	tr[i]=max(tr[i<<1],tr[i<<1|1]);
}
void add(int x,int y)
{
	int z=2*y-x;if(z>n) return;
	upd(1,1,n,z,n,a[x]+a[y]);
}
int ask(int i,int l,int r,int L,int R)
{
	if(l>R || L>r) return 0;
	if(L<=l && r<=R) return tr[i];
	int mid=(l+r)>>1;
	zxy(i<<1,mx[i]);zxy(i<<1|1,mx[i]);
	return max(ask(i<<1,l,mid,L,R),
	ask(i<<1|1,mid+1,r,L,R));
}
signed main()
{
	freopen("fz.in","r",stdin);
	freopen("fz.out","w",stdout);
	n=read();build(1,1,n);m=read();
	for(int i=1;i<=m;i++)
	{
		int l=read(),r=read();
		q[l].push_back({r,i});
	}
	for(int i=n;i>=1;i--)
	{
		while(tp && a[i]>=a[s[tp]]) add(i,s[tp--]);
		if(tp) add(i,s[tp]);s[++tp]=i;
		for(node t:q[i])
			ans[t.y]=ask(1,1,n,i,t.x);
	}
	for(int i=1;i<=m;i++)
		write(ans[i]),puts("");
}

旅行

题目描述

给定 \(n\) 个点 \(m\) 条边的无向图,边权为 \(1\),点 \(i\) 可以花费 \(c_i\) 的代价到达所有距离 \(\leq d_i\) 的城市。保证图联通,求出 \(1\) 到所有点的最短路。

\(n\leq 2\cdot 10^5,c_i\leq 10^9,n-1\leq m\leq n+50\)

解法

首先考虑树的情况怎么做,显然的思路是点分治优化建图,然后跑最短路即可。

那么对于图的情况,我们先任取一棵生成树,然后对其点分治优化建图。现在非树边是没有考虑到的,我们只需要让路径强制经过非树边的某个端点即可,所以我们以取非树边的一个端点为根 \(\tt bfs\),然后优化建图即可。

时间复杂度 \(O(n\log ^2n+nk\log n)\)


为了去掉复杂度中的 \(O(\log n)\) 其实有更好的方法,由于代价是点代价,所以每个点只会被访问一次,那么树的情况我们可以不把图显式地建出来,在点分树上维护指针即可做到 \(O(n\log n)\)\(\tt bfs\) 也可以类似地维护指针,时间复杂度 \(O(n\log n+nk)\)

下面贴上我深度卡常的法 \(1\) 代码,注意这个代码只能获得 \(80\) 分。

总结

点代价的最短路有其特殊性(访问即确定,只会访问一次),一定要注意。

#pragma GCC optimize("Ofast")
#include <cstdio>
#include <vector>
#include <cassert>
#include <cstring> 
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int M = 200005;
const int N = 80*M;
#define pb push_back
#define ll long long
#define pii pair<int,int>
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;
}
void write(ll x)
{
	if(x>=10) write(x/10);
	putchar(x%10+'0');
}
int n,m,rt,sz,a[M],b[M],c[M],d[M],vis[M],mx[M],siz[M];
vector<int> g[M],G[M];int cnt,m1,m2,tot,f[N];ll dis[N];
struct edge{int v,next;}e[N*3];
struct node
{
	int u,c;
	bool operator < (const node &b) const
		{return c<b.c;}
}A[M],B[M];
struct shit
{
	int u;ll c;
	bool operator < (const shit &b) const
		{return c>b.c;}
};
void pre(int u,int fa)
{
	vis[u]=1;
	for(int v:G[u]) if(v^fa)
	{
		if(vis[v]) {a[u]=1;continue;}
		pre(v,u);g[u].pb(v);g[v].pb(u);
	}
}
void find(int u,int fa)
{
	siz[u]=1;mx[u]=0;
	for(int v:g[u])
	{
		if(v==fa || vis[v]) continue;
		find(v,u);
		siz[u]+=siz[v];
		mx[u]=max(mx[u],siz[v]);
	}
	mx[u]=max(mx[u],sz-siz[u]);
	if(mx[u]<mx[rt]) rt=u;
}
void dfs(int u,int fa,int d)
{
	A[++m1]={u,d};
	if(b[u]>=d) B[++m2]={u,b[u]-d};
	for(int v:g[u])
	{
		if(v==fa || vis[v]) continue;
		dfs(v,u,d+1);
	}
}
void add(int u,int v)
{
	e[++tot]=edge{v,f[u]},f[u]=tot;
}
void work()
{
	sort(B+1,B+1+m2);
	for(int i=1,j=1,lst=0;i<=m2;i++)
	{
		int x=lst;
		while(j<=m1 && A[j].c<=B[i].c)
		{
			if(x==lst) x=++cnt;
			add(x,A[j].u);j++;
		}
		if(x) add(B[i].u,x);
		if(x!=lst && lst) add(x,lst);lst=x;
	}
}
void solve(int u)
{
	vis[u]=1;m1=m2=0;dfs(u,0,0);
	sort(A+1,A+1+m1);work();
	for(int v:g[u])
	{
		if(vis[v]) continue;
		rt=0;sz=siz[v];
		find(v,0);solve(rt);
	}
}
void bfs(int s)
{
	queue<int> q;q.push(s);d[s]=0;
	while(!q.empty())
	{
		int u=q.front();q.pop();
		A[++m1]={u,d[u]};
		if(b[u]>=d[u]) B[++m2]={u,b[u]-d[u]};
		for(int v:G[u]) if(d[v]==-1)
			d[v]=d[u]+1,q.push(v);
	}
}
void dijk()
{
	memset(dis,0x3f,sizeof dis);dis[1]=0;
	priority_queue<shit> q;q.push({1,0});
	while(!q.empty())
	{
		int u=q.top().u,w=q.top().c;q.pop();
		if(w>dis[u]) continue;
		for(int i=f[u];i;i=e[i].next)
		{
			int v=e[i].v,tmp=u<=n?c[u]:0;
			if(dis[v]>dis[u]+tmp)
			{
				dis[v]=dis[u]+tmp;
				q.push({v,dis[v]});
			}
		}
	}
}
signed main()
{
	freopen("travel.in","r",stdin);
	freopen("travel.out","w",stdout);
	n=read();m=read();cnt=n;
	for(int i=1;i<=m;i++)
	{
		int u=read(),v=read();
		G[u].pb(v);G[v].pb(u);
	}
	for(int i=1;i<=n;i++)
		b[i]=read(),c[i]=read();
	pre(1,0);
	//work for tree
	memset(vis,0,sizeof vis);
	mx[rt=0]=sz=n;find(1,0);solve(1);
	//work for graph
	for(int i=1;i<=n;i++) if(a[i])
	{
		for(int j=1;j<=n;j++) d[j]=-1;
		m1=m2=0;bfs(i);work();
	}
	dijk();
	for(int i=2;i<=n;i++)
		write(dis[i]),puts("");
}
posted @ 2022-03-14 16:49  C202044zxy  阅读(238)  评论(0编辑  收藏  举报