2026 NOI 做题记录(二十二)



Contest Link

\(\text{By DaiRuiChen007}\)



A. [P15714] Distance Permutation (4)

Problem Link

限制是 \(\min p_{1\sim i-1} -k\le p_i\le \max p_{1\sim i-1}+k\),直接做难以计数,不过我们发现分讨 \(p_i,p_1\) 之间的大小关系后有一侧的限制会自然满足。

所以只要考虑 \(p_1=1\) 以及 \(p_1=n\) 的两个子问题,设长度为 \(m+1\) 的序列答案为 \(f_m\),则答案为 \(\sum_{k=l}^r f_{k-1}f_{n-k}\times \binom{n-1}{k-1}\),对 \(k\in[l,r]\) 分块,整块用 NTT 计算,散块暴力。

\(f\) 的计算只要枚举 \(p_2\) 的值,\(f_m=\sum _{i=1}^k (i-1)!f_{m-i}\times \binom{m-1}{i-1}\),可以分治 NTT 计算。

时间复杂度 \(\mathcal O(n\sqrt{q\log n})\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int MOD=998244353,N=1<<18;
int fac[N],ifac[N];
namespace P {
int rev[N],inv[N],w[N<<1];
int ksm(int a,int b=MOD-2) { int s=1; for(;b;a=1ll*a*a%MOD,b=b>>1) if(b&1) s=1ll*s*a%MOD; return s; }
void poly_init() {
	inv[1]=1;
	for(int i=2;i<N;++i) inv[i]=1ll*(MOD-MOD/i)*inv[MOD%i]%MOD;
	fac[0]=ifac[0]=1;
	for(int i=1;i<N;++i) fac[i]=1ll*fac[i-1]*i%MOD,ifac[i]=1ll*ifac[i-1]*inv[i]%MOD;
	for(int k=1;k<=N;k<<=1) {
		int x=ksm(3,(MOD-1)/k); w[k]=1;
		for(int i=1;i<k;++i) w[i+k]=1ll*x*w[i+k-1]%MOD;
	}
}
int plen(int x) { int y=1; for(;y<x;y<<=1); return y;  }
void ntt(int *f,bool idft,int n) {
	for(int i=0;i<n;++i) {
		rev[i]=(rev[i>>1]>>1);
		if(i&1) rev[i]|=n>>1;
	}
	for(int i=0;i<n;++i) if(rev[i]<i) swap(f[i],f[rev[i]]);
	for(int k=2,x,y;k<=n;k<<=1) {
		for(int i=0;i<n;i+=k) {
			for(int j=i;j<i+k/2;++j) {
				x=f[j],y=1ll*f[j+k/2]*w[k+j-i]%MOD;
				f[j]=(x+y>=MOD)?x+y-MOD:x+y,f[j+k/2]=(x>=y)?x-y:x+MOD-y;
			}
		}
	}
	if(idft) {
		reverse(f+1,f+n);
		for(int i=0,x=ksm(n);i<n;++i) f[i]=1ll*f[i]*x%MOD;
	}
}
void poly_mul(const int *f,const int *g,int *h,int n,int m) {
	static int a[N],b[N];
	for(int i=0;i<n;++i) a[i]=f[i];
	for(int i=0;i<m;++i) b[i]=g[i];
	int len=plen(n+m-1);
	ntt(a,0,len),ntt(b,0,len);
	for(int i=0;i<len;++i) h[i]=1ll*a[i]*b[i]%MOD;
	ntt(h,1,len);
	memset(a,0,sizeof(int)*len);
	memset(b,0,sizeof(int)*len);
}
}
const int n=1e5,B=1000;
int m,K,L[N],R[N],S[N],Z[N],f[N],a[N],b[N],c[N];
void cdq(int l,int r) {
	if(l==r) return ;
	int mid=(l+r)>>1;
	cdq(l,mid);
	for(int i=l;i<=mid;++i) a[i-l]=1ll*f[i]*ifac[i]%MOD;
	for(int i=1;i<=r-l;++i) b[i-1]=i<=K;
	P::poly_mul(a,b,c,mid-l+1,r-l);
	for(int i=mid+1;i<=r;++i) f[i]=(f[i]+1ll*fac[i-1]*c[i-l-1])%MOD;
	cdq(mid+1,r);
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>K>>m,P::poly_init(),f[0]=1;
	cdq(0,n-1);
	for(int i=0;i<n;++i) f[i]=1ll*f[i]*ifac[i]%MOD;
	for(int i=1;i<=m;++i) {
		cin>>S[i]>>L[i]>>R[i],--L[i],--R[i],--S[i];
		if(L[i]/B==R[i]/B) {
			for(int j=L[i];j<=R[i];++j) Z[i]=(Z[i]+1ll*f[j]*f[S[i]-j])%MOD;
		} else {
			for(int j=L[i]/B*B+B-1;j>=L[i];--j) Z[i]=(Z[i]+1ll*f[j]*f[S[i]-j])%MOD;
			for(int j=R[i]/B*B;j<=R[i];++j) Z[i]=(Z[i]+1ll*f[j]*f[S[i]-j])%MOD;
		}
		L[i]=L[i]/B+1,R[i]=R[i]/B-1;
	}
	for(int i=0;i<n;++i) a[i]=f[i];
	P::ntt(a,0,N/2);
	for(int k=0;k<n/B;++k) {
		memset(b,0,sizeof(b));
		for(int i=0;i<B;++i) b[i]=f[i+k*B];
		P::ntt(b,0,N/2);
		for(int i=0;i<N/2;++i) c[i]=1ll*a[i]*b[i]%MOD;
		P::ntt(c,1,N/2);
		for(int i=1;i<=m;++i) if(L[i]<=k&&k<=R[i]) Z[i]=(Z[i]+c[S[i]-k*B])%MOD;
	}
	for(int i=1;i<=m;++i) cout<<1ll*Z[i]*fac[S[i]]%MOD<<"\n";
	return 0;
}



B. [P14808] 子序列计数 (3)

Problem Link

经典题,子序列计数可以用一个 \((m+1)\times (m+1)\) 的矩阵描述 \(s'\) 上一个区间的信息,要做的就是快速描述 \(ik\bmod L\) 的结构。

考虑每次 \(i\to (i+1)k\) 跨过 \(L-1\to 0\) 的时刻,也就是从 \(0\sim k-1\) 分别出发,走若干步直到穿过 \(L-1\to 0\),处理出从这 \(k\) 个点出发的路径,能递归成一个 \((-L\bmod k,k)\) 的子问题,而翻转序列能变成 \((L,-k \bmod L)\) 的子问题,所以每次递归后 \(2k\le L\),从而只要计算 \(\log L\) 次。

每次递归的时候按 \(0\sim k-1\) 扫描线,分别计算每个区间中的点经过了几次,线段树维护矩阵顺序乘积,这个值只会变化 \(\mathcal O(1)\) 次,而区间数始终是 \(\mathcal O(n)\) 的。

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

代码:

#include<bits/stdc++.h>
using namespace std;
const int MOD=998244353;
int m;
struct Mat {
	unsigned long long f[11][11];
	Mat() { memset(f,0,sizeof(f)); }
	friend Mat operator*(const Mat&u,const Mat&v) {
		Mat w;
		for(int i=0;i<=m;++i) for(int k=i;k<=m;++k) if(u.f[i][k]) for(int j=k;j<=m;++j) {
			if(v.f[k][j]) w.f[i][j]+=u.f[i][k]*v.f[k][j];
		}
		for(int i=0;i<=m;++i) for(int j=i;j<=m;++j) w.f[i][j]%=MOD;
		return w;
	}
}	I;
Mat ksm(Mat a,int b) { Mat s=I; for(;b;a=a*a,b>>=1) if(b&1) s=s*a; return s; }
struct Segt {
	int N; Mat f[1<<18|5];
	void init(int d) { N=2<<__lg(d),fill(f,f+(N<<1),I); }
	void upd(int x,Mat&o) { for(f[x+=N]=o,x>>=1;x;x>>=1) f[x]=f[x<<1]*f[x<<1|1]; }
}	T;
struct info { int l,r; Mat w; };
bool operator<(const info&a,const info&b) { return a.l<b.l; }
void solve(int n,int k,vector<info>&a) {
	if(n<=1000) {
		vector<int>b(n);
		for(int i=0;i<(int)a.size();++i) for(int j=a[i].l;j<=a[i].r;++j) b[j]=i;
		Mat o=I;
		for(int i=0;i<n;++i) o=o*a[b[1ll*i*k%n]].w;
		return cout<<o.f[0][m]<<"\n",void();
	}
	if(2*k>n) {
		if(a[0].r>0) a[0].l=1,a.insert(a.begin(),{0,0,a[0].w});
		reverse(a.begin()+1,a.end()),k=n-k;
		for(auto i=1;i<(int)a.size();++i) swap(a[i].l,a[i].r),a[i].l=n-a[i].l,a[i].r=n-a[i].r;
	}
	T.init(a.size());
	vector<info>c,b;
	for(int i=0;i<(int)a.size();++i) {
		int l=a[i].l%k,r=a[i].r%k;
		Mat x=ksm(a[i].w,(a[i].r-a[i].l)/k),y=x*a[i].w;
		if(l<=r) T.upd(i,x),c.push_back({l,i,y}),c.push_back({r+1,i,x});
		else if(l==r+1) T.upd(i,y);
		else T.upd(i,y),c.push_back({r+1,i,x}),c.push_back({l,i,y});
	}
	sort(c.begin(),c.end()),c.push_back({k,-1,I});
	if(0<c[0].l) b.push_back({0,c[0].l-1,T.f[1]});
	for(int i=0;c[i].l<k;++i) {
		T.upd(c[i].r,c[i].w);
		if(c[i].l<c[i+1].l) b.push_back({c[i].l,c[i+1].l-1,T.f[1]});
	}
	solve(k,k-n%k,b);
}
int str[15],q,n,k;
void exgcd(int a,int b,int&x,int&y) {
	if(!b) return x=1,y=0,void();
	return exgcd(b,a%b,y,x),y-=a/b*x,void();
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>q>>m>>k>>n;
	int u,v;
	exgcd(n,k,u,v);
	for(int i=1;i<=m;++i) cin>>str[i];
	for(int i=0;i<=m;++i) I.f[i][i]=1;
	vector<info>a;
	for(int i=1,s=0,d,x;i<=q;++i) {
		cin>>d>>x; Mat X=I;
		for(int j=1;j<=m;++j) if(str[j]==x) X.f[j-1][j]=1;
		a.push_back({s,s+d-1,X}),s+=d;
	}
	solve(n,(v%n+n)%n,a);
	return 0;
}



C. [P14816] Ferris Wheel (3.5)

Problem Link

Polya 定理一下,枚举 \(k\mid 2n\),如果 \(2\mid k\) 相当于计数可以删空的序列数,否则相当于计数删除后变成回文串的序列数。

考虑每个点的颜色只和前面的序列是否能删空有关,所以 \(k\) 为偶数时就是要计算多少长度为 \(k\)\(\pm 1\) 折线经过 \(y=0\) 恰好 \(t\) 次,每次经过 \(y=0\) 的时候加入一个 \(+1\) 即可构造双射,\(k\) 为奇数的情况类似,都可以化简成组合数 \(\mathcal O(k)\) 计算。

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

代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=6e6+5,MOD=998244353;
int n,m;
ll fc[MAXN],ic[MAXN],iv[MAXN],ct[MAXN],f[MAXN],p0[MAXN],p1[MAXN];
ll C(int x,int y) { return y<0||y>x?0:fc[x]*ic[y]%MOD*ic[x-y]%MOD; }
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m,iv[1]=1;
	for(int i=2;i<MAXN;++i) iv[i]=iv[MOD%i]*(MOD-MOD/i)%MOD;
	for(int i=ic[0]=fc[0]=1;i<MAXN;++i) fc[i]=fc[i-1]*i%MOD,ic[i]=ic[i-1]*iv[i]%MOD;
	for(int i=p0[0]=p1[0]=1;i<MAXN;++i) p0[i]=p0[i-1]*m%MOD,p1[i]=p1[i-1]*(m-1)%MOD;
	for(int i=0;i<=n;++i) ct[i]=fc[2*i]*ic[i]%MOD*ic[i+1]%MOD;
	f[0]=1,f[1]=m;
	for(int i=1;i<=n;++i) {
		if(n%i==0) {
			for(int j=1;j<=i;++j) f[2*i]=(f[2*i]+p0[j]*p1[i-j]%MOD*(C(2*i-j-1,i-1)-C(2*i-j-1,i)+MOD))%MOD;
		}
		if((2*n)%(2*i+1)==0) {
			for(int k=0;k<=i;++k) f[2*i+1]=(f[2*i+1]+p0[k+1]*p1[i-k]%MOD*C(2*i-k,i))%MOD;
		}
	}
	ll z=0;
	for(int i=1;i<=2*n;++i) z=(z+f[__gcd(2*n,i)])%MOD;
	cout<<z*iv[2*n]%MOD<<"\n";
	return 0;
}



*D. [P15711] Tea time in the grand garden (7)

Problem Link

直接把 \(a_i\) 写成折线时无法无法处理序列长度的问题,考虑把 \(a\) 差分值 \(>0\)\(<0\) 的部分 \(a^+,a^-\) 分开。

那么这两部分都可以看成从 \((0,0)\to (n,m)\) 的折线,每步向右或向上,高度表示当前的前缀和。

要求是每一列 \(a^-\) 的高度不超过 \(a^+\) 的高度,以及两个折线不在同一列同时向上。

对于限制二可以容斥,钦定它们有 \(k\) 次同时向上,那么把这些操作同时删掉,设 \(f_m,g_m\) 表示可以同时向上 / 不能同时向上的方案数,则 \(f_m=\sum _{k\le m}g_{m-k}\binom{n+k}{k}\).

手动反演一下,\(F=G\times \dfrac{1}{(1-x)^{n+1}}\),则 \(G=F(1-x)^{n+1}\),即 \(g_m=\sum _{k\le m}f_{m-k}(-1)^k\binom{n+1}k\)

然后要算 \(g_0\sim g_m\),计算 \(a^+\) 包含 \(a^-\) 的折线,可以把 \(a^-\) 向右下方平移一格,限制变成两条直线不交,直接 LGV 计算。

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

代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=4e5+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 n,m,fc[MAXN],ic[MAXN];
ll C(int x,int y) { return y<0||y>x?0:fc[x]*ic[y]%MOD*ic[x-y]%MOD; }
ll f(int k) {
	return (C(n+k,n)*C(n+k,n)+(MOD-C(n+k,n-1))*C(n+k,n+1))%MOD;
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	for(int i=fc[0]=1;i<MAXN;++i) fc[i]=fc[i-1]*i%MOD;
	ic[MAXN-1]=ksm(fc[MAXN-1]);
	for(int i=MAXN-1;i;--i) ic[i-1]=ic[i]*i%MOD;
	cin>>n>>m;
	ll z=0;
	for(int k=0;k<=m;++k) z=(z+(k&1?MOD-C(n+1,k):C(n+1,k))*f(m-k))%MOD;
	cout<<z<<"\n";
	return 0;
}



E. [P15762] Inside Yamanote (3.5)

Problem Link

限制是 \(d_0=0,|d_{i}-d_{i+1}|\le L_i\),注意到我们只关心 \(d\) 之间的差,所以不用限制 \(d_0\)

写成线性规划的形式并对偶,变成 \((x_i-y_i)-(x_{i-1}-y_{i-1})\ge c_i,\min \sum L_i(x_i+y_i)\),其中 \(c_i\)\(d_i\) 对答案的贡献系数。

发现 \(x_i-y_i=k\)\(x_i+y_i\) 的最小值一定是 \(|k|\),所以只要考虑 \(p_i\ge p_{i-1}+c_i\) 的限制最小化 \(\sum L_i|p_i|\)

线性规划经典结论是可以通过调整法证明被取等的限制构成环,在本题上就是 \(p_i=p_0+\sum _{j\le i} c_j\),那么 \(p_0\) 只要取中位数,可以简单树状数组维护。

时间复杂度 \(\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,t[MAXN],w[MAXN],p[MAXN],o[MAXN];
ll h[MAXN];
struct BIT {
	ll tr[MAXN];
	void add(int x,ll v) { for(;x<=n;x+=x&-x) tr[x]+=v; }
	ll qs(int x) { ll s=0; for(;x;x&=x-1) s+=tr[x]; return s; }
	ll fd(ll s) {
		int x=0;
		for(int k=1<<17;k;k>>=1) if(x+k<=n&&tr[x+k]<=s) s-=tr[x+=k];
		return x+1;
	}
}	S,V;
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m>>q;
	for(int i=0;i<n;++i) cin>>w[i],p[i+1]=i;
	for(int i=0,x,y;i<m;++i) cin>>x>>y,++t[x],--t[y];
	for(int i=1;i<n;++i) h[i]=h[i-1]+t[i];
	sort(p+1,p+n+1,[&](int x,int y){ return h[x]<h[y]; });
	for(int i=1;i<=n;++i) o[p[i]]=i,S.add(i,w[p[i]]),V.add(i,w[p[i]]*h[p[i]]);
	for(int u,v,x;q--;) {
		cin>>u>>v,v-=w[u],w[u]+=v,S.add(o[u],v),V.add(o[u],v*h[u]);
		x=S.fd(S.qs(n)/2),cout<<V.qs(n)-2*V.qs(x)+h[p[x]]*(2*S.qs(x)-S.qs(n))<<"\n";
	}
	return 0;
}



F. [P15496] Minus Operator (4)

Problem Link

增量维护树结构,每次加入一个点的时候要在右链上二分。

对于右链上的每个点,我们可以让其左儿子 \(=1\),只要把最左侧的叶子填 \(1\),其他填 \(0\) iu行。

这样我们把新叶子填 \(1\) 就能知道其深度奇偶性 \(c\),判定的位置是否为 \(\le k\) 的时候只要把链上前 \(k-1\) 个点左儿子填 \(1\) 就行。

注意到每次右链长度最多 \(+1\),所以只要每次判断叶子是否挂在链底,如果不合法就弹出,这个复杂度是均摊线性的。

具体分析操作次数就是 \(2.5n\)\(n\) 次询问求奇偶性,\(0.5n\) 次询问判定链底不合法,右链长度 \(-2\)\(n\) 次询问得到正确深度。

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

代码:

#include<bits/stdc++.h>
using namespace std;
int n,fa[405],ls[405],rs[405],o;
vector <int> G[405];
void dfs(int u) {
	if(u<=n) return cout<<"x",void();
	cout<<"(",dfs(ls[u]),cout<<"-",dfs(rs[u]),cout<<")";
}
signed main() {
	cin>>n,rs[0]=1;
	for(int x=2;x<=n;++x) {
		vector <array<int,2>> d;
		for(int u=x-1,v;u;u=fa[u]) {
			for(v=u;v>n;v=ls[v]);
			d.push_back({u,v});
		}
		reverse(d.begin(),d.end());
		string s(n+1,'0'); s[0]=' ',s[x]='1';
		for(auto e:d) s[e[1]]='1';
		cout<<"query"<<s<<endl,cin>>o;
		if(o==(int)d.size()%2) s[d.back()[1]]='0',d.pop_back();
		for(;d.size()>2;s[d.back()[1]]='0',d.pop_back(),d.pop_back()) {
			s[(*(d.end()-2))[1]]='0',cout<<"query"<<s<<endl,cin>>o;
			if(o==(int)d.size()%2) break;
		}
		int u=d.back()[0],v=fa[u];
		rs[v]=n+x,ls[n+x]=u,rs[n+x]=x,fa[n+x]=v,fa[u]=fa[x]=n+x;
	}
	cout<<"answer ",dfs(rs[0]),cout<<endl;
	return 0;
}



*G. [P15747] Ball Passing (7)

Problem Link

首先手玩一下交换 \(a_{x,i},a_{y,j}\) 旋转第 \(i\) 列,当 \(a_{x,i}\) 落到第 \(y\) 行时交换一下就行。

进一步这个操作可以一口气交换若干对所属行两两不同的元素,那么我们重排每一行使得每一列都是排列,这是经典二分图染色问题,然后每列的置换可以用两轮操作解决,操作次数 \(2n(m-1)\) 因为第一列不用动。

考虑优化,发现如果每次操作都是在同一列 \(x\) 内原地操作,如果存在一个未还原的列 \(t\),我们可以借用这个列还原,我们直接旋转 \(t\),每个元素到位置之后交换到 \(x\) 上,那么这样会把 \(t\) 列进一步打乱,\(x\) 列还原,这显然是优秀的。

那么对于第 \(2\sim m-1\) 列用该操作复原,次数 \(2n+n(m-2)=nm\),满足要求。

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

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,a[105][105],g[205][105],p[105];
bool ok[105];
void link(int u,int v) {
	int x=1,y=1;
	while(g[u][x]) ++x;
	while(g[v][y]) ++y;
	g[u][x]=v,g[v][y]=u;
	if(x^y) for(int w=v,z=y;w;w=g[w][z],z^=x^y) swap(g[w][x],g[w][y]);
}
vector<int>op[105];
void slv(vector<array<int,2>>&C) {
	for(int i=0;i<n;++i) op[i].clear();
	for(auto o:C) {
		swap(a[1][o[0]],a[m][o[0]]);
		op[(o[1]+n-o[0])%n].push_back(o[1]);
	}
	for(int t=1;t<=n;++t) {
		for(int i=0;i<n;++i) cout<<a[1][i]<<" \n"[i==n-1];
		rotate(a[1],a[1]+n-1,a[1]+n);
		for(int x:op[t]) swap(a[1][x],a[m][x]);
	}
	for(auto o:C) swap(a[1][o[0]],a[m][o[0]]);
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1,x;i<=n;++i) for(int j=1;j<=m;++j) cin>>x,link(i,x+n);
	for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) a[j][i-1]=g[i][j]-n;
	cout<<m*n<<"\n";
	for(int t=2;t<m;++t) {
		memset(ok,0,sizeof(ok));
		for(int d=0;d<n;++d) {
			for(int i=0;i<n;++i) cout<<a[t][i]<<" \n"[i==n-1];
			rotate(a[t],a[t]+n-1,a[t]+n),rotate(ok,ok+n-1,ok+n);
			for(int i=0;i<n;++i) if(!ok[i]&&a[t][i]==a[1][i]) ok[i]=1,swap(a[t][i],a[m][i]);
		}
		swap(a[m],a[t]);
	}
	for(int i=0;i<n;++i) p[i]=g[a[m][i]+n][1]-1,ok[i]=0;
	vector<array<int,2>>A,B;
	for(int i=0;i<n;++i) if(!ok[i]) {
		vector <int> c;
		for(int j=i;!ok[j];ok[j]=1,j=p[j]) c.push_back(j);
		for(int l=1,r=c.size()-1;l<r;++l,--r) swap(p[c[l]],p[c[r]]),A.push_back({c[l],c[r]});
	}
	for(int i=0;i<n;++i) if(p[i]!=i) B.push_back({i,p[i]}),swap(p[i],p[p[i]]);
	slv(A),slv(B);
	return 0;
}



