2025 NOI 做题记录(五)
\(\text{By DaiRuichen007}\)
Round #77 - 20250521
A. 串联(link)
题目大意
给定一棵树,每个点有两个权值 \(a_i,b_i\)。对于树上的一条简单路径,若这条路径上 \(b\) 之和乘上 \(a\) 的最小值大于等于一个常数 \(V\),那么这条路径被称作一条好的路径,求所有好的路径中,\(\sum b\) 的最小值。
数据范围:\(n\le 2\times 10^5\)。
思路分析
点分治变成 \((B_u+B_v)\min(A_u,A_v)\ge V\),不妨设 \(A_u\le A_v\),则只要找到最小的 \(B_v\ge \left\lceil \dfrac V{A_u}\right\rceil-B_u\),并且 \(u,v\) 来自不同的子树。
那么按 \(A\) 降序加入每条路径,按 \(B\) 离散化后建树状数组,维护后缀最小值,限制不同的子树就维护不同色的最小值和次小值。
时间复杂度 \(\mathcal O(n\log^2 n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
const ll inf=2e18;
struct info {
ll v1,v2;
int c1,c2;
inline friend info operator +(info u,info v) {
if(u.v1>v.v1) swap(u,v);
if(v.c1==u.c1) {
if(v.v2<u.v2) u.v2=v.v2,u.c2=v.c2;
} else if(v.v1<u.v2) u.v2=v.v1,u.c2=v.c1;
return u;
}
} tr[MAXN],I={inf,inf,0,0};
int n,tp,siz[MAXN],cur[MAXN];
ll V,a[MAXN],b[MAXN],ans=inf;
vector <int> G[MAXN];
bool vis[MAXN];
ll st[MAXN];
void solve(int u) {
if(a[u]*b[u]>=V) ans=min(ans,b[u]);
vector <array<ll,3>> Q;
for(int v:G[u]) if(!vis[v]) {
function<void(int,int,ll,ll)> dfs3=[&](int x,int fz,ll mn,ll su) {
mn=min(mn,a[x]),su+=b[x],Q.push_back({mn,su,v}),siz[x]=1;
for(int y:G[x]) if(!vis[y]&&y!=fz) dfs3(y,x,mn,su),siz[x]+=siz[y];
};
dfs3(v,u,a[u],b[u]);
}
tp=0,sort(Q.begin(),Q.end(),greater<>());
for(auto i:Q) st[++tp]=i[1];
sort(st+1,st+tp+1),tp=unique(st+1,st+tp+1)-st-1;
fill(tr,tr+tp+1,I);
auto add=[&](int x,info v) { for(;x;x&=x-1) tr[x]=tr[x]+v; };
auto qry=[&](int x) { info s=I; for(;x<=tp;x+=x&-x) s=s+tr[x]; return s; };
for(auto i:Q) {
if(i[1]>=(V+i[0]-1)/i[0]) ans=min(ans,i[1]);
int id=lower_bound(st+1,st+tp+1,(V+i[0]-1)/i[0]-i[1]+b[u])-st;
info z=qry(id);
ans=min(ans,i[1]+(z.c1==i[2]?z.v2:z.v1)-b[u]);
id=lower_bound(st+1,st+tp+1,i[1])-st;
add(id,{i[1],inf,(int)i[2],0});
}
}
void dfs1(int u) {
solve(u),vis[u]=true;
for(int v:G[u]) if(!vis[v]) {
int rt=0;
function<void(int,int)> dfs2=[&](int x,int fz) {
cur[x]=siz[v]-siz[x];
for(int y:G[x]) if(y!=fz&&!vis[y]) dfs2(y,x),cur[x]=max(cur[x],siz[y]);
if(!rt||cur[x]<cur[rt]) rt=x;
};
dfs2(v,u),dfs1(rt);
}
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>V;
for(int i=1;i<=n;++i) cin>>a[i]>>b[i];
for(int i=1,u,v;i<n;++i) cin>>u>>v,G[u].push_back(v),G[v].push_back(u);
dfs1(1),cout<<ans<<"\n";
return 0;
}
*B. 音乐(desive)
题目大意
给定 \(a_1\sim a_{2^n}\),满足 \(a_i\in[0,2^n)\),定义 \(f(l,r)\) 表示 \(\max_x\mathrm{mex}(a_l\oplus x,a_{l+1}\oplus x,\dots,a_r\oplus x)\)。
支持 \(q\) 次询问一个区间的 \(f\) 值,或者所有子区间的 \(f\) 值之和。
数据范围:\(n\le 18,q\le 10^6\)。
思路分析
首先刻画 \(f\),把所有元素建 Trie,计算每个子树的 \(f_p\),如果有一个子树是满的,则 \(f_p=f_{ls}+f_{rs}\),否则 \(f_p=\max (f_{ls},f_{rs})\)。
但这个做法不能很好的刻画 \(f(l,r)\)。
注意到 \(f\) 的本质就是选择 Trie 树上的一个点,然后把他到根的链删掉,剩余的满子树大小之和。
那么如果 \(a\) 是一个排列,我们就可以扫描线,在每个子树被填满的时候更新他的兄弟子树内每个叶子的权值。
注意到每个叶子的权值都是一个 \(n\) 段的分段函数,因此总的更新次数是 \(\mathcal O(n^22^n)\) 的,再用一个线段树维护即可。
进一步你把每个叶子的分段函数先归并起来,这样的更新次数是 \(\mathcal O(n2^n)\) 的。
接下来回到原问题,此时每个子树会被多次填满,我们就不能每次暴力更新兄弟子树的分段函数。
考虑一个比较方便的维护分段函数的方法,对每个子树建一棵刚才的 Trie 树,并且维护最大出现位置最小的一个,然后把这个元素删掉,重复此过程,每次取出 \(f_{rt}\) 即可。
然后当一个子树填满的时候,设此前子树内最大出现位置最小的元素时刻为 \(t'\),而现在被更新为 \(t\),那么我们只关心其兄弟子树在 \([t',t]\) 范围内的分段函数。
那么我们对每个子树建 Trie 树维护 dp,然后我们不断弹出兄弟子树最小时刻在 \([t',t]\) 范围内的元素,就能得到这个范围的分段函数。
很显然得到的总段数不超过每个 Trie 树上插入操作的次数,那么操作次数就是 \(\mathcal O(n2^n)\) 级别的。
由于 \(f\) 具有单调性,那么区间 chkmax 相当于区间赋值,用线段树支持区间赋值区间历史和即可。
时间复杂度 \(\mathcal O(n^22^n+nq)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=(1<<18)+5,inf=1e9;
int ty,n,m,q;
struct Trie {
int N,lo,*f,*s;
array <int,2> *mn;
void psu(int p) {
mn[p]=min(mn[p<<1],mn[p<<1|1]),f[p]=max(f[p<<1],f[p<<1|1]);
if(f[p]==s[p]/2) f[p]=f[p<<1]+f[p<<1|1];
}
void init(int l,int r) {
N=r-l+1,lo=l,f=new int[2*N],s=new int[2*N],mn=new array<int,2>[2*N];
for(int i=N;i<2*N;++i) s[i]=1,f[i]=0,mn[i]={inf,i};
for(int i=N-1;i;--i) psu(i),s[i]=s[i<<1]+s[i<<1|1];
}
void upd(int p,int t) {
p=p-lo+N,f[p]=1,mn[p]={t,p};
for(p>>=1;p;p>>=1) psu(p);
}
void pop() {
int p=mn[1][1]; f[p]=0,mn[p]={inf,p};
for(p>>=1;p;p>>=1) psu(p);
}
} tr[MAXN*2];
struct SegmentTree {
int mn[MAXN*2],mx[MAXN*2],tg[MAXN*2],len[MAXN*2],ct[MAXN*2];
ll su[MAXN*2],hs[MAXN*2],ht[MAXN*2];
//ct -> tg -> ht
void adt(int p,int k) { hs[p]+=1ll*k*su[p],(~tg[p]?ht[p]+=1ll*tg[p]*k:ct[p]+=k); }
void cov(int p,int k) { mn[p]=mx[p]=tg[p]=k,su[p]=1ll*len[p]*k; }
void adh(int p,ll h) { hs[p]+=h*len[p],ht[p]+=h; }
void psd(int p) {
if(ct[p]) adt(p<<1,ct[p]),adt(p<<1|1,ct[p]),ct[p]=0;
if(~tg[p]) cov(p<<1,tg[p]),cov(p<<1|1,tg[p]),tg[p]=-1;
if(ht[p]) adh(p<<1,ht[p]),adh(p<<1|1,ht[p]),ht[p]=0;
}
void psu(int p) {
su[p]=su[p<<1]+su[p<<1|1],hs[p]=hs[p<<1]+hs[p<<1|1];
mn[p]=min(mn[p<<1],mn[p<<1|1]),mx[p]=max(mx[p<<1],mx[p<<1|1]);
}
void init(int l=0,int r=n-1,int p=1) {
tg[p]=-1,len[p]=r-l+1,tr[p].init(l,r);
if(l==r) return ;
int mid=(l+r)>>1;
init(l,mid,p<<1),init(mid+1,r,p<<1|1);
}
void upd(int ul,int ur,int v,int l=0,int r=n-1,int p=1) {
if(mn[p]>=v) return ;
if(ul<=l&&r<=ur&&mx[p]<=v) return cov(p,v);
int mid=(l+r)>>1; psd(p);
if(ul<=mid) upd(ul,ur,v,l,mid,p<<1);
if(mid<ur) upd(ul,ur,v,mid+1,r,p<<1|1);
psu(p);
}
ll qhs(int ul,int ur,int l=0,int r=n-1,int p=1) {
if(ul<=l&&r<=ur) return hs[p];
int mid=(l+r)>>1; ll s=0; psd(p);
if(ul<=mid) s+=qhs(ul,ur,l,mid,p<<1);
if(mid<ur) s+=qhs(ul,ur,mid+1,r,p<<1|1);
return s;
}
int qv(int u,int l=0,int r=n-1,int p=1) {
if(l==r) return su[p];
int mid=(l+r)>>1; psd(p);
return u<=mid?qv(u,l,mid,p<<1):qv(u,mid+1,r,p<<1|1);
}
} T;
int a[MAXN],mn[MAXN*2];
ll ans[MAXN*4];
vector <array<int,2>> qy[MAXN];
struct info {
int d,w,t,o;
};
void upd(int x,int t) {
vector <info> op;
op.push_back({0,1,t,0});
for(int d=0;d<m;++d) {
int p=(x+n)>>d,lst=mn[p];
tr[p].upd(x,t),mn[p]=(d?min(mn[p<<1],mn[p<<1|1]):t);
if(mn[p^1]>=0) op.push_back({d+1,1<<d,mn[p^1],1});
if(mn[p]==lst) continue;
for(;tr[p^1].mn[1][0]<=mn[p];tr[p^1].pop()) {
op.push_back({d+1,tr[p^1].f[1]+(1<<d),tr[p^1].mn[1][0],0});
}
if(tr[p^1].mn[1][0]<=t) op.push_back({d+1,tr[p^1].f[1]+(1<<d),mn[p],0});
}
sort(op.begin(),op.end(),[&](auto i,auto j){ return i.t>j.t; });
static int f[20],w[20];
memset(f,0,sizeof(f)),memset(w,0,sizeof(w));
for(auto e:op) {
if(e.o) for(int i=0;i<e.d;++i) w[i]+=e.w;
else f[e.d]=max(f[e.d],e.w);
int z=0;
for(int i=0;i<=m;++i) z=max(z,f[i]+w[i]);
T.upd(0,e.t,z);
}
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>m>>q>>ty,n=1<<m,T.init();
for(int i=0;i<n;++i) cin>>a[i];
for(int i=1,l,r;i<=q;++i) cin>>l>>r,qy[r-1].push_back({l-1,i});
memset(mn,-0x3f,sizeof(mn));
for(int i=0;i<n;++i) {
upd(a[i],i),T.adt(1,1);
for(auto o:qy[i]) ans[o[1]]=(ty==1?T.qv(o[0]):T.qhs(o[0],i));
}
for(int i=1;i<=q;++i) cout<<ans[i]<<"\n";
return 0;
}
C. 字符串(string)
题目大意
给定一个长度为 \(n\) 的字符串 \(s\)。有 \(q\) 次询问,每次询问给定两个参数 \(x,v\)。你需要求出有多少 \(k\in[1,v]\),满足 \(s[x,x+k-1]\) 字典序小于 \(s[x+k,x+2k-1]\) 。
数据范围:\(n,q\le 5\times 10^5\)。
思路分析
首先 \((x,k)\) 合法要求 \(rk_x<rk_{x+k}\) 且 \(s[x,x+k-1]\ne s[x+k,x+2k-1]\)。
那么先二维数点计算满足第一个条件的 \(k\) 个数,然后减掉 \(rk_x<rk_{x+k}\) 的平方串 \((x,k)\) 个数。
根据经典技巧,我们可以把平方串写成 \(\mathcal O(n\log n)\) 个 \(x\) 的区间 \([l,r]\)。
但我们不太好对于所有 \(x\in[l,r]\) 都计算 \(rk_x,rk_{x+k}\) 的大小关系。
注意到 \(s_l=s_{l+1}\),因此 \(x=l,x=l+1\) 的大小关系相等,进一步所有 \(x\) 的大小关系都相等。
只要统计 \(rk_r<rk_{r+k}\) 的区间有多少满足 \(k\le v,x\in [l,r]\),容易二维数点。
时间复杂度 \(\mathcal O(n\log^2n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+5;
struct SA {
char str[MAXN];
int n,sa[MAXN],rk[MAXN],ct[MAXN],t[MAXN],f[MAXN][20];
int bit(int x) { return 1<<x; }
void build() {
int m=26;
for(int i=1;i<=n;++i) rk[i]=str[i]-'a'+1,++ct[rk[i]];
for(int i=1;i<=m;++i) ct[i]+=ct[i-1];
for(int i=n;i;--i) sa[ct[rk[i]]--]=i;
for(int k=1;;k<<=1) {
for(int i=1;i<=k;++i) t[i]=n-k+i;
for(int i=1,h=k;i<=n;++i) if(sa[i]>k) t[++h]=sa[i]-k;
memset(ct,0,sizeof(ct));
for(int i=1;i<=n;++i) ++ct[rk[i]];
for(int i=1;i<=m;++i) ct[i]+=ct[i-1];
for(int i=n;i;--i) sa[ct[rk[t[i]]]--]=t[i];
memcpy(t,rk,sizeof(t)),m=0;
for(int i=1;i<=n;++i) {
rk[sa[i]]=m+=(i==1||t[sa[i]]!=t[sa[i-1]]||t[min(n+1,sa[i]+k)]!=t[min(n+1,sa[i-1]+k)]);
}
if(m==n) break;
}
for(int i=1,k=0;i<=n;++i) {
for(k=max(0,k-1);str[i+k]==str[sa[rk[i]-1]+k];++k);
f[rk[i]][0]=k;
}
for(int k=1;k<20;++k) for(int i=1;i+bit(k)-1<=n;++i) {
f[i][k]=min(f[i][k-1],f[i+bit(k-1)][k-1]);
}
}
int qry(int x,int y) {
if(x==y) return n-x+1;
int l=min(rk[x],rk[y])+1,r=max(rk[x],rk[y]),k=__lg(r-l+1);
return min(f[l][k],f[r-bit(k)+1][k]);
}
} lcp,lcs;
int ty,n,q,ans[MAXN];
char str[MAXN];
vector <array<int,2>> sg[MAXN],qy1[MAXN],qy2[MAXN];
struct BIT {
int tr[MAXN],s;
void init() { memset(tr,0,sizeof(tr)); }
void add(int x,int v) { for(;x<=n;x+=x&-x) tr[x]+=v; }
int qry(int x) { for(s=0;x;x&=x-1) s=s+tr[x]; return s; }
} T;
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>ty>>n>>q>>(str+1),lcp.n=lcs.n=n;
for(int i=1;i<=n;++i) lcp.str[i]=lcs.str[n-i+1]=str[i];
lcp.build(),lcs.build();
for(int k=1;k<=n/2;++k) for(int i=k;i+k<=n;i+=k) if(str[i]==str[i+k]) {
int dl=lcs.qry(n-i+1,n-i-k+1),dr=lcp.qry(i,i+k);
int l=max(i-k+1,i-dl+1),r=min(i,i+dr-k);
if(l<=r&&lcp.rk[l]<lcp.rk[l+k]) {
sg[k].push_back({l,r});
}
}
for(int i=1,x,v;i<=q;++i) {
cin>>x>>v;
qy1[x].push_back({lcp.rk[x],-i});
qy1[x+v].push_back({lcp.rk[x],i});
qy2[v].push_back({x,i});
}
for(int i=1;i<=n;++i) {
T.add(lcp.rk[i],1);
for(auto o:qy1[i]) {
if(o[1]>0) ans[o[1]]+=i-T.qry(o[0]);
else ans[-o[1]]-=i-T.qry(o[0]);
}
}
T.init();
for(int i=1;i<=n;++i) {
for(auto o:sg[i]) T.add(o[0],1),T.add(o[1]+1,-1);
for(auto o:qy2[i]) ans[o[1]]-=T.qry(o[0]);
}
for(int i=1;i<=q;++i) cout<<ans[i]<<"\n";
return 0;
}
Round #78 - 2020522
A. 图(graph)
题目大意
给定 \(n\) 个点 \(m\) 条边的无向图,\(q\) 次操作给定区间 \([l,r]\),把每个点的邻域点权 \(+x\) 或者查询点权和。
数据范围:\(n,m,q\le 2\times 10^5\)。
思路分析
考虑序列分块,讨论整块散块之间的贡献。
整块对区间的贡献是平凡的,直接逐块处理该块对每个点的贡献,以及其前缀和。
同理散块对整块的贡献也很简单,散块之间的贡献暴力,但复杂度是块内的 \(\sum \deg\) 级别。
由于 \(\sum \deg=2m\),因此按 \(\deg_u+1\) 跑带权分块即可。
注意一个度数 \(>\sqrt m\) 的点要独立分块,并且如果询问左端点是一个块的左端点,那么不能把这个块当散块处理。
时间复杂度 \(\mathcal O((n+m+q)\sqrt m)\)。
代码呈现
#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int MAXN=2e5+5;
int n,m,q,d[MAXN],e[MAXN*2];
vector <int> G[MAXN];
int lp[MAXN],rp[MAXN],bl[MAXN],op[MAXN],ql[MAXN],qr[MAXN];
int lb[MAXN],rb[MAXN];
ull z[MAXN],f[MAXN],g[MAXN];
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m>>q;
for(int i=1,u,v;i<=m;++i) cin>>u>>v,G[u].push_back(v),G[v].push_back(u);
for(int i=1;i<=n;++i) {
d[i+1]=d[i];
for(int x:G[i]) e[d[i+1]++]=x;
}
int B=sqrt(n+2*m)*2;
for(int i=1,j,s,k=0;i<=n;) {
for(j=i+1,s=G[i].size()+1;j<=n&&s<B;++j) s+=G[j].size()+1;
lp[++k]=i,rp[k]=max(i,j-2),fill(bl+lp[k],bl+rp[k]+1,k),i=rp[k]+1;
}
bl[n+1]=bl[n]+1,lp[bl[n+1]]=n+1;
for(int i=1;i<=q;++i) {
cin>>op[i]>>ql[i]>>qr[i];
if(op[i]==1) cin>>z[i];
lb[i]=bl[ql[i]-1]+1,rb[i]=bl[qr[i]+1]-1;
if(lb[i]-rb[i]>1) {
for(int j=d[ql[i]];j<d[qr[i]+1];++j) {
op[i]==1?f[e[j]]+=z[i]:z[i]+=f[e[j]];
}
} else {
for(int j=d[ql[i]];j<d[lp[lb[i]]];++j) {
op[i]==1?f[e[j]]+=z[i]:z[i]+=f[e[j]];
}
for(int j=d[rp[rb[i]]+1];j<d[qr[i]+1];++j) {
op[i]==1?f[e[j]]+=z[i]:z[i]+=f[e[j]];
}
}
}
for(int o=1;o<=bl[n];++o) {
memset(f,0,sizeof(f)),memset(g,0,sizeof(g));
for(int i=d[lp[o]];i<d[rp[o]+1];++i) ++g[e[i]];
for(int i=1;i<=n;++i) for(int j=d[i];j<d[i+1];++j) f[i]+=g[e[j]];
for(int i=1;i<=n;++i) f[i]+=f[i-1];
ull tg=0,su=0;
for(int i=1;i<=q;++i) {
if(op[i]==1) {
if(lb[i]<=o&&o<=rb[i]) tg+=z[i];
if(lb[i]-rb[i]>1) su+=(f[qr[i]]-f[ql[i]-1])*z[i];
else su+=(f[qr[i]]-f[rp[rb[i]]]+f[lp[lb[i]]-1]-f[ql[i]-1])*z[i];
} else {
z[i]+=(f[qr[i]]-f[ql[i]-1])*tg;
if(lb[i]<=o&&o<=rb[i]) z[i]+=su;
}
}
}
for(int i=1;i<=q;++i) if(op[i]==2) cout<<z[i]<<"\n";
return 0;
}
B. 木桶效应(bucket)
题目大意
给定 \(m\times n\) 网格,每行要是一个 \(1\sim n\) 排列,权值定义为每列最小值乘积,给定 \(q\) 个位置的值,求每种方案的权值乘积。
数据范围:\(n\le 50,m\le 10^9,q\le 10\)。
思路分析
考虑 \(q=0\) 时怎么做,注意到权值相当于选定 \(a_1\sim a_n\),要求第 \(i\) 列所有元素 \(\ge a_i\),求 \(a\) 的方案数。
那么对 \(a\) 的形态 dp,已知 \(a_i\) 的情况下,递减排序后方案数为 \(\prod (n-a_i+1-i+1)\),总方案数就乘上 \(m\) 次方。
因此从大到小加入每种 \(a\) 即可,我们只关心已经填入的列总个数。
然后考虑有 \(q\) 的情况,对于没有确定位置的列,正常 dp,而有确定位置的列状压。
同理求普通行的方案数可以直接计算,而特殊行提前删掉已经确定的位置和元素,特殊计算方案数即可。
时间复杂度 \(\mathcal O(2^qq^2n^2+2^qn^3)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
#define pc __builtin_popcount
using namespace std;
const int MOD=998244353;
ll ksm(ll a,ll b=MOD-2) { ll s=1; for(;b;a=a*a%MOD,b>>=1) if(b&1) s=s*a%MOD; return s; }
int n,m,q;
ll f[55][1<<10],pw[55],fac[55],ifac[55],w[55];
vector <array<int,2>> a[15];
map <int,vector<array<int,2>>> A;
map <int,int> yid;
int S[55],to[15][55],vl[15][55];
bool vis[15][55];
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m>>q;
int qn=0,qm=0;
for(int i=1,x,y,v;i<=q;++i) {
cin>>x>>y>>v;
if(!yid.count(y)) yid[y]=qn++;
A[x].push_back({yid[y],v});
}
for(auto it:A) a[qm++]=it.second;
for(int i=0;i<qm;++i) {
for(auto o:a[i]) vis[i][o[1]]=true,vl[i][o[0]]=o[1],S[i]|=1<<o[0];
for(int j=1,e=1;j<=n;++j) to[i][j]=e,e+=!vis[i][j];
}
for(int i=fac[0]=ifac[0]=1;i<=n;++i) ifac[i]=ksm(fac[i]=fac[i-1]*i%MOD);
for(int i=0;i<=n;++i) pw[i]=ksm(i,m-qm);
f[0][0]=1;
for(int v=n;v>=1;--v) {
for(int k=0;k<qn;++k) {
for(int i=0;i<=n-qn;++i) for(int s=(1<<qn)-1;~s;--s) {
int o=pc(s);
if(!f[i][s]||(s>>k&1)||n-v+1<i+o+1) continue;
ll p=pw[(n-v+1)-(i+o)];
for(int t=0;t<qm;++t) {
if(S[t]>>k&1) {
if(v>vl[t][k]) { p=0; break; }
} else {
int tv=to[t][v],tc=i+o+1-pc(s&S[t]);
if(n-pc(S[t])-tv+1<tc) { p=0; break; }
p=p*((n-pc(S[t])-tv+1)-(tc-1))%MOD;
}
}
if(p) f[i][s|1<<k]=(f[i][s|1<<k]+f[i][s]*p)%MOD;
}
}
for(int s=0;s<(1<<qn);++s) {
int o=pc(s);
memset(w,0,sizeof(w));
for(int i=1;i+o<=n-v+1&&i<=n-qn;++i) {
w[i]=pw[(n-v+1)-(i+o-1)];
for(int t=0;t<qm;++t) {
int tv=to[t][v],tc=i+o-pc(s&S[t]);
if(n-pc(S[t])-tv+1<tc) { w[i]=0; break; }
w[i]=w[i]*((n-pc(S[t])-tv+1)-(tc-1))%MOD;
}
}
for(int i=n-qn;~i;--i) if(f[i][s]) {
ll p=1;
for(int c=1;i+o+c<=n-v+1&&i+c<=n-qn;++c) {
p=p*w[i+c]%MOD; if(!p) break;
f[i+c][s]=(f[i+c][s]+f[i][s]*p%MOD*ifac[c])%MOD;
}
}
}
}
cout<<f[n-qn][(1<<qn)-1]*fac[n-qn]%MOD<<"\n";
return 0;
}
*C. 人间应又雪(snow)
题目大意
给定 \([1,m]\) 范围内的 \(a_1\sim a_n\),每次操作可以选择一个 \(a_i\) 减去 \(c+1\),然后把 \(a[1,i-1]\) 或 \(a[i+1,n]\) 全部 \(-1\),求把所有元素变成负数的最小操作次数。
数据范围:\(n,m\le 5\times 10^5\)。
思路分析
首先如果在 \(x\) 位置操作前缀,\(y\) 位置操作后缀且 \(x<y\),那么交换方向得到更优的解。
因此一定存在一个 \(i\) 满足后缀操作 \(\le i\) ,且前缀操作 \(\ge i\)。
二分答案 \(k\),枚举后缀操作 \(j\) 次,则前缀操作对这部分的影响就是减去 \(k-j\)。
那么就能计算最多能清空多少前缀,以及前缀操作 \(k-j\) 次最多能清空多少后缀。
设为 \(pl_j,pr_{k-j}\),以及清空后缀后剩余的操作次数 \(cl_j,cr_{k-j}\)。
如果 \(pl_j\ge pr_{k-j}\),那么已经合法,如果 \(pl_j+2=pr_{k-j}\),则判断 \(a_{pl_j+1}\) 是否 \(\ge k+c(cl_j+cr_k)\)。
考虑如何维护 \(pl_j,cl_j\),求 \(pr,cr\) 只需翻转序列。
设 \(f_{i,j}\) 表示给所有 \(a\) 减去 \(j\) 之后清空 \(a[1,i]\) 至少几次操作(算上初始 \(j\) 次)。
很显然决策就是从前到后操作,转移为 \(f_{i,j}=f_{i-1,j}+\left\lceil\dfrac{\max(0,a_i-f_{i-1,j})}{c+1}\right\rceil\)。
首先很显然 \(f_{i,j-1}+1\ge f_{i,j}\ge f_{i,j-1}\),因为一个在 \(a_1\) 的后缀操作比一个全局 \(-1\) 更强。
那么我们只要维护差分数组为 \(1\) 的位置:\(S_i=\{k\mid f_{i,j}>f{i,j-1}\}\),维护 \(S_{i-1}\to S_i\)。
那么转移时找到 \(f_{i-1,j}=a_i\) 且 \(f_{i-1,j-1}=a_{i-1}\) 的位置,转移后有 \(f_{i,j}=f_{i,j-1}=a_i\)。
进一步首个值为 \(a_i-(c+1),a_i-2(c+1)\) 的元素都会从 \(S_i\) 中删除,其他元素不变。
观察原序列,实际上就是对于这些被删除的位置 \(j\),给 \(f[1,j-1]\) 区间 \(+1\)。
可以用树状数组维护差分,二分找到被删除的元素。
然后考虑如何计算 \(pl_j,cl_j\),动态维护最大的 \(p\) 使得 \(f_{i,p}\le k\),很显然 \(p\) 是递减的,每次弹出 \(p\) 时就能计算 \(pl_p,cl_p\)。
时间复杂度 \(\mathcal O((n+m)\log^2m)\),瓶颈在于每次二分时树状数组维护被删除的元素。
注意到 dp 过程不受二分的 \(k\) 影响,只有 \(f\to pl,cl\) 的过程和 \(k\) 有关,那么提前处理 dp 时 \(f\) 的变化即可。
时间复杂度 \(\mathcal O((n+m)\log m)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+5;
int n,m,c,a[MAXN];
struct BIT {
int tr[MAXN],z;
void init() { z=0; for(int i=1;i<=m;++i) tr[i]=i&-i; }
int qry(int k) {
int x=0;
for(int i=18;~i;--i) if(x+(1<<i)<=m&&tr[x+(1<<i)]<k) k-=tr[x+=1<<i];
return x+1;
}
int del(int k) {
int x=qry(k-z); ++z;
for(int i=x;i<=m;i+=i&-i) --tr[i];
return x;
}
} T;
vector <int> dp[2][MAXN];
void init(int o) {
T.init();
for(int i=1;i<=n;++i) {
dp[o][i].clear();
for(int x=a[i];T.z<x;x-=c) dp[o][i].push_back(T.del(x));
}
}
struct ds {
int f[MAXN],v,p;
void init(int k) { fill(f,f+k+1,-1),v=p=k; }
void upd(int x) { p<x?++v:++f[x-1]; }
void del() { v+=f[--p]; }
} pre,cur;
int f[2][MAXN],g[2][MAXN];
void gen(int k,int o) {
for(int i=0;i<=k;++i) f[o][i]=n+1,g[o][i]=0;
pre.init(k),cur.init(k);
for(int i=1,j=k;i<=n;++i) {
for(int x:dp[o][i]) cur.upd(x);
while(cur.v>k) {
f[o][j]=i,g[o][j]=k-pre.v;
if(!j) return ;
--j,pre.del(),cur.del();
}
for(int x:dp[o][i]) pre.upd(x);
}
}
bool chk(int k) {
for(int o:{0,1}) gen(k,o),reverse(a+1,a+n+1);
for(int i=0;i<=k;++i) {
int w=f[0][i]+f[1][k-i];
if(w>n+1||(w==n+1&&1ll*c*(g[0][i]+g[1][k-i])+k>=a[f[0][i]])) return true;
}
return false;
}
void solve() {
cin>>n>>m>>c;
for(int i=1;i<=n;++i) cin>>a[i];
for(int o:{0,1}) init(o),reverse(a+1,a+n+1);
int l=0,r=m-1,z=m;
while(l<=r) {
int mid=(l+r)>>1;
if(chk(mid)) z=mid,r=mid-1;
else l=mid+1;
}
cout<<z<<"\n";
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _,ty; cin>>_>>ty;
while(_--) solve();
return 0;
}
Round #79 - 20250526
A. 树上路径(path)
题目大意
给定 \(m\) 个点的树,以及 \(n\) 个操作,每个操作都是对树上一条路径的权值加 \(x\)。
维护 \(q\) 次修改,形如执行 \(l\sim r\) 的操作各一次,或换根和求子树和。
数据范围:\(n,m,q\le 10^5\)。
思路分析
首先换根可以转成求子树补权值和。
然后维护修改可以直接对操作分块。
对于 \([l,r]\) 内部的每一个整块,直接逐块处理,对每块算出进行块内每个操作一次,对每个子树和的贡献系数。
然后维护散块操作,可以简单树上差分成二维数点,用 Sqrt-Tree 维护即可。
时间复杂度 \(\mathcal O((n+m+q)\sqrt n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+5;
struct SqrtTree {
ll s1[MAXN],s2[(MAXN>>7)+5],s3[(MAXN>>14)+5];
void add(int x,ll y) { s1[x]+=y,s2[x>>7]+=y,s3[x>>14]+=y; }
ll qry(int x) {
ll s=0;
for(int i=((x>>7)<<7);i<=x;++i) s+=s1[i];
for(int i=((x>>14)<<7);i<(x>>7);++i) s+=s2[i];
for(int i=0;i<(x>>14);++i) s+=s3[i];
return s;
}
ll qry(int l,int r) { return qry(r)-qry(l-1); }
} T1,T2;
vector <int> G[MAXN],Q[MAXN];
int n,m,q,dfn[MAXN],efn[MAXN],dep[MAXN],fa[MAXN],dcnt,rk[MAXN];
int u[MAXN],v[MAXN],w[MAXN],h[MAXN],dsu[MAXN];
int find(int x) { return x^dsu[x]?dsu[x]=find(dsu[x]):x; }
void dfs(int p,int fz) {
fa[p]=fz,dfn[p]=++dcnt,dep[p]=dep[fz]+1,rk[dcnt]=p;
for(int e:G[p]) if(e^fz) dfs(e,p),dsu[e]=p;
efn[p]=dcnt; if(fz) G[p].erase(find(G[p].begin(),G[p].end(),fz));
for(int o:Q[p]) if(dfn[u[o]^v[o]^p]) h[o]=find(u[o]^v[o]^p);
}
void upd(int x,int c) {
if(x) T1.add(dfn[x],c*dep[x]),T2.add(dfn[x],c);
}
void add(int x) {
upd(u[x],w[x]),upd(v[x],w[x]),upd(h[x],-w[x]),upd(fa[h[x]],-w[x]);
}
int x[MAXN],y[MAXN],lp[MAXN],rp[MAXN],bl[MAXN];
ll f[MAXN],ans[MAXN];
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m>>q;
for(int i=1;i<=n;++i) cin>>u[i]>>v[i]>>w[i],Q[u[i]].push_back(i),Q[v[i]].push_back(i);
for(int i=1,s,t;i<m;++i) cin>>s>>t,G[s].push_back(t),G[t].push_back(s);
int B=sqrt(n);
iota(dsu+1,dsu+m+1,1),dfs(1,0);
for(int i=1;i<=(n-1)/B+1;++i) {
lp[i]=(i-1)*B+1,rp[i]=min(n,i*B),fill(bl+lp[i],bl+rp[i]+1,i);
}
for(int i=1,o,rt=1;i<=q;++i) {
cin>>o;
if(o==1) {
cin>>x[i]>>y[i];
if(bl[x[i]]==bl[y[i]]) for(int j=x[i];j<=y[i];++j) add(j);
else {
for(int j=x[i];j<=rp[bl[x[i]]];++j) add(j);
for(int j=lp[bl[y[i]]];j<=y[i];++j) add(j);
}
} else if(o==3) cin>>rt;
else {
cin>>x[i]; int p=x[i];
if(rt==p) x[i]=p=1;
else if(dfn[p]<dfn[rt]&&dfn[rt]<=efn[p]) {
x[i]=p=-(*--upper_bound(G[p].begin(),G[p].end(),rt,[&](int s,int t){ return dfn[s]<dfn[t]; }));
}
if(p>0) {
ans[i]+=T1.qry(dfn[p],efn[p])-(dep[p]-1)*T2.qry(dfn[p],efn[p]);
} else {
ans[i]+=T1.qry(m),p=-p;
ans[i]-=T1.qry(dfn[p],efn[p])-(dep[p]-1)*T2.qry(dfn[p],efn[p]);
}
}
}
for(int o=1;o<=bl[n];++o) {
memset(f,0,sizeof(f));
for(int i=lp[o];i<=rp[o];++i) f[u[i]]+=w[i],f[v[i]]+=w[i],f[h[i]]-=w[i],f[fa[h[i]]]-=w[i];
for(int _:{0,1}) for(int i=m;i>1;--i) f[fa[rk[i]]]+=f[rk[i]];
for(int i=1,tg=0;i<=q;++i) {
if(x[i]&&y[i]) tg+=(bl[x[i]]<o&&o<bl[y[i]]);
else if(x[i]) {
if(x[i]>0) ans[i]+=tg*f[x[i]];
else ans[i]+=tg*(f[1]-f[-x[i]]);
}
}
}
for(int i=1;i<=q;++i) if(x[i]&&!y[i]) cout<<ans[i]<<"\n";
return 0;
}
B. 线段树与区间加(segt)
题目大意
给定 \([1,n]\) 的线段树(每次不一定从中点分治),支持 \(q\) 次区间加,动态维护 \(\sum_u tg_ua_u+s_ub_u\)。
数据范围:\(n,q\le 2\times 10^5\)。
思路分析
首先线段树上的 \(s_u\) 相当于 \(u\) 子树内实际的元素和,减去所有祖先处的懒标记之和减去区间长度。
因此可以把 \(s_ub_u\) 的贡献拆到祖先的 \(tg_ua_u\) 以及原序列上,这样就去掉了 \(s_ub_u\)。
然后考虑怎么维护 \(\sum tg_ua_u\)。
考虑线段树上的修改过程,类比 zkw 线段树,定义 \(p_i\) 表示 \([i,i]\) 线段树上的对应区间。
对 \([l,r]\) 区间加,相当于从 \(p_{l-1},p_{r+1}\) 向上跳到 \(\mathrm{LCA}\) 为止,如果当前点是左 / 右儿子,就给兄弟打懒标记。
但此前我们还要 pushdown,具体来说 pushdown 的点也是两条链,即 \(\mathrm{LCA}(p_{l-1},p_l),\mathrm{LCA}(p_{r+1},p_r)\) 到根的链,从上往下把每个点 pushdown。
先维护 pushdown,那么就是把链上每个点的兄弟加上链上懒标记的前缀和。
那么树剖,对每个重链处理,那么就是把 \([l,i]\) 的元素和加到 \(i\) 的轻儿子上,然后在路径上的轻边特殊处理。
注意到我们不一定要实时维护轻儿子的权值,而是把一部分权值留在父亲处,这样 pushdown 时只要简单打标记,等到修改的时候遇到一个轻儿子再手动更新其权值。
然后是打标记,依然树链剖分,相当于把 \([l,r]\) 中重儿子在左边 / 右边的点加权值。
那么都可以直接用线段树维护,实现的时候维护打每种标记后答案的变化量即可。
时间复杂度 \(\mathcal O(n+q\log^2n)\)。
代码呈现
#include<bits/stdc++.h>
#define ui unsigned
using namespace std;
const int MAXN=4e5+5,MAXS=1<<20|5;
int n,m,q,rt,ch[MAXN][2],lp[MAXN],rp[MAXN],at[MAXN],fa[MAXN];
ui len[MAXN],w[MAXN],h[MAXN],sh[MAXN];
int tb[MAXN][20],siz[MAXN],hson[MAXN],lson[MAXN],ty[MAXN];
int top[MAXN],dfn[MAXN],dcnt,rk[MAXN];
int bit(int k) { return 1<<k; }
int cmp(int x,int y) { return dfn[x]<dfn[y]?x:y; }
int LCA(int x,int y) {
if(x==y) return x;
int l=min(dfn[x],dfn[y])+1,r=max(dfn[x],dfn[y]),k=__lg(r-l+1);
return cmp(tb[l][k],tb[r-bit(k)+1][k]);
}
struct Segt1 {
ui tr[MAXS],ws[MAXS],tg[MAXS];
void adt(int p,ui k) { tg[p]+=k,tr[p]+=ws[p]*k; }
void psd(int p) { adt(p<<1,tg[p]),adt(p<<1|1,tg[p]),tg[p]=0; }
void psu(int p) { tr[p]=tr[p<<1]+tr[p<<1|1]; }
void init(int l=1,int r=m,int p=1) {
if(l==r) return ws[p]=h[at[l]],void();
int mid=(l+r)>>1; psd(p);
init(l,mid,p<<1),init(mid+1,r,p<<1|1);
ws[p]=ws[p<<1]+ws[p<<1|1];
}
void add(int ul,int ur,ui k,int l=1,int r=m,int p=1) {
if(ul<=l&&r<=ur) return adt(p,k);
int mid=(l+r)>>1; psd(p);
if(ul<=mid) add(ul,ur,k,l,mid,p<<1);
if(mid<ur) add(ul,ur,k,mid+1,r,p<<1|1);
psu(p);
}
} TB;
void dfs1(int u) {
siz[u]=1;
if(len[u]==1) return ;
for(int v:ch[u]) {
h[v]+=h[u],dfs1(v),siz[u]+=siz[v],sh[u]+=sh[v];
if(siz[v]>siz[hson[u]]) hson[u]=v;
}
w[u]-=sh[u],lson[u]=ch[u][0]^ch[u][1]^hson[u],ty[u]=(lson[u]==ch[u][1]);
}
void dfs2(int u,int t) {
top[u]=t,dfn[u]=++dcnt,tb[dcnt][0]=fa[u],rk[dcnt]=u;
if(len[u]==1) return ;
dfs2(hson[u],t);
for(int v:ch[u]) if(v^hson[u]) dfs2(v,v);
}
struct Segt2 {
ui sw[MAXS],sv[MAXS][2];
ui f[MAXS],g[MAXS],h[MAXS],z[MAXS],tv[MAXS][2],lf[MAXS];
//f=x[u]*w[u], g=lz[ls]*w[ls], h=x[u]*sufw[ls], z=x[u]
bool tg[MAXS];
void psu(int p) {
f[p]=f[p<<1]+f[p<<1|1],g[p]=g[p<<1]+g[p<<1|1],z[p]=z[p<<1]+z[p<<1|1];
h[p]=h[p<<1]+h[p<<1|1]+z[p<<1]*sw[p<<1|1];
}
void adt(int p) {
tg[p]=1,g[p]+=h[p],lf[p]+=z[p];
f[p]=h[p]=z[p]=0,tg[p]=true;
}
void adv(int p,int c,ui k) { tv[p][c]+=k,g[p]+=sv[p][c]*k; }
void psd(int p) {
if(tg[p]) {
for(int o:{0,1}) adv(p<<1|1,o,z[p<<1]);
adt(p<<1),adt(p<<1|1),tg[p]=0;
}
for(int o:{0,1}) adv(p<<1,o,tv[p][o]),adv(p<<1|1,o,tv[p][o]),tv[p][o]=0;
}
void init(int l=1,int r=n,int p=1) {
if(l==r) {
sw[p]=sv[p][ty[rk[l]]]=w[lson[rk[l]]];
return ;
}
int mid=(l+r)>>1;
init(l,mid,p<<1),init(mid+1,r,p<<1|1);
sw[p]=sw[p<<1]+sw[p<<1|1];
for(int o:{0,1}) sv[p][o]=sv[p<<1][o]+sv[p<<1|1][o];
}
ui qlz(int u,int l=1,int r=n,int p=1) {
if(l==r) {
ui s=tv[p][ty[rk[l]]]+lf[p];
tv[p][0]=tv[p][1]=lf[p]=g[p]=0;
return s;
}
int mid=(l+r)>>1; psd(p);
ui s=(u<=mid?qlz(u,l,mid,p<<1):qlz(u,mid+1,r,p<<1|1));
psu(p);
return s;
}
void adx(int u,ui x,int l=1,int r=n,int p=1) {
if(l==r) {
f[p]+=x*w[rk[l]],h[p]+=x*sw[p],z[p]+=x;
return ;
}
int mid=(l+r)>>1; psd(p);
u<=mid?adx(u,x,l,mid,p<<1):adx(u,x,mid+1,r,p<<1|1);
psu(p);
}
void upd(int ul,int ur,int o,ui &x,int l=1,int r=n,int p=1) {
if(ul<=l&&r<=ur) {
if(~o) adv(p,o,x);
else adv(p,0,x),adv(p,1,x),x+=z[p],adt(p);
return ;
}
int mid=(l+r)>>1; psd(p);
if(ul<=mid) upd(ul,ur,o,x,l,mid,p<<1);
if(mid<ur) upd(ul,ur,o,x,mid+1,r,p<<1|1);
psu(p);
}
} T;
void upd(int s) {
vector <int> st;
for(int u=s;u;u=fa[top[u]]) st.push_back(u);
reverse(st.begin(),st.end());
for(int x:st) {
if(fa[top[x]]) T.adx(dfn[top[x]],T.qlz(dfn[fa[top[x]]]));
ui z=0; T.upd(dfn[top[x]],dfn[x],-1,z);
T.adx(dfn[hson[x]],z);
}
}
void add(int x,int y,bool o,ui z) {
while(top[x]^top[y]) {
if(x!=top[x]) T.upd(dfn[top[x]],dfn[x]-1,o,z);
x=fa[top[x]];
if(ty[x]!=o) T.adx(dfn[hson[x]],z);
}
if(x!=y) T.upd(dfn[y],dfn[x]-1,o,z);
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>m>>q,n=2*m-1;
for(int i=1;i<=n;++i) {
cin>>lp[i]>>rp[i]>>h[i]>>w[i],len[i]=rp[i]-lp[i]+1;
if(len[i]>1) cin>>ch[i][0]>>ch[i][1],fa[ch[i][0]]=fa[ch[i][1]]=i;
else at[lp[i]]=i;
if(lp[i]==1&&rp[i]==m) rt=i;
}
for(int i=1;i<=n;++i) sh[fa[i]]+=len[i]*h[i];
dfs1(rt),dfs2(rt,rt),TB.init(),T.init();
for(int k=1;k<20;++k) for(int i=1;i+bit(k)-1<=n;++i) tb[i][k]=cmp(tb[i][k-1],tb[i+bit(k-1)][k-1]);
for(int l,r;q--;) {
ui z; cin>>l>>r>>z,TB.add(l,r,z);
if(l>1) upd(LCA(at[l-1],at[l]));
if(r<m) upd(LCA(at[r+1],at[r]));
if(1<l&&r<m) {
int x=LCA(at[l-1],at[r+1]);
add(at[l-1],ch[x][0],1,z);
add(at[r+1],ch[x][1],0,z);
} else if(1<l) {
add(at[l-1],rt,1,z);
} else if(r<m) {
add(at[r+1],rt,0,z);
} else T.adx(dfn[rt],z);
cout<<TB.tr[1]+T.f[1]+T.g[1]<<"\n";
}
return 0;
}
*C. 数位 DP(digit)
题目大意
给定 \(s_1\sim s_{n-1}\),其中每个元素都是 \(\mathrm{AND},\mathrm{OR},\oplus\) 三种运算符之一,\(q\) 次询问 \(m\)。
求有多少 \(c_1\sim c_n\in [0,2^k)\) 并且存在 \(a_1\sim a_n\) 满足 \(a_i\in[0,c_i]\) 且 \(((a_1 \otimes_{s_1}a_2)\otimes_{s_2}a_3\cdots)\otimes_{s_{n-1}}a_n=m\)。
答案对 \(2^{32}\) 取模。
数据范围:\(n,q\le 1000,k\le 30\)。
思路分析
考虑如何检验一组 \(c\)。
打表发现能表示出的 \(m\) 一定是一段前缀 \([0,x]\),下面给出归纳证明:
- \(s_i=\mathrm{AND}\):则 \(x\gets \min(x,c)\)。
- \(s_i=\mathrm{OR}\):则 \(x\gets x\operatorname{OR}c\operatorname{OR}(\mathrm{highbit}(x\operatorname{AND}c)-1)\)。
- \(s_i=\oplus\):可以通过 \(a\gets a-(a\operatorname{AND} x)\) 转成 \(s_i=\mathrm{OR}\) 的情况。
那么我们只要考虑操作为 \(\mathrm{AND}\) 或 \(\mathrm{OR}\) 的情况。
但是正着依然不好维护判定过程考虑再倒过来,此时我们只要贪心让要得出的 \(x\) 尽可能小即可。
-
\(s_i=\mathrm{AND}\):则 \(c_i\ge x\) 即可,\(x\) 显然不变最优,方案数 \(2^k-x\)。
-
\(s_i=\mathrm{OR}\):很显然最优的情况下更新后的 \(x',c\) 无交,否则把 \(2^k=\mathrm{highbit}(x'\operatorname{AND}c)\) 变成 \(2^0+\cdots+2^{k-1}\) 更优。
因此只要 \(x'\operatorname{OR}c\ge x\) 即可,从高到低考虑,如果这一位上 \(c=0\) 则不更新,如果这一位 \(c=1,x=1\) 则 \(x\) 这一位变成 \(0\),否则低位全部清空。
更具体一点,我们找到 \(x\) 不包含的一个位 \(2^p\),然后把低 \(p\) 位清零,系数 \(2^p\)(\(c\) 的低位填法),然后每个 \(>p\) 的位上的 \(1\) 可以选择是否变成 \(0\)。
那么对这个过程 dp,但这次从前往后 dp。
对于 \(s_i=\mathrm{AND}\) 的位置,我们很难在不知道 \(x\) 的情况下算方案数 \(2^k-x\)。
可以把 \(x\) 拆成二进制 \(2^{t_1}+\cdots+2^{t_q}\),然后枚举每个 \(2^{t_i}\in x\),系数为 \(-2^{t_i}\),或者不枚举,系数为 \(2^k\)。
因此对于每个 \(s_i=\mathrm{AND}\) 的位置,我们可以选择直接 \(\times 2^k\),或选择一个 \(v\),钦定此时 \(2^v\in x\),系数为 \(-2^v\)。
那么我们从前往后,会确定若干个位置必定属于 \(x\)。
然后考虑对 \(s_i=\mathrm{OR}\) 的影响,找到此前被钦定的所有位置中最小的一个 \(d\),选择的那么 \(p<d\)。
并且在这个位置,我们具体选择哪个 \(p\) 没有影响,因为再往前的所有方案数的计算都不关心 \(<d\) 的位的任何值,所以方案数就是任填的 \(2^d\)。
那么动态维护从前往后 \(d\) 的轮廓线,此时还有一些没确定的决策,也就就是 \(s_i=\mathrm{OR}\) 的时候 \(>d\) 的位可以选择从 \(1\) 变成 \(0\)。
对于每个位,考虑他从前往后首次被钦定为 \(1\) 的时刻 \(t_r\),以及其首次高于轮廓线的时刻 \(t_l\),则 \([t_l,t_r]\) 中任意一个 \(\mathrm{OR}\) 操作都可以把这个位从 \(1\) 变成 \(0\),系数就是这一段的 \(\mathrm{OR}\) 个数。
注意到转移系数大部分都是 \(2\) 的幂,而模数又是 \(2^{32}\),所以很多转移最后在模意义下系数为 \(0\)。
考虑这样刻画轮廓线:\(c_0\sim c_{k-1}\),其中 \(c_i>1\) 表示 \(d=i\) 时的 \(\mathrm{OR}\) 操作次数为 \(c_i-1\),\(c_i=1\) 表示已存在一个 \(\mathrm{AND}\) 操作钦定了这个位,否则表示不存在。
容易发现系数一定是 \(2^{\sum i\times c_i}\) 的倍数,钦定 \(c_0\in\{0,1\}\),则状态数是拆分数级别的。
查询答案的时候暴力枚举每个合法的状态即可。
时间复杂度 \(\mathcal O((n+q)k|S|)\),其中 \(|S|\) 为状态数,\(|S|\le 7.2\times 10^4\)。
代码呈现
#include<bits/stdc++.h>
#define ui unsigned
#define ull unsigned long long
using namespace std;
const int MAXN=1005,MAXM=72005,MAXE=2e6+5;
typedef array<int,32> info;
int n,k,m,q,op[MAXN],lo[MAXM],vl[MAXM][32];
info st[MAXM],s;
mt19937_64 rnd(time(0));
ull wt[32],hs[MAXM];
const int P=1e7+19;
int hd[P],nxt[MAXM],msk[MAXM];
int qry(ull x) {
for(int i=hd[x%P];i;i=nxt[i]) if(hs[i]==x) return i;
return 0;
}
void dfs(int i,int c) {
if(i>k) {
++s[k],st[++m]=s;
for(int j=0;j<=k;++j) hs[m]+=s[j]*wt[j];
nxt[m]=hd[hs[m]%P],hd[hs[m]%P]=m,--s[k];
return ;
}
for(s[i]=0;s[i]*i<=c;++s[i]) dfs(i+1,c-i*s[i]);
}
int to[MAXM][2],R[MAXE],Z[MAXE],e,h[MAXM];
ui pr[MAXM][2],f[MAXM],g[MAXM];
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>k>>q;
for(int i=0;i<=k;++i) wt[i]=rnd();
s.fill(0),dfs(1,31),s.fill(0),s[0]=1,dfs(1,31);
for(int i=2;i<=n;++i) { char c; cin>>c,op[i]=(c!='A'); }
for(int i=1;i<=m;++i) {
int &p=lo[i]=0;
for(;!st[i][p];++p);
to[i][1]=qry(p?hs[i]+wt[p]:hs[i]),pr[i][1]=1u<<p;
to[i][0]=i,pr[i][0]=1u<<k;
for(int j=0;j<k;++j) {
if(st[i][j]) pr[i][0]-=1u<<j,msk[i]|=1<<j;
else R[++e]=qry(hs[i]+wt[j]),Z[e]=j;
}
h[i]=e;
for(int j=k;~j;--j) vl[i][j]=vl[i][j+1]+max(0,st[i][j]-1);
}
f[qry(wt[k])]=1;
int ct=0;
for(int o=1;o<=n;++o) {
memset(g,0,sizeof(g)),ct+=op[o];
for(int i=1;i<=m;++i) if(f[i]) {
g[to[i][op[o]]]+=f[i]*pr[i][op[o]];
if(!op[o]) for(int j=h[i-1]+1;j<=h[i];++j) {
if(Z[j]<lo[i]) g[R[j]]-=f[i]<<Z[j];
else g[R[j]]-=f[i]*(ct-vl[i][Z[j]]+1)<<Z[j];
}
}
memcpy(f,g,sizeof(f));
}
for(int x;q--;) {
cin>>x; ui z=0;
for(int i=1;i<=m;++i) if((x|msk[i])==x) {
ui t=f[i];
for(int j=0;j<k;++j) if((x>>j&1)&&!st[i][j]) t*=ct-vl[i][j]+1;
z+=t;
}
cout<<z<<"\n";
}
return 0;
}
Round #80 - 20250529
A. 贪腐(corrupt)
题目大意
给定 \(0\sim 2^{n}-1\),把他们两两匹配成 \((x,y)\),要求所有 \(x\oplus y\) 是 \(2\) 的幂,且 \(\sum[x\oplus y=2^k]=a_k\)。
数据范围:\(n\le 20\)。
思路分析
首先按最高位分开,除了 \(a_{n-1}\) 对匹配以外,剩余的匹配高位都一样。
因此对左右两侧递归 \(2^{n-1}-a_{n-1}\) 个点的子问题,很显然在 \(n>1\) 时要求 \(2\mid a_{n-1}\)。
注意到我们能任意排列每个二进制位,所以所有有解的必要条件是 \(a_i\) 都是偶数。
那么考虑递归,如果 \(a_0\sim a_{n-2}\) 都是 \(4\) 的倍数,则递归 \(b_i=\dfrac {a_i}2\) 的情况。
对于剩余的 \(a_{n-1}\) 个点,直接给 \(b_{n-2}\) 加上 \(\dfrac{a_{n-1}}2\) 即可,那么这样递归出大小为 \(n-1\) 的问题的解,然后把最高位设为 \(1\) 复制一遍。
找到 \((x,x+2^{n-2}),(x+2^{n-1},x+2^{n-2}+2^{n-1})\) 这样的两对匹配,交换后就能得到两个异或和为 \(2^{n-1}\) 的匹配,这样就能构造出大小为 \(n\) 的解。
然后考虑有 \(a_i\bmod 4=2\) 的情况,那么给 \(b_i\) 加上 \(1\),最后把一对 \(2^i\) 的匹配调整成 \(2^{n-1}\) 的匹配即可。
如果 \(\dfrac{a_{n-1}}2\) 大于 \(a_i\bmod 4=2\) 的点数就能调整,如果取 \(a_{n-1}\) 为 \(\max a_i\),则无法调整时 \(\dfrac{2^{n-1}}{n}<n\),可以发现此时 \(n\le 6\),爆搜即可。
时间复杂度 \(\mathcal O(2^n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
int f[1<<20];
void bf(vector<int>&a) {
int n=a.size(); fill(f,f+(1<<n),-1);
function<bool(int)> dfs=[&](int i) {
if(i==(1<<n)) return true;
if(~f[i]) return dfs(i+1);
for(int x=0;x<n;++x) if(a[x]&&f[i^(1<<x)]==-1) {
f[i]=f[i^(1<<x)]=x,--a[x];
if(dfs(i+1)) return true;
f[i]=f[i^(1<<x)]=-1,++a[x];
}
return false;
};
dfs(0);
}
void build(vector<int>&a) {
if(a.size()<=2) return bf(a);
int n=a.size(),x=max_element(a.begin(),a.end())-a.begin();
swap(a[x],a[n-1]);
vector <int> b(n-1),w(n-1);
int ct=0,m=1<<(n-1);
for(int i=0;i<n-1;++i) ct+=a[i]>>1&1;
if(ct>a[n-1]/2) return bf(a);
for(int i=0;i<n-1;++i) w[i]=a[i]>>1&1;
w[n-2]+=a[n-1]/2-ct;
for(int i=0;i<n-1;++i) b[i]=a[i]/2+w[i];
build(b);
for(int i=0;i<m;++i) if((i>>f[i]&1)&&w[f[i]]) --w[f[i]],f[i]=f[i^(1<<f[i])]=n-1;
for(int i=0;i<m;++i) f[i+m]=f[i];
for(int i=0;i<(1<<n);++i) if((i>>x&1)^(i>>(n-1)&1)) {
int j=i^(1<<x|m);
if(i<j) swap(f[i],f[j]);
}
for(int i=0;i<(1<<n);++i) if(f[i]==x||f[i]==n-1) {
f[i]^=x^(n-1);
}
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n; cin>>n;
if(n==1) return cout<<"0 1\n",0;
vector <int> a(n);
bool ok=1;
for(int i=0;i<n;++i) cin>>a[i],ok&=a[i]%2==0;
if(!ok) return cout<<"-1\n",0;
build(a);
for(int i=0;i<(1<<n);++i) if(i>>f[i]&1) cout<<(i^(1<<f[i]))<<" "<<i<<"\n";
return 0;
}
*B. 生命的循环(cycle)
题目大意
给定 \(n\) 个点 \(m\) 条带权边的有向图,定义 \(s_x\) 表示是否存在 \(1\to n\) 长度为 \(x\) 的路径,求 \(s\) 的周期。
数据范围:\(n\le 5000,m\le 10^4,B\le 100\),其中 \(B\) 为简单环权值和上界。
思路分析
先考虑如何求出 \(s_x\)。
考虑充分大的地方的 \(s\),那么路径必然要经过一些环,则周期就是这些环的 \(\mathrm{gcd}\)。
所以可以对原图缩点,然后对于 DAG 上的每条路径,算对应的周期。
那么周期的最大值为 \(B\),所以可以表示成若干 \((p,r)\) 表示把 \(x\bmod p=r\) 的 \(s\) 都设为 \(1\)。
考虑如何维护, 表示是否存在走到 \(u\),长度 \(\bmod p=r\) 的路径。
然后设 \(g_{u,p,r}\) 表示是否存在走到 \(u\),周期为 \(p\),长度 \(\bmod p=r\) 的路径,这里从 \(f_{u,p,r}=1\) 的状态开始搜索,然后 \(p\) 对经过的每个大小 \(>1\) 的强连通分量所有环取 \(\gcd\)。
预处理每个强连通分量内所有环长的 \(\gcd\) 是经典问题。
由于状态总数是 \(\mathcal O(nB^2)\) 的,因此可以直接记忆化搜索。
然后考虑如何通过若干 \((p,r)\) 求 \(f\) 的周期。
这里 \(s_x=1\) 是若干条件的并,对 \((p,r)\) 取反变成若干条件的交,即 \(\forall p,x\bmod p\in S_p\) 时合法。
首先对 \(S_p\) 求出最小整周期,如果所有 \(p\) 两两互质,那么答案就是 \(\prod p\)。
首先 \(\prod p\) 一定合法,然后如果答案更小,则周期 \(d\) 一定不是某个 \(p\) 的倍数,那么就导出矛盾。
注意这里导出矛盾是因为所有 \(S_p\) 中的元素都存在一个 \(s_x=1\) 满足 \(x\bmod p=r\),则 \(s_{x+d}=s_x=1\),从而 \((x+d)\bmod p\in S_p\),最终 \(S_p\) 有 \(\gcd(d,p)\) 这个更小的周期,那么最终肯定无法全部满足。
由于 \(p\) 两两互质因此 CRT 总能找到一组解。
否则也能类似,如果所有 \(S_p\) 都满足该条件,答案就是 \(\mathrm{lcm}(p)\)。
那么我们只要把每个 \(S_p\) 中无效的 \(r\) 都删除即可。
考虑如何进行此操作,我们选定一个 \(K\),枚举 \(r\in[0,K)\),然后只考虑所有 \(\bmod K=r\) 的 \(s_x\)。
把这些元素看成一个新串,则原先的 \(S_p\) 在这上面的周期为 \(d_p=\dfrac{p}{\gcd(p,K)}\)。
取 \(K=2^8\times 3^2\times 5\times 7=2520\),则所有 \(p\in[1,B]\),\(d_p\) 都只有一个质因子。
那么所有 \(d_p\) 要么互质要么有倍数关系。
对于有倍数关系的直接延长合并,剩下所有的串长互质,两两无法消除。
最后会形成 \(B\) 个为 \(B\times K\) 的周期,求最小整周期后求 \(\mathrm{lcm}\) 即可。
时间复杂度 \(\mathcal O((n+m+B^2)K)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5005,V=100,B=2520,MAXL=252005,MOD=1e9+9;
int n,m,_;
struct Edge { int v,w; };
vector <Edge> G[MAXN];
int dfn[MAXN],low[MAXN],dcnt,stk[MAXN],tp,bl[MAXN],scnt;
bool ins[MAXN],vis[MAXN];
void tarjan(int u) {
dfn[u]=low[u]=++dcnt,stk[++tp]=u,ins[u]=true;
for(auto e:G[u]) {
if(!dfn[e.v]) tarjan(e.v),low[u]=min(low[u],low[e.v]);
else if(ins[e.v]) low[u]=min(low[u],dfn[e.v]);
}
if(low[u]==dfn[u]) for(++scnt;ins[u];ins[stk[tp--]]=false) bl[stk[tp]]=scnt;
}
int dis[MAXN],c[MAXN];
void dfs0(int u) {
vis[u]=true;
for(auto e:G[u]) if(bl[e.v]==bl[u]) {
if(!vis[e.v]) dis[e.v]=dis[u]+e.w,dfs0(e.v);
else c[bl[u]]=__gcd(c[bl[u]],abs(dis[u]+e.w-dis[e.v]));
}
}
bool f[MAXN][V+5][V+5],g[MAXN][V+5][V+5];
void dfs1(int u,int p,int r) {
if(f[u][p][r]) return ;
f[u][p][r]=true;
for(auto e:G[u]) dfs1(e.v,p,(r+e.w)%p);
}
void dfs2(int u,int p,int r) {
if(c[bl[u]]) p=__gcd(p,c[bl[u]]),r%=p;
if(g[u][p][r]) return ;
g[u][p][r]=true;
for(auto e:G[u]) dfs2(e.v,p,(r+e.w)%p);
}
bool isc[V+5],h[30][V+5],str[30][MAXL];
int t=0,ty[V+5],w[30],pw[V+5],kmp[MAXL];
void solve(int o) {
memset(h,0,sizeof(h));
for(int i=1;i<=100;++i) {
if(B%i==0) {
if(g[n][i][o%i]) return ;
continue;
}
int p=ty[i/__gcd(i,B)];
for(int j=0;j<w[p];++j) h[p][j]|=g[n][i][(j*B+o)%i];
}
for(int i=1;i<=t;++i) if(!count(h[i],h[i]+w[i],0)) return ;
for(int i=1;i<=t;++i) for(int j=0;j<w[i];++j) str[i][j*B+o+1]=!h[i][j];
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
for(int i=2;i<=V;++i) if(!isc[i]) {
for(w[++t]=i,ty[i]=t;w[t]*i<=V;) ty[w[t]*=i]=t;
for(int j=i*2;j<=V;j+=i) isc[j]=true;
}
cin>>n>>m>>_;
for(int i=1,u,v,z;i<=m;++i) cin>>u>>v>>z,G[u].push_back({v,z});
for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i);
for(int i=1;i<=n;++i) if(!vis[i]) dfs0(i);
for(int i=1;i<=V;++i) dfs1(1,i,0);
for(int u=1;u<=n;++u) if(c[bl[u]]) for(int r=0,p=c[bl[u]];r<p;++r) if(f[u][p][r]) dfs2(u,p,r);
for(int o=0;o<B;++o) solve(o);
for(int o=1;o<=t;++o) {
int len=w[o]*B;
for(int i=2,j=0;i<=len;++i) {
while(j&&str[o][j+1]!=str[o][i]) j=kmp[j];
kmp[i]=j+=(str[o][j+1]==str[o][i]);
}
int d=len-kmp[len];
if(len%d) d=len;
for(int i=2;i*i<=d;++i) if(d%i==0) {
int z=0; for(;d%i==0;d/=i,++z);
pw[i]=max(pw[i],z);
}
if(d>1) pw[d]=max(pw[d],1);
}
int ans=1;
for(int i=1;i<=V;++i) for(int j=0;j<pw[i];++j) ans=1ll*ans*i%MOD;
cout<<ans<<"\n";
return 0;
}
*C. 轮盘赌游戏(bet)
题目大意
给定长度为 \(n\) 的环,在第 \(i\) 个点有 \(1-p_i\) 的概率停止,否则走到 \((i+d)\bmod n\)。
初始给定 \(p'_0\sim p'_{m-1}\),\(p_i=p'_{i\bmod m}\),然后给出一棵树,\(q\) 个叶子,每个叶子表示对 \(p\) 的单点修改(修改的位置不同)。
对每个节点,求出进行其子树内所有叶子代表的修改后,随机从一个点出发后期望走多少步停止(开始算一步)。
数据范围:\(n\le 10^{16},m\le 5000,q\le 10^5\),\(\gcd(n,d)=1\)。
思路分析
从 \(d=1\) 开始,写成 dp 的形式就是 \(f_i=1+p_if_{i+1}\),然后在长度为 \(n\) 的环上 dp。
那么解 \(f\) 只要设 \(f_0=x\),然后带入一次函数复合一圈即可。
如果 \(n\) 很小,直接建线段树,区间 \([l,r]\),把 \(f_l,\sum f_i\) 表示成关于 \(f_{r+1}\) 的一次函数,合并很简单。
那么 \(n\) 很大的时候,先对 \(p'_0\sim p'_{m-1}\) 建线段树,然后类似倍增的方式拷贝 \(\dfrac nm\) 份,修改的时候类似主席树,把一条链复制一遍。
然后维护子树内所有操作,启发式合并比较简单,但更优的做法是线段树合并,即两棵子树中有一棵被修改过,另一棵则是原树上节点,就返回修改过的一棵,复杂度不变。
这样就在 \(\mathcal O(q\log n)\) 的复杂度内完成了修改。
然后回到原问题,把 \(p_i\) 变成 \(p_{id\bmod n}\) 然后又回到 \(d=1\) 的情况,那么对 \(q=0\) 的情况建出类线段树结构,然后仿照上面的过程维护即可。
先考虑 \(q=0\) 时如何单纯维护答案。
那么我们可以看成 \(\prod_{i=0}^{n-1} F((id\bmod n)\bmod m)\),其中 \(F(i)\) 是 \(p'_i\) 对应信息。
两个取模太困难了,写成 \(\prod_{i=0}^{n-1}F((id-\lfloor id/n\rfloor n)\bmod m)\),包含取整和求和可以想到万能欧几里得。
考察直线 \(y=\left\lfloor\dfrac{xd}n\right\rfloor\),维护 \(u=0\),每经过一条横线 \(u\gets u-n\),每经过一条竖线 \(s\gets s\times F(u),u\gets u+d\)。
那么维护初始 \(u\in [0,m)\) 时的答案,以及最终 \(u\) 的偏移量就能解决。
然后考虑如何建立线段树结构。
注意到万欧过程中维护 \(u\in[0,m)\) 的答案是每次合并两个信息得到的,那么每个维护的答案可以看成 \(F\) 的一个区间积,因此不维护答案,而是把合并的过程建树,每个值对应一个树上节点。
由于万欧只有 \(\mathcal O(\log n)\) 次合并,因此初始节点个数 \(\mathcal O(m\log n)\),最大深度 \(\mathcal O(\log n)\),直接看成类线段树结构进行刚才的过程即可。
时间复杂度 \(\mathcal O((m+q)\log n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
#define LL __int128
using namespace std;
const int MAXN=1e5+5,MOD=998244353,MAXS=2e7+5;
ll ksm(ll a,ll b=MOD-2) { ll s=1; for(;b;a=a*a%MOD,b>>=1) if(b&1) s=s*a%MOD; return s; }
struct func {
ll a,b,c,d; //f=ax+b, s=cx+d
inline friend func operator +(const func &l,const func &r) {
return {l.a*r.a%MOD,(l.b+l.a*r.b)%MOD,(l.c*r.a+r.c)%MOD,(l.d+r.d+l.c*r.b)%MOD};
}
ll val() { return (b*ksm(1+MOD-a)%MOD*c+d)%MOD; }
};
func f[MAXS];
ll n,d,siz[MAXS];
int m,q1,q2,ls[MAXS],rs[MAXS],tot;
int comb(int x,int y) {
if(!x||!y) return x|y;
++tot,ls[tot]=x,rs[tot]=y;
siz[tot]=siz[x]+siz[y],f[tot]=f[x]+f[y];
return tot;
}
struct info {
int d,f[5005];
info() { d=0,memset(f,0,sizeof(f)); }
inline friend info operator *(const info &u,const info &v) {
info w; w.d=(u.d+v.d)%m;
for(int i=0;i<m;++i) w.f[i]=comb(u.f[i],v.f[(i+u.d)%m]);
return w;
}
};
info ksm(info a,ll b) { info s; for(;b;a=a*a,b>>=1) if(b&1) s=s*a; return s; }
info euclid(ll N,ll P,ll Q,ll R,info X,info Y) {
if((LL)P*N+R<Q) return ksm(Y,N);
if(P>=Q) return euclid(N,P%Q,Q,R,X,ksm(X,P/Q)*Y);
ll M=((LL)P*N+R)/Q;
return ksm(Y,(Q-R-1)/P)*X*euclid(M-1,Q,P,(Q-R-1)%P,Y,X)*ksm(Y,N-((LL)Q*M-R-1)/P);
}
int rt[MAXN*2],lim;
void upd(ll x,int z,int q,int &p) {
siz[p=++tot]=siz[q];
if(siz[p]==1) return f[p]={z,1,z,1},void();
if(x<=siz[ls[q]]) upd(x,z,ls[q],ls[p]),rs[p]=rs[q];
else upd(x-siz[ls[q]],z,rs[q],rs[p]),ls[p]=ls[q];
f[p]=f[ls[p]]+f[rs[p]];
}
int merge(int p,int q) {
if(p<=lim||q<=lim) return max(p,q);
ls[p]=merge(ls[p],ls[q]),rs[p]=merge(rs[p],rs[q]);
f[p]=f[ls[p]]+f[rs[p]];
return p;
}
void exgcd(ll a,ll b,ll &x,ll &y) {
if(!b) x=1,y=0;
else exgcd(b,a%b,y,x),y-=x*(a/b);
}
ll inv(ll u,ll p){
ll a,b; exgcd(u,p,a,b);
return (a%p+p)%p;
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m>>d>>q1>>q2;
for(int i=1,x;i<=m;++i) cin>>x,f[i]={x,1,x,1},siz[i]=1;
info X,Y,Z;
X.d=(m-n%m)%m,Y.d=d%m,tot=m;
for(int i=0;i<m;++i) Y.f[i]=i+1;
rt[0]=euclid(n,d,n,0,X,Y).f[d%m];
cout<<f[rt[0]].val()<<"\n",lim=tot;
ll iv=inv(d,n);
for(int i=1;i<=q1;++i) {
ll x,y; cin>>x>>y,x=(LL)x*iv%n;
upd(x?x:n,y,rt[0],rt[i]);
cout<<f[rt[i]].val()<<"\n";
}
for(int i=q1+1,x,y;i<=q1+q2;++i) {
cin>>x>>y;
rt[i]=merge(rt[x],rt[y]);
cout<<f[rt[i]].val()<<"\n";
}
return 0;
}
Round #81 - 20250605
A. 玻利维亚(Bolivia)
题目大意
给定直方图,保证中间一列最高,求有多少 \([l,r]\),使得仅保留这些行时直方图对称,\(q\) 次单点修改,动态维护答案。
数据范围:\(n\le 2\times 10^5\)。
思路分析
对于对称的两个点,设他们高度为 \(x,y(x<y)\),则合法区间要么 \(r\le x\),要么 \(l>y\),可以把限制看成 \([l,r]\) 与 \([x+1,y]\) 无交。
所以直接线段树,维护所有没被覆盖的极长连续段长度的平方和,只要维护节点左右两侧的最长段大小。
加入和删除区间用标记永久化实现。
时间复杂度 \(\mathcal O((n+q)\log n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
int n,m,q,a[MAXN];
struct info {
int l,r; ll s; bool c;
inline friend info operator +(const info &u,const info &v) {
return {u.l+(u.c?v.l:0),v.r+(v.c?u.r:0),u.s+v.s+1ll*u.r*v.l,u.c&&v.c};
}
};
const info O={0,0,0,0};
struct SegmentTree {
static const int MAXS=1<<21|5;
info tr[MAXS]; int tg[MAXS];
void psu(int p) { tr[p]=(tg[p<<1]?O:tr[p<<1])+(tg[p<<1|1]?O:tr[p<<1|1]); }
void init(int l=1,int r=m,int p=1) {
if(l==r) return tr[p]={1,1,1,1},void();
int mid=(l+r)>>1;
init(l,mid,p<<1),init(mid+1,r,p<<1|1);
psu(p);
}
void upd(int ul,int ur,int k,int l=1,int r=m,int p=1) {
if(ul<=l&&r<=ur) return tg[p]+=k,void();
int mid=(l+r)>>1;
if(ul<=mid) upd(ul,ur,k,l,mid,p<<1);
if(mid<ur) upd(ul,ur,k,mid+1,r,p<<1|1);
psu(p);
}
ll val() { return tg[1]?0:tr[1].s; }
} T;
void upd(int i,int c) {
int x=a[i],y=a[n-i+1];
if(x>y) swap(x,y);
if(x<y) T.upd(x+1,y,c);
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>q;
for(int i=1;i<=n;++i) cin>>a[i];
m=a[(n+1)/2],T.init();
for(int i=1;i<(n+1)/2;++i) upd(i,1);
cout<<T.val()<<"\n";
for(int x,v;q--;) cin>>x>>v,upd(x,-1),a[x]=v,upd(x,1),cout<<T.val()<<"\n";
return 0;
}
*B. 子集和(subset)
题目大意
给定 \(n\) 个元素 \(a_i,b_i\),定义 \(f(S)\) 表示 \(S\) 中每个元素选择 \(0,a_i,b_i\) 中的一个,求有多少种选法使得元素总和 \(\bmod m=0\)。
\(q\) 次询问给定无交 \([l_1,r_1],[l_2,r_2]\),在两个区间中各选一个元素 \(i,j\),求 \(\sum f(S\setminus\{i,j\})\)。
数据范围:\(n\le 10^4,m\le 200,q\le 10^6\)。
思路分析
首先肯定要维护循环卷积,即 \(\bmod m\) 意义下的卷积,设 \(g(S)\) 表示元素和 \(\bmod m\) 为 \([0,m)\) 每种的方案数。
在这题的情况下合并两个 \(g\) 信息很难做到低于 \(\mathcal O(m^2)\),很显然不能接受。
但注意到 \(S\) 中加入单个元素只要 \(\mathcal O(m)\),并且用 \(g(A),g(B)\) 求 \(f(A+B)\) 也是 \(\mathcal O(m)\) 的。
那么我们肯定要把查询的答案表示成两个集合 \(A,B\) 的 \(g\) 信息之和。
但每个询问有四个变量,很难拆成两个集合,但注意到 \([l_1,r_1]\) 可以差分成 \([1,r_1]-[1,l_1-1]\),\([l_2,r_2]\) 可以差分成 \([l_2,n]-[r_2+1,n]\)。
那么我们只要维护 \(l_1=1,r_2=n\) 的询问 \((1,x,y,n)\) 即可。
拆成两个信息之和自然想到猫树,找到 \(x\le mid<y\) 的点,那么我们要算 \([1,mid]\) 删掉 \([1,x]\) 中的一个点的 \(g\) 信息,以及 \([mid+1,n]\) 中删掉 \([y,n]\) 一个点的 \(g\) 信息。
两者对称,只考虑第一个。
首先我们可以提前处理 \([1,x]\) 中删掉一个点的 \(g\) 信息,只要逐个加入 \([x+1,mid]\) 范围内的点即可。
但对每个 \(x\) 都处理复杂度无法通过。
分开,先处理 \([1,l-1]\) 中删掉一个点的信息,然后加入 \([l,mid]\) 的点,接下来对于每个 \(u\in [l,mid]\),算删掉这个点后的 \(g\) 信息。
由于初始每个点的信息都是 \([1,l-1]\) 的乘积,因此可以 CDQ 分治,每次把右边的元素加入左边,左边的元素加入右边。
那么对第二部分维护一个前缀和即可。
时间复杂度 \(\mathcal O((n\log^2n+q)m)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e4+5,MAXQ=1e6+5,MOD=1e9+7;
int n,m,q,a[MAXN],b[MAXN],ans[MAXQ];
inline void add(int &x,const int &y) { if((x+=y)>=MOD) x-=MOD; }
struct poly {
int f[205];
poly() { memset(f,0,sizeof(f)); }
inline friend poly operator +(const poly &u,const poly &v) {
poly w=u;
for(int i=0;i<m;++i) add(w.f[i],v.f[i]);
return w;
}
inline friend poly operator *(const poly &u,const int &o) {
poly w=u;
for(int i=0;i<m;++i) add(w.f[(i+a[o])%m],u.f[i]);
for(int i=0;i<m;++i) add(w.f[(i+b[o])%m],u.f[i]);
return w;
}
} L[MAXN][2],R[MAXN][2],f[MAXN],dp[MAXN];
struct Qry { int l,r,i; };
void cdq(int l,int r) {
if(l==r) return ;
int mid=(l+r)>>1;
f[mid+1]=f[l];
for(int i=l;i<=mid;++i) f[mid+1]=f[mid+1]*i;
for(int i=mid+1;i<=r;++i) f[l]=f[l]*i;
cdq(l,mid),cdq(mid+1,r);
}
void solve(int l,int r,const vector<Qry>&qy) {
if(qy.empty()) return ;
int mid=(l+r)>>1;
f[l]=L[l-1][0],f[mid+1]=R[r+1][0];
cdq(l,mid),cdq(mid+1,r);
dp[l]=L[l-1][1],dp[r]=R[r+1][1];
for(int i=l;i<=mid;++i) dp[l]=dp[l]*i;
for(int i=mid+1;i<=r;++i) dp[r]=dp[r]*i;
dp[l]=dp[l]+f[l],dp[r]=dp[r]+f[r];
for(int i=l+1;i<=mid;++i) dp[i]=dp[i-1]+f[i];
for(int i=r-1;i>mid;--i) dp[i]=dp[i+1]+f[i];
vector<Qry> lq,rq;
for(auto i:qy) {
if(i.r<=mid) lq.push_back(i);
else if(i.l>mid) rq.push_back(i);
else {
int z=0;
for(int j=0;j<m;++j) z=(z+1ll*dp[i.l].f[j]*dp[i.r].f[(m-j)%m])%MOD;
if(i.i<0) z=MOD-z,i.i*=-1;
ans[i.i]=(ans[i.i]+z)%MOD;
}
}
solve(l,mid,lq),solve(mid+1,r,rq);
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m>>q;
for(int i=1;i<=n;++i) cin>>a[i]>>b[i];
L[0][0].f[0]=R[n+1][0].f[0]=1;
for(int i=1;i<=n;++i) L[i][0]=L[i-1][0]*i,L[i][1]=L[i-1][1]*i+L[i-1][0];
for(int i=n;i>=1;--i) R[i][0]=R[i+1][0]*i,R[i][1]=R[i+1][1]*i+R[i+1][0];
vector <Qry> qy;
for(int i=1,lx,rx,ly,ry;i<=q;++i) {
cin>>lx>>rx>>ly>>ry;
qy.push_back({rx,ly,i});
if(lx>1) qy.push_back({lx-1,ly,-i});
if(ry<n) qy.push_back({rx,ry+1,-i});
if(lx>1&&ry<n) qy.push_back({lx-1,ry+1,i});
}
solve(1,n,qy);
for(int i=1;i<=q;++i) cout<<ans[i]<<"\n";
return 0;
}
*C. 链覆盖(chain)
题目大意
给定 \(n\),对于所有 \(m,k\) 计算:所有 \(n\) 个点的有标号树中(根为 \(1\)),有多少个树选出 \(k\) 条一端为根的路径,其并集的最大值为 \(m\)。
数据范围:\(n\le 300\)。
思路分析
假设所求的树根节点任意,最后除以 \(n\)。
从 \(k\) 算 \(m\) 是经典长剖问题,但这里肯定不能 dp 树的长链集合。
考虑换一种方式刻画,设 \(h_u\) 表示 \(u\) 子树最大深度,\(c_x\) 表示 \(h_u=x\) 的点数,则 \(m=\sum\min(c_x,k)\)。
那么对 \(c\) 数组 dp,显然 \(c\) 必须递减。
从大往小确定 \(c\),考虑 \(c_i\) 的这些点怎么填:首先他们的父亲肯定是 \(h_u>i\) 的点,且 \(c_{i+1}\) 个 \(h=i+1\) 的点,每个点至少有一个 \(h=i\) 的儿子,而连向 \(h_u>i+1\) 的点则无区别。
因此我们只要记录 \(f_{i,j,k}\) 表示 \(c_i=j,\sum_{t>i}c_t=k\)。
转移的时候要算一个系数,\(c_{x}=i,c_{x+1}=j,\sum_{t>x+1}c_t=k\) 时 \(h=i\) 的父亲选法,每次加入一个点,如果父亲 \(h=i+1\),讨论该点是不是其父亲的最后一个 \(h=i\) 的儿子即可。
然后考虑如何根据 \(f\) 计算答案。
在首个 \(c_i\ge k\) 的位置算答案,枚举 \(c_i,c_{i+1}\),则贡献到 \(k\in[c_{i+1},c_i)\) ,\(m=ik+\sum_{t>i}c_t\)。
直接在 dp \(f\) 的过程中实现,只要提前求出从 \(f_{i,j,k}\to f_{1}\) 的系数即可,转置原理就能解决。
注意 \(k\ge c_1\) 的情况没算,此时 \(m=n\),只要减去 \(m<n\) 的方案数即可。
注意到 \(j\le c_i\le \dfrac ni\),因此转移的复杂度 \(\mathcal O(n^3)\)。
暴力实现复杂度 \(\mathcal O(n^4)\),瓶颈在枚举 \(k\),算答案的时候前缀和优化一下可以做到 \(\mathcal O(n^3)\)。
时间复杂度 \(\mathcal O(n^3)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
namespace FastMod {
typedef unsigned long long ull;
typedef __uint128_t uLL;
ull b,q,r; uLL m;
inline void init(const ull &B) { b=B,m=(uLL(1)<<64)/B; }
inline ull mod(const ull &a) {
r=a-((m*a)>>64)*b;
return r>=b?r-b:r;
}
}
#define o(x) FastMod::mod(x)
const int MAXN=305;
int n,MOD;
ll fac[MAXN],ifac[MAXN],f[MAXN][MAXN][MAXN],g[MAXN][MAXN][MAXN],h[MAXN][MAXN][MAXN],w[MAXN][MAXN],dp[MAXN][MAXN];
ll ksm(ll a,ll b=MOD-2) { ll s=1; for(;b;a=o(a*a),b>>=1) if(b&1) s=o(s*a); return s; }
void add(ll &x,const ll &y) { x=o(x+y); }
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>MOD,FastMod::init(MOD);
if(n==1) return cout<<"1\n",0;
for(int i=fac[0]=ifac[0]=1;i<MAXN;++i) ifac[i]=ksm(fac[i]=o(fac[i-1]*i));
for(int k=0;k<=n;++k) h[0][0][k]=1;
for(int i=1;i<=n;++i) for(int j=0;j<=i;++j) for(int k=0;i+j+k<=n;++k) {
h[i][j][k]=o(h[i-1][j][k]*(j+k)+(j?h[i-1][j-1][k]*j:0));
}
for(int i=1;i<=n;++i) g[1][i][n-i]=ifac[i];
for(int i=1;i<n;++i) {
for(int j=0;j<=n/i;++j) for(int k=0;j+k<=n;++k) if(g[i][j][k]) {
for(int v=0;v<=n/(i+1)&&v<=k&&v<=j;++v) {
add(g[i+1][v][k-v],o(g[i][j][k]*ifac[v])*h[j][v][k-v]);
}
}
}
f[n][1][0]=1;
for(int i=n-1;i;--i) {
f[i][1][0]=1,memset(w,0,sizeof(w));
for(int j=1;j<=n/(i+1);++j) for(int k=0;j+k<=n;++k) if(f[i+1][j][k]) {
for(int v=j;v<=n/i&&v+j+k<=n;++v) {
add(f[i][v][j+k],o(f[i+1][j][k]*ifac[v])*h[v][j][k]);
ll z=o(o(o(f[i+1][j][k]*g[i][v][j+k])*h[v][j][k])*fac[n-1]);
add(w[j+k][j],z),add(w[j+k][v],MOD-z);
}
}
for(int s=1;s<=n;++s) for(int t=1;t<=n;++t) {
add(w[s][t],w[s][t-1]),add(dp[t][s+t*i],w[s][t]);
}
}
ll wys=ksm(n,n-2);
for(int i=1;i<=n;++i) {
dp[i][n]=wys;
for(int j=1;j<n;++j) cout<<dp[i][j]<<" ",add(dp[i][n],MOD-dp[i][j]);
cout<<dp[i][n]<<"\n";
}
return 0;
}

浙公网安备 33010602011771号