2026 NOI 做题记录(十)



Contest Link

\(\text{By DaiRuiChen007}\)



*A. [CF1882E2] Two Permutations (7)

Problem Link

首先只有一个排列的情况,显然可以当且增量维护 \([1,2,\dots,k]\) 子串完成构造。

问题是如何让两个排列同时复原,首先可以用两次操作保持排列不变,那么只要调整某个操作序列长度的奇偶性,注意到 \(n\) 为奇数时每次可以任意调整排列奇偶性,否则奇偶性必定变化,因此无解容易判断。

那么我们只要对每个排列求出用奇数和偶数次操作复原的最小步数。

充分刻画题目条件,依然考虑类似循环移位的构造,我们把 \(0,p_1,\dots,p_n\) 写到环上,那么操作 \(i\) 就是交换 \(0,p_i\),得到排列就是从 \(0\) 开始的序列。

那么直接枚举序列意义下最终 \(0\) 的位置,此时每次操作是交换 \(0\) 和某个元素,因此奇偶性之和初始以及最终状态有关,所以只要求最小代价。

对每个环考虑操作次数,设所有环长为 \(c_0\sim c_k\),其中 \(0\) 在环 \(c_0\) 上,则最小操作次数为 \(c_0-1+\sum_{i=1}^k[c_i>1](c_i+1)\),构造就是把 \(c_i>1\) 的环和 \(c_0\) 连接起来再复原。

暴力枚举计算。

时间复杂度 \(\mathcal O(n^2)\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=2505,inf=1e9;
int p[MAXN],q[MAXN];
bool vis[MAXN];
int qry(int n) {
	memset(vis,0,sizeof(vis));
	int z=0;
	for(int i=0;i<=n;++i) if(!i||(!vis[i]&&p[i]!=i)) {
		++z; for(int u=i;!vis[u];u=p[u]) vis[u]=1,++z;
	}
	return z-2;
}
void sol(int n,basic_string<int>&w) {
	int c=p[0];
	for(int i=0;i<=n;++i) q[p[i]]=i;
	auto opr=[&](int x) {
		w+=(q[x]+n+1-q[c])%(n+1);
		swap(q[c],q[x]);
		swap(p[q[c]],p[q[x]]);
	};
	memset(vis,false,sizeof(vis));
	for(int i=0;i<=n;++i) if(!vis[i]&&p[i]!=i) {
		for(int u=i;!vis[u];u=p[u]) vis[u]=1;
		if(i) opr(i);
	}
	while(q[c]^c) opr(q[c]);
}
int n,m,a[MAXN],b[MAXN];
array <int,2> f[2],g[2];
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;++i) cin>>a[i];
	for(int i=1;i<=m;++i) cin>>b[i];
	f[0]=f[1]=g[0]=g[1]={inf,0};
	for(int t=0;t<=n;++t) {
		for(int i=0;i<=n;++i) p[i]=(a[i]+n+1-t)%(n+1);
		int z=qry(n); f[z&1]=min(f[z&1],array<int,2>{z,t});
	}
	for(int t=0;t<=m;++t) {
		for(int i=0;i<=m;++i) p[i]=(b[i]+m+1-t)%(m+1);
		int z=qry(m); g[z&1]=min(g[z&1],array<int,2>{z,t});
	}
	int z0=max(f[0][0],g[0][0]),z1=max(f[1][0],g[1][0]);
	if(z0>=inf&&z1>=inf) return cout<<"-1\n",0;
	cout<<min(z0,z1)<<"\n";
	basic_string <int> L,R;
	for(int i=0;i<=n;++i) p[i]=(a[i]+n+1-f[z1<z0][1])%(n+1);
	sol(n,L);
	for(int i=0;i<=m;++i) p[i]=(b[i]+m+1-g[z1<z0][1])%(m+1);
	sol(m,R);
	while(L.size()<R.size()) L+=1,L+=n;
	while(R.size()<L.size()) R+=1,R+=m;
	for(int i=0;i<(int)L.size();++i) cout<<L[i]<<" "<<R[i]<<"\n";
	return 0;
}



*B. [CF1887F] Minimum Segments (7)

Problem Link

首先我们想要确定颜色总数 \(m\),一个显然的上界是 \(m\le r_i-i+1\),同理下界是 \(m\ge 1+|R\cap [i+1,r_i]|\),其中 \(R=\{r_i\}\),这是因为 \([j,r_j)\) 中元素与 \(r_j\) 异色。

可以证明该范围内的 \(m\) 总是合法,构造方法就是每个 \(i\) 的同色前驱是 \(\min \{j-1\mid r_j=i\}\),没有同色前驱的点匹配首个没有同色后继的点即可。

证明可以归纳地加入每个 \(i\),分讨 \(r_i,r_{i-1}\) 的关系即可。