H. [P14784] Cacti Classification (4.5)

Problem Link

询问就是求 \(S\) 中有没有两个同色元素。

首先询问次数限制不允许我们关心环的具体结构。

考虑记 \(d_i\) 表示 \(i\) 前面有多少同色元素,那么计算 \(d_i\) 时只要找最小的 \(v\) 使得 \(S_v=\{j\mid j<i,d_j=v\}\) 满足 \(S_v\cup \{i\}\) 不同色,因为一个当前有 \(k\) 个元素的环,会在 \(S_1\sim S_k\) 的集合中分别恰好出现一次,每次只要向 \(S_{d_i}\) 中插入一个元素即可。

那么二分一下可以用 \(4m\) 次计算 \(e_i=\min(d_i,15)\),倒过来计算一轮就能做到 \(8m\) 次,不过我们没法处理桥,需要额外询问那么代价变成 \(9m\)

考虑优化,发现对于一个桥,一定有 \(e_i=15\),考虑在倒过来的过程中,分别处理 \(e_i=15\)\(e_i<15\) 的点。

对于 \(e_i=15\) 的点,其答案 \(\in\{0,-1\}\),只要一次询问其是不是桥即可,但是如果我们把这些点删掉,那么对于一个大小 \(>14\) 的环,其中 \(e_i=14\) 的点后面所有同色点 \(e_i=15\),都被删除了,那么他就会错误地认为自己后缀上的 \(d'_i=1\),从而计算错误。

为了解决这个问题,我们只要对每个环中首个 \(e_i=15\) 的点插进 \(S_1\) 中即可,那么这些点依然只用两次询问就能解决,总次数不超过 \(8m\)

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

代码:

#include<bits/stdc++.h>
using namespace std;
int n,q,o,a[20],f[10005];
int ask(int x,int y) { return ++q,cout<<"? "<<x<<" "<<y<<endl,cin>>o,o; }
void solve() {
	cin>>n,q=0,memset(a,0,sizeof(a));
	for(int i=1;i<=n;++i) {
		int c=0,&z=f[i]=15;
		for(int k=8;k;k>>=1) if(z>k&&ask(a[z-k],i)) z-=k,c=q;
		a[z]=c;
	}
	memset(a,0,sizeof(a));
	for(int i=n;i>=1;--i) {
		if(f[i]==15) {
			if(!ask(0,i)) f[i]=-1;
			else if(ask(a[1],i)) a[1]=q;
		} else {
			int c=0,z=15;
			for(int k=8;k;k>>=1) if(z>k&&ask(a[z-k],i)) z-=k,c=q;
			a[z]=c,f[i]+=z-1;
		}
	}
	cout<<"! ";
	for(int i=1;i<=n;++i) cout<<(f[i]>14?0:f[i])<<" ";
	cout<<endl,cin>>o;
}
signed main() {
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



I. [P15750] Coins on a Tree (2.5)

Problem Link

显然的 Exchange Argument on Tree 问题,只要快速维护若干字符串的拼接和字典序比较即可。

启发式合并的时候维护一下倍增即可。

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

代码:

#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int MAXN=2e5+5;
const ull X=568568568568568571ll;
char s[MAXN];
int n,fa[MAXN],sz[MAXN],tl[MAXN],L[18][MAXN],R[18][MAXN],dsu[MAXN];
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
ull pw[18],hs[18][MAXN];
struct cmp {
	bool operator()(int x,int y) const {
		int u=x,v=y,k=0,l=sz[x],r=sz[y];
		while(1) {
			for(int i=17;~i;--i) if(k+(1<<i)<=min(l,r)&&hs[i][u]==hs[i][v]) k+=1<<i,u=R[i][u],v=R[i][v];
			if(k==sz[x]+sz[y]) return x<y;
			if(k==l) u=y,l+=sz[y];
			if(k==r) v=x,r+=sz[x];
			if(hs[0][u]!=hs[0][v]) return hs[0][u]<hs[0][v];
		}
	}
};
set <int,cmp> S;
void ul(int x,int y) {
	for(int u=tl[x],v=y,i=sz[y]+1;u;v=u,u=L[0][u],++i) {
		R[0][u]=v,L[0][v]=u;
		for(int k=1;(1<<k)<=i;++k) {
			R[k][u]=R[k-1][R[k-1][u]];
			hs[k][u]=hs[k-1][u]*pw[k-1]+hs[k-1][R[k-1][u]];
			if(R[k][u]) L[k][R[k][u]]=u;
		}
	}
	tl[x]=tl[y],sz[x]+=sz[y];
}
void ur(int x,int y) {
	for(int u=y,v=tl[x],i=sz[x]+1;u;v=u,u=R[0][u],++i) {
		L[0][u]=v,R[0][v]=u;
		for(int k=1,o;(1<<k)<=i;++k) {
			L[k][u]=L[k-1][L[k-1][u]],o=x;
			if(L[k][u]) R[k][L[k][u]]=u,o=R[0][L[k][u]];
			hs[k][o]=hs[k-1][o]*pw[k-1]+hs[k-1][R[k-1][o]];
		}
	}
	tl[x]=tl[y],sz[x]+=sz[y];
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n,pw[0]=X;
	for(int i=1;i<18;++i) pw[i]=pw[i-1]*pw[i-1];
	for(int i=2;i<=n;++i) cin>>fa[i];
	for(int i=1;i<=n;++i) {
		cin>>s[i],sz[i]=1,tl[i]=i,hs[0][i]=s[i],dsu[i]=i;
		if(i>1) S.insert(i);
	}
	for(int u,x;S.size();) {
		u=*S.begin(),x=dsu[u]=find(fa[u]),S.erase(u);
		if(x>1) S.erase(x);
		sz[x]<sz[u]?ul(x,u):ur(x,u);
		if(x>1) S.insert(x);
	}
	for(int u=1;u;u=R[0][u]) cout<<s[u];
	cout<<"\n";
	return 0;
}



J. [P14867] Colorful Rectangle (2.5)

Problem Link

分讨一下颜色之间的关系,要么 \(x,y\) 同时递增,要么一个在右上角,另外两个分别在左下边界上,对 \(y\) 扫描线后容易用 \(x\) 轴上的线段树维护矩阵信息处理。

枚举所有顺序和方向分别处理。

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

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5,MAXS=1<<18|5,inf=1e9;
int n,b[MAXN],m,Z=inf;
struct info { int x,y,c; } a[MAXN];
struct BIT1 {
	int tr[MAXN],s;
	void init() { fill(tr,tr+n+1,inf); }
	void upd(int x,int v) { for(;x<=n;x+=x&-x) tr[x]=min(tr[x],v); }
	int qry(int x) { for(s=inf;x;x&=x-1) s=min(s,tr[x]); return s; }
}	X,Y;
int mn[MAXS],z[MAXS],tg[MAXS];
void adt(int p,int k) { z[p]=min(z[p],mn[p]+k),tg[p]=min(tg[p],k); }
void psd(int p) { if(tg[p]!=inf) adt(p<<1,tg[p]),adt(p<<1|1,tg[p]),tg[p]=inf; }
void psu(int p) { mn[p]=min(mn[p<<1],mn[p<<1|1]),z[p]=min(z[p<<1],z[p<<1|1]); }
void init(int l=1,int r=m,int p=1) {
	mn[p]=z[p]=tg[p]=inf;
	if(l==r) return ;
	int mid=(l+r)>>1;
	init(l,mid,p<<1),init(mid+1,r,p<<1|1);
}
void ins(int x,int c,int l=1,int r=m,int p=1) {
	if(l==r) return mn[p]=min(mn[p],c),void();
	int mid=(l+r)>>1; psd(p);
	x<=mid?ins(x,c,l,mid,p<<1):ins(x,c,mid+1,r,p<<1|1);
	psu(p);
}
void upd(int ul,int ur,int w,int l=1,int r=m,int p=1) {
	if(ul<=l&&r<=ur) return adt(p,w);
	int mid=(l+r)>>1; psd(p);
	if(ul<=mid) upd(ul,ur,w,l,mid,p<<1);
	if(mid<ur) upd(ul,ur,w,mid+1,r,p<<1|1);
	psu(p);
}
int qry(int ul,int ur,int l=1,int r=m,int p=1) {
	if(ul<=l&&r<=ur) return z[p];
	int mid=(l+r)>>1,s=inf; psd(p);
	if(ul<=mid) s=min(s,qry(ul,ur,l,mid,p<<1));
	if(mid<ur) s=min(s,qry(ul,ur,mid+1,r,p<<1|1));
	return s;
}
void slv(int A,int B) {
	X.init(),Y.init(),init();
	for(int i=1;i<=n;++i) {
		if(a[i].c==A) {
			X.upd(a[i].y,-a[i].x-b[a[i].y]);
			ins(a[i].y,-a[i].x);
		} else if(a[i].c==B) {
			Y.upd(a[i].y,X.qry(a[i].y));
			upd(a[i].y,m,-b[a[i].y]);
		} else {
			Z=min(Z,a[i].x+b[a[i].y]+Y.qry(a[i].y));
			Z=min(Z,a[i].x+b[a[i].y]+qry(1,a[i].y));
		}
	}
}
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].x>>a[i].y>>a[i].c,b[i]=a[i].y;
	sort(b+1,b+n+1),m=unique(b+1,b+n+1)-b-1;
	sort(a+1,a+n+1,[&](auto i,auto j){ return i.x<j.x; });
	for(int i=1;i<=n;++i) a[i].y=lower_bound(b+1,b+m+1,a[i].y)-b;
	for(int x:{0,1}) {
		for(int y:{0,1}) {
			slv(0,1),slv(0,2),slv(1,0),slv(1,2),slv(2,0),slv(2,1);
			for(int i=1;i<=n;++i) a[i].y=m+1-a[i].y;
			for(int i=1;i<=m;++i) b[i]*=-1;
			reverse(b+1,b+m+1);
		}
		for(int i=1;i<=n;++i) a[i].x*=-1;
		reverse(a+1,a+n+1);
	}
	cout<<Z*2<<"\n";
	return 0;
}



