做题笔记 - 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;
}
posted @ 2026-01-22 20:06  JoeyJiang  阅读(1)  评论(0)    收藏  举报