做题笔记 - Jan. 2026
题目整理
[eJOI 2018] Cycle Sort
先考虑排列的做法。首先排除所有不动点,即 \(a_i=i\)。\(s=n\) 时直接找出所有置换环并交换,否则若 \(s-n>1\),则可以通过额外花费 \(k\) 的代价是 \(k\) 个环在 \(2\) 次操作内还原,详见 Sample #4。
如果 \(a\) 不是排列,考虑给所有权相同的点建一个相同的汇点,从这个汇点再连到它们应该在的位置,这样就完成了本质相同的点的交换。注意到新图上每个点出入度相同,因此每个连通块存在欧拉回路,则将所有欧拉回路作为环按排列的方式输出答案即可。
时间复杂度 \(O(n\log n)\),瓶颈在排序。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N=2e5+9;
int fi[N<<1],ne[N<<2],to[N<<2],adj;
inline void AddEdge(int x,int y){
ne[++adj]=fi[x];
fi[x]=adj;
to[adj]=y;
}
int a[N],v[N],n,m,s;
vector<vector<int>> cyc;
inline void DFS(int x){
for(int &i=fi[x];i;){
int y=to[i];
i=ne[i];
DFS(y);
}
if(x<=n) cyc.back().push_back(x);
}
signed main(){
cin.tie(0),cout.tie(0);
ios::sync_with_stdio(0);
cin>>n>>s;
for(int i=1;i<=n;i++) cin>>a[i];
vector<int> val(a,a+n+1);
sort(val.begin(),val.end());
val.erase(unique(val.begin(),val.end()),val.end());
vector<int> cnt(val.size(),0);
for(int i=1;i<=n;i++){
a[i]=lower_bound(val.begin(),val.end(),a[i])-val.begin();
cnt[a[i]]++;
}
partial_sum(cnt.begin(),cnt.end(),cnt.begin());
m=val.size()-1;
for(int i=1;i<=n;i++){
if(cnt[a[i]-1]<i&&i<=cnt[a[i]]) v[i]=1;
else AddEdge(i,n+a[i]);
}
for(int i=1;i<=m;i++){
for(int j=cnt[i-1]+1;j<=cnt[i];j++) if(!v[j]) AddEdge(n+i,j);
}
int t=0;
for(int i=1;i<=n;i++){
if(!fi[i]) continue ;
cyc.push_back(vector<int>());
DFS(i);
reverse(cyc.back().begin(),cyc.back().end());
cyc.back().pop_back();
t+=cyc.back().size();
}
if(t>s){
cout<<-1<<endl;
return 0;
}
if(s<=t+1||cyc.size()<=2){
cout<<cyc.size()<<endl;
for(auto &v:cyc){
cout<<v.size()<<endl;
for(int x:v) cout<<x<<' ';cout<<endl;
}
}else{
int k=min(s-t,(signed)cyc.size());
cout<<cyc.size()-k+2<<endl;
int sum=0;
for(int i=0;i<k;i++) sum+=cyc[i].size();
cout<<sum<<endl;
for(int i=0;i<k;i++) for(int x:cyc[i]) cout<<x<<' ';cout<<endl;
cout<<k<<endl;
for(int i=k-1;~i;i--) cout<<cyc[i].front()<<' ';cout<<endl;
for(int i=k;i<cyc.size();i++){
cout<<cyc[i].size()<<endl;
for(int x:cyc[i]) cout<<x<<' ';cout<<endl;
}
}
return 0;
}
[EGOI 2021] Double Move
先计算已知的答案,考虑差分,用在第 \(i\) 步之后还存活的方案数计算恰在第 \(i\) 步停止的方案数。
考虑用限制建出图,那么一颗树可以有点数大小的贡献,基环树有 \(2\) 的贡献,否则就是 \(0\),总方案数就是 \(2^{n-i+1}\) 乘上每个连通块的贡献。
对于不确定的限制,考虑记录所有树的大小以及基环树的数量,直接搜索可能的决策。由于 \(n\) 只有 \(35\),状态数是少的。
#include<bits/stdc++.h>
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/hash_policy.hpp>
using namespace std;
#define endl '\n'
using ll=long long;
using ull=unsigned long long;
const int N=3e1+9;
ll f[N],ans;
int a[N],b[N],n,k;
int fa[N],vsiz[N],esiz[N];
inline void Init(){iota(fa+1,fa+n+1,1),fill(vsiz+1,vsiz+n+1,1);}
inline int Find(int x){return fa[x]==x?x:fa[x]=Find(fa[x]);}
inline void Merge(int x,int y){
x=Find(x),y=Find(y);
if(x==y) return ;
fa[y]=x;
esiz[x]+=esiz[y];
vsiz[x]+=vsiz[y];
}
__gnu_pbds::gp_hash_table<ull,ll> mF;
inline ull Hash(vector<int> &v,int c){
ull ans=131+c;
for(int x:v) ans=ans*131+x;
return ans;
}
inline ll F(int o,vector<int> v,int c,ll tot){
if(o>n+1) return 0;
ull h=Hash(v,c);
if(mF[h]) return mF[h];
ll ans=0;
vector<int> u;
for(int i=0;i<v.size();i++){
u.insert(u.end(),v.begin(),v.begin()+i);
u.insert(u.end(),v.begin()+i+1,v.end());
ans=max(ans,(tot/v[i]>>1)-F(o+1,u,c,tot/v[i]>>1));
ans=max(ans,(tot/v[i]<<1>>1)-F(o+1,u,c+1,tot/v[i]<<1>>1));
u.clear();
for(int j=i+1;j<v.size();j++){
u.insert(u.end(),v.begin(),v.begin()+i);
u.insert(u.end(),v.begin()+i+1,v.begin()+j);
u.insert(u.end(),v.begin()+j+1,v.end());
u.insert(lower_bound(u.begin(),u.end(),v[i]+v[j]),v[i]+v[j]);
ans=max(ans,(tot/v[i]/v[j]*(v[i]+v[j])>>1)-F(o+1,u,c,tot/v[i]/v[j]*(v[i]+v[j])>>1));
u.clear();
}
}
return mF[h]=ans;
}
signed main(){
cin.tie(0),cout.tie(0);
ios::sync_with_stdio(0);
cin>>n>>k;
for(int i=1;i<=k;i++) cin>>a[i]>>b[i];
Init();
f[0]=1ll<<n+1;
for(int i=1,p,q,x,y;i<=k;i++){
x=esiz[Find(a[i])]-(p=vsiz[Find(a[i])]);
y=esiz[Find(b[i])]-(q=vsiz[Find(b[i])]);
if(Find(a[i])==Find(b[i])&&!~x) f[i]=(f[i-1]>>1)/p<<1;
else if(!~x&&!~y) f[i]=(f[i-1]>>1)/p/q*(p+q);
else if(!x&&!~y) f[i]=(f[i-1]>>1)/q;
else if(!~x&&!y) f[i]=(f[i-1]>>1)/p;
Merge(a[i],b[i]);
esiz[Find(a[i])]++;
if(esiz[Find(a[i])]>vsiz[Find(a[i])]) f[i]=0;
if(~i&1) ans+=f[i-1]-f[i];
}
if(!f[k]){
cout<<ans<<' '<<(1ll<<n+1)-ans<<endl;
return 0;
}
int c=0;
vector<int> v;
for(int i=1;i<=n;i++){
if(fa[i]!=i) continue ;
if(esiz[i]<vsiz[i]) v.push_back(vsiz[i]);
else if(esiz[i]==vsiz[i]) c++;
}
sort(v.begin(),v.end());
if(~k+1&1) ans+=f[k]-F(k+1,v,c,f[k]);
else ans+=F(k+1,v,c,f[k]);
cout<<ans<<' '<<(1ll<<n+1)-ans<<endl;
return 0;
}
[集训队互测 2024] 木桶效应
考虑 \(q=0\) 咋做。
先把题目转化成对每个位置设一个 \(t_i\),要求 \(\forall i,j,~p_{i,j}>t_j\) 的方案数。放到值域上考虑,设 \(f_{i,j}\) 表示当前考虑了值域 \([1,i]\),有 \(j\) 个位置的 \(t_x\) 已经确定了。每次枚举设 \(\Delta\) 个 \(t_x\) 被新设成 \(i\),转移是简单的,时间复杂度 \(O(n^3)\)。
\(q\neq 0\) 类似地,设 \(f_{i,j,S}\) 表示表示当前考虑了值域 \([1,i]\),有 \(j\) 个非关键位置的 \(t_x\) 已经确定了,关键位置确定了集合 \(|S|\)。非关键位置的转移同上。关键行如果存在 \(w_p=i\) 的限制则为 \([y_p\in S]\),否则贡献为非关键位置加上 \(S\) 中不会占用的位置。从 \(S\) 转移到新集合 \(S'\) 的贡献是 \(1\),可以直接高维前缀和。时间复杂度 \(O(n^2(n+q)2^q)\)。
#include<bits/stdc++.h>
using namespace std;
#define popc __builtin_popcount
using ll=long long;
const int N=5e1+9;
const int Q=1e1+9;
const int S=(1<<10)+9;
const int mod=998244353;
inline void AddAs(int &x,int y){if((x+=y)>=mod) x-=mod;}
inline void SubAs(int &x,int y){if((x-=y)<0) x+=mod;}
inline void MulAs(int &x,int y){x=1ll*x*y%mod;}
inline int Add(int x,int y){if((x+=y)>=mod) x-=mod;return x;}
inline int Sub(int x,int y){if((x-=y)<0) x+=mod;return x;}
inline int Mul(int x,int y){return 1ll*x*y%mod;}
inline int QPow(int x,int y){
int res=1;
while(y){
if(y&1) MulAs(res,x);
MulAs(x,x);
y>>=1;
}
return res;
}
inline int Inv(int x){return QPow(x,mod-2);}
int fac[N],ifac[N];
inline void Init(int lim){
fac[0]=1;
for(int i=1;i<=lim;i++) fac[i]=Mul(fac[i-1],i);
ifac[lim]=Inv(fac[lim]);
for(int i=lim-1;~i;i--) ifac[i]=Mul(ifac[i+1],i+1);
}
inline int C(int n,int m){
if(m<0||m>n) return 0;
else return Mul(fac[n],Mul(ifac[m],ifac[n-m]));
}
ll cst[Q];
int x[Q],y[Q],w[Q],iys[N],msk[Q],use[Q][N],f[N][N][S],g[S],n,m,q;
signed main(){
cin>>n>>m>>q;
for(int i=1;i<=q;i++) cin>>x[i]>>y[i]>>w[i];
vector<int> ys(y+1,y+q+1);
sort(ys.begin(),ys.end());
ys.erase(unique(ys.begin(),ys.end()),ys.end());
int yl=ys.size();
vector<int> xs(x+1,x+q+1);
sort(xs.begin(),xs.end());
xs.erase(unique(xs.begin(),xs.end()),xs.end());
int xl=xs.size();
map<array<int,2>,int> mp;
for(int i=1;i<=q;i++){
int xp=lower_bound(xs.begin(),xs.end(),x[i])-xs.begin();
int yp=lower_bound(ys.begin(),ys.end(),y[i])-ys.begin();
cst[xp]|=1ll<<w[i];
if(mp[{xp,w[i]}]&&mp[{xp,w[i]}]!=yp){
cout<<0<<endl;
return 0;
}
mp[{xp,w[i]}]=yp;
msk[xp]|=1<<yp;
use[xp][w[i]]|=1<<yp;
}
for(int i=0;i<xl;i++) for(int j=1;j<=n;j++) use[i][j]|=use[i][j-1];
Init(n);
f[0][0][0]=1;
for(int i=0;i<=n;i++){
for(int j=0;j<=n-yl;j++){
for(int sta=0;sta<(1<<yl);sta++){
MulAs(f[i][j][sta],QPow(j+popc(sta)-(i-1),m-xl));
for(int k=0;k<xl;k++){
if(cst[k]>>i&1) MulAs(f[i][j][sta],sta>>mp[{k,i}]&1);
else MulAs(f[i][j][sta],j+popc(sta^(sta&msk[k]))-(i-1-popc(sta&msk[k]&use[k][i-1])));
}
g[sta]=f[i][j][sta];
}
for(int k=0;k<yl;k++){
for(int sta=0;sta<(1<<yl);sta++){
if(~sta>>k&1) AddAs(g[sta|1<<k],g[sta]);
}
}
for(int k=0;j+k<=n-yl;k++){
for(int sta=0;sta<(1<<yl);sta++){
AddAs(f[i+1][j+k][sta],Mul(C(n-yl-j,k),g[sta]));
}
}
}
}
cout<<f[n][n-yl][(1<<yl)-1]<<endl;
return 0;
}
骷髅打金服
考虑分治,对于跨过终点的区间进行分讨:
- 左边右边出现的数集相同:对每种颜色随机负权,记左边/右边的数总和为 \(S_l,S_r\),本质不同的数的总和为 \(A_l,A_r\),则 \(A_l=A_r,~S_l+S_r\equiv 0 \pmod {A_l}\)。
- 有数单独出现在左边/右边:假设在左边,设左边众数为 \(M_l\),同时维护需求集合 \(R_l\),表示向左补齐还需的值和为 \(R_l\),则 \(R_l=S_r\)。
- 两边都有数单独出现:设右边非众数的数的和为 \(N_r\),右边最左第一次出现左边的众数位于 \(p\),则 \(M_l=M_r,~R_l=N_r,~r<p\)。
以上的情况均可以用哈希表简单维护,特别地,最后一种情况需要从左向右扫描的同时删数。
时间复杂度 \(O(n\log n)\)。
#include<bits/stdc++.h>
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/hash_policy.hpp>
using namespace std;
#define endl '\n'
using ll=long long;
using ull=unsigned long long;
const int N=1e6+9;
const ll V=1e12;
int v[N],cnt[N],pos[N],n;
ll w[N],a[N],ans;
vector<ull> o[N];
mt19937_64 rng(4649);
inline ull F(ull x,ull y){
x^=x<<3;
x^=x>>7;
y^=y<<7;
y^=y>>3;
return x^y;
}
const int mod=1145141;
struct H{
ull key[N];
int fi[mod],ne[N],val[N],adj;
inline int& operator [](ull x){
int y=x%mod,i=fi[y];
while(i){
if(key[i]==x) return val[i];
i=ne[i];
}
ne[++adj]=fi[y];
fi[y]=adj;
key[adj]=x;
return val[adj]=0;
}
inline void Clear(){
for(int i=1;i<=adj;i++) fi[key[i]%mod]=0;
adj=0;
}
}mp1,mp2,mp3,mp4;
inline void Conquer(int l,int r){
if(l==r) return ans++,void();
int mid=l+r>>1;
Conquer(l,mid),Conquer(mid+1,r);
ll c=0,s=0,m=0,p=0,q=0;
for(int i=r;i>mid;i--) pos[v[i]]=i;
for(int i=mid,j=r+1;i>=l;i--){
if(!cnt[v[i]]) c+=a[i],p+=m*a[i];
s+=a[i];
q+=a[i];
if(++cnt[v[i]]>m) m++,p+=c,q=s,j=r+1;
if(cnt[v[i]]==m){
q-=m*a[i];
if(pos[v[i]]) j=min(j,pos[v[i]]);
}
p-=a[i];
mp1[F(c,s%c)]++;
mp2[p]++;
mp3[s]++;
mp4[F(m,p)]++;
if(j<=r) o[j].push_back(F(m,p));
}
for(int i=l;i<=mid;i++) cnt[v[i]]=0;
c=0,s=0,m=0,p=0,q=0;
for(int i=mid+1;i<=r;i++){
for(auto j:o[i]) mp4[j]--;
o[i].clear();
if(!cnt[v[i]]) c+=a[i],p+=m*a[i];
s+=a[i];
q+=a[i];
if(++cnt[v[i]]>m) m++,p+=c,q=s;
if(cnt[v[i]]==m) q-=m*a[i];
p-=a[i];
ans+=mp1[F(c,(c-s%c)%c)];
ans+=mp2[s];
ans+=mp3[p];
ans+=mp4[F(m,q)];
}
for(int i=mid+1;i<=r;i++) cnt[v[i]]=pos[v[i]]=0;
mp1.Clear();
mp2.Clear();
mp3.Clear();
mp4.Clear();
}
inline void Solve(){
cin>>n;
for(int i=1;i<=n;i++) cin>>v[i];
for(int i=1;i<=n;i++) w[i]=rng()%V;
for(int i=1;i<=n;i++) a[i]=w[v[i]];
Conquer(1,n);
cout<<ans<<endl;
ans=0;
}
signed main(){
cin.tie(0),cout.tie(0);
ios::sync_with_stdio(0);
int T;
cin>>T;
while(T--) Solve();
return 0;
}
重返现世
我也不知道为什么现在才写这个题。
先令 \(k\leftarrow n-k+1\) 变成第 \(k\) 大,则答案为 \(\displaystyle E(\operatorname{kthmax}(S))=\sum_{T} (-1)^{|T|-k}\binom{|T|-1}{k-1}\operatorname{kthmin}(T)\)。
设 \(f_{i,j,s}\) 表示考虑到第 \(i\) 位,\(|T|=j\),\(\displaystyle \sum_{x\in T} p_x=s\),转移是简单的,时间复杂度 \(O(n^2m)\),无法接受。
考虑扔掉 \(j\) 维。转移的时候有 \(\displaystyle (-1)^{|T|-k}\binom{|T|-1}{k-1}\rightarrow (-1)^{|T|+1-k}\binom{|T|}{k-1}\),那么把 \(\dbinom {|T|}{k-1}\) 拆成 \(\dbinom{|T|-1}{k-1}+\dbinom{|T|-1}{k-2}\) 即可,同时增设 \(k\) 维。因此有转移 \(\displaystyle f_{k,i,s}=f_{k,i-1,s}+f_{k-1,i-1,s-p_i}-f_{k,i-1,s-p_i}\)。时间复杂度 \(O(nmk)\)。
边界考虑扩展二项式系数的计算方式,得出 \(f_{k,0,0}=-[k>0]\)。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
using ll=long long;
const int N=1e3+9;
const int M=1e4+9;
const int K=1e1+9;
const int mod=998244353;
inline void AddAs(int &x,int y){if((x+=y)>=mod) x-=mod;}
inline void SubAs(int &x,int y){if((x-=y)<0) x+=mod;}
inline void MulAs(int &x,int y){x=1ll*x*y%mod;}
inline int Add(int x,int y){if((x+=y)>=mod) x-=mod;return x;}
inline int Sub(int x,int y){if((x-=y)<0) x+=mod;return x;}
inline int Mul(int x,int y){return 1ll*x*y%mod;}
inline int QPow(int x,int y){
int res=1;
while(y){
if(y&1) MulAs(res,x);
MulAs(x,x);
y>>=1;
}
return res;
}
inline int Inv(int x){return QPow(x,mod-2);}
int p[N],f[K][M],n,k,m;
signed main(){
cin.tie(0),cout.tie(0);
ios::sync_with_stdio(0);
cin>>n>>k>>m;
for(int i=1;i<=n;i++) cin>>p[i];
k=n-k+1;
for(int l=1;l<=k;l++) f[l][0]=mod-1;
for(int i=1;i<=n;i++){
for(int l=k;l>=1;l--){
for(int j=m;j>=p[i];j--){
SubAs(f[l][j],f[l][j-p[i]]);
AddAs(f[l][j],f[l-1][j-p[i]]);
}
}
}
int ans=0;
for(int j=1;j<=m;j++) AddAs(ans,Mul(Mul(m,Inv(j)),f[k][j]));
cout<<ans<<endl;
return 0;
}

浙公网安备 33010602011771号