*K. [P14863] Distributing the Treasure (8)

Problem Link

对所有物品增量构造,定义 \(w_i(S_j)\) 表示第 \(j\) 个人当前的集合在 \(i\) 眼中的估值。

如果物品 \(m\) 给了第 \(k\) 个人,要求任意 \(u\) 都有 \(w_u(S_u)\ge w_u(S_k)\)

\(w_i(S_i)<w_i(S_j)\)\(i\to j\) 连边,要求这张图有无出度点。

考虑通过轮换 \(S\) 让这个图变成 DAG。

对于图上的一个环 \(1\sim q\),我们有 \(w_i(S_i)<w_i(S_{i+1})\),那么直接让第 \(i\) 个人匹配集合 \(S_i\),由于我们只对每个 \(S\) 进行整体轮换,所以 \(w_i(S_{i+1})>w_i(S_i)\) 自然大于每个 \(w_i(S_j)-\min v_{i}(S_j)\)

考虑调整的终点,发现如果 \(\sum w_i(S_i)\) 最大时肯定没有环,所以我们只要每次把所有人和所有 \(S\) 跑一次二分图最大权匹配就行,时间复杂度 \(\mathcal O(n^3m)\)

进一步优化复杂度:假设物品 \(m\) 插入了 \(S_x\) 上,那么其他集合不变,则图删除点 \(x\) 后依然是 DAG,我们要调整的环一定过 \(x\),并且只会调整一个环(因为二分图最大权匹配上插入一个新点时如果增广 \(>1\) 个环时可以合并成一个环),在 DAG 上按拓扑序 dp 出权值最大的路径即可。