时间复杂度 \(\mathcal O(n)\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5;
int n,r[MAXN],a[MAXN],c[MAXN],fa[MAXN];
void solve() {
	cin>>n;
	for(int i=0;i<=n+1;++i) r[i]=a[i]=c[i]=0,fa[i]=-1;
	for(int i=1;i<=n;++i) cin>>r[i],c[r[i]]=1;
	if(r[1]>n) return cout<<"NO\n",void();
	int L=0,R=n;
	for(int i=1;i<=n;++i) if(r[i]<=n) R=min(r[i]-i+1,R);
	for(int i=1;i<=n+1;++i) c[i]+=c[i-1];
	for(int i=1;i<=n;++i) L=max(L,c[r[i]]-c[i]+1);
	if(L>R) return cout<<"NO\n",void();
	for(int i=0;i<n;++i) {
		if(r[i]>r[i+1]) return cout<<"NO\n",void();
		if(r[i]!=r[i+1]) fa[r[i+1]]=i;
	}
	queue <int> Q;
	for(int i=1;i<L;++i) Q.push(0);
	for(int i=1,k=0;i<=n;++i) {
		if(fa[i]<0) fa[i]=Q.front(),Q.pop();
		a[i]=fa[i]?a[fa[i]]:++k;
		if(r[i]==r[i+1]) Q.push(i);
	}
	cout<<"YES\n";
	for(int i=1;i<=n;++i) cout<<a[i]<<" \n"[i==n];
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



C. [CF1805F2] Survival of the Weakest (4.5)

Problem Link

\(a\) 有序。

首先解决值域的问题,只要每次把所有元素减去 \(a_1\) 就能把值域控制在 \(\mathcal O(V)\),因此进行一次迭代可以简单做到 \(\mathcal O(n\log n)\)

朴素的观察是:如果 \(a_2+a_3\le a_4\),那么 \((a_1,a_2,a_3)\) 每次迭代都不会涉及 \(a[4,n]\) 的元素。

所以猜测进行 \(T\) 轮后只要保留最小的 \(B\) 个元素,取 \(T=40,B=100\) 可以通过。

时间复杂度 \(\mathcal O(n(T\log n+B\log B))\)

严格的做法只要取 \(T=0,B=2\log V+\mathcal O(1)\) 即可。

考虑如果迭代后没有元素由 \(a_n\) 转移,则可以忽略该元素。

如果存在这样的转移,则 \(a_2+a_3\ge a_n\),迭代后 \(a'_{n-1}=a_n-a_2\),再次迭代,序列末尾变成 \((a_n-a_2)-(a_3-a_2)=a_n-a_3\),而 \(a_n\ge 2a_3\ge a_2+a_3\),因此两次操作后 \(a_n\) 折半。

同理对于任意的 \(a_i\),保留两次后就折半。

那么如果 \(a_B\) 需要保留,则两次操作后前 \(B-2\) 个元素依然正确,那么这种情况发生的次数不超过 \(\log V\),所以最终 \(a_1\) 正确。

时间复杂度 \(\mathcal O(n\log V\log\log V)\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5,T=40,B=100,MOD=1e9+7;
int pw[MAXN],ans=0;
void solve(vector<int>&a) {
	int n=a.size();
	priority_queue <array<int,3>,vector<array<int,3>>,greater<>> q;
	for(int i=0;i<n-1;++i) q.push({a[i]+a[i+1],i,i+1});
	vector<int>b;
	while(q.size()&&b.size()<a.size()) {
		auto o=q.top(); q.pop();
		b.push_back(o[0]);
		if(o[2]+1<n) q.push({a[o[1]]+a[o[2]+1],o[1],o[2]+1});
	}
	a.swap(b);
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int n; cin>>n;
	for(int i=pw[0]=1;i<=n;++i) pw[i]=pw[i-1]*2%MOD;
	vector <int> a(n);
	for(int &x:a) cin>>x;
	sort(a.begin(),a.end());
	for(int i=n;i>1;--i) {
		if(i<n-T) a.resize(min(i,B));
		else a.resize(i);
		ans=(ans+1ll*a[0]*pw[i-1])%MOD;
		for(int j=a.size()-1;~j;--j) a[j]-=a[0];
		solve(a);
	}
	cout<<(ans+a[0])%MOD<<"\n";
	return 0;
}



D. [CF2027E2] Bit Game (3.5)

Problem Link

首先求 SG 函数,设当前有一个 \(x\) 个石子的堆,每次都要取 \(y\) 的子集个,设 \(y\) 的最高二进制位为 \(h(y)\)

考虑把 SG 函数降成一维的,从高往低位考虑:大于 \(h(y)\) 的位无贡献,某位 \(x,y\) 都为 \(0\) 也可以删掉,找到某个 \(y\)\(1\)\(x\)\(0\) 的位,此时 \(x\) 中低于该位的元素可以任取,则可以删去该位并将 \(y\) 的低位全部设为 \(1\)

此时得到 \((x',y')\) 注意到 \(x'=2^{h(y')+1}-1\),因此对于每个 \((2^{h(y)+1}-1,y)\) 对应的的 SG 函数打表,发现只有 \(y=2^{h(y)+1}-2/2^{h(y)}\) 的时候取值特殊,其他时候都是 \(\log_2 y\)

对于原问题,首先 SG 函数值域 \(\mathcal O(\log V)\),所以可以对每个堆数位 dp,记录每种 SG 函数值的方案数,不难用自动机判定 \(y\) 是否为 \(2^{h(y)}\)\(2^{h(y)+1}-2\)

时间复杂度 \(\mathcal O(n\log^2V)\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e4+5,MOD=1e9+7,tr[5][2]={{2,1},{4,1},{2,3},{3,3},{3,3}};
int z[32],f[2][32][5][2],g[2][32][5][2];
void add(int &x,int y) { x=x+y>=MOD?x+y-MOD:x+y; }
void dp(int k,int n) {
	memset(f,0,sizeof(f)),memset(z,0,sizeof(z));
	f[1][0][0][0]=1;
	int b=__lg(k);
	for(int i=30;;--i) {
		memset(g,0,sizeof(g));
		for(int o:{0,1}) for(int w=0;w<32;++w) for(int x:{0,1,2,3,4}) for(int e:{0,1}) {
			const int &v=f[o][w][x][e];
			if(!v) continue;
			if(i<0) {
				if(!w) add(z[0],v);
				else if(x==4) add(z[0],v);
				else if(x==2&&w%2==0) add(z[w-2],v);
				else add(z[w],v);
				continue;
			}
			for(int c=0;c<=(o?n>>i&1:1);++c) {
				int no=o&&c==(n>>i&1),nw=w,nx=x,ne=e,h=(k>>i&1)||e;
				if(i<=b&&(h||c)) {
					if(!c) ne=1;
					else ++nw,nx=w?tr[nx][h]:0;
				}
				add(g[no][nw][nx][ne],v);
			}
		}
		if(i<0) break;
		memcpy(f,g,sizeof(f));
	}
}
int a[MAXN],b[MAXN],h[32],w[32];
void solve() {
	int n;
	cin>>n,memset(h,0,sizeof(h)),h[0]=1;
	for(int i=1;i<=n;++i) cin>>a[i];
	for(int i=1;i<=n;++i) cin>>b[i];
	for(int i=1;i<=n;++i) {
		dp(a[i],b[i]),--z[0],memset(w,0,sizeof(w));
		for(int x=0;x<32;++x) for(int y=0;y<32;++y) {
			w[x^y]=(w[x^y]+1ll*h[x]*z[y])%MOD;
		}
		memcpy(h,w,sizeof(h));
	}
	cout<<h[0]<<"\n";
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



E. [CF1832F] Zombies (2.5)

Problem Link

只要找一组 \(w\) 最大化 \(\sum |[w_{p_i},w_{p_i}+x)\cap [l_i,r_i)|\) 即可。

观察到对 \(w_{p_i}\) 的限制是分段一次函数,其中 \([u_i,v_i)\) 段取到最小值,然后两侧是斜率为 \(\pm 1\) 的一段,剩余部分是 \(m\)

那么可以证明每个 \(w_{p_i}\) 取值都是某个 \(u_i\)\(v_i\)

所以可以离散化,如果已知 \(\{w\}\) 的情况下计算每个区间的代价是容易的,只要考虑 \([u_i,v_i)\) 被相邻两个 \(w\) 包含的贡献,此时匹配较近的一个即可。

那么只要 dp \(f_{i,j}\) 表示第 \(j\)\(w\) 在位置 \(i\) 上,提前用差分预处理相邻两个 \(w\)\((x,y)\) 时的代价即可,可以猜测转移具有决策单调性,分治优化之。

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

代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=4005;
int n,m,k,q,V,a[MAXN],L[MAXN],R[MAXN];
ll f[MAXN][MAXN],g[MAXN],h[MAXN],v1[MAXN],v2[MAXN],sum;
void cdq(int l,int r,int le,int ri) {
	if(l>r) return ;
	int mid=(l+r)>>1,p=0;
	for(int i=le;i<=ri&&i<mid;++i) {
		if(g[i]+f[i][mid]<h[mid]) h[mid]=g[i]+f[i][mid],p=i;
	}
	cdq(l,mid-1,le,p),cdq(mid+1,r,p,ri);
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>k>>V>>m,a[0]=-m;
	for(int i=1;i<=n;++i) cin>>L[i]>>R[i],a[++q]=L[i],a[++q]=R[i]-m,sum+=L[i]+V-R[i];
	sort(a,a+q+1),q=unique(a,a+q+1)-a-1,a[++q]=V;
	for(int i=1;i<=q;++i) {
		memset(v1,0,sizeof(v1)),memset(v2,0,sizeof(v2));
		for(int j=1;j<=n;++j) {
			int l=min(L[j],R[j]-m),r=max(L[j],R[j]-m);
			if((l<=a[i]&&a[i]<=r)||l>a[i]) continue;
			int d=min({m,R[j]-L[j],a[i]-r}),p=lower_bound(a,a+q+1,l-d)-a,t=lower_bound(a,a+q+1,l)-a;
			v2[0]+=d,v2[p]+=l-d,--v1[p],++v1[t],v2[t]-=l;
		}
		for(int x=1;x<i;++x) v1[x]+=v1[x-1],v2[x]+=v2[x-1];
		for(int x=0;x<i;++x) f[x][i]=v2[x]+v1[x]*a[x];
	}
	ll ans=f[0][q];
	for(int i=1;i<q;++i) g[i]=f[0][i];
	for(int i=1;i<=k&&i<q;++i) {
		for(int j=i;j<q;++j) ans=min(ans,g[j]+f[j][q]);
		if(i<min(k,q-1)) memset(h,0x3f,sizeof(h)),cdq(i+1,q-1,i,q-1),memcpy(g,h,sizeof(g));
	}
	for(int i=1;i<=n;++i) ans+=max(0,m-R[i]+L[i]);
	cout<<sum-ans<<"\n";
	return 0;
}



F. [CF2124F2] Appending Permutations (2)

Problem Link

观察判定的过程,发现当前段开头不为 \(1\) 时结束位置唯一,否则结束位置越后越好。

那么填入的时候如果前一个段为 \([1,2,\dots,x]\),那么这个段开头不为 \(x+1\),记为 \(f_{i,x}\),转移优化较为平凡。

时间复杂度 \(\mathcal O(n^2)\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5005,MOD=998244353;
void add(int &x,const int &y) { x=x+y>=MOD?x+y-MOD:x+y; }
bitset<MAXN> ban[MAXN];
int n,m,f[MAXN][MAXN],g[MAXN][MAXN],d[MAXN];
vector <int> a[MAXN*2];
void solve() {
	cin>>n>>m;
	for(int i=0;i<=n+1;++i) memset(f[i],0,sizeof(f[i])),memset(g[i],0,sizeof(g[i])),ban[i].reset();
	for(int i=0;i<=2*n;++i) a[i].clear();
	for(int i=1,x,y;i<=m;++i) cin>>x>>y,ban[x][y]=1,a[x+n-y].push_back(x);
	for(int i=0;i<=2*n;++i) a[i].push_back(n+1),sort(a[i].begin(),a[i].end(),greater<>());
	for(int i=1;i<=n;++i) {
		d[i]=n-i+1;
		for(int j=1;i+j-1<=n;++j) if(ban[i+j-1][j]) { d[i]=j-1; break; }
	}
	f[1][0]=1;
	for(int i=1;i<=n;++i) {
		for(int j=1;j<=n;++j) add(f[i][0],f[i][j]);
		for(int j=1;j<=n;++j) {
			int z=(f[i][0]+MOD-f[i][j])%MOD,e=i+n-j-1;
			while(a[e].back()<i) a[e].pop_back();
			add(g[i][j],z),add(g[a[e].back()][j],MOD-z);
		}
		for(int j=1;j<=n;++j) add(g[i][j],g[i-1][j]);
		for(int j=1;j<=n;++j) if(d[i+1]>=j) add(f[i+j+1][0],g[i][j]);
		for(int k=i,w=1;k<=n&&w<=n&&!ban[k][w];++k,++w) add(f[k+1][w],f[i][0]);
	}
	int ans=0;
	for(int i=0;i<=n;++i) ans=(ans+f[n+1][i])%MOD;
	cout<<ans<<"\n";
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



G. [CF1815E] Bosco and Particle (4)

Problem Link

简单 dp,不妨设所有 \(s_i\) 没有非平凡整周期,且不存在 \(s_i=1\)

那么此时按前缀 dp,只要维护 \(s_{i+1}=1\) 时,\(s\) 的周期 \(f\),以及几次 \(s_{i+1}\) 的次数 \(g\),转移只要把 \(g\)\(s_{i+1}\to s_i\) 中次数对齐即可。

可以发现只要维护 \(g\) 的质因子分解以及 \(f\bmod P\) 的结果即可。

时间复杂度 \(\mathcal O(n\log n)\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+5,MOD=998244353;
int n,kmp[MAXN];
void upd(string &s) {
	int m=s.size();
	for(int i=2,j=0;i<=m;++i) {
		while(j&&s[j]!=s[i-1]) j=kmp[j];
		kmp[i]=j+=s[i-1]==s[j];
	}
	if(m%(m-kmp[m])==0) s.resize(m-kmp[m]);
}
int ksm(int a,int b) { int s=1; for(;b;a=1ll*a*a%MOD,b>>=1) if(b&1) s=1ll*s*a%MOD; return s; }
int g[MAXN],inv[MAXN];
vector <int> Pr;
vector <array<int,2>> fc[MAXN];
bool isc[MAXN];
string s;
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n,inv[1]=1;
	for(int i=2;i<MAXN;++i) {
		inv[i]=1ll*inv[MOD%i]*(MOD-MOD/i)%MOD;
		if(!isc[i]) Pr.push_back(i),fc[i]={{i,1}};
		for(int p:Pr) {
			if(i*p>=MAXN) break;
			fc[i*p]=fc[i],isc[i*p]=true;
			if(i%p==0) { ++fc[i*p].back()[1]; break; }
			else fc[i*p].push_back({p,1});
		}
	}
	int vf=1,vg=1;
	for(int i=1;i<=n;++i) {
		cin>>s,upd(s);
		if(s=="1") break;
		int c=0,d=0,o=0;
		for(auto x:s) o^=(x=='0'),(o?++d:++c);
		if(o) for(auto x:s) o^=(x=='0'),(o?++d:++c);
		int l=vg,r=c;
		for(auto x:fc[c]) {
			int z=ksm(x[0],min(g[x[0]],x[1]));
			l=1ll*l*inv[z]%MOD,r/=z,g[x[0]]-=min(g[x[0]],x[1]);
		}
		for(auto x:fc[d]) g[x[0]]+=x[1];
		vf=(1ll*vf*r+1ll*(c+d)*l)%MOD,vg=1ll*l*d%MOD;
	}
	cout<<(vf+vg)%MOD<<"\n";
	return 0;
}



*H. [CF1864G] Magic Square (7)

Problem Link

首先特判所有行或列偏移量为 \(0\) 的情况。

此时所有行非零的的偏移量应该两两不同,否则选择一个偏移量非零的列就导出矛盾。

首先如果某个点行不变,则所在行的偏移量就是这个点的列偏移量。

而一个行中如果每个点所在行都变化了,那么根据抽屉原理,存在两个列的偏移量相同,那么此时就能得到两个位移相同的元素。

所以我们能直接确定每个行列的变化量。

计数部分我们可以证明一个行可以操作当且仅当操作后每个点都在正确的列上。

那么任何时候不存在可以同时操作的行和列,否则显然有偏移量相同的行和列。

因此答案就是每个时刻可以进行操作数阶乘乘积。

时间复杂度 \(\mathcal O(n^3)\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=505,MAXM=2.5e5+5,MOD=998244353;
int n,a[MAXN][MAXN],b[MAXN][MAXN],d[MAXN],r[MAXN],c[MAXN],px[MAXM],py[MAXM],fac[MAXN];
bitset<MAXN> vis[MAXN],vr,vc;
void solve() {
	cin>>n;
	for(int i=0;i<=n;++i) vis[i].reset(),r[i]=c[i]=-1;
	for(int i=0;i<n;++i) for(int j=0;j<n;++j) cin>>a[i][j];
	for(int i=0;i<n;++i) for(int j=0;j<n;++j) cin>>b[i][j],px[b[i][j]]=i,py[b[i][j]]=j;
	bool fx=0,fy=0;
	for(int i=0;i<n;++i) for(int j=0;j<n;++j) {
		int x=(px[a[i][j]]+n-i)%n,y=(py[a[i][j]]+n-j)%n;
		if(x&&y&&vis[x][y]) return cout<<"0\n",void();
		vis[x][y]=1;
		if(!x) {
			if(~r[i]&&r[i]!=y) return cout<<"0\n",void();
			r[i]=y;
		} else fx=1;
		if(!y) {
			if(~c[j]&&c[j]!=x) return cout<<"0\n",void();
			c[j]=x;
		} else fy=1;
	}
	int qr=0,qc=0;
	for(int i=0;i<n;++i) r[i]=max(r[i],0),c[i]=max(c[i],0),qr+=r[i]>0,qc+=c[i]>0;
	if(!fx||!fy) return cout<<fac[qr+qc]<<"\n",void();
	int ans=1; vr.reset(),vc.reset();
	while(true) {
		vector <int> er,ec;
		for(int i=0;i<n;++i) if(r[i]) {
			bool ok=1;
			for(int j=0,k;j<n;++j) {
				k=(j+r[i])%n,ok&=k==py[a[i][j]];
				if(vc[j]) ok&=px[a[i][j]]==i&&!c[k];
				else ok&=px[a[i][j]]==(i+c[k])%n;
			}
			if(ok) er.push_back(i);
		}
		for(int j=0;j<n;++j) if(c[j]) {
			bool ok=1;
			for(int i=0,k;i<n;++i) {
				k=(i+c[j])%n,ok&=k==px[a[i][j]];
				if(vr[i]) ok&=py[a[i][j]]==j&&!r[k];
				else ok&=py[a[i][j]]==(j+r[k])%n;
			}
			if(ok) ec.push_back(j);
		}
		if(er.empty()&&ec.empty()) break;
		ans=1ll*ans*fac[er.size()+ec.size()]%MOD;
		for(int i:er) {
			rotate(a[i],a[i]+n-r[i],a[i]+n);
			r[i]=0,vr[i]=1;
		}
		for(int j:ec) {
			for(int i=0;i<n;++i) d[i]=a[i][j];
			rotate(d,d+n-c[j],d+n);
			for(int i=0;i<n;++i) a[i][j]=d[i];
			c[j]=0,vc[j]=1;
		}
	}
	for(int i=0;i<n;++i) for(int j=0;j<n;++j) if(a[i][j]!=b[i][j]) return cout<<"0\n",void();
	cout<<ans<<"\n";
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	for(int i=fac[0]=1;i<MAXN;++i) fac[i]=1ll*i*fac[i-1]%MOD;
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



I. [CF2159D2] Inverse Minimum Partition (4)

Problem Link

首先如果有两个相邻段的最小值 \(x,y\) 满足 \(x\ge y\),那么合并这两个段会直接减少第一个段的代价。

所以每个段的最小值递减,那么每个段的最小值都是原序列的后缀最小值。

进一步,对于两个相邻的后缀最小值 \(x,y\),如果最小值为 \(y\) 的段开头为 \(x+1<k\le y\),那么把 \(k\) 调整到 \(x+1\) 不会影响当前段最小值,且会让前一个段末尾元素减少。

所以我们只要对原序列的后缀最小值序列考虑该问题。

此时序列递增,对于一个权值为 \(k\) 的段,开头为 \(x\),末尾元素 \(\in((k-1)x,kx]\),如果我们找到最后一个 \(\le 2x\) 的元素并划分,那么分成两端权值为 \(2+\lceil \frac k2\rceil\),在 \(k\ge 4\) 时更优。

所以只要考虑权值为 \(\{1,2,3\}\) 的段,注意到答案值域 \(\mathcal O(\log V)\),因此交换定义域值域,计算代价 \(\le v\) 的最长后缀,原问题只要扫描线维护后缀单调栈并 dp 即可。

时间复杂度 \(\mathcal O(n\log V)\)

代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=4e5+5;
int n,m,f[135],b[MAXN],c[MAXN],st[MAXN];
ll a[MAXN],w[MAXN],ans;
void solve() {
	cin>>n,ans=0;
	for(int i=1;i<=n;++i) cin>>a[i];
	for(int i=1,tp=0;i<=n;++i) {
		while(tp&&a[st[tp]]>=a[i]) --tp;
		st[++tp]=i,w[tp]=a[i];
		b[tp]=lower_bound(w,w+tp+1,(a[i]+1)/2)-w;
		c[tp]=lower_bound(w,w+tp+1,(a[i]+2)/3)-w;
		memset(f,0x3f,sizeof(f)),f[0]=tp;
		for(int x=0;f[x];++x) {
			ans+=st[f[x]];
			f[x+1]=min(f[x+1],f[x]-1);
			f[x+2]=min(f[x+2],b[f[x]]-1);
			f[x+3]=min(f[x+3],c[f[x]]-1);
		}
	}
	cout<<ans<<"\n";
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



J. [CF2154F2] Bombing (3)

Problem Link

枚举 \(k\) 表示序列由 \([1,k],[k+1,n]\) 归并得到,那么只会在 \(p=[1,2,\dots,n]\) 时重复计数,容易解决。

对于一个确定的 \(k\),每个已知的 \(p_i\) 相当于确定 \([1,i]\) 中有多少个 \(\le k\) 的元素,那么我们只要对每对相邻的 \(p_i\) 计算一个组合数作为系数即可。

对于两个相邻的 \(p_i,p_j\),写出系数发现 \(k<\min(p_i,p_j),k\ge \max(p_i,p_j)\) 的情况贡献相同,而剩余的情况中只有 \(j-i\) 个非零的贡献,可以暴力枚举,剩余部分打标记清零即可。

时间复杂度 \(\mathcal O(n)\)

代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+5,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; }
ll fac[MAXN],ifac[MAXN],f[MAXN],pl[MAXN],pr[MAXN];
ll C(int x,int y) { return y<0||y>x?0:fac[x]*ifac[y]%MOD*ifac[x-y]%MOD; }
int a[MAXN],n,b[MAXN],m,c[MAXN];
void solve() {
	cin>>n,m=0; bool ok=0;
	for(int i=1;i<=n;++i) {
		cin>>a[i],ok|=~a[i]&&a[i]!=i;
		if(~a[i]) b[++m]=i;
	}
	for(int k=0;k<=n;++k) f[k]=pl[k]=pr[k]=1,c[k]=0;
	a[0]=b[0]=0,a[n+1]=b[m+1]=n+1;
	for(int i=0;i<=m;++i) {
		int l=b[i],r=b[i+1],u=min(a[l],a[r]),v=max(a[l],a[r]);
		if(!i||i==m) {
			for(int k=1;k<u;++k) f[k]=f[k]*C(r-l-1,a[r]-a[l]-1)%MOD;
			for(int k=u;k<v;++k) f[k]=f[k]*C(r-l-1,r-u-v+k)%MOD;
			for(int k=v;k<n;++k) f[k]=f[k]*C(r-l-1,a[r]-a[l]-1)%MOD;
			continue;
		}
		pl[u-1]=pl[u-1]*C(r-l-1,a[r]-a[l]-1)%MOD;
		pr[v]=pr[v]*C(r-l-1,a[r]-a[l]-1)%MOD;
		int L=max(u,u+v-r),R=min(u+v-l,v);
		if(L>R) ++c[u],--c[v];
		else {
			++c[u],--c[L],++c[R],--c[v];
			for(int k=L;k<R;++k) f[k]=f[k]*C(r-l-1,r-u-v+k)%MOD;
		}
	}
	for(int i=1;i<n;++i) pr[i]=pr[i]*pr[i-1]%MOD,f[i]=f[i]*pr[i]%MOD;
	for(int i=n-1;i;--i) pl[i]=pl[i]*pl[i+1]%MOD,f[i]=f[i]*pl[i]%MOD;
	ll z=0;
	for(int i=1;i<n;++i) if(!(c[i]+=c[i-1])) z=(z+f[i])%MOD;
	if(!ok) z=(z+MOD-n+2)%MOD;
	cout<<z<<"\n";
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	for(int i=fac[0]=1;i<MAXN;++i) fac[i]=fac[i-1]*i%MOD;
	ifac[MAXN-1]=ksm(fac[MAXN-1]);
	for(int i=MAXN-1;i;--i) ifac[i-1]=ifac[i]*i%MOD;
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



K. [CF1322F] Assigning Fares (5)

Problem Link

首先路径同向可以用 2-SAT 描述为相邻的若干对边同向或异向,因此可以并查集维护,此时我们得到若干边连通块,连通块间只在点相交。

我们的操作就是选择某些连通块,将所有边翻转,最小化图上最长路。

考虑子树信息,我们要维护子树最长路,以及到根和从根出发的最长路。

显然子树最长路的信息不好维护,可以通过二分消除该信息。

对于到根和从根出发的最长路,可以发现每个连通块内这两个值是独立的,翻转时就会交换对应值。

因此我们只要维护 \(f_u,g_u\) 表示该点所属连通块不翻转时的最长路,则 \(u\) 为块根时向上的贡献为 \((f_u,g_u)\)\((g_u,f_u)\)

那么和 \((u,fa_u)\) 同块的点转移平凡,而异块的点我们需要交换若干块间的 \((f,g)\) 使得贡献最小,显然有一侧会取到 \(\max\max (f,g)\),则另一侧最优就是 \(\max\min(f,g)\)

所以每个点 \(u\) 要维护和 \((u,fa_u)\) 同块的子树到 \(u\) 的最长路和出发的最长路 \((fx,gx)\),以及以 \(u\) 为根的其他连通块对应的 \((fp,gp)\),这里可以选择交换 \((fp,gp)\)

转移分讨两种边即可,注意 \(fp+fx> k\)\(gp+fx> k\)\((fp,gp)\) 只有唯一情况,否则会非法。

还原的时候记录一下是否出现刚才的情况即可。

时间复杂度 \(\mathcal O((n+m)\log n)\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+5;
vector <int> G[MAXN],ord;
int n,m,fa[MAXN],dsu[MAXN*2],dfn[MAXN],dcnt,cv[MAXN],st[MAXN][20];
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
void merge(int x,int y) { dsu[find(x)]=find(y); }
int cmp(int x,int y) { return dfn[x]<dfn[y]?x:y; }
int LCA(int x,int y) {
	int l=min(dfn[x],dfn[y])+1,r=max(dfn[x],dfn[y]),k=__lg(r-l+1);
	return cmp(st[l][k],st[r-(1<<k)+1][k]);
}
int in(int y,int x) {
	return *--upper_bound(G[x].begin(),G[x].end(),y,[&](int u,int v){ return dfn[u]<dfn[v]; });
}
void dfs1(int u,int fz) {
	dfn[u]=++dcnt,st[dcnt][0]=fa[u]=fz;
	if(fz) G[u].erase(find(G[u].begin(),G[u].end(),fz));
	for(int v:G[u]) dfs1(v,u);
	ord.push_back(u);
}
void dfs2(int u) {
	for(int v:G[u]) dfs2(v),cv[u]+=cv[v];
	if(fa[u]&&cv[u]) merge(u,fa[u]),merge(u+n,fa[u]+n);
}
int b[MAXN],d[MAXN],fx[MAXN],gx[MAXN],fp[MAXN],gp[MAXN],fz[MAXN],gz[MAXN],lm[MAXN];
vector <int> E[MAXN],S[MAXN],T[MAXN],I[MAXN];
bool chk(int k) {
	for(int u:ord) {
		fx[u]=gx[u]=fp[u]=gp[u]=lm[u]=0;
		for(int v:E[u]) {
			if(d[v]) gx[u]=max(gx[u],max(gx[v],gp[v])+1);
			else fx[u]=max(fx[u],max(fx[v],gp[v])+1);
		}
		for(int c:T[u]) {
			fz[c]=gz[c]=0;
			for(int v:S[c]) {
				if(d[v]) gz[c]=max(gz[c],max(gx[v],gp[v])+1);
				else fz[c]=max(fz[c],max(fx[v],gp[v])+1);
			}
			fp[u]=max({fp[u],fz[c],gz[c]}),gp[u]=max(gp[u],min(fz[c],gz[c]));
		}
		if(fx[u]+fp[u]>k) fx[u]=max(fx[u],fp[u]),gx[u]=max(gx[u],gp[u]),fp[u]=gp[u]=0,lm[u]=2;
		if(gx[u]+fp[u]>k) gx[u]=max(gx[u],fp[u]),fx[u]=max(fx[u],gp[u]),fp[u]=gp[u]=0,lm[u]=1;
		if(fx[u]+gx[u]>k||max(fx[u],gx[u])+gp[u]>k||fp[u]+gp[u]>k) return false;
	}
	return true;
}
int dg[MAXN],e[MAXN];
bool rs[MAXN];
void dfs3(int u,bool o) { //o=0: gp=fx, o=1: gp=gx
	if(lm[u]) o=(lm[u]-1)^rs[b[u]];
	for(int v:E[u]) dfs3(v,d[v]^rs[b[v]]);
	for(int c:T[u]) {
		rs[c]=(fz[c]>gz[c])^o;
		for(int v:S[c]) dfs3(v,d[v]^rs[c]);
	}
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m,iota(dsu+1,dsu+2*n+1,1);
	for(int i=1,u,v;i<n;++i) cin>>u>>v,G[u].push_back(v),G[v].push_back(u);
	dfs1(1,0);
	for(int k=1;k<20;++k) for(int i=1;i+(1<<k)-1<=n;++i) {
		st[i][k]=cmp(st[i][k-1],st[i+(1<<(k-1))][k-1]);
	}
	for(int i=1,u,v;i<=m;++i) {
		cin>>u>>v;
		int w=LCA(u,v);
		if(u==w||v==w) u^=v^w,++cv[u],--cv[in(u,w)];
		else {
			int x=in(u,w),y=in(v,w);
			++cv[u],++cv[v],--cv[x],--cv[y];
			merge(y,x+n),merge(y+n,x);
		}
	}
	dfs2(1);
	for(int i=2;i<=n;++i) {
		if(find(i)==find(i+n)) return cout<<"-1\n",0;
		d[i]=find(i)>n,b[i]=find(i)-d[i]*n;
 	}	
	for(int u=1;u<=n;++u) {
		sort(G[u].begin(),G[u].end(),[&](int x,int y){ return b[x]<b[y]; });
		for(auto l=G[u].begin(),r=l;l!=G[u].end();l=r) {
			for(r=l;r!=G[u].end()&&b[*l]==b[*r];++r);
			if(b[*l]==b[u]) E[u]=vector<int>(l,r);
			else S[b[*l]]=vector<int>(l,r),T[u].push_back(b[*l]);
		}
	}
	int z=n;
	for(int k=1<<__lg(n);k;k>>=1) if(z-k>0&&chk(z-k)) z-=k;
	cout<<z+1<<"\n";
	chk(z),dfs3(1,0);
	for(int u=2;u<=n;++u) {
		if(d[u]^rs[b[u]]) I[u].push_back(fa[u]),++dg[fa[u]];
		else I[fa[u]].push_back(u),++dg[u];
	}
	queue <int> Q;
	for(int i=1;i<=n;++i) if(!dg[i]) e[i]=1,Q.push(i);
	while(Q.size()) {
		int u=Q.front(); Q.pop();
		for(int v:I[u]) {
			e[v]=max(e[v],e[u]+1);
			if(!--dg[v]) Q.push(v);
		}
	}
	for(int i=1;i<=n;++i) cout<<e[i]<<" \n"[i==n];
	return 0;
}



L. [CF1863G] Swaps (2)

Problem Link

首先观察发现每个点 \(u\) 只有首个 \(a_i=u\) 的操作有效。

那么答案应该是每个点入度加一的乘积,表示对应选择的边,但是如果大小为 \(k\) 的环上选了 \(k-1\) 条边时,最后一条边一定被选,则剩下的一个点选择任何一条边都和不选任何边的情况等价,因此每个环上方案数要减少入度之和。

时间复杂度 \(\mathcal O(n)\)

代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+5,MOD=1e9+7;
int n,a[MAXN],d[MAXN],dep[MAXN],fa[MAXN],fe[MAXN];
bool inc[MAXN];
struct Edge { int v,i; };
vector <Edge> G[MAXN];
vector <int> V;
void dfs(int u) {
	V.push_back(u);
	for(auto e:G[u]) if(e.i^fe[u]) {
		if(!dep[e.v]) dep[e.v]=dep[u]+1,fe[e.v]=e.i,fa[e.v]=u,dfs(e.v);
		else if(dep[e.v]<=dep[u]) {
			for(int x=u;x!=e.v;x=fa[x]) inc[x]=true;
			inc[e.v]=true;
		}
	}
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i],++d[a[i]],G[i].push_back({a[i],i}),G[a[i]].push_back({i,i});
	ll p=1;
	for(int i=1;i<=n;++i) if(!dep[i]) {
		V.clear(),dep[i]=1,dfs(i);
		ll x=1,y=1,z=0;
		for(int u:V) {
			if(inc[u]) y=y*(d[u]+1)%MOD,z+=d[u];
			else x=x*(d[u]+1)%MOD;
		}
		p=p*x%MOD*(y+MOD-z)%MOD;
	}
	cout<<p<<"\n";
	return 0;
}



M. [CF1887E] Good Colorings (2.5)

Problem Link

首先可以想到行列建立二分图,每个有色点连接行列,权值为颜色,那么我们目标是找到异色四元环。

首先初始的图有 \(2n\) 条权值不同的边,很显然能找到环,那么我们对环分治,每次加边均分环,则这条边的颜色在环的左右两侧至多出现一次,则能找到一侧构成新的异色环递归。

操作次数不超过 \(\log n-1\)

时间复杂度 \(\mathcal O(n\log n)\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=2005;
struct Edge { int v,w; };
vector <Edge> G[MAXN],cyc;
int n,fa[MAXN]; Edge ed[MAXN];
bool vis[MAXN];
void dfs(int u) {
	vis[u]=true;
	for(auto e:G[u]) if(e.v^fa[u]) {
		if(!vis[e.v]) ed[e.v]={e.v,e.w},fa[e.v]=u,dfs(e.v);
		else if(cyc.empty()) {
			for(int x=u;x^e.v;x=fa[x]) cyc.push_back(ed[x]);
			cyc.push_back(e);
		}
	}
}
int qry(int x,int y) {
	if(x>y) swap(x,y);
	cout<<"? "<<x<<" "<<y-n<<endl;
	int o; cin>>o; return o;
}
void solve(const vector<Edge>&a) {
	if(a.size()==4) {
		vector<int>p{a[0].v,a[1].v,a[2].v,a[3].v};
		sort(p.begin(),p.end()),cout<<"! "<<p[0]<<" "<<p[1]<<" "<<p[2]-n<<" "<<p[3]-n<<endl;
		string o; cin>>o; return ;
	}
	int p=a.size()/2;
	p-=(p^1)&1;
	int c=qry(a[0].v,a[p].v);
	vector<Edge>b(a.begin(),a.begin()+p); b.push_back({a[p].v,c});
	for(int i=0;i<p;++i) if(a[i].w==c) {
		b=vector<Edge>(a.begin()+p,a.end()),b.push_back({a[0].v,c});
		return solve(b);
	}
	solve(b);
}
void solve() {
	cin>>n,cyc.clear();
	for(int i=1;i<=2*n;++i) G[i].clear(),fa[i]=vis[i]=0;
	for(int i=1,x,y;i<=2*n;++i) cin>>x>>y,y+=n,G[x].push_back({y,i}),G[y].push_back({x,i});
	for(int i=1;i<=2*n;++i) if(!vis[i]) dfs(i);
	solve(cyc);
}
signed main() {
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



N. [CF1799H] Tree Cutting (2)

Problem Link

首先假设最终保留了根,那么 \(f_{u,s}\) 表示 \(u\) 的子树中完成了 \(s\) 对应的操作。

转移的时候子集卷积,然后选择一个 \(>\max(s)\) 的位置填 \(u\),此时该操作对大小的减少量必定是 \(\mathrm{siz}(u)\) 减去 \(s\) 对应减少量之和。

然后换根对最浅连通块中节点求和即可。

时间复杂度 \(\mathcal O(n3^k)\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5005,MOD=998244353;
typedef array<int,1<<6> arr;
int n,m,U,siz[MAXN],a[6];
arr f[MAXN],g[MAXN],h[MAXN],su;
arr add(const arr&x,int y) {
	arr z=x;
	for(int s=0;s<=U;++s) for(int i=0;i<m;++i) if((1<<i)>s) {
		if(a[i]==y-su[s]) z[s|1<<i]=(z[s|1<<i]+x[s])%MOD;
	}
	return z;
}
arr mul(const arr&x,const arr&y) {
	arr z; z.fill(0);
	for(int s=0;s<=U;++s) for(int o=U-s,t=o;;t=(t-1)&o) {
		z[s|t]=(z[s|t]+1ll*x[s]*y[t])%MOD;
		if(!t) break;
	}
	return z;
}
vector <int> G[MAXN];
void dfs1(int u,int fz) {
	siz[u]=1,f[u][0]=1;
	if(fz) G[u].erase(find(G[u].begin(),G[u].end(),fz));
	for(int v:G[u]) {
		dfs1(v,u),siz[u]+=siz[v];
		f[u]=mul(f[u],add(f[v],siz[v]));
	}
}
int ans=0;
void dfs2(int u) {
	if(u>1) {
		arr z=add(g[u],n-siz[u]);
		for(int s=0;s<=U;++s) ans=(ans+1ll*f[u][U-s]*(z[s]+MOD-g[u][s]))%MOD;
		g[u]=z;
	}
	if(G[u].empty()) return ;
	int k=G[u].size(); h[k]=g[u];
	for(int i=k-1;i;--i) h[i]=mul(h[i+1],add(f[G[u][i]],siz[G[u][i]]));
	arr p; p.fill(0),p[0]=1;
	for(int i=0;i<k;++i) {
		int v=G[u][i];
		g[v]=mul(p,h[i+1]);
		p=mul(p,add(f[v],siz[v]));
	}
	for(int v:G[u]) dfs2(v);
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1,u,v;i<n;++i) cin>>u>>v,G[u].push_back(v),G[v].push_back(u);
	cin>>m,U=(1<<m)-1;
	for(int i=0,x=n,y;i<m;++i,x=y) {
		cin>>y,a[i]=x-y;
		for(int s=0;s<=U;++s) if(s>>i&1) su[s]+=x-y;
	}
	dfs1(1,0),ans=f[1][U],g[1][0]=1,dfs2(1);
	cout<<ans<<"\n";
	return 0;
}



*O. [CF2124I] Lexicographic Partition (9)

Problem Link

首先考虑如何求 \(b=f(a)\),很显然 \(b_1=a_1\),那么找到首个 \(a_i<a_1\),则下一个区间的开头就是 \(\arg\max a[2,i)\),记 \(b_i=a_{p_i}\)

考虑增量维护 \(f(a[1,n-1])\to f(a[1,n])\),那么找到首个 \(k=p_i\) 使得 \(a[k,n]\) 的后继是 \(n\),那么 \(b[i+1,|b|]\) 都会删掉变成 \(a_n\)

则此时的 \(a_k=\min a[k,n],a_n=\max a[k,n]\),可以发现 \(b_i\)\(\arg\min b(j,|b|]\),其中 \(b_j\) 是最后一个 \(<a_n\) 的元素。

证明只要考虑任意 \(i\) 都满足 \(\arg \min a(p_i,n],\arg\max a(p_i,n]\in P\)

那么此时我们可以用栈维护 \(b\) 的结构,知道每个时刻的 \(|b|\) 就能还原出每个时刻 \(b\) 的形态,加入每个元素时连向栈中前一个元素,则我们能得到树状结构。

考虑如何构造合法的树权值,朴素的想法就是把每个子树的权值填成连续区间,假设我们尝试用 \([l,r]\) 处理 \(u\) 的子树。

那么简单的想法就是把 \(a_u\) 填成 \(l\)\(r\),观察得到的效果。

如果 \(a_u=l\),那么内部的每个子树只要从左到右从小到大分配区间即可,但是此时 \(u\) 的每个儿子至少连接到 \(u\),为了防止连接到 \(u\) 祖先,则 \(a_{fa(u)}\) 必须取所属区间的右端点。

如果 \(a_u=r\),那么按刚才的分析,影响就是 \(u\) 的儿子可以填左端点,但此时儿子个数 \(>1\) 则无法连接到 \(u\) 上。

所以上述策略当且仅当不存在相邻两个儿子数 \(>1\) 的点时成立。

实际上如果存在这种情况简单分讨几个点大小关系即可导出无解。

因此该构造是充分必要的。

时间复杂度 \(\mathcal O(n)\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5;
int n,a[MAXN],b[MAXN],st[MAXN],fa[MAXN],sz[MAXN];
vector <int> G[MAXN];
void dfs(int u,int l,int r) {
	if(G[u].size()>1) {
		a[u]=l++;
		for(int v:G[u]) dfs(v,l,l+sz[v]-1),l+=sz[v];
	} else {
		a[u]=r--;
		for(int v:G[u]) dfs(v,l,r);
	}
}
void solve() {
	cin>>n;
	for(int i=0;i<=n;++i) G[i].clear(),sz[i]=0;
	for(int i=1;i<=n;++i) cin>>b[i];
	for(int i=1,tp=0;i<=n;++i) {
		if(b[i]-1>tp) return cout<<"NO\n",void();
		tp=b[i]-1,G[fa[i]=st[tp]].push_back(i),st[++tp]=i;
	}
	if(count(fa+2,fa+n+1,0)) return cout<<"NO\n",void();
	for(int i=1;i<=n;++i) if(fa[i]&&G[i].size()>1&&G[fa[i]].size()>1)  return cout<<"NO\n",void();
	cout<<"YES\n";
	for(int i=n;i>1;--i) sz[fa[i]]+=++sz[i];
	dfs(1,1,n);
	for(int i=1;i<=n;++i) cout<<a[i]<<" \n"[i==n];
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



P. [CF1889E] Doremy's Swapping Trees (5)

Problem Link

考虑刻画两张图的公共连通块,首先如果 \(T_1\) 中选择 \((u,v)\),则 \(T_2\)\(u\to v\) 路径都要被选,然后会把每条边在 \(T_1\) 上对应的路径选择。

由于生成的连通块间只有包含或无交,因此把本质不同连通块建树,每个节点都可以选择是否交换,所以只要求出本质不同连通块数即可。

那么建图描述该过程,对于任意边 \((u,v)\),向其在另一棵树上的路径的每条边链边,那么每条边导出的连通块就是该图上的闭合子图。

对该图缩点,求出 SCC 个数即为答案,可以用树剖优化建图。

注意 \(T_1\cap T_2\) 中的边单独成 SCC 且交换无贡献,需要删掉。

时间复杂度 \(\mathcal O(n\log n)\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5,MOD=1e9+7,i2=(MOD+1)/2;
int n,m;
vector <int> E[MAXN*6];
void link(int x,int y) { E[x].push_back(y); }
struct Tree {
	vector <int> G[MAXN];
	int o,fa[MAXN],siz[MAXN],hson[MAXN],dep[MAXN],dfn[MAXN],dcnt,top[MAXN],rk[MAXN],up[MAXN],tr[1<<18|5];
	void dfs1(int u,int fz) {
		fa[u]=fz,dep[u]=dep[fz]+1,siz[u]=1,hson[u]=0;
		for(int v:G[u]) if(v^fz) {
			dfs1(v,u),siz[u]+=siz[v];
			if(siz[v]>siz[hson[u]]) hson[u]=v;
		}
	}
	void dfs2(int u,int rt) {
		top[u]=rt,dfn[u]=++dcnt,rk[dcnt]=u;
		if(u==rt) up[u]=u+o;
		else up[u]=++m,link(up[u],up[fa[u]]),link(up[u],u+o);
		if(hson[u]) dfs2(hson[u],rt);
		for(int v:G[u]) if(v!=fa[u]&&v!=hson[u]) dfs2(v,v);
	}
	void init(int l,int r,int p) {
		if(l==r) return tr[p]=rk[l]+o,void();
		int mid=(l+r)>>1;
		init(l,mid,p<<1),init(mid+1,r,p<<1|1);
		tr[p]=++m,link(tr[p],tr[p<<1]),link(tr[p],tr[p<<1|1]);
	}
	void upd(int ul,int ur,int x,int l,int r,int p) {
		if(ul<=l&&r<=ur) return link(x,tr[p]);
		int mid=(l+r)>>1;
		if(ul<=mid) upd(ul,ur,x,l,mid,p<<1);
		if(mid<ur) upd(ul,ur,x,mid+1,r,p<<1|1);
	}
	void init() {
		for(int i=1;i<=n;++i) G[i].clear();
		for(int i=1,u,v;i<n;++i) cin>>u>>v,G[u].push_back(v),G[v].push_back(u);
		dcnt=0,dfs1(1,0),dfs2(1,1),init(1,n,1);
	}
	void add(int u,int v,int x) {
		while(top[u]^top[v]) {
			if(dep[top[u]]<dep[top[v]]) swap(u,v);
			link(x,up[u]),u=fa[top[u]];
		}
		if(dfn[u]>dfn[v]) swap(u,v);
		if(dfn[u]<dfn[v]) upd(dfn[u]+1,dfn[v],x,1,n,1);
	}
}	tr[2];
int dfn[MAXN*6],low[MAXN*6],dcnt,st[MAXN*6],tp,z;
bool ins[MAXN*6],ok[MAXN*6];
void tarjan(int u) {
	dfn[u]=low[u]=++dcnt,ins[st[++tp]=u]=1;
	for(int v:E[u]) {
		if(!dfn[v]) tarjan(v),low[u]=min(low[u],low[v]);
		else if(ins[v]) low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u]) {
		int c=0;
		for(;ins[u];ins[st[tp--]]=0) c|=ok[st[tp]];
		if(c) z=z*2%MOD;
	}
}
void solve() {
	cin>>n,tr[1].o=n,m=2*n,z=1,dcnt=0;
	tr[0].init(),tr[1].init();
	for(int i=2;i<=n;++i) {
		int x=tr[0].fa[i],y=tr[1].fa[i];
		tr[1].add(i,x,i),tr[0].add(i,y,i+n),ok[i]=ok[i+n]=1;
		if(x==y||tr[1].fa[x]==i) z=1ll*z*i2%MOD;
	}
	for(int i=1;i<=m;++i) if(!dfn[i]) tarjan(i);
	cout<<z<<"\n";
	for(int i=1;i<=m;++i) E[i].clear(),dfn[i]=ok[i]=0;
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



*Q. [CF2129F2] Top-K Tracker (8)

Problem Link

首先如果只进行一、三询问,且保证 \(m\le k\),那么我们相当于每次询问划分把元素划分等价类,当且仅当两个元素每次询问时状态相同无法区分其取值。

那么此时 \(\sum m=2040\),构造若干个 \([1,30]\) 的不同子集 \(Q_1\sim Q_n\) 表示被哪些询问覆盖即可,从按集合大小小到大分配 \(Q_i\),精细构造可以通过 \(n\le 845\)

\(n\le 890\) 需要利用 \(m>k\) 的操作。

考虑如果每个等价类中元素个数 \(\le 2\) 设为 \(\{p_i,p_j\}=\{x,y\}\),那么我们只要确定 \(p_i=x\) 是否成立即可。

利用前 \(m\) 大的性质,我们把每个等价类中的 \(i\) 取出来进行询问,只要返回的值 \(\le \max(x,y)\),我们就能知道 \(p_i\) 是否为 \(\max(x,y)\)

那么我们用操作二对值划分等价类,只要选用 \(|Q|\le 2\) 的集合以及 \(9\)\(|Q|=3\) 的集合就能满足要求。

此时所有等价类形如 \(y=n+1-x\),然后随机交换 \(p_i,p_j\),相当于要求 \(\dfrac n2\) 个随机变量和 \(<300\),错误概率大约是 \(10^{-12}\) 级别。

时间复杂度 \(\mathcal O(n)\)

代码:

#include<bits/stdc++.h>
using namespace std;
mt19937 rnd(time(0));
const int MAXN=1005,q=29;
vector<int> a[MAXN],f[MAXN],g[MAXN],h[MAXN];
map <vector<int>,int> b;
int vis[MAXN],w[MAXN];
void solve() {
	int n;
	cin>>n;
	for(int i=1;i<=(n+1)/2;++i) for(int x:a[i]) {
		f[x].push_back(i);
		if(i!=n+1-i) f[x].push_back(n+1-i);
	}
	for(int i=1;i<=q;++i) if(f[i].size()) {
		cout<<"2 "<<f[i].size()<<" ";
		for(int o:f[i]) cout<<o<<" ";
		cout<<endl;
		for(int x,c=f[i].size();c--;) cin>>x,g[x].push_back(i);
	}
	for(int i=1;i<=n;++i) h[b[g[i]]].push_back(i);
	cout<<"3 "<<n/2<<" ";
	for(int i=1;i<=n/2;++i) {
		if(rnd()&1) swap(h[i][0],h[i][1]);
		cout<<h[i][0]<<" ";
	}
	cout<<endl;
	for(int x,c=min(300,n/2);c--;) cin>>x,vis[x]=1;
	for(int i=1;i<=n/2;++i) {
		if(vis[n+1-i]) swap(h[i][0],h[i][1]);
		w[h[i][0]]=i,w[h[i][1]]=n+1-i;
	}
	if(n&1) w[h[n/2+1][0]]=n/2+1;
	cout<<"! ";
	for(int i=1;i<=n;++i) cout<<w[i]<<" ";
	cout<<endl;
	for(int i=1;i<=q;++i) f[i].clear();
	for(int i=1;i<=n;++i) g[i].clear(),h[i].clear(),vis[i]=0;
}
signed main() {
	int m=1,_;
	for(int i=1;i<=q;++i) a[++m]={i};
	for(int i=1;i<=q;++i) for(int j=i+1;j<=q;++j) a[++m]={i,j};
	for(int i=1;i<=9;++i) a[++m]={i,i+9,i+18};
	for(int i=1;i<=m;++i) b[a[i]]=i;
	cin>>_;
	while(_--) solve();
	return 0;
}



R. [CF1819E] Roads in E City (3)

Problem Link

首先如果我们能检验当前图是否连通,那么做法是简单的,先把能删的边都删掉得到一棵生成树,然后校验非树边时删掉一条成环的树边,然后加入非树边并判断即可。

注意到我们的过程中只要删 \((u,v)\) 后判断 \((u,v)\) 是否连通,不妨把交互库的 \(s\) 看作随机选取,则交替询问 \(u,v\),两次 \(s\) 都落在对应连通块的概率为 \(p(1-p)\le \dfrac 14\),因此只要 \(B=\mathcal O(\log \epsilon)\) 次就能校验。

\(B=20\) 即可通过。

时间复杂度 \(\mathcal O(m)\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=2005;
struct Edge { int v,i; };
vector <Edge> G[MAXN];
int n,m,l[MAXN],r[MAXN],d[MAXN],fa[MAXN];
bool ok[MAXN];
bool chk(int e) {
	for(int t=20,x;t;--t) {
		cout<<"? "<<l[e]<<endl;
		cin>>x; if(!x) return 1;
		cout<<"? "<<r[e]<<endl;
		cin>>x; if(!x) return 1;
	}
	return 0;
}
void dfs(int u) {
	for(auto e:G[u]) if(e.i^fa[u]) d[e.v]=d[u]+1,fa[e.v]=e.i,dfs(e.v);
}
void solve() {
	cin>>n>>m;
	for(int i=1;i<=n;++i) G[i].clear();
	for(int i=1;i<=m;++i) cin>>l[i]>>r[i],ok[i]=0;
	for(int i=1;i<=m;++i) {
		cout<<"- "<<i<<endl;
		if(!chk(i)) continue;
		cout<<"+ "<<i<<endl,ok[i]=1;
		G[l[i]].push_back({r[i],i});
		G[r[i]].push_back({l[i],i});
	}
	dfs(1);
	for(int i=1;i<=m;++i) if(!ok[i]) {
		int j=d[l[i]]<d[r[i]]?fa[r[i]]:fa[l[i]];
		cout<<"+ "<<i<<endl;
		cout<<"- "<<j<<endl;
		ok[i]=!chk(i);
		cout<<"- "<<i<<endl;
		cout<<"+ "<<j<<endl;
	}
	cout<<"! "; for(int i=1;i<=m;++i) cout<<ok[i]<<" "; cout<<endl;
	int o; cin>>o;
}
signed main() {
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



S. [CF2113E] From Kazan with Love (4)

Problem Link

首先答案肯定不超过 \(2n\),因为敌人消失后可以直接走过去。

考虑朴素 dp,\(f_{t,i}\) 表示 \(t\) 时刻能否到达 \(i\),转移就是每个 \(f_{t,i}=1\) 的点拓展到 \(f_{t+1,i}\) 中点,然后删掉当前被敌人占据的点。

注意到删掉的点很少,因此我们动态维护新转移到的点,具体来说 \(t\to t+1\) 时我们只要暴力计算 \(t-1\) 时刻被删掉的点,维护出 \(t\) 时刻新到达的点,然后加入它们的邻域即可。

可以发现复杂度和删掉的点度数和同量级,显然每条边的贡献是 \(\mathcal O(n)\) 的。

时间复杂度 \(\mathcal O(nm)\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5;
basic_string <int> G[MAXN],ban[MAXN],D,Q,nw;
int n,m,S,T,dep[MAXN],fa[MAXN];
void dfs1(int u) { for(int v:G[u]) if(v^fa[u]) fa[v]=u,dep[v]=dep[u]+1,dfs1(v); }
void lca(int x,int y) {
	if(x==y) D+=x;
	else if(dep[x]<dep[y]) lca(x,fa[y]),D+=y;
	else D+=x,lca(fa[x],y);
}
bool f[MAXN],inq[MAXN];
void solve() {
	cin>>n>>m>>S>>T,Q.clear();
	for(int i=0;i<=n;++i) G[i].clear(),ban[i].clear(),f[i]=inq[i]=0;
	for(int i=1,u,v;i<n;++i) cin>>u>>v,G[u]+=v,G[v]+=u;
	dfs1(1);
	for(int i=1,x,y;i<=m;++i) {
		cin>>x>>y,D.clear(),lca(x,y);
		for(int j=1;j<(int)D.size();++j) ban[j].push_back(D[j]);
	}
	f[S]=1;
	for(int x:G[S]) Q+=x,inq[x]=1;
	for(int t=0;t<2*n;++t) {
		if(f[T]) return cout<<t+1<<"\n",void();
		for(int x:ban[t]) {
			bool o=0;
			for(int y:G[x]) if(o|=f[y]) break;
			if(o) Q+=x,inq[x]=1;
		}
		for(int x:Q) f[x]=1,inq[x]=0;
		for(int x:ban[t+1]) f[x]=0;
		nw.clear();
		for(int x:Q) if(f[x]) for(int y:G[x]) if(!f[y]&&!inq[y]) nw+=y,inq[y]=1;
		Q.swap(nw);
	}
	cout<<"-1\n";
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



T. [CF1804H] Code Lock (4.5)

Problem Link

如果 \(i\to j\) 代价是 \(|i-j|\),那么只要逐个字符填入,状压已填入字符,维护每对 \((i,j)\) 的出现次数,把绝对值拆开简单算代价即可。

但现在 \(|i-j|>\frac k2\) 的贡献变成 \(k-|i-j|\),考虑把环分成长为 \(\frac k2\) 的两部分,依次填入环上 $1,\frac k2,2,\frac k2+1,\dots $,这样就能分别计算四种可能的贡献。

枚举前 \(\frac k2\) 个点的集合后 dp,状态数 \(\mathcal O(\binom{k}{k/2}\sum_i \binom{k/2}{i}^2)=\mathcal O(\binom{k}{k/2}^2)\)

预处理一些子集和可以做到 \(\mathcal O(k)\) 转移。

时间复杂度 \(\mathcal O(n+\binom{k}{k/2}k(2^{k/2}+\binom{k}{k/2}))\)

代码:

#include<bits/stdc++.h>
#define ll long long
#define pc __builtin_popcount
using namespace std;
const int MAXN=65536;
const ll inf=1e18;
struct info {
	ll z,f;
	info(ll Z=inf,ll F=0): z(Z),f(F) {}
	inline void operator +=(const info &v) {
		if(v.z<z) z=v.z,f=v.f;
		else if(v.z==z) f+=v.f;
	}
}	f[MAXN],ans;
int n,m,h,u,v,a[16][16],id[1<<8][1<<8],L[16],R[16];
int S[MAXN],T[MAXN],g[MAXN][16],sl[16][1<<8],sr[16][1<<8];
void solve(int X) {
	L[0]=h;
	for(int i=0,l=1,r=0;i<n;++i) if(i^h) X>>i&1?L[l++]=i:R[r++]=i;
	for(int i=1;i<=m;++i) f[i]=info();
	for(int i=0;i<n;++i) {
		for(int s=1;s<(1<<u);++s) {
			int x=__builtin_ctz(s);
			sl[i][s]=sl[i][s^(1<<x)]+a[i][L[x]];
		}
		for(int s=1;s<(1<<v);++s) {
			int x=__builtin_ctz(s);
			sr[i][s]=sr[i][s^(1<<x)]+a[i][R[x]];
		}
	}
	int U=(1<<u)-1,V=(1<<v)-1;
	f[1]={n*sr[h][V],1};
	for(int e=1;e<=m;++e) {
		int s=S[e],t=T[e],x=pc(s)>pc(t)?pc(t)+u:pc(s);
		if(pc(s)>pc(t)) {
			for(int i=0;i<v;++i) if(!(t>>i&1)) f[g[e][i]]+=info(f[e].z+x*(sl[R[i]][U^s]-sl[R[i]][s]+sr[R[i]][t]-sr[R[i]][V^t]),f[e].f);
		} else {
			for(int i=0;i<u;++i) if(!(s>>i&1)) f[g[e][i]]+=info(f[e].z+x*(sl[L[i]][s]-sl[L[i]][U^s]-sr[L[i]][t])+(x+n)*sr[L[i]][V^t],f[e].f);
		}
	}
	ans+=f[m];
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int N; string st;
	cin>>n>>N>>st,h=st[0]-'a';
	for(int i=1;i<N;++i) ++a[st[i-1]-'a'][st[i]-'a'],++a[st[i]-'a'][st[i-1]-'a'];
	for(int i=0;i<n;++i) a[i][i]=0;
	v=n/2,u=n-n/2;
	queue <array<int,2>> Q;
	Q.push({1,0}),id[1][0]=++m,S[1]=1,T[1]=0;
	while(Q.size()) {
		int s=Q.front()[0],t=Q.front()[1]; Q.pop();
		if(pc(s)>pc(t)) {
			for(int i=0;i<v;++i) if(!(t>>i&1)) {
				if(!id[s][t|1<<i]) id[s][t|1<<i]=++m,S[m]=s,T[m]=t|1<<i,Q.push({s,t|1<<i});
				g[id[s][t]][i]=id[s][t|1<<i];
			}
		} else {
			for(int i=0;i<u;++i) if(!(s>>i&1)) {
				if(!id[s|1<<i][t]) id[s|1<<i][t]=++m,S[m]=s|1<<i,T[m]=t,Q.push({s|1<<i,t});
				g[id[s][t]][i]=id[s|1<<i][t];
			}
		}
	}
	for(int x=0;x<(1<<n);++x) if(pc(x)==u&&(x>>h&1)) solve(x);
	cout<<ans.z+N<<"\n"<<ans.f<<"\n";
	return 0;
}



U. [CF2062E2] The Game(4)

Problem Link

可以猜测游戏轮数很短,首先如果先手操作 \(x\) 使得 \(x\) 子树外的点 \(y\) 满足 \(w_y>w_x\) 都必败,先手直接获胜。

那么我们按 \(w\) 倒序加点,如果某个点不是已有点的公共祖先,就加入,很显然此前加入的点都是必败的。

可以证明先手必败当且仅当不存在 \((x,y)\) 使得操作 \(x\) 后可以操作 \(y\)

那么降序加点,每次加入 \(x\) 时求出 \(>w_x\) 的点的 LCA,合法点必须在这两点的路径上,树状数组打标记即可。

时间复杂度 \(\mathcal O(n\log n)\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=4e5+5;
int n,dcnt,dfn[MAXN],rk[MAXN],efn[MAXN],a[MAXN],st[MAXN][20];
struct BIT1 {
	int tr[MAXN],s;
	void init() { fill(tr,tr+n+1,0); }
	void add(int x,int z) { for(;x<=n;x+=x&-x) tr[x]+=z; }
	int qry(int x) { for(s=0;x;x&=x-1) s+=tr[x]; return s; }
}	X,Y;
vector <int> G[MAXN],id[MAXN];
int cmp(int x,int y) { return dfn[x]<dfn[y]?x:y; }
int LCA(int x,int y) {
	if(!x||!y||x==y) return x|y;
	int l=min(dfn[x],dfn[y])+1,r=max(dfn[x],dfn[y]),k=__lg(r-l+1);
	return cmp(st[l][k],st[r-(1<<k)+1][k]);
}
void dfs1(int u,int fz) {
	dfn[u]=++dcnt,rk[dcnt]=u,st[dcnt][0]=fz;
	for(int v:G[u]) if(v^fz) dfs1(v,u);
	efn[u]=dcnt;
}
void solve() {
	cin>>n,dcnt=0,X.init(),Y.init();
	for(int i=1;i<=n;++i) G[i].clear(),id[i].clear();
	for(int i=1;i<=n;++i) cin>>a[i],id[a[i]].push_back(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,0);
	for(int k=1;k<20;++k) for(int i=1;i+(1<<k)-1<=n;++i) {
		st[i][k]=cmp(st[i][k-1],st[i+(1<<(k-1))][k-1]);
	}
	set <int> S;
	vector <int> w;
	for(int z=n,cx=0,cy=0;z>=1;--z) {
		for(int u:id[z]) {
			if(X.qry(efn[u])-X.qry(dfn[u]-1)<cx&&Y.qry(efn[u])-Y.qry(dfn[u]-1)==cy) w.push_back(u);
		}
		for(int u:id[z]) {
			X.add(dfn[u],1),++cx;
			if(S.empty()) continue;
			auto l=S.begin(),r=S.end();
			if(*l>=dfn[u]) l=S.upper_bound(efn[u]);
			if(*prev(r)<=efn[u]) r=S.lower_bound(dfn[u]);
			int p=LCA(l==S.end()?0:rk[*l],r==S.begin()?0:rk[*--r]);
			if(p) Y.add(dfn[u],1),Y.add(dfn[p],1),Y.add(dfn[LCA(u,p)],-1),++cy;
		}
		for(int u:id[z]) S.insert(dfn[u]);
	}
	sort(w.begin(),w.end());
	cout<<w.size()<<" "; for(int u:w) cout<<u<<" "; cout<<"\n";
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



*V. [CF2147G] Modular Tetration (9)

Problem Link

首先 \(a\) 合法显然需要 \(\gcd(a,m)=1\),那么要求 \(\delta_m(a)\mid b_{n-1}\),设 \(P(x)\) 表示 \(x\) 所有不同的质因子之积,则要求 \(P(\delta_m(a))\mid a\)

进一步观察该条件,由于 \(\gcd(a,m)=1\),所以 \(\gcd(P(\delta_m(a)),m)=1\),因此对于所有 \(a\bmod m=r\) 的数,\(a+km\)\(k\in[0,\delta_m(a))\) 范围内恰有一个满足 \(a+km\equiv 0\pmod{P(\delta_m(a))}\)

所以枚举 \(x=\delta_m(a)\) 满足 \(\gcd(P(x),m)=1\),记这样的 \(a\)\(c_x\) 个,则对应的密度就是 \(\dfrac 1m\sum_x\dfrac{c_x}{P(x)}\)

考虑刻画 \(c_x\),打表发现 \(m\) 是质数时 \(c_x=[x\mid \varphi(m)]\varphi(x)\),这是因为此时取原根 \(g\)\(\delta_m(g^k)=\dfrac{m-1}{\gcd(m-1,k)}\)

\(m\) 没有原根时不一定满足此条件,但我们只要对每种 \(P(x)\) 求和,可以猜测 $P(x)\mid\varphi(m) $ 时 \(\sum_{P(x)=p} c_x=\sum_{P(x)=p} \varphi(x)\)

首先对每个质因数集合 \(S\),记 \(w(S)\) 表示内部质因数之积,先在 \(m\) 有原根时刻画每个 \(w(S)\) 的答案。

容斥枚举 \(T\subseteq S\),计算 \(P(x)\mid w(T)\)\(x\) 贡献和,则答案为:

\[\begin{aligned} \mathrm{Ans} &=\sum_T(-1)^{|S|-|T|}\sum_{p(x)\mid w(T)} \varphi(x)\\ &=\sum_T(-1)^{|S|-|T|}\prod_{p\in T}(1+(p-1)(1+\cdots+p^{v_{\varphi(m)}(p)-1}))\\ &=\sum_T(-1)^{|S|-|T|}\prod_{p\in T}p^{v_{\varphi(m)}(p)}\\ &=\prod_{p\in S} (p^{v_{\varphi(m)}(p)}-1) \end{aligned} \]

其中 \(v_{\varphi(m)}(p)\) 表示 \(\varphi(m)\)\(p\) 因子个数。

那么一般情况考虑归纳地合并 \(m=m_1\times m_2\),其中 \(\gcd(m_1,m_2)=1\),此时 \(\varphi(m)=\varphi(m_1)\times \varphi(m_2)\)

对于每个质因子 \(p\),设其在 \(\varphi(m_1),\varphi(m_2)\) 中的次数为 \(x,y\),则 \(p\in S_1\cup S_2\) 时贡献和为 \((p^x-1)(p^y-1)+(p^x-1)+(p^y-1)=p^{x+y}-1=p^{v_{\varphi(m)}(p)}-1\)

因此可以归纳地证明 \(m=m_1\times m_2\) 时正确。

对于 \(2^k\mid m\) 的情况,\(k=1\) 时无贡献,否则 \(2\mid \varphi(m)\),由于我们只要考虑 \(\gcd(w(S),m)=1\)\(S\),所以 \(2\in S\) 的情况可以忽略。

那么最终答案是 \(\dfrac 1m\sum_{\gcd(w(S),m)=1}\prod_{p\in S} \dfrac{p^{v_{\varphi(m)}(p)}-1}p=\dfrac 1m\prod_{p\in S,p\nmid m} 1+\dfrac{p^{v_{\varphi(m)}(p)}-1}p\)

所以只要维护 \(m,\varphi(m)\) 的分解即可。

时间复杂度 \(\mathcal O(\log m\log\log m)\)

代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+5,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; }
bool isc[MAXN];
vector <int> Pr;
vector <array<int,2>> fc[MAXN];
void solve() {
	ll x,y,z;
	cin>>x>>y>>z;
	map <int,int> M,P;
	for(int q:{x,y,z}) for(auto o:fc[q]) M[o[0]]+=o[1];
	for(auto it:M) {
		if(it.second>1) P[it.first]+=it.second-1;
		for(auto o:fc[it.first-1]) P[o[0]]+=o[1];
	}
	for(auto it:M) P.erase(it.first);
	ll s=ksm(x*y*z%MOD);
	for(auto it:P) s=((ksm(it.first,it.second)-1)*ksm(it.first)+1)%MOD*s%MOD;
	cout<<s<<"\n";
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	for(int i=2;i<MAXN;++i) {
		if(!isc[i]) Pr.push_back(i),fc[i]={{i,1}};
		for(int p:Pr) {
			if(i*p>=MAXN) break;
			fc[i*p]=fc[i],isc[i*p]=true;
			if(i%p==0) { ++fc[i*p].back()[1]; break; }
			else fc[i*p].push_back({p,1});
		}
	}
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



W. [CF1864H] Asterism Stream (5)

Problem Link

计算每个 \(\le n\) 被经过的期望次数。

枚举 \(\times 2\) 次数 \(k\),那么最终的数可以表示为 \(\sum_{i=0}^{k}c_i2^i\),其中 \(c_k>0\)

由于我们要求 \(\le n\) 的状态总和,因此添加虚拟变量 \(c_{-1}\),限制变为 \(\sum_{i=-1}^k c_i2^{\max (i,0)}=n-2^k\),不限制任何值。

对答案的贡献是 \(2^{-k-\sum_{i\ge 0} c_i}\)

直接考虑 \(c\) 的二进制表示,对于答案的第 \(j\) 位,一定由 \(c_x\) 的第 \(j-\max(x,0)\) 位转移而来,那么按照这个顺序确定 \(c\) 的每一位,预处理这位贡献为 \(0\sim k+1\) 的系数,然后 dp 时记录 \(j-1\to j\) 的进位即可。

时间复杂度 \(\mathcal O(\log^4 n)\)

代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MOD=998244353,i2=(MOD+1)/2;
ll pw[75],z[75],ans,f[75],g[75],h[75];
void sol(int k,ll n) {
	memset(f,0,sizeof(f)),f[0]=1;
	for(int i=0,c;n;++i,n>>=1) {
		memset(h,0,sizeof(h)),h[0]=h[1]=1,c=min(i,k);
		for(int j=0;j<=c;++j) for(int x=j+1;~x;--x) h[x+1]=(h[x+1]+h[x]*z[i-j])%MOD;
		memset(g,0,sizeof(g));
		for(int x=0;x<64;++x) if(f[x]) for(int y=0;y<64;++y) if((x^y^n^1)&1) {
			g[(x+y)/2]=(g[(x+y)/2]+f[x]*h[y])%MOD;
		}
		memcpy(f,g,sizeof(f));
	}
	ans=(ans+f[0]*pw[k])%MOD;
}
void solve() {
	ll n; cin>>n,--n,ans=0;
	for(int i=0;n>>i;++i) sol(i,n-(1ll<<i));
	cout<<ans<<"\n";
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	z[0]=i2;
	for(int i=1;i<64;++i) z[i]=z[i-1]*z[i-1]%MOD;
	for(int i=pw[0]=1;i<64;++i) pw[i]=pw[i-1]*i2%MOD;
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



X. [CF1886F] Diamond Theft (4)

Problem Link

首先答案具有单调性,可以二分之。

然后枚举偷第一个钻石的时间,把时间分成前后两段,长度为 \(x,y\),此时每个监控有几种可能:

  • \(t=1\) 必须在第一段的某个后缀中开始。
  • \(t=2\) 如果 \(a\le y\) 必须在第二段的某个后缀中开始,要么在第一段的某个后缀或第二段任意位置开始。
  • \(t=3\) 如果 \(a\le y\) 分别在第一第二段的某个后缀中开始,否则可以可以选择在第一段的某个后缀 \([p,x]\) 中开始,或者在第一段的后缀 \([p-y-1,x]\) 和第二段的任意位置开始。

每个点先尽可能满足第二段的限制,要求就是每个后缀长度大于点数,维护每个后缀的长度减点数,要求所有值非负。

那么此时调整就是给第一段的某个长为 \(y+1\) 的区间或前缀 \(+1\),然后给第二段末尾 \(-1\),扫描线,每次优先贪心弹出前缀,然后弹出最靠前的区间即可。

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

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=3005,inf=2e9;
int n,t[MAXN],a[MAXN],f[MAXN],g[MAXN],h[MAXN],c[MAXN],q[MAXN],w[MAXN];
bool chk(int L,int R) {
	memset(f,0,sizeof(f)),memset(g,0,sizeof(g));
	memset(h,0,sizeof(h)),memset(c,0,sizeof(c));
	memset(w,0,sizeof(w));
	for(int i=1;i<=n;++i) {
		if(t[i]==1) ++f[min(a[i],L)];
		else if(R+2>a[i]) {
			++g[min(a[i],R)];
			if(t[i]==3) ++f[min(a[i],L)];
		} else {
			++f[min(L,a[i]-R-1)];
			if(t[i]==3) ++h[min(L,a[i]-R-1)];
			else ++w[min(L,a[i]-R-1)];
		}
	}
	for(int i=1;i<=L;++i) f[i]+=f[i-1]-1;
	for(int i=1;i<=R;++i) g[i]+=g[i-1]-1;
	if(*max_element(g,g+R+1)>0) return false;
	for(int i=1,v=0,p=0,o=0,e=0;i<=L;++i) {
		for(;h[i]--;) q[++p]=i;
		v-=c[i],o+=w[i];
		for(;f[i]>v;++e,++v) {
			if(e>=-g[R]) return false;
			if(o) --o;
			else {
				if(i==L||!p||q[p]<i-R) return false;
				++c[min(L,q[p]+R+1)],--p;
			}
		}
	}
	return true;
}
bool sol(int s) {
	for(int i=1;i<=s;++i) if(i<=2*n&&s-i<=2*n&&chk(i,s-i)) return true;
	return false;
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;++i) cin>>t[i]>>a[i];
	if(count(t+1,t+n+1,2)==n) {
		sort(a+1,a+n+1);
		for(int i=1;i<=n;++i) if(a[i]<i) return cout<<"-1\n",0;
		return cout<<n+2<<"\n",0;
	}
	if(!sol(4*n)) return cout<<"-1",0;
	int z=4*n;
	for(int k=1<<__lg(z);k;k>>=1) if(z>k&&sol(z-k)) z-=k;
	cout<<z+2<<"\n";
	return 0;
}



*Y. [CF2062G] Permutation Factory (7.5)

Problem Link

首先 \(\min(|i-j|,|p_i-p_j|)\) 可以看成手动选择 \(|i-j|\)\(|p_i-p_j|\) 作为代价。

把排列看成 \(n\) 个点 \((i,p_i)\),那么每次可以看成选择 \((i,p_i),(j,p_j)\),然后让 \(i,j\) 分别左右移 \(|i-j|\),或上下移 \(|p_i-p_j|\)

那么我们确定每个点 \((i,p_i)\) 匹配 \((a_i,q_{a_i})\) 后,答案不小于 \(\dfrac 12\sum |i-a_i|+|p_i-q_{a_i}|\),可以证明这个界一定能取到,可以用费用流或 KM 求出带权匹配。

注意到两种操作独立,我们先考虑用 \(|i-j|\) 的代价交换 \(a_i,a_j\),那么求出每个置换环,要求 \(i,j\) 属于同一个环且 \(a_j<i<j<a_i\)

找到环上最大值 \(p\) 及其前驱后继 \(x\to p\to y\),设 \(x<y\),那么找到首个 \(y\)\(\le x\) 的前驱满足条件。

实现时直接枚举每对 \((i,j)\) 操作即可,交换 \(p_i,p_j\) 同理。

时间复杂度 \(\mathcal O(n^4)\)

代码:

#include<bits/stdc++.h>
using namespace std;
namespace F {
const int MAXV=205,MAXE=3e5+5,inf=1e9;
struct Edge { int v,e,f,w; } G[MAXE];
int S,T,ec=1,hd[MAXV],dis[MAXV],pre[MAXV];
bool inq[MAXV];
void init() { ec=1,memset(hd,0,sizeof(hd)); }
void adde(int u,int v,int f,int w) { G[++ec]={v,hd[u],f,w},hd[u]=ec; }
void link(int u,int v,int f,int w) { adde(u,v,f,w),adde(v,u,0,-w); }
bool SPFA() {
	memset(dis,0x3f,sizeof(dis));
	memset(pre,0,sizeof(pre));
	memset(inq,false,sizeof(inq));
	queue <int> Q; Q.push(S),inq[S]=true,dis[S]=0;
	while(Q.size()) {
		int u=Q.front(); Q.pop(),inq[u]=false;
		for(int i=hd[u],v;i;i=G[i].e) if(G[i].f&&dis[v=G[i].v]>dis[u]+G[i].w) {
			dis[v]=dis[u]+G[i].w,pre[v]=i;
			if(!inq[v]) Q.push(v),inq[v]=true;
		}
	}
	return pre[T];
}
array<int,2> ssp() {
	int f=0,c=0;
	while(SPFA()) {
		int g=inf;
		for(int u=T;u!=S;u=G[pre[u]^1].v) g=min(g,G[pre[u]].f);
		f+=g,c+=g*dis[T];
		for(int u=T;u!=S;u=G[pre[u]^1].v) G[pre[u]].f-=g,G[pre[u]^1].f+=g;
	}
	return {f,c};
}
}
int n,a[105],b[105],c[105],h[105],p[105],e[105][105];
void solve() {
	cin>>n,F::init();
	int s=F::S=2*n+1,t=F::T=s+1;
	for(int i=1;i<=n;++i) cin>>a[i],F::link(s,i,1,0),h[a[i]]=i;
	for(int i=1;i<=n;++i) cin>>b[i],F::link(i+n,t,1,0);
	for(int i=1;i<=n;++i) for(int j=1;j<=n;++j) F::link(i,j+n,1,abs(i-j)+abs(a[i]-b[j])),e[i][j]=F::ec;
	F::ssp()[1];
	for(int i=1;i<=n;++i) for(int j=1;j<=n;++j) if(F::G[e[i][j]].f) p[i]=j;
	vector <array<int,2>> wys;
	auto opr=[&](int x,int y) {
		swap(a[x],a[y]),swap(h[a[x]],h[a[y]]),swap(p[x],p[y]),wys.push_back({x,y});
	};
	for(int q=1;q<=n;++q) {
		memset(c,0,sizeof(c));
		for(int i=1,k=0,u;i<=n;++i) if(!c[i]) for(++k,u=i;!c[u];u=p[u]) c[u]=k;
		for(int i=1;i<=n;++i) for(int j=i+1;j<=n;++j) {
			if(c[i]==c[j]&&p[j]<=i&&j<=p[i]) { opr(i,j); goto n1; }
		} n1:;
	}
	for(int q=1;q<=n;++q) {
		memset(c,0,sizeof(c));
		for(int i=1;i<=n;++i) p[i]=b[h[i]];
		for(int i=1,k=0,u;i<=n;++i) if(!c[i]) for(++k,u=i;!c[u];u=p[u]) c[u]=k;
		for(int i=1;i<=n;++i) for(int j=i+1;j<=n;++j) {
			if(i!=j&&c[i]==c[j]&&p[j]<=i&&j<=p[i])  { opr(h[i],h[j]); goto n2; }
		} n2:;
	}
	cout<<wys.size()<<"\n";
	for(auto z:wys) cout<<z[0]<<" "<<z[1]<<"\n";
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



*Z. Longest Good Subsequence (8)

Problem Link

首先 \(i\to b_i-1\) 建树,则 \(i\to 0\) 的链描述了 \(p[1,i]\) 的单调栈。

只要所有的线段 \([b_i,i]\) 都满足相交或包含,则可以从建出的树中还原 \(p\)

考虑朴素的区间 dp,我们要记录 \([l,r]\),以及 \([1,l)\) 中选了几个数。

观察一些性质,对于每个左端点 \(l\) 考虑,如果存在 \(b_i=l\),则必定要求 \(b_l=l\)

使用该性质,则在区间 \([l,r]\) 时,\([1,l)\) 中必定选了 \(a_{l}-1\) 个数。

考虑如何转移,首先不加入 \(a_r\)\(f_{l,r}\gets f_{l,r-1}\)

  • 如果 \(a_r=a_l\),则可以直接加入。
  • 如果 \(a_r\) 是首次加入,那么 \(f_{l,r}\gets a_r\),要求 \(f_{l,r-1}\ge a_r-1\),很显然合法序列的前缀也合法,所以可以转移。
  • 否则枚举 \(a_r\) 首次加入的位置 \(k\),要求 \(l<k<r,a_k=a_r\)\(f_{l,k-1}\ge a_k-1\),转移 \(f_{l,r}\gets f_{k,r}\),很显然多个 \(k\) 中只要求最小的一个。

因此可以 \(\mathcal O(1)\) 转移。

时间复杂度 \(\mathcal O(n^2)\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1.5e4+5;
short n,s,a[MAXN],f[MAXN][MAXN],p[MAXN];
void solve() {
	cin>>n,s=0;
	for(int i=1;i<=n;++i) cin>>a[i];
	for(int l=n;l>=1;--l) {
		memset(p,0,sizeof(p));
		short z=f[l][l]=a[l];
		for(int r=l+1;r<=n;f[l][r++]=z) {
			if(a[l]==a[r]) ++z;
			else if(a[r]>a[l]) {
				if(!p[a[r]]&&f[l][r-1]>=a[r]-1) p[a[r]]=r;
				if(p[a[r]]) z=max(z,f[p[a[r]]][r]);
			}
		}
		if(a[l]==1) s=max(s,z);
	}
	cout<<s<<"\n";
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}
posted @ 2025-11-16 10:14  DaiRuiChen007  阅读(42)  评论(0)    收藏  举报