[省选集训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("");
}