时间复杂度 \(\mathcal O(n^2m)\),特判掉 \(n>m\) 的情况即可。

时间复杂度 \(\mathcal O((nm)^{1.5})\)

代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
int n,m,b[455],d[455],in[200005],fa[455];
ll w[455][455],f[455];
vector<int>a[455],G[455];
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	if(m<=n) {
		for(int i=1;i<=m;++i) cout<<i<<" \n"[i==m];
		return 0;
	}
	for(int i=1;i<=n;++i) {
		a[i].resize(m),b[i]=i;
		for(int j=0;j<m;++j) cin>>a[i][j];
	}
	for(int t=0;t<m;++t) {
		int p=0;
		for(int i=1;i<=n;++i) {
			for(int j=1;j<=n;++j) if(w[j][b[j]]<w[j][b[i]]) goto nx;
			p=i; break; nx:;
		}
		in[t]=b[p];
		for(int i=1;i<=n;++i) w[i][b[p]]+=a[i][t];
		for(int i=1;i<=n;++i) G[i].clear(),d[i]=fa[i]=0,f[i]=-1e18;
		for(int u=1;u<=n;++u) for(int v=1;v<=n;++v) if(v!=p&&w[u][b[v]]>w[u][b[u]]) G[u].push_back(v),++d[v];
		queue <int> Q; f[p]=0;
		for(int i=1;i<=n;++i) if(!d[i]) Q.push(i);
		while(Q.size()) {
			int u=Q.front(); Q.pop();
			for(int v:G[u]) {
				ll e=f[u]+w[u][b[v]]-w[u][b[u]];
				if(e>f[v]) f[v]=e,fa[v]=u;
				if(!--d[v]) Q.push(v);
			}
		}
		ll z=0; int x=0;
		for(int i=1;i<=n;++i) if(w[i][b[i]]<w[i][b[p]]) {
			ll e=f[i]+w[i][b[p]]-w[i][b[i]];
			if(e>z) z=e,x=i;
		}
		vector <int> c;
		for(;x;x=fa[x]) c.push_back(x);
		for(int i=(int)c.size()-1;i>0;--i) swap(b[c[i]],b[c[i-1]]);
	}
	for(int i=1;i<=n;++i) fa[b[i]]=i;
	for(int i=0;i<m;++i) cout<<fa[in[i]]<<" \n"[i==m-1];
	return 0;
}



L. [P15285] Towns (5)

Problem Link

首先 \(2n\) 次操作求直径 \(x\to y\),然后根据 \(\mathrm{dis}(x,u)+\mathrm{dis}(y,u)-\mathrm{dis}(x,y)\) 可以算出每个点首次进入 \(x\to y\) 的点,以及这个点到 \(x\) 的距离,从而得到链上所有点。

然后判断直径中点 \(u\) 是否是重心,判断 \(a,b\) 是否来自 \(u\) 的同一子树只要询问 \(\mathrm{dis}(a,b)\),所以考虑摩尔投票解决。

由于摩尔投票还需要检验,所以判断一个点需要 \(2n\) 次询问。

如果直径中点有两个,考虑如果一个点在之直径外的子树大小和 \(\le\frac n2\) 则不用检验,那么不可能同时检验两个点。

第一部分需要重新询问 \(\mathrm{dis}(y,u)\),考虑优化掉这部分,直接用 \(\mathrm{dis}(1,u),\mathrm{dis}(x,u)\) 来提取 \(1\to x\) 路径,由于 \(\mathrm{dis}(1,x)\) 是最大值,所以所有直径中点都在这条路径上,枚举这里面的点计算即可。

然后还要优化掉 \(0.5n\) 次询问,考虑精细实现摩尔投票的检验部分。

考虑把摩尔投票时已经确定来自同一子树的点缩起来,只用一次询问检验。

把摩尔投票的计数器每次清零的时刻拉出来分段,设有 \(k\) 个段,则摩尔投票询问次数不超过 \(n-k\),而每段中非众数元素至多 \(0.5n\) 次检验,每段众数检验 \(k\) 次,总次数刚好 \(3.5n\),满足条件。

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

代码:

#include<bits/stdc++.h>
#include "towns.h"
using namespace std;
map <array<int,2>,int> F;
int qry(int u,int v) {
	if(u>v) swap(u,v);
	if(u==v) return 0;
	if(!F.count({u,v})) F[{u,v}]=getDistance(u,v);
	return F[{u,v}];
}
int a[125],b[125],d[125],dsu[125];
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
int hubDistance(int n,int) {
	F.clear();
	for(int i=0;i<n;++i) a[i]=qry(0,i),dsu[i]=i;
	int x=max_element(a,a+n)-a;
	for(int i=0;i<n;++i) b[i]=qry(x,i);
	int z=*max_element(b,b+n);
	map <int,vector<int>> f;
	for(int i=0;i<n;++i) d[i]=(a[i]+b[i]-a[x])/2,f[b[i]-d[i]].push_back(i);
	int w=z;
	for(auto i:f) w=min(w,max(i.first,z-i.first));
	for(auto i:f) if(max(i.first,z-i.first)==w) {
		int L=0,R=0;
		for(auto j:f) {
			if(j.first<i.first) L+=j.second.size();
			if(j.first>i.first) R+=j.second.size();
		}
		vector <int> h;
		for(int u:i.second) if(d[u]) h.push_back(u);
		if(max(L,R)>n/2) continue;
		if((int)h.size()<=n/2) return w;
		int u=0,c=0;
		for(int j=L+R;j<(int)h.size();++j) {
			if(!c) u=h[j],c=1;
			else if(d[u]+d[h[j]]>qry(u,h[j])) dsu[h[j]]=u,++c;
			else --c;
		}
		if(!c) return w;
		c=0;
		for(int v:h) c+=d[u]+d[find(v)]>qry(u,find(v));
		if(c<=n/2) return w;
	}
	return -w;
}



M. [P14985] Pluses and Minuses (3.5)

Problem Link

对于单行要求就是至多一个 \(++\) 子串,且剩余部分 \(-,+\) 交替拓展。

那么把每行的 \(++\) 位置提出来,发现相邻两行的 \(++\) 位置之差为 \(\pm 1\),而且所有的 \(++\) 方向要相同。

所以每条对角线符号相同,我们要选择若干行列范围不交的相邻对角线填 \(+\)

\(m\ge n\),那么选择至多两组对角线,且分别只经过左上边界和右下边界,简单讨论能做到线性。

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

代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+5;
int n,m,q,c[MAXN][2],f[MAXN][2];
long long z; bool b[MAXN][2];
vector<array<int,3>>a;
void slv() {
	memset(b,0,(n+m+1)*2);
	for(auto o:a) b[o[0]+o[1]][!o[2]]=1;
	for(int i=2;i<=n+m;++i) for(int o:{0,1}) c[i][o]=c[i-1][o]+b[i][(i-o)&1];
	for(int i=2;i<n+m;++i) if(!b[i][1]&&!b[i+1][1]&&!c[i-1][(i+1)&1]&&c[i+1][i&1]==c[n+m][i&1]) ++z;
	if(n==1||n+m<=4) return ;
	for(int i=2;i<=n+m;++i) f[i][(i+1)&1]=f[i-1][(i+1)&1],f[i][i&1]=f[i-1][i&1]+(!b[i][1]&&!b[i+1][1]&&c[i+1][i&1]==c[n+m][i&1]);
	int e[2]={n+m-2,n+m-1};
	if(e[0]&1) swap(e[0],e[1]);
	for(int i=n;i>=2;--i) if(!b[i][1]&&!b[i+1][1]&&!c[i-1][(i+1)&1]) {
		int&j=e[(i+1)&1];
		while(j>=i+m-1&&c[i+1][i&1]<c[j-1][i&1]) j-=2;
		if(i+m-1<=j) z+=f[j][j&1]-f[i+m-2][j&1];
	}
}
void solve() {
	cin>>n>>m>>q,a.resize(q);
	int ok[3]={1,1,1};
	for(auto&o:a) { char x; cin>>x>>o[0]>>o[1],o[2]=x=='+',ok[(o[0]+o[1]+o[2])&1]=0,ok[2]&=o[2]; }
	z=ok[0]+ok[1];
	if(n==2&&m==2) z+=ok[2];
	if(n>m) {
		swap(n,m);
		for(auto&o:a) swap(o[0],o[1]);
	}
	slv();
	for(auto&o:a) o[1]=m+1-o[1];
	if(n>1) slv();
	cout<<z<<"\n";
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



N. [P14844] Secure the Top Secret (4.5)

Problem Link

最短路 \(\ge 2\) 转成对偶图的流,相当于选择两条边不交的路径把 \(U\)\(S,T\) 隔离开来。

由于这三个点都在边界上,所以我们只要这两条路径从边界的 \(U\sim S\) 部分走到 \(U\sim T\) 部分,但是不经过 \(S\sim T\) 部分即可。

然后跑一个最小费用最大流即可。

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

代码:

#include<bits/stdc++.h> 
using namespace std;
const int MAXN=2e4+5;
namespace F {
const int MAXE=3e5+5;
struct Edge { int v,e,f,w; } G[MAXE];
int S,T,ec=1,hd[MAXN],dis[MAXN],pre[MAXN];
bool inq[MAXN];
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) { if(u&&v) 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];
}
int ssp() {
	int c=0,e=2;
	while(e&&SPFA()) {
		int g=e;
		for(int u=T;u!=S;u=G[pre[u]^1].v) g=min(g,G[pre[u]].f);
		c+=g*dis[T],e-=g;
		for(int u=T;u!=S;u=G[pre[u]^1].v) G[pre[u]].f-=g,G[pre[u]^1].f+=g;
	}
	return e?-1:c;
}
}
int n,m,q,K,ec,id[105][105],b[105][105],dsu[MAXN],cl[MAXN];
char a[105][105];
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
void mrg(int x,int y) { dsu[find(x)]=find(y); }
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n+1;++i) for(int j=1;j<=m+1;++j) id[i][j]=++q,dsu[q]=q;
	for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) {
		cin>>a[i][j];
		if(a[i][j]=='#') mrg(id[i][j],id[i+1][j]),mrg(id[i][j],id[i][j+1]),mrg(id[i][j],id[i+1][j+1]);
	}
	string X; vector <int> E;
	for(int i=1;i<m;++i) X+=a[1][i],E.push_back(id[1][i+1]);
	for(int i=1;i<n;++i) X+=a[i][m],E.push_back(id[i+1][m+1]);
	for(int i=m;i>1;--i) X+=a[n][i],E.push_back(id[n+1][i]);
	for(int i=n;i>1;--i) X+=a[i][1],E.push_back(id[i][1]);
	int sz=X.size(),p[3]; 
	for(int o:{0,1,2}) p[o]=find(X.begin(),X.end(),"STU"[o])-X.begin();
	for(int i=p[2];i!=p[0];i=(i+1)%sz) if(i==p[1]) { swap(p[0],p[1]); break; }
	for(int i=p[0];i!=p[1];i=(i+1)%sz) cl[find(E[i])]=1;
	for(int i=1;i<=q;++i) if(dsu[i]==i) cl[i]=(cl[i]?0:++K);
	for(int i=1;i<=n+1;++i) for(int j=1;j<=m+1;++j) b[i][j]=cl[find(id[i][j])];
	cin>>ec;
	for(int x,y;ec--;) {
		char o; cin>>x>>y>>o;
		if(o=='r') F::link(b[x][y+1],b[x+1][y+1],1,1),F::link(b[x+1][y+1],b[x][y+1],1,1);
		else F::link(b[x+1][y],b[x+1][y+1],1,1),F::link(b[x+1][y+1],b[x+1][y],1,1);
	}
	F::S=K+1,F::T=K+2;
	for(int i=p[2];i!=p[0];i=(i+1)%sz) F::link(F::S,cl[find(E[i])],2,0);
	for(int i=p[1];i!=p[2];i=(i+1)%sz) F::link(cl[find(E[i])],F::T,2,0);
	cout<<F::ssp()<<"\n";
	return 0;
}



O. [P14681] Minimizing Wildlife Damage (4.5)

Problem Link

观察最终序列的结构,一定是全一个 \(0\) 前缀,剩下每次操作都覆盖首个非 \(0\) 位置到首个 \(0\) 的位置,否则我们可以在一开始就清空一个在中间某个时刻才被清零的位置。

枚举这个全 \(0\) 前缀 \([1,i]\),考虑其内部最多能进行多少次操作,发现对于一个长度 \(>1\) 的非 \(0\) 连续段,删掉最小值总是最优,所以操作次数就是 \([1,i-1]\) 的最大独立集大小 \(f_{i-1}\)

然后枚举一个下一个被删除的 \(0\) 的位置 \(j\),显然 \(a_j<\min a[i+2,j-1]\),可以用单调栈维护。

我们只会更新所有 \(f_{i-1}\le d\le f_{i-1}+a_{i+1}\) 的询问,然后枚举 \(j \ge i+2\),用 \((\sum_{t>i}a_t)-a_j-(d-f_{i-1})\times (j-i-1)\) 更新答案。

注意到 \(f_{i+1}\ge f_{i-1}+a_{i+1}\),因此每个 \(d\) 至多被 \(\mathcal O(1)\)\((f_{i-1},f_{i-1}+a_{i+1}]\),覆盖,暴力维护并用李超线段树处理查询即可。

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

代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
const ll V=1e12,inf=1e18;
struct info {
	ll k,b;
	ll f(ll x) { return k*x+b; }
}	e[MAXN];
int tr[MAXN],ls[MAXN],rs[MAXN],tot;
void ins(int x,ll l,ll r,int &p) {
	if(!p) return tr[p=++tot]=x,void();
	ll mid=(l+r)>>1;
	if(e[tr[p]].f(mid)<e[x].f(mid)) swap(x,tr[p]);
	if(e[tr[p]].f(l)<e[x].f(l)) ins(x,l,mid,ls[p]);
	if(e[tr[p]].f(r)<e[x].f(r)) ins(x,mid+1,r,rs[p]);
}
ll qry(ll x,ll l,ll r,int p) {
	ll z=e[tr[p]].f(x);
	if(l==r||!p) return z;
	ll mid=(l+r)>>1;
	return max(z,(x<=mid?qry(x,l,mid,ls[p]):qry(x,mid+1,r,rs[p])));
}
int n,m,c[MAXN],rt;
ll a[MAXN],f[MAXN],d[MAXN],Z[MAXN],S;
int id(ll w) {
	int k=0;
	for(int i=1<<17;i;i>>=1) if(k+i<=m&&d[c[k+i]]<=w) k+=i;
	return k;
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m,e[0]={0,-inf};
	for(int i=1;i<=n;++i) cin>>a[i];
	for(int i=1;i<=m;++i) cin>>d[i],c[i]=i;
	f[1]=a[1];
	sort(c+1,c+m+1,[&](int x,int y){ return d[x]<d[y]; });
	for(int i=2;i<=n;++i) f[i]=max(f[i-2]+a[i],f[i-1]);
	map <ll,ll> F;
	e[n+1]={-n-1,0},ins(n+1,0,V,rt);
	for(int i=n;i>=1;--i) {
		ll w=f[max(i-2,0)]; F[w]=S+=a[i];
		int l=id(w)+1,r=id(w+a[i]);
		for(int j=l;j<=r;++j) Z[c[j]]=max(Z[c[j]],S+(d[c[j]]-w)*i+qry(d[c[j]]-w,0,V,rt));
		e[i]={-i,-a[i]},ins(i,0,V,rt);
	}
	for(int i=1;i<=m;++i) {
		if(F.count(d[i])) Z[i]=max(Z[i],F[d[i]]);
		cout<<Z[i]<<"\n";
	}
	return 0;
}



P. [P14857] Planning Railroad Discontinuation (3)

Problem Link

\(a_i\) 排序处理,加入一条横边的时候考虑左右两个连续段内的连通性,发现同标号的点联通,不同的标号只要考虑 \(b_{\min}\) 上的联通情况。

设左右两侧的 \(b_{\min}\) 分别为 \(b_x,b_y\)\(b_x<b_y\))那么 \(a\) 边的加入次数就是树上加入 \(<a_i-b_y\) 的所有边后有多少至少包含一个 \(S\) 点的连通块。

对于一条内部的边,求一下 MST 如果两侧至少有一个不包含 \(S\) 点,那么 \(n\) 个树上都要加入这条边。

否则对于每棵树 \(b_x\),一条边 \(w\) 在这棵树上被加入当且仅当连接 \(a_i\le b_x+w\) 的边之后 \(b_x\) 依旧是连通块 \(b_{\min}\),也可以在加入 \(a_i\) 合并的过程中维护。

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

代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
int n,m,q,K,dsu[MAXN],a[MAXN],b[MAXN];
int L[MAXN],R[MAXN],mn[MAXN];
bool is[MAXN];
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	vector<array<int,3>>E(m),S;
	for(auto&e:E) cin>>e[1]>>e[2]>>e[0];
	cin>>q;
	for(int i=0;i<q;++i) cin>>a[i]>>b[i],S.push_back({a[i],i,(i+1)%q});
	cin>>K;
	for(int i=1,x;i<=K;++i) cin>>x,is[x]=1;
	ll z=0,sb=accumulate(b,b+q,0ll);
	sort(E.begin(),E.end()),iota(dsu,dsu+n,0);
	vector <ll> W,SW;
	for(auto e:E) {
		int u=find(e[1]),v=find(e[2]);
		if(u==v) continue;
		if(is[u]&&is[v]) W.push_back(e[0]);
		else z+=1ll*q*e[0]+sb;
		dsu[v]=u,is[u]|=is[v];
	}
	SW=W;
	for(int i=1;i<(int)SW.size();++i) SW[i]+=SW[i-1];
	sort(S.begin(),S.end()),S.pop_back();
	for(int i=0;i<q;++i) L[i]=R[i]=mn[i]=i;
	for(auto e:S) {
		int x=e[1],y=e[2],l=L[x],r=R[y],u=mn[l],v=mn[y];
		if(b[u]>b[v]) swap(u,v);
		int p=lower_bound(W.begin(),W.end(),e[0]-b[v])-W.begin();
		z+=1ll*e[0]*(K-p);
		if(p) z+=SW[p-1]+1ll*p*b[v];
		R[l]=r,L[r]=l,mn[l]=u;
	}
	if(K>1) z+=SW.back()+1ll*(K-1)*(*min_element(b,b+q));
	cout<<z<<"\n";
	return 0;
}
posted @ 2026-06-24 18:57  DaiRuiChen007  阅读(88)  评论(0)    收藏  举报