做题笔记 - Apr. 2026

题目整理

[ARC217] C - Greedy Customers 2

假设最后至少能配上 \(k\) 个商品,则限制是最大的 \(k\) 额度依次不比最小的 \(k\) 个商品小。枚举这个 \(k\),从大到小依次钦定每个人的额度,相同的元素一起钦定,时间复杂度 \(O(N^4)\)

#include<bits/stdc++.h>

using namespace std;

using ll=long long;
const int N=1e2+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=1ull*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 1ull*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]));
}

int a[N],f[N][N],ans[N],n,c;

inline void Solve(){
	cin>>n>>c;
	for(int i=1;i<=n;i++) cin>>a[i];

	vector<int> val(a+1,a+n+1);
	val.push_back(1),val.push_back(c+1);
	sort(val.begin(),val.end());
	val.erase(unique(val.begin(),val.end()),val.end());

	Init(n);
	sort(a+1,a+n+1);
	for(int i=1;i<=n;i++) a[i]=lower_bound(val.begin(),val.end(),a[i])-val.begin();
	for(int i=0;i<=n;i++){
		memset(f,0,sizeof f);
		f[val.size()-1][0]=1;
		for(int j=val.size()-2;~j;j--){
			int p=Mul(val[j+1]-val[j],Inv(c));
			for(int k=0;k<=n;k++){
				for(int l=0,q=1;l+k<=n;l++,MulAs(q,p)){
					if(k>=i||j>=a[i-k]) AddAs(f[j][k+l],Mul(Mul(f[j+1][k],C(n-k,l)),q));
				}
			}
		}
		ans[i]=f[0][n];
	}
	for(int i=0;i<n;i++) SubAs(ans[i],ans[i+1]);

	for(int i=0;i<=n;i++) cout<<ans[i]<<' ';cout<<endl;
}

signed main(){
	int T;
	cin>>T;
	while(T--) Solve();

	return 0;
}

当然也有 \(O(N^3)\) 做法。考虑对 \(A\) 升序排序,找出所有选了的连续段。对于中间的连续段可以区间 DP 计算单就选取这段区间的方案数,令该方案数为 \(f_{l,r}\),转移考虑枚举最后选取的位置 \(k\),则 \(f_{l,r} \leftarrow f_{i,k-1}f_{k+1,j}\dbinom{j-i}{j-k}(A_{j+1}-A_k)\)。而对于第一个连续段,可能有操作什么都不选,因此设 \(g_{i,j}\) 表示当前已经用了 \(i\) 次操作,\([1,j]\) 都选了,即做了 \(j\) 次有效操作。可以类似地枚举最后一个填的数,即 \(g_{i,j}\leftarrow g_{i-(j-k+1),k-1}f_{k+1,j}\dbinom{i-1}{j-k}(A_{j+1}-A_k)\),同时也可以什么都不选,即 \(g_{i,j}\leftarrow g_{i-1,j}(A_{j+1}-1)\)。最后再从后向前 DP 把这连续段合并以下即可,时间复杂度 \(O(N^3)\)

#include<bits/stdc++.h>

using namespace std;

using ll=long long;
const int N=1e2+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=1ull*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 1ull*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]));
}

int a[N],f[N][N],g[N][N],h[N][N],n,c;

inline void Solve(){
	cin>>n>>c;
	for(int i=1;i<=n;i++) cin>>a[i];

	Init(n);
	memset(f,0,sizeof f);
	memset(g,0,sizeof g);
	memset(h,0,sizeof h);
	sort(a+1,a+n+1),a[n+1]=c+1;
	for(int i=1;i<=n+1;i++) f[i][i-1]=1;
	for(int len=1;len<=n;len++){
		for(int i=1;i<=n-len+1;i++){
			int j=i+len-1;
			for(int k=i;k<=j;k++){
				AddAs(f[i][j],Mul(Mul(Mul(f[i][k-1],f[k+1][j]),C(j-i,j-k)),a[j+1]-a[k]));
			}
		}
	}
	g[0][0]=1;
	for(int i=0;i<=n;i++){
		for(int j=0;j<=n;j++){
			for(int k=j+1;i+(k-j)<=n;k++){
				AddAs(g[i+(k-j)][k],Mul(Mul(Mul(g[i][j],f[j+2][k]),C(i+(k-j)-1,i)),a[k+1]-a[j+1]));
			}
			AddAs(g[i+1][j],Mul(g[i][j],a[j+1]-1));
		}
	}
	h[n+1][0]=h[n+2][0]=1;
	for(int i=n;i>=1;i--){
		for(int j=0;j<=n-i+1;j++){
			for(int k=i;k<=i+j-1;k++){
				AddAs(h[i][j],Mul(Mul(h[k+2][j-(k-i+1)],f[i][k]),C(j,k-i+1)));
			}
			AddAs(h[i][j],h[i+1][j]);
		}
	}

	for(int k=0;k<=n;k++){
		int ans=0;
		for(int i=0;i<=n;i++){
			AddAs(ans,Mul(Mul(g[n-(k-i)][i],h[i+2][k-i]),C(n,k-i)));
		}
		MulAs(ans,QPow(Inv(c),n));
		cout<<ans<<' ';
	}
	cout<<endl;
}

signed main(){
	int T;
	cin>>T;
	while(T--) Solve();

	return 0;
}

[ARC217] D - Greedy Customer

神人题目,考虑计算每种情况最后剩多少,每次操作会把 \(f\) 劈成两个值域连续段,把打的并到小的里面计算即可,时间复杂度 \(O(N+M)\)

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
using ll=long long;
const int N=5e5+9;
const int M=5e7+9;

int a[N],ans[M],n,m;
inline void Work(int i,int s,int k){
	if(i>n){
		for(int j=0;j<=k;j++) ans[s+j]=j;
		return ;
	}
	if(a[i]>k) return Work(i+1,s,k);
	if(a[i]*2<=k){
		Work(i+1,s+a[i],k-a[i]);
		for(int j=0;j<a[i];j++) ans[s+j]=ans[s+j+a[i]];
	}else{
		Work(i+1,s,a[i]-1);
		for(int j=a[i];j<=k;j++) ans[s+j]=ans[s+j-a[i]];
	}
}
inline void Solve(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];

	ll sum=0;
	Work(1,0,m);
	for(int i=1;i<=m;i++) sum^=1ll*i*(i-ans[i]);

	cout<<sum<<endl;
}

signed main(){
	cin.tie(0),cout.tie(0);
	ios::sync_with_stdio(0);

	int T;
	cin>>T;
	while(T--) Solve();

	return 0;
}

[ARC217] E - Tree Growing

考虑将 \(\operatorname{dist}(i,j)\) 转化成 \((i,j)\) 路径上点的个数加 \(1\),则变成统计虚树构成一条链的三元组 \((i,j,k)\) 个数。继续转化,考虑反向计算不构成链的三元组个数,则必然存在一个点使得以该点为根时,这三个在不同的子树内,且这样的点有且只有一个。考虑该点每个子树的大小,分别设为 \(S_1,\ldots,S_k\),则问题变为 \(K\) 次操作,每次以 \(\dfrac {S_i}{\sum S}\) 的该概率给 \(S_i\)\(1\),求 \(\sum S_iS_jS_k\) 的期望。对于单个 \(S_iS_jS_k\),其操作完一次的期望为 \(S_iS_jS_k+S_iS_j\dfrac {S_k}{\sum S}+S_iS_k\dfrac {S_k}{\sum S}+S_iS_j\dfrac {S_k}{\sum S}\),化简得到 \(S_iS_jS_k\left(1+\dfrac 3{\sum S}\right)\),即每次期望变大 \(S_iS_jS_k\left(1+\dfrac 3{\sum S}\right)\)。观测对象变为 \(\sum S_iS_jS_k\) 时性质显然不变。因此若当且共有 \(n\) 个点,则三元组个数操作后会变大 \(\dfrac {n+2}{n-1}\) 倍。值得注意的是,当 \(n\leftarrow n+1\) 时,\(\dbinom{n}{2}+\dbinom{n}{3}\) 同样会变大 \(\dfrac {n+2}{n-1}\) 倍,因此可以直接在原树的路径长度和的基础上计算答案,时间复杂度 \(O(n)\)

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
using ll=long long;
const int N=3e5+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=1ull*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 1ull*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 siz[N],n,k;
vector<int> e[N];
inline void DFS(int x,int fa){
	siz[x]=1;
	for(int y:e[x]) if(y!=fa) DFS(y,x),siz[x]+=siz[y];
}

inline void Solve(){
	cin>>n>>k;
	for(int i=1,u,v;i<n;i++){
		cin>>u>>v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	
	DFS(1,0);
	
	int sum=0;
	for(int i=1;i<=n;i++) AddAs(sum,Mul(siz[i],n-siz[i]));
	for(int i=n;i<n+k;i++) MulAs(sum,Add(1,Mul(3,Inv(i-1))));

	cout<<sum<<endl;

	for(int i=1;i<=n;i++) e[i].clear();
}

signed main(){
	cin.tie(0),cout.tie(0);
	ios::sync_with_stdio(0);

	int T;
	cin>>T;
	while(T--) Solve();

	return 0;
}

[3rd Zhili Cup] 01 Game

考虑最终答案形态,设原序列被操作点划分成 \(s_1,s_2,\ldots\),则操作后的序列形如 \(s_1,s_3,\ldots,s_{2k\pm 1},s_{2k}^{-1},\ldots,s_4^{-1},s_2^{-1}\),其中 \(s^{-1}\) 表示 \(s\) 逐位取反再反转的结果,注意该形态是基于若 \(s_1\) 被取反则取反整个序列的。

根据该形态,从在原序列上后向前填数相当于在答案序列上往左右两边加数。若答案出现在左侧或右侧,则答案上界即为最长 01 交替出现子序列长度,可以证明一定可以把答案调整到碰到中央分界线。因此考虑直接从中央分界线开始向左右两边填数。贪心地,把 01 交替出现子序列全部先扔到左边,然后把失配的位全部扔到右边。

通过记录左侧取值和右侧状态,可以把原问题规约到一个 6 状态的自动机上。因此可以平方合并,在线段树上维护每个起点开始的终点以及路径权值和,时间复杂度 \(O(n\log n)\)

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
const int N=5e5+9;

struct Data{
	array<int,2> to[6];
	Data(){memset(to,0,sizeof to);}
	inline array<int,2>& operator [](int x){return to[x];}
	friend inline Data operator *(Data A,Data B){
		Data C;
		for(int i=0;i<6;i++) C[i][0]=B[A[i][0]][0],C[i][1]=A[i][1]+B[A[i][0]][1];
		return C;
	}
}A[N],dat[N<<2];

inline void Build(int x,int L,int R){
	if(L==R) return dat[x]=A[L],void();
	int mid=L+R>>1;
	Build(x<<1,L,mid),Build(x<<1|1,mid+1,R);
	dat[x]=dat[x<<1|1]*dat[x<<1];
}
inline Data Query(int x,int L,int R,int l,int r){
	if(l>r) return Data();
	if(l<=L&&R<=r) return dat[x];
	int mid=L+R>>1;
	if(r<=mid) return Query(x<<1,L,mid,l,r);
	else if(l>mid) return Query(x<<1|1,mid+1,R,l,r);
	else return Query(x<<1|1,mid+1,R,l,r)*Query(x<<1,L,mid,l,r);
}

int a[N],n,q;

signed main(){
	cin.tie(0),cout.tie(0);
	ios::sync_with_stdio(0);

	cin>>n>>q;
	for(int i=1;i<=n;i++){
		char c;
		cin>>c;
		a[i]=c-'0';
		for(int p:{0,1}){
			for(int q:{0,1,2}){
				if(p!=a[i]) A[i][p*3+q]={!p*3+q,1};
				else if(q!=2){
					if(!a[i]!=q) A[i][p*3+q]={p*3+!a[i],1};
					else A[i][p*3+q]={p*3+2,0};
				}else A[i][p*3+q]={p*3+q,0};
			}
		}
	}

	Build(1,1,n);
	while(q--){
		int l,r;
		cin>>l>>r;

		auto D=Query(1,1,n,l,r-1);

		cout<<D[a[r]*3+a[r]][1]+1<<endl;
	}

	return 0;
}

[3rd Zhili Cup] Balancing

由于每次操作不能该改变 \([l,r]\) 内的大小关系,因此至多消掉两个大于号,考虑构造方案顶到这个上界。

设原来有 \(k\) 个大于号,最左侧的位于 \((p,p+1)\) 处,最右侧的位于 \((q,q+1)\) 处。

  • \(k\bmod 2\equiv 0\),则答案是 \(\dfrac k2\) 还是 \(\dfrac k2+1\) 取决于 \(a_q-a_p\geq q-p\) 是否成立。若成立,则操作 \([p=1,q-1]\),令最左侧的上升段调整为 \(a_p+1,a_p+2,\ldots\),右侧上升段调整为 \(\ldots,a_q-2,a_q-1\)。此时大于号减少恰好两个,且仍然满足 \(k'\bmod 2\equiv 0\)\(a_{q'}-a_{p'}\geq q'-p'\)
    否则一定无法在 \(\dfrac k2\) 操作内消除所有大于号。

  • \(k\bmod 2\equiv 1\),则可以抬高最后一段,因此答案总为 \(\dfrac {k+1}2\)

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

#include<bits/stdc++.h>

using namespace std;

const int N=2e5+9;

int a[N],n;

inline void Solve(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];

	int cnt=0;
	for(int i=1;i<n;i++) cnt+=(a[i]>a[i+1]);

	int l=1,r=n;
	while(l<n&&a[l]<a[l+1]) l++;
	while(r>1&&a[r]>a[r-1]) r--;

	if(!cnt||(cnt&1)) cout<<(cnt+1>>1)<<endl;
	else cout<<(cnt>>1)+(a[r]-a[l]<r-l)<<endl;
}

signed main(){
	int T;
	cin>>T;
	while(T--) Solve();

	return 0;
}

[3rd Zhili Cup] Datalog

考虑阈值分治,称集合大小 \(\geq B\) 的为大块,否则为小块。记 \(Q=n+m\)

对于大块,考虑用桶记录每种元素是否出现。对于小块,则有序地维护所有出现的元素。同时动态地维护所有大块两两之间的答案。插入是简单的,因此不讨论。

对于合并。大块和大块的合并可以直接暴力合并,因为大块至多 \(\dfrac QB\) 个。小块到大块的合并可以直接视作插入,均摊 \(O(QB)\)。小块和小块则可以直接 \(O(B)\) 归并。

对于询问。大块和大块的询问可以直接 \(O(1)\) 回答。小块和大块的询问可以枚举小块元素回答。小块和小块的询问依旧归并处理。

时间复杂度 \(O(QB+\frac {Q^2}B)\),平衡复杂度可以得到 \(O(Q\sqrt Q)\),空间复杂度 \(O(\frac{Q\sqrt Q}{w})\)

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
using ll=long long;
const int N=4e5+9;
const int Q=6e5+9;
const int M=1e6+9;
const int T=1e3+9;

bitset<M> b[T];
vector<int> v[N],val,w;

ll sum[N],ans[T][T];
int a[N],op[Q],qx[Q],qy[Q],cnt[N],id[N],p[N],n,q,typ,B,tot;

inline void Insert(int x,int y){
	if(cnt[x]>=B){
		if(!b[id[x]][y]){
			cnt[x]++,sum[x]+=w[y],b[id[x]][y]=1;
			for(int i=1;i<id[x];i++) if(!b[i][y]) ans[i][id[x]]+=w[y];
			for(int i=tot;i>id[x];i--) if(!b[i][y]) ans[id[x]][i]+=w[y];
		}
	}else{
		auto it=lower_bound(v[x].begin(),v[x].end(),y);
		if(it==v[x].end()||*it!=y) cnt[x]++,sum[x]+=w[y],v[x].insert(it,y);
		if(cnt[x]==B){
			id[x]=++tot,p[tot]=x;
			for(int i=1;i<tot;i++) ans[i][id[x]]=sum[p[i]]+sum[x];
			for(int z:v[x]){
				b[id[x]][z]=1;
				for(int i=1;i<tot;i++) if(b[i][z]) ans[i][id[x]]-=w[z];
			}
			v[x].clear(),v[x].shrink_to_fit();
		}
	}
}
inline void Merge(int x,int y){
	if(cnt[x]>=B&&cnt[y]>=B){
		auto t=b[id[x]]&b[id[y]];
		for(int i=1;i<=tot;i++){
			if(i==id[x]||i==id[y]) continue ;
			ans[min(id[x],i)][max(id[x],i)]+=ans[min(id[y],i)][max(id[y],i)]-sum[p[i]];
		}
		cnt[x]+=cnt[y],sum[x]+=sum[y];
		for(int i=t._Find_first();i<t.size();i=t._Find_next(i)){
			cnt[x]--,sum[x]-=w[i];
			for(int j=1;j<=tot;j++){
				if(j==id[x]||j==id[y]) continue ;
				if(!b[j][i]) ans[min(id[x],j)][max(id[x],j)]-=w[i];
			}
		}
		b[id[x]]|=b[id[y]];
		b[id[y]].reset();
	}else if(cnt[x]>=B||cnt[y]>=B){
		if(cnt[y]>=B){
			p[id[y]]=x;
			swap(id[x],id[y]);
			swap(v[x],v[y]);
			swap(cnt[x],cnt[y]);
			swap(sum[x],sum[y]);
		}
		for(int i:v[y]) Insert(x,i);
		v[y].clear(),v[y].shrink_to_fit();
	}else{
		vector<int> tmp(v[x].size()+v[y].size());
		merge(v[x].begin(),v[x].end(),v[y].begin(),v[y].end(),tmp.begin());
		tmp.erase(unique(tmp.begin(),tmp.end()),tmp.end());
		v[x].swap(tmp);
		cnt[x]=0,sum[x]=0;
		for(int i:v[x]) cnt[x]++,sum[x]+=w[i];
		tmp.clear(),tmp.shrink_to_fit();
		v[y].clear(),v[y].shrink_to_fit();
		if(cnt[x]>=B){
			id[x]=++tot,p[tot]=x;
			for(int i=1;i<tot;i++) ans[i][id[x]]=sum[p[i]]+sum[x];
			for(int z:v[x]){
				b[id[x]][z]=1;
				for(int i=1;i<tot;i++) if(b[i][z]) ans[i][id[x]]-=w[z];
			}
			v[x].clear(),v[x].shrink_to_fit();
		}
	}
}
inline ll Query(int x,int y){
	if(cnt[x]>=B&&cnt[y]>=B) return ans[min(id[x],id[y])][max(id[x],id[y])];
	else if(cnt[x]>=B||cnt[y]>=B){
		ll ans=sum[x]+sum[y];
		if(cnt[y]>=B) swap(x,y);
		for(int i:v[y]) if(b[id[x]][i]) ans-=w[i];
		return ans;
	}else{
		ll ans=0;
		vector<int> tmp(v[x].size()+v[y].size());
		merge(v[x].begin(),v[x].end(),v[y].begin(),v[y].end(),tmp.begin());
		tmp.erase(unique(tmp.begin(),tmp.end()),tmp.end());
		for(int i:tmp) ans+=w[i];
		tmp.clear(),tmp.shrink_to_fit();
		return ans;
	}
}

signed main(){
	cin.tie(0),cout.tie(0);
	ios::sync_with_stdio(0);

	cin>>n>>q>>typ;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=q;i++) cin>>op[i]>>qx[i]>>qy[i];

	B=sqrt(n+q);
	for(int i=1;i<=n;i++) val.push_back(a[i]);
	for(int i=1;i<=q;i++) if(op[i]==1) val.push_back(qy[i]);
	sort(val.begin(),val.end());
	val.erase(unique(val.begin(),val.end()),val.end());
	w=typ&1?vector<int>(val.size(),1):val;

	for(int i=1;i<=n;i++) Insert(i,lower_bound(val.begin(),val.end(),a[i])-val.begin());
	for(int i=1;i<=q;i++){
		if(op[i]==1) Insert(qx[i],lower_bound(val.begin(),val.end(),qy[i])-val.begin());
		else if(op[i]==2) Merge(qx[i],qy[i]);
		else if(op[i]==3) cout<<Query(qx[i],qy[i])<<endl;
	}

	return 0;
}

[3rd Zhili Cup] Matrix

考虑如下构造:

0 1 2 3 4 5 6 7 8 9
1 3 0 5 2 7 4 9 6 8
2 0 4 1 6 3 8 5 9 7
3 5 1 7 0 9 2 8 4 6
4 2 6 0 8 1 9 3 7 5
5 7 3 9 1 8 0 6 2 4
6 4 8 2 9 0 7 1 5 3
7 9 5 8 3 6 1 4 0 2
8 6 9 4 7 2 5 0 3 1
9 8 7 6 5 4 3 2 1 0

即每两种颜色构成顶到四条边界的环。

正确性显然。

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'

const int N=5e3+9;

int a[N][N],n;

signed main(){
	cin.tie(0),cout.tie(0);
	ios::sync_with_stdio(0);

	cin>>n;

	if(n==1) cout<<"YES"<<endl,cout<<0<<endl;
	else if(n&1) cout<<"NO"<<endl;
	else{
		cout<<"YES"<<endl;

		for(int i=0;i<n;i+=2){
			for(int j=0;j<=i;j++) a[j][i-j]=i^j&1;
			for(int j=0;j<=i;j++) a[n-j-1][n-(i-j)-1]=i^j&1;
			for(int j=0;j<=n-(i|1)-1;j++) a[j][j+(i|1)]=(i|1)^j&1;
			for(int j=0;j<=n-(i|1)-1;j++) a[n-j-1][n-(j+(i|1))-1]=(i|1)^j&1;
		}

		for(int i=0;i<n;i++){
			for(int j=0;j<n;j++) cout<<a[i][j]<<' ';
			cout<<endl;
		}
	}

	return 0;
}

[3rd Zhili Cup] Quantifier

因为操作都是可逆的,考虑贪心地先把每个元素往下拨,再一起向上合并。则会产生顺序问题的地方就是两个不同子树之间的合并以及同色的儿子前缀以及父亲后缀之间的交换。因此设 \(f_{x,i,0/1}\) 表示考虑到 \(x\) 到其父亲的边以及其子树内所有边,现在有长为 \(i\) 的前缀,开头元素是 \(0/1\) 的方案数。转移时先转移子树合并,再转移父亲-儿子的交换。时间复杂度和树上背包相同,即 \(O(n^2)\)

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
using ll=long long;
const int N=5e3+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=1ull*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 1ull*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]));
}

vector<int> e[N],v[N];
int fa[N],c[N],d[N],blk[N],s[N],f[N][N][2],F[N][N][2],g[N][2],n,m;
inline void Calc(int x){
	f[x][0][0]=F[x][0][0]=1;
	for(int y:e[x]){
		Calc(y);
		for(int i=0;i<=s[x];i++){
			for(int j=0;j<=s[y];j++){
				for(int p:{0,1}){
					AddAs(g[i+j][p],Mul(Mul(Mul(C(i+j,i),C(s[x]-i+s[y]-j-1,s[x]-i)),f[y][j][p]),F[x][i][p]));
					AddAs(g[i+j][p],Mul(Mul(Mul(C(i+j,i),C(s[x]-i+s[y]-j-1,s[y]-j)),f[x][i][p]),F[y][j][p]));
				}
			}
		}
		for(int p:{0,1}) AddAs(g[s[x]+s[y]][p],Mul(Mul(C(s[x]+s[y],s[x]),f[x][s[x]][p]),f[y][s[y]][p]));
		for(int i=1;i<=s[x];i++){
			for(int p:{0,1}){
				AddAs(g[i][p],Mul(Mul(C(s[x]-i+s[y]-1,s[x]-i),F[x][i+1][p]),F[y][0][!p]));
				AddAs(g[i][p],Mul(Mul(C(s[x]-i+s[y],s[x]-i),f[x][i][p]),F[y][0][!p]));
			}
		}
		for(int i=1;i<=s[y];i++){
			for(int p:{0,1}){
				AddAs(g[i][p],Mul(Mul(C(s[y]-i+s[x]-1,s[y]-i),F[y][i+1][p]),F[x][0][!p]));
				AddAs(g[i][p],Mul(Mul(C(s[y]-i+s[x],s[y]-i),f[y][i][p]),F[x][0][!p]));
			}
		}
		s[x]+=s[y];
		for(int i=s[x];i>=0;i--){
			for(int p:{0,1}){
				f[x][i][p]=exchange(g[i][p],0);
				F[x][i][p]=Add(f[x][i][p],F[x][i+1][p]);
			}
		}
	}

	reverse(v[x].begin(),v[x].end());
	if(v[x].size()){
		int a=0,p=v[x].front(),coef=1;
		while(a<v[x].size()&&v[x][a]==p) a++;
		for(int i=0,r=0;i<v[x].size();i++){
			if(i&&v[x][i-1]!=v[x][i]) r=0;
			MulAs(coef,++r);
		}
		if(a==v[x].size()){
			for(int i=0;i<=s[x];i++){
				AddAs(g[i+a][p],Mul(C(i+a,a),f[x][i][p]));
				AddAs(g[a][p],f[x][i][!p]);
			}
		}else{
			int b=0,q=v[x].back();
			while(b<v[x].size()&&v[x][v[x].size()-1-b]==q) b++;
			for(int i=0;i<=s[x];i++){
				AddAs(g[a][p],Mul(C(i+b,b),f[x][i][q]));
				AddAs(g[a][p],f[x][i][!q]);
			}
		}
		s[x]+=v[x].size();
		for(int i=s[x];i>=0;i--){
			for(int p:{0,1}){
				f[x][i][p]=Mul(coef,exchange(g[i][p],0));
				F[x][i][p]=Add(f[x][i][p],F[x][i+1][p]);
			}
		}
	}
}

signed main(){
	cin.tie(0),cout.tie(0);
	ios::sync_with_stdio(0);

	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>fa[i],e[fa[i]].push_back(i);
	for(int i=1;i<=m;i++) cin>>c[i];
	for(int i=1;i<=m;i++) cin>>d[i];

	for(int i=m;i>=1;i--){
		int x=d[i],y=d[i];
		vector<int> o;
		while(x) o.push_back(x),x=fa[x];
		reverse(o.begin(),o.end());
		for(int z:o){
			if(v[z].size()&&(blk[z]||c[i]!=v[z].back())){
				y=z;
				break ;
			}
		}
		if(v[y].size()&&c[i]!=v[y].back()) blk[y]=1;
		v[y].push_back(c[i]);
	}

	Init(m);
	Calc(1);

	cout<<Add(F[1][0][0],F[1][0][1])<<endl;

	return 0;
}

[JOIST 2025] Exhibition 3

从高权值向低权值考虑,相当于有若干个放点的额度,要求覆盖到编号尽量靠前的区间。

覆盖到的区间一定是先尽可能放编号最小的几个,直到塞不下了后面再带上零散的几个,即后面加入的区间不会改变最小覆盖的点数。

考虑先二分出前面至多能放几个,数量设为 \(c\)。对于计算一个线段集合的最小点覆盖数,有经典贪心:按照右端点升序排序,如果当前最右的点覆盖不到了就在当前线段的右端点再放一个。然而直接二分可能因为 \(c\) 远小于其上界导致复杂度无法接受,因此考虑先倍增出 \(c\) 的数量级,即找到一个 \(k\) 使得 \(c\in [2^k,2^{k+1})\),然后再二分 \(c\) 的具体取值,这样复杂度就是 \(O(c\log c)\) 的,而 \(\sum c\leq m\),因此这部分总复杂度就降到了 \(O(m\log m)\),可以接受。

接下来再放入后面的区间。先思考如何快速判定一个区间可以被放入当前的答案集合,考虑分别向左、向右各贪心一次找到每个覆盖点最左最优可以取到哪里。设从左到右第 \(i\) 个点可以取到的范围是 \([l_i,r_i]\),调整法可知 \(i\) 可以取遍 \([l_i,r_i],因此区间 [L,R] 可以放进答案集合的充要条件就是 \exists i,~[l_i,r_i]\cap [L,R]\neq \varnothing。同时,可以证明的是,\forall i<j,~r_i<l_j\),证明考虑 \([l_i,r_i]\) 的限制来源于答案集合中若干组不交的区间,则每个区间都必须消耗掉一个点,因此 \([l_i,r_i]\) 就必须是 \(i\) 对应区间的子集,因此 \([l_i,r_i]\) 也两两无交。

在知道了如何快速判定 \([L,R]\) 是否可以被放入答案集合后,考虑加入 \([L,R]\)\([l_i,r_i]\) 的影响:

  • \([L,R]\) 包含某个 \([l_i,r_i]\),则不会产生任何影响。
  • \([L,R]\) 只和一个 \([l_i,r_i]\) 有交,则 \([l_i,r_i]\)\([L,R]\) 取交。
  • 否则 \([L,R]\) 和相邻的两个区间 \([l_i,r_i],[l_{i+1},r_{i+1}]\) 有交,则加入时不会产生影响,但如果未来区间 \(i\)\(i+1\) 缩减使得其不再和 \([L,R]\) 有交,则另外一个区间必须和 \([L,R]\) 取交。

考虑先找出所有第一类区间并记录,然后对于每个 \([l_i,r_i]\) 寻找和其有交的编号最小的区间,放入小根堆里超级钢琴,重复执行以上操作。同时对每个区间维护左右两个堆维护第三类区间的限制,在产生改变后暴力更新,可以证明后面加入的合法区间不会使之前的限制产生矛盾,证明考虑对限制放缩。

至此已经可以在和选出的线段集合相关的时间复杂度内找出所有能覆盖的线段了。将 \(N,M\) 视作同阶,总时间复杂度 \(O(N\log N)\)

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
using pii=pair<int,int>;
const int N=1e5+9;

int a[N],c[N],sl[N],sr[N],ans[N],vis[N],n,m;

template<class T,class C=less<T>> struct DelHeap{
	priority_queue<T,vector<T>,C> p,q;
	inline void Insert(T x){p.push(x);}
	inline void Erase(T x){q.push(x);}
	inline T Top(){
		while(q.size()&&p.top()==q.top()) p.pop(),q.pop();
		return p.top();
	}
	inline int Size(){return p.size()-q.size();}
};
template<class T,class C=less<T>> struct SgT{
	C Cmp;
	T dat[N<<2],null;
	DelHeap<T,C> q[N];
	inline T Max(T x,T y){return !Cmp(x,y)?x:y;}
	inline void Init(T _null){fill(dat,dat+4*n+1,null=_null);}
	inline void PushUp(int x){dat[x]=Max(dat[x<<1],dat[x<<1|1]);}
	inline void Insert(int x,int L,int R,int pos,T k){
		if(L==R) return q[L].Insert(k),dat[x]=q[L].Size()?q[L].Top():null,void();
		int mid=L+R>>1;
		if(pos<=mid) Insert(x<<1,L,mid,pos,k);
		else Insert(x<<1|1,mid+1,R,pos,k);
		PushUp(x);
	}
	inline void Erase(int x,int L,int R,int pos,T k){
		if(L==R) return q[L].Erase(k),dat[x]=q[L].Size()?q[L].Top():null,void();
		int mid=L+R>>1;
		if(pos<=mid) Erase(x<<1,L,mid,pos,k);
		else Erase(x<<1|1,mid+1,R,pos,k);
		PushUp(x);
	}
	inline T Query(int x,int L,int R,int l,int r){
		if(l<=L&&R<=r) return dat[x];
		int mid=L+R>>1;
		if(r<=mid) return Query(x<<1,L,mid,l,r);
		else if(l>mid) return Query(x<<1|1,mid+1,R,l,r);
		else return Max(Query(x<<1,L,mid,l,r),Query(x<<1|1,mid+1,R,l,r));
	}
};
SgT<pii> ST;
SgT<int,greater<int>> LT,RT;

DelHeap<int,greater<int>> rem;
inline bool CmpSl(int i,int j){return sl[i]>sl[j];}
inline bool CmpSr(int i,int j){return sr[i]<sr[j];}
inline int Count(vector<int> &v){
	int cnt=0,r=0;
	for(int i:v) if(r<sl[i]) cnt++,r=sr[i];
	return cnt;
}
inline void Mark(int p,int x){
	ans[p]=x,vis[p]=1;
	LT.Erase(1,1,n,sl[p],p);
	RT.Erase(1,1,n,sr[p],p);
	ST.Erase(1,1,n,sl[p],{sr[p],p});
}
inline void FindPrefix(int x,vector<int> &v){
	vector<int> u;
	v.push_back(rem.Top());
	rem.Erase(v.back());
	while(rem.Size()){
		vector<int> t,cur;
		for(int i=1;rem.Size()&&i<=v.size();i++){
			t.push_back(rem.Top());
			rem.Erase(t.back());
		}
		sort(t.begin(),t.end(),CmpSr);
		cur.resize(v.size()+t.size());
		merge(v.begin(),v.end(),t.begin(),t.end(),cur.begin(),CmpSr);
		if(Count(cur)>c[x]){
			u.swap(t);
			break ;
		}else v.swap(cur);
	}
	vector<int> id(u);
	sort(id.begin(),id.end());
	int l=0,r=id.size()+1;
	while(l+1<r){
		int mid=l+r>>1;
		vector<int> t,cur;
		for(int i:u) if(i<=id[mid-1]) t.push_back(i);
		cur.resize(v.size()+t.size());
		merge(v.begin(),v.end(),t.begin(),t.end(),cur.begin(),CmpSr);
		if(Count(cur)>c[x]) r=mid;
		else l=mid;
	}
	for(int i=0;i<l;i++) v.push_back(id[i]);
	for(int i=l;i<id.size();i++) rem.Insert(id[i]);
	for(int i:v) Mark(i,x);
}
inline void Greedy(vector<int> &v,vector<int> &tr){
	int r=0;
	sort(v.begin(),v.end(),CmpSr);
	for(int i:v) if(r<sl[i]) tr.push_back(r=sr[i]);
}
inline void Gleedy(vector<int> &v,vector<int> &tl){
	int l=n+1;
	sort(v.begin(),v.end(),CmpSl);
	for(int i:v) if(l>sr[i]) tl.push_back(l=sl[i]);
	reverse(tl.begin(),tl.end());
}
inline void Work(int x){
	vector<int> v,tl,tr;
	FindPrefix(x,v);
	Greedy(v,tr),Gleedy(v,tl),c[x]=tr.size();
	priority_queue<pii,vector<pii>,greater<pii>> pno;
	vector<priority_queue<pii,vector<pii>,greater<pii>>> lq(c[x]);
	vector<priority_queue<pii>> rq(c[x]);

	auto Constr=[&](int i,int p){
		rq[i].push({sl[p]-1,sr[p]});
		lq[i+1].push({sr[p]+1,sl[p]});
	};
	for(int i:v){
		int lp=lower_bound(tr.begin(),tr.end(),sl[i])-tr.begin();
		int rp=upper_bound(tl.begin(),tl.end(),sr[i])-tl.begin()-1;
		if(lp+1==rp&&tl[lp]<sl[i]&&sr[i]<tr[rp]) Constr(lp,i);
	}

	auto TryNew=[&](int i){
		while(true){
			int p=ST.Query(1,1,n,1,tl[i]).second;
			if(!p||sr[p]<tr[i]) break ;
			Mark(p,x),rem.Erase(p);
		}
		int p=min(LT.Query(1,1,n,tl[i],tr[i]),RT.Query(1,1,n,tl[i],tr[i]));
		if(p<=m) pno.push({p,i});
	};
	for(int i=0;i<c[x];i++) TryNew(i);

	while(pno.size()){
		int p=pno.top().first,i=pno.top().second;
		pno.pop();
		if(!vis[p]&&max(sl[p],tl[i])<=min(sr[p],tr[i])){
			Mark(p,x),rem.Erase(p);
			if(i>0&&sl[p]<=tr[i-1]) Constr(i-1,p);
			else if(i+1<c[x]&&sr[p]>=tl[i+1]) Constr(i,p);
			else{
				for(int j=i,tar=sl[p];~j;j--){
					if(tar<=tl[j]) break ;
					tl[j]=exchange(tar,0);
					while(lq[j].size()&&lq[j].top().first<=tl[j]){
						tar=max(tar,lq[j].top().second);
						lq[j].pop();
					}
				}
				for(int j=i,tar=sr[p];j<c[x];j++){
					if(tar>=tr[j]) break ;
					tr[j]=exchange(tar,n+1);
					while(rq[j].size()&&rq[j].top().first>=tr[j]){
						tar=min(tar,rq[j].top().second);
						rq[j].pop();
					}
				}
			}
		}
		TryNew(i);
	}
}

signed main(){
	cin.tie(0),cout.tie(0);
	ios::sync_with_stdio(0);

	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i],c[a[i]]++;
	for(int i=1;i<=m;i++) cin>>sl[i]>>sr[i];

	LT.Init(m+1),RT.Init(m+1),ST.Init({0,0});
	for(int i=1;i<=m;i++){
		rem.Insert(i);
		LT.Insert(1,1,n,sl[i],i);
		RT.Insert(1,1,n,sr[i],i);
		ST.Insert(1,1,n,sl[i],{sr[i],i});
	}
	for(int x=n;rem.Size()&&x>=1;x--) if(c[x]) Work(x);

	for(int i=1;i<=m;i++) cout<<ans[i]<<endl;

	return 0;
}

[JOIST 2025] Bitaro's Travel 2

因为不能二段跳,所以一次跳高可以看成是先跳到一个能达到的尽量高的山峰,再向下滑翔的过程。而一个点走到另一个点所需要的高度则可以在 Kruskal 重构树上找 LCA 实现,因此倍增跳到所需的高度即可。时间复杂度 \(O(HW\log HW)\)

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
const int N=3e5+9;
const int lgN=2e1;

int fa[N<<1],mx[N<<1],w[N<<1],a[N<<1],qx[N],qy[N],n,m,q,L,tot,lim;
inline int Cmp(int x,int y){return a[x]>a[y]?x:y;}
inline int Find(int x){return fa[x]==x?x:fa[x]=Find(fa[x]);}
inline void Merge(int x,int y){
	fa[y=Find(y)]=x=Find(x);
	mx[x]=Cmp(mx[x],mx[y]);
}

int st[N<<1][lgN],ts[N<<1][lgN],dep[N<<1];
inline void Init(){
	for(int i=tot;i;i--) dep[i]=dep[st[i][0]]+1;
	for(int k=1;k<lgN;k++){
		for(int i=1;i<=tot;i++){
			st[i][k]=st[st[i][k-1]][k-1];
			ts[i][k]=ts[ts[i][k-1]][k-1];
		}
	}
}
inline int LCA(int x,int y){
	if(dep[x]<dep[y]) swap(x,y);
	for(int k=lgN-1;~k;k--) if(dep[st[x][k]]>=dep[y]) x=st[x][k];
	if(x==y) return x;
	for(int k=lgN-1;~k;k--) if(st[x][k]!=st[y][k]) x=st[x][k],y=st[y][k];
	return st[x][0];
}
inline int Calc(int x,int y){
	int tar=w[LCA(x,y)],ans=0;
	for(int k=lgN-1;~k;k--) if(a[ts[x][k]]<tar) ans|=(1<<k),x=ts[x][k];
	ans++,x=ts[x][0];
	return a[x]>=tar?ans:-1;
}

signed main(){
	cin.tie(0),cout.tie(0);
	ios::sync_with_stdio(0);

	cin>>n>>m>>L,tot=lim=n*m;
	for(int i=1;i<=lim;i++) cin>>a[i];

	vector<array<int,3>> o;
	for(int i=1;i<=lim;i++) o.push_back({a[i],0,i}),o.push_back({a[i]+L,1,i});
	sort(o.begin(),o.end());
	iota(fa,fa+lim*2,0),iota(mx,mx+lim*2,0);
	auto Try=[&](int u,int v,int val){
		if(a[u]>val||a[v]>val) return ;
		if(Find(u)==Find(v)) return ;
		w[++tot]=val;
		u=Find(u),v=Find(v);
		Merge(tot,u),Merge(tot,v);
		st[u][0]=st[v][0]=tot;
	};
	for(auto p:o){
		int i=p[2];
		if(!p[1]){
			if(i-m>0) Try(i,i-m,p[0]);
			if(i+m<=lim) Try(i,i+m,p[0]);
			if((i-1)%m) Try(i,i-1,p[0]);
			if(i%m) Try(i,i+1,p[0]);
		}else ts[i][0]=mx[Find(i)];
	}

	Init();
	cin>>q;
	while(q--){
		int a,b,c,d;
		cin>>a>>b>>c>>d;
		cout<<Calc((a-1)*m+b,(c-1)*m+d)<<endl;
	}

	return 0;
}

[JOIST 2025] Collecting Stamps 4

从起点断环为链,令遇到第一次的位置为左括号,第二次的位置为右括号,则初始可以得到的二元组种类数 \(z_i\) 等于所有右括号左边的左括号个数之和。而一次操作最优必然只会交换左边的右括号和右边的左括号,且恰好令二元组种类数增加 \(1\),因此对于固定的起点 \(i\),得到 \(k\) 中二元组的代价 \(w_i(K)=\max(K-z_i,0)\cdot X+C_i\)

所有 \(z_i\) 可以用线段树先求出来,而后对 \(z_i\) 排序,分别求 \(C_i-z_i\cdot X\) 的前缀最小值和 \(C_i\) 的后缀最小值即可单次 \(O(\log N)\) 回答询问,时间复杂度 \(O((N+Q)\log N)\)

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
using ll=long long;
const int N=5e5+9;
const ll inf=2e18;

struct Data{
	int a,b;
	ll s;
	Data(){}
	Data(int _a,int _b,ll _s){a=_a,b=_b,s=_s;}
	friend inline Data operator +(Data x,Data y){return Data(x.a+y.a,x.b+y.b,x.s+y.s+1ll*x.a*y.b);}
};
struct Node{
	int l,r;
	Data dat;
}tr[N<<4];

inline void PushUp(int x){tr[x].dat=tr[x<<1].dat+tr[x<<1|1].dat;}
inline void Build(int x,int l,int r){
	tr[x].l=l,tr[x].r=r;
	if(tr[x].l==tr[x].r) return ;
	int mid=tr[x].l+tr[x].r>>1;
	Build(x<<1,l,mid),Build(x<<1|1,mid+1,r);
	PushUp(x);
}
inline void Set(int x,int pos,int k){
	if(tr[x].l==tr[x].r) return tr[x].dat=!k?Data(1,0,0):Data(0,1,0),void();
	int mid=tr[x].l+tr[x].r>>1;
	if(pos<=mid) Set(x<<1,pos,k);
	else Set(x<<1|1,pos,k);
	PushUp(x);
}
inline Data Query(int x,int l,int r){
	if(l<=tr[x].l&&tr[x].r<=r) return tr[x].dat;
	int mid=tr[x].l+tr[x].r>>1;
	if(r<=mid) return Query(x<<1,l,r);
	else if(l>mid) return Query(x<<1|1,l,r);
	else return Query(x<<1,l,r)+Query(x<<1|1,l,r);
}

ll f[N<<1],g[N<<1],h[N<<1],c[N<<1];
int a[N<<1],b[N<<1],d[N<<1],p[N<<1],n,k,q;

signed main(){
	cin.tie(0),cout.tie(0);
	ios::sync_with_stdio(0);

	cin>>n>>k;
	for(int i=1;i<=(n<<1);i++) cin>>a[i];
	for(int i=1;i<=(n<<1);i++) cin>>c[i];

	Build(1,1,(n<<2));
	for(int i=1;i<=(n<<1);i++){
		Set(1,i,!!b[a[i]]);
		if(b[a[i]]){
			d[b[a[i]]]=i-b[a[i]];
			d[i]=(n<<1)-(i-b[a[i]]);
		}
		b[a[i]]=i;
	}
	for(int i=1;i<=(n<<1);i++){
		f[i]=Query(1,i,i+(n<<1)-1).s;
		Set(1,i+d[i],0);
		Set(1,i+(n<<1),1);
	}

	iota(p+1,p+(n<<1)+1,1);
	sort(p+1,p+(n<<1)+1,[](int i,int j){return f[i]<f[j];});
	sort(f+1,f+(n<<1)+1);

	g[0]=inf,h[(n<<1)+1]=inf;
	for(int i=1;i<=(n<<1);i++) g[i]=min(g[i-1],c[p[i]]-f[i]*k);
	for(int i=(n<<1);i>=1;i--) h[i]=min(h[i+1],c[p[i]]);

	cin>>q;
	while(q--){
		ll x;
		cin>>x;

		int i=upper_bound(f+1,f+(n<<1)+1,x)-f-1;
		
		cout<<min(g[i]+x*k,h[i+1])<<endl;
	}

	return 0;
}

[JOIST 2025] Migration Plan

直接做没啥前途,考虑对于每种深度维护一个动态开点线段树,每个点一开始全部插在其 \(dfn\) 处,有合并操作时直接合并两个线段树即可,则每个点的实际权值时其深度对应的线段树内该节点管辖区间的权值和。

时空复杂度均为 \(O((N+Q)\log N)\)

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
using ll=long long;
const int N=2e6+9;

vector<int> e[N],v[N];
int a[N],fa[N],dfn[N],dep[N],siz[N],n,q,dcnt;

inline void DFS(int x){
	siz[x]=1;
	dfn[x]=++dcnt;
	for(int y:e[x]) DFS(y),siz[x]+=siz[y];
}

struct Node{
	int lc,rc,dat;
}tr[N<<6];

int cnt;
inline int Allc(){return ++cnt;}
inline void PushUp(int x){tr[x].dat=tr[tr[x].lc].dat+tr[tr[x].rc].dat;}
inline void Insert(int &x,int L,int R,int pos,int k){
	if(!x) x=Allc();
	if(L==R) return tr[x].dat+=k,void();
	int mid=L+R>>1;
	if(pos<=mid) Insert(tr[x].lc,L,mid,pos,k);
	else Insert(tr[x].rc,mid+1,R,pos,k);
	PushUp(x);
}
inline void Merge(int &x,int y,int L,int R){
	if(!x||!y) return x=x|y,void();
	if(L==R) return tr[x].dat+=tr[y].dat,void();
	int mid=L+R>>1;
	Merge(tr[x].lc,tr[y].lc,L,mid),Merge(tr[x].rc,tr[y].rc,mid+1,R);
	PushUp(x);
}
inline int Query(int x,int L,int R,int l,int r){
	if(!x) return 0;
	if(l<=L&&R<=r) return tr[x].dat;
	int mid=L+R>>1;
	if(r<=mid) return Query(tr[x].lc,L,mid,l,r);
	else if(l>mid) return Query(tr[x].rc,mid+1,R,l,r);
	else return Query(tr[x].lc,L,mid,l,r)+Query(tr[x].rc,mid+1,R,l,r);
}

int root[N];

signed main(){
	cin.tie(0),cout.tie(0);
	ios::sync_with_stdio(0);

	cin>>n;
	for(int i=2;i<=n;i++){
		cin>>fa[i];
		dep[i]=dep[fa[i]]+1;
		e[fa[i]].push_back(i);
	}
	for(int i=1;i<=n;i++) cin>>a[i];

	DFS(1);
	for(int i=1;i<=n;i++) Insert(root[dep[i]],1,n,dfn[i],a[i]);
	
	cin>>q;
	while(q--){
		int op;
		cin>>op;
		if(op==1){
			int x,y;
			cin>>x>>y;
			Merge(root[y],root[x],1,n),root[x]=0;
		}else if(op==2){
			int x,v;
			cin>>x>>v;
			Insert(root[dep[x]],1,n,dfn[x],v);
		}else{
			int x;
			cin>>x;
			cout<<Query(root[dep[x]],1,n,dfn[x],dfn[x]+siz[x]-1)<<endl;
		}
	}
	
	return 0;
}

[JOI Open 2025] Lottery

首先直觉上来讲如果能取 \(k\) 轮则以下条件必定满足:

  • \(X_i+Y_i\geq k\)

  • \(\displaystyle \sum_{j=L_i}^{R_i} \min(X_j,k)\geq \dfrac 12 (R_i-L_i+1)k\)

  • \(\displaystyle \sum_{j=L_i}^{R_i} \min(Y_j,k)\geq \dfrac 12 (R_i-L_i+1)k\)

事实上可以对于满足上述条件的状态在删去没用的额度之后给出严格的构造。

上面的条件可以二分答案,用主席树维护第二第三条限制,在线段树上二分即可做到 \(O(N+Q\log N)\)

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
const int N=4e5+9;

vector<int> e[N];
int t[N],ans[N],n,d;

int vis[N],siz[N];
inline void GetGrv(int x,int fa,int tot,int &grv){
	bool flag=1;
	siz[x]=1;
	for(int y:e[x]){
		if(y==fa) continue ;
		if(vis[y]) continue ;
		GetGrv(y,x,tot,grv);
		flag&=(siz[y]<=tot/2);
		siz[x]+=siz[y];
	}
	flag&=(tot-siz[x]<=tot/2);
	if(flag) grv=x;
}
vector<int> v[N];
int dep[N],up[N],down[N];
inline void DFS(int x,int fa,int u,int d){
	siz[x]=1;
	u=min(t[x],u-1),d=min(d,t[x]-dep[x]);
	up[x]=(u<=0),down[x]=d;
	for(int y:e[x]){
		if(y==fa) continue ;
		if(vis[y]) continue ;
		dep[y]=dep[x]+1;
		DFS(y,x,u,d);
		siz[x]+=siz[y];
	}
}
inline void GetNode(int x,vector<int> &v){
    queue<int> q;
    q.push(x);
    while(q.size()){
        int x=q.front();
        q.pop();
        v.push_back(x);
        for(int y:e[x]){
            if(vis[y]) continue ;
            if(dep[y]<dep[x]) continue ;
            q.push(y);
        }
    }
}
struct Fenwick{
	int tr[N];
	inline void Add(int x,int k){x++;while(x<=n+1) tr[x]+=k,x+=x&-x;}
	inline int Ask(int x){x++;int sum=0;while(x) sum+=tr[x],x&=x-1;return sum;}
	inline int Ask(int l,int r){return Ask(r)-Ask(l-1);}
}T;
inline void Conquer(int x,int tot){
	GetGrv(x,-1,tot,x);	

	vis[x]=1,dep[x]=0;
	DFS(x,-1,n+1,n+1);
	vector<int> node;
	for(int y:e[x]) if(!vis[y]) GetNode(y,v[y]);
	GetNode(x,node);
	auto Calc=[&](vector<int> &v,int w){
		sort(v.begin(),v.end(),[](int i,int j){return dep[i]<dep[j];});
		int i=v.size(),j=0;
		while(i--){
			while(j<v.size()&&dep[v[i]]+dep[v[j]]<=d) T.Add(max(down[v[j++]],0),1);
			if(!up[v[i]]) ans[v[i]]+=T.Ask(dep[v[i]])*w;
			else ans[v[i]]+=j*w;
		}
		for(int k=0;k<j;k++) T.Add(max(down[v[k]],0),-1);
	};
	Calc(node,1);
	for(int y:e[x]) if(!vis[y]) Calc(v[y],-1),v[y].clear();

	for(int y:e[x]) if(!vis[y]) Conquer(y,siz[y]);
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);

	cin>>n>>d;
	for(int i=1;i<=n;i++) cin>>t[i];
	for(int i=1,u,v;i<n;i++){
		cin>>u>>v;
		e[u].push_back(v);
		e[v].push_back(u);
	}

	Conquer(1,n);

	for(int i=1;i<=n;i++) cout<<ans[i]<<endl;

	return 0;
}

[JOI Final 2026] Legendary Dango Eater

令甜的权重为 \(1\),辣的权重为 \(-1\)。设当前积压了权重为 \(x\) 的丸子,现在加进来权重为 \(y\) 的丸子,如果 \(x+y<0\) 则清空,否则令 \(x\leftarrow x+y\),撇出去 \(\left\lfloor \dfrac xK\right\rfloor\) 组,剩下令 \(x\leftarrow x\bmod K\)

考虑用平衡树维护上述操作,标记回收处理多次询问,时间复杂度 \(O((Q+N)\log Q)\)

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
using ll=long long;
const int N=5e5+9;

mt19937 rng(4649);
struct Data{
	ll rem,ans;
	Data(){}
	Data(ll _rem,ll _ans){rem=_rem,ans=_ans;}
	friend inline Data operator +(Data x,Data y){return Data(x.rem+y.rem,x.ans+y.ans);}
	friend inline void operator +=(Data &x,Data y){x.rem+=y.rem,x.ans+=y.ans;}
};
struct Node{
	Data val,tag;
	int lc,rc,fa,prio;
}tr[N];

int cnt;
inline int Allc(int rem){
	int x=++cnt;
	tr[x].val.rem=rem;
	tr[x].prio=rng();
	return x;
}
inline void Push(int x,Data tag){if(x) tr[x].tag+=tag,tr[x].val+=tag;}
inline void PushDown(int x){
	if(tr[x].tag.rem||tr[x].tag.ans){
		Push(tr[x].lc,tr[x].tag);
		Push(tr[x].rc,tr[x].tag);
		tr[x].tag.rem=tr[x].tag.ans=0;
	}
}
inline void PushUp(int x){
	tr[x].fa=0;
	if(tr[x].lc) tr[tr[x].lc].fa=x;
	if(tr[x].rc) tr[tr[x].rc].fa=x;
}
inline int Merge(int x,int y){
	if(!x||!y) return x|y;
	if(tr[x].prio<tr[y].prio){
		PushDown(x);
		tr[x].rc=Merge(tr[x].rc,y);
		PushUp(x);
		return x;
	}else{
		PushDown(y);
		tr[y].lc=Merge(x,tr[y].lc);
		PushUp(y);
		return y;
	}
}
inline void Split(int x,ll val,int &u,int &v){
	if(!x) return u=0,v=0,void();
	PushDown(x);
	if(tr[x].val.rem<=val) return u=x,Split(tr[x].rc,val,tr[x].rc,v),PushUp(x);
	else return v=x,Split(tr[x].lc,val,u,tr[x].lc),PushUp(x);
}
inline void GetNode(int x,vector<int> &node){
	if(!x) return ;
	PushDown(x);
	node.push_back(x);
	GetNode(tr[x].lc,node);
	GetNode(tr[x].rc,node);
}
inline void PushAll(int x){
	if(tr[x].fa) PushAll(tr[x].fa);
	PushDown(x);
}

int fa[N],siz[N];
inline int DFind(int x){return fa[x]==x?x:DFind(fa[x]);}
inline ll DCalc(int x){return tr[x].val.ans+(fa[x]==x?0:DCalc(fa[x]));}
inline int DMerge(int x,int y){
	if(!x||!y) return x|y;
	if(siz[x]<siz[y]) swap(x,y);
	fa[y]=x;
	siz[x]+=siz[y];
	tr[y].val.ans-=tr[x].val.ans;
	return x;
}

ll ans[N],k;
vector<int> ins[N],ers[N];
int a[N],ql[N],qr[N],n,q,root;
inline void Init(){for(int i=1;i<=q;i++) Allc(0),fa[i]=i,siz[i]=1;}
inline void Insert(int x){
	int L=0,M=0,T=0,R=0;
	Split(root,tr[x].val.rem,T,R);
	Split(T,tr[x].val.rem-1,L,M);
	M=DMerge(M,x);
	root=Merge(Merge(L,M),R);
}
inline void Add(ll x){
	int L=0,R=0;
	Push(root,Data(0,x/k)),x%=k;
	Split(root,k-x-1,L,R);
	Push(L,Data(x,0));
	Push(R,Data(x-k,1));
	root=Merge(R,L);
}
inline void Sub(ll x){
	int L=0,R=0,Z=0;
	vector<int> node;
	Split(root,x,L,R);
	GetNode(L,node);
	for(int u:node) tr[u].val.rem=0,Z=DMerge(Z,u);
	Push(R,Data(-x,0));
	tr[Z].lc=tr[Z].rc=0;
	root=Merge(Z,R);
}

signed main(){
	cin.tie(0),cout.tie(0);
	ios::sync_with_stdio(0);

	cin>>n>>q>>k;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=q;i++) cin>>ql[i]>>qr[i];

	Init();
	for(int i=1;i<=q;i++) ins[ql[i]].push_back(i),ers[qr[i]].push_back(i);
	for(int i=1;i<=n;i++){
		for(int j:ins[i]) Insert(j);
		if(i&1) Add(a[i]);
		else Sub(a[i]);
		for(int j:ers[i]) PushAll(DFind(j)),ans[j]=DCalc(j);
	}

	for(int i=1;i<=q;i++) cout<<ans[i]<<endl;

	return 0;
}

[JOI Final 2026] Garden 3

正着做不好做,考虑倒着扫描线。

以上边界举例,则相当于维护当前边界这条横着的直线上最大值是否不小于 \(X\),若不满足则向下平移,同时维护移出去的区间和新加进来的区间。然后时间向前拨的时候注意删除最后的时刻加的矩形的贡献。

时间复杂度 \(O(N\log N)\)

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
using ll=long long;
const int N=4e5+9;

ll x;
vector<int> ins[N<<1],ers[N<<1],xval,yval;
int U[N],D[N],L[N],R[N],W[N],u[N],d[N],l[N],r[N],h,w,n;

inline void Discretize(){
	xval=yval={0};
	for(int i=1;i<=n;i++){
		xval.push_back(U[i]);
		xval.push_back(D[i]+1);
		yval.push_back(L[i]);
		yval.push_back(R[i]+1);
	}
	xval.push_back(h+1),yval.push_back(w+1);
	sort(xval.begin(),xval.end());
	sort(yval.begin(),yval.end());
	xval.erase(unique(xval.begin(),xval.end()),xval.end());
	yval.erase(unique(yval.begin(),yval.end()),yval.end());
	h=xval.size()-1,w=yval.size()-1;
	for(int i=1;i<=n;i++){
		U[i]=lower_bound(xval.begin(),xval.end(),U[i])-xval.begin();
		D[i]=upper_bound(xval.begin(),xval.end(),D[i])-xval.begin()-1;
		L[i]=lower_bound(yval.begin(),yval.end(),L[i])-yval.begin();
		R[i]=upper_bound(yval.begin(),yval.end(),R[i])-yval.begin()-1;
		ins[U[i]].push_back(i);
		ers[D[i]].push_back(i);
	}
}
inline void Cretize(){
	for(int i=1;i<=n;i++){
		U[i]=xval[U[i]];
		D[i]=xval[D[i]+1]-1;
		L[i]=yval[L[i]];
		R[i]=yval[R[i]+1]-1;
	}
	for(int i=0;i<=h;i++) ins[i].clear(),ers[i].clear();
	h=xval.back()-1,w=yval.back()-1;
	xval.clear(),yval.clear();
}
inline void SwapUD(){
	for(int i=1;i<=n;i++){
		swap(U[i],D[i]);
		swap(u[i],d[i]);
		U[i]=h-U[i]+1;
		D[i]=h-D[i]+1;
		if(u[i]) u[i]=h-u[i]+1;
		if(d[i]) d[i]=h-d[i]+1;
	}
}
inline void SwapUL(){
	for(int i=1;i<=n;i++){
		swap(U[i],L[i]);
		swap(D[i],R[i]);
		swap(u[i],l[i]);
		swap(d[i],r[i]);
	}
	swap(h,w);
}

struct Node{
	int l,r;
	ll dat,tag;
}tr[N<<2];

inline void PushUp(int x){tr[x].dat=max(tr[x<<1].dat,tr[x<<1|1].dat);}
inline void Push(int x,ll k){tr[x].tag+=k,tr[x].dat+=k;}
inline void PushDown(int x){
	if(tr[x].tag){
		Push(x<<1,tr[x].tag);
		Push(x<<1|1,tr[x].tag);
		tr[x].tag=0;
	}
}
inline void Build(int x,int l,int r){
	tr[x].l=l,tr[x].r=r,tr[x].dat=tr[x].tag=0;
	if(tr[x].l==tr[x].r) return ;
	int mid=tr[x].l+tr[x].r>>1;
	Build(x<<1,l,mid),Build(x<<1|1,mid+1,r);
	PushUp(x);
}
inline void Modify(int x,int l,int r,ll k){
	if(l<=tr[x].l&&tr[x].r<=r) return Push(x,k);
	PushDown(x);
	int mid=tr[x].l+tr[x].r>>1;
	if(l<=mid) Modify(x<<1,l,r,k);
	if(r>mid) Modify(x<<1|1,l,r,k);
	PushUp(x);
}

inline void Solve(int *u){
	Discretize();

	Build(1,1,w);
	for(int i=n,j=0;i>=1;i--){
		while(j<=h&&tr[1].dat<x){
			for(int k:ers[j]) if(k<=i) Modify(1,L[k],R[k],-W[k]);
			j++;
			for(int k:ins[j]) if(k<=i) Modify(1,L[k],R[k],W[k]);
		}
		if(j<=h) u[i]=xval[j];
		if(U[i]<=j&&j<=D[i]) Modify(1,L[i],R[i],-W[i]);
	}

	Cretize();
}

signed main(){
	cin.tie(0),cout.tie(0);
	ios::sync_with_stdio(0);

	cin>>h>>w>>n>>x;
	for(int i=1;i<=n;i++) cin>>U[i]>>D[i]>>L[i]>>R[i]>>W[i];

	for(int p:{0,1}){
		for(int q:{0,1}){
			Solve(u);
			SwapUD();
		}
		SwapUL();
	}

	for(int i=1;i<=n;i++) cout<<(u[i]?1ll*(d[i]-u[i]+1)*(r[i]-l[i]+1):0)<<endl;

	return 0;
}

[JOI Final 2026] Teleporter 2

和 Exhibition 3 刻画的是对偶问题,依然可以刻画成放置至多 \(K\) 个点,使得所有线段至少都被一个点覆盖,由于线段都是开区间,令 \(T_i\leftarrow T_i-1\)

有朴素 DP \(f_{i,j}\) 表示当前考虑到 \(i\),放了 \(j\) 个点的答案,有转移 \(f_{i+1,q}\leftarrow f_{i,p}+w(p,q)\),其中 \(w(p,q)\) 是 严格被 \([p,q]\) 包含的 \([S_i.T_i]\) 的权值和,显然 \(w(p,q)\) 的二维差分始终为正,因此 \(w(p,q)\) 满足四边形不等式,即 \(f_{i,j}\)\(j\) 有凸性。考虑 wqs 二分把第二维消去,线段树维护转移,时间复杂度 \(O(N\log N\log V)\)

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
using ll=long long;
using bint=__int128;
const int N=1e5+9;
const ll C=1e14;
const bint inf=1e36;

struct SgT{
	struct Node{
		pair<bint,int> dat;
		bint tag;
	}tr[N<<2];
	inline void PushUp(int x){tr[x].dat=min(tr[x<<1].dat,tr[x<<1|1].dat);}
	inline void Push(int x,bint k){tr[x].dat.first+=k,tr[x].tag+=k;}
	inline void PushDown(int x){
		if(tr[x].tag){
			Push(x<<1,tr[x].tag);
			Push(x<<1|1,tr[x].tag);
			tr[x].tag=0;
		}
	}
	inline void Build(int x,int L,int R){
		tr[x].tag=0,tr[x].dat={inf,0};
		if(L==R) return ;
		int mid=L+R>>1;
		Build(x<<1,L,mid),Build(x<<1|1,mid+1,R);
	}
	inline void Set(int x,int L,int R,int pos,pair<bint,int> k){
		if(L==R) return tr[x].dat=k,void();
		PushDown(x);
		int mid=L+R>>1;
		if(pos<=mid) Set(x<<1,L,mid,pos,k);
		else Set(x<<1|1,mid+1,R,pos,k);
		PushUp(x);
	}
	inline void Modify(int x,int L,int R,int l,int r,bint k){
		if(l<=L&&R<=r) return Push(x,k);
		PushDown(x);
		int mid=L+R>>1;
		if(l<=mid) Modify(x<<1,L,mid,l,r,k);
		if(r>mid) Modify(x<<1|1,mid+1,R,l,r,k);
		PushUp(x);
	}
	inline pair<bint,int> Query(int x,int L,int R,int l,int r){
		if(l<=L&&R<=r) return tr[x].dat;
		PushDown(x);
		int mid=L+R>>1;
		if(r<=mid) return Query(x<<1,L,mid,l,r);
		else if(l>mid) return Query(x<<1|1,mid+1,R,l,r);
		else return min(Query(x<<1,L,mid,l,r),Query(x<<1|1,mid+1,R,l,r));
	}
}T;

vector<int> s[N];
pair<bint,int> f[N];
int sl[N],sr[N],sw[N],n,m,k;
inline pair<bint,int> Calc(ll k){
	T.Build(1,0,n);
	T.Set(1,0,n,0,f[0]={0,0});
	for(int i=1;i<=n;i++){
		f[i]=T.tr[1].dat;
		f[i].first-=k,f[i].second++;
		T.Set(1,0,n,i,f[i]);
		for(int j:s[i]) T.Modify(1,0,n,0,sl[j]-1,sw[j]);
	}
	return f[n];
}

signed main(){
	cin.tie(0),cout.tie(0);
	ios::sync_with_stdio(0);

	cin>>n>>m>>k,k=min(n,k+1);
	for(int i=1;i<=m;i++) cin>>sl[i]>>sr[i]>>sw[i],s[--sr[i]].push_back(i);

	ll l=-C-1,r=C+1;
	while(l+1<r){
		ll mid=l+r>>1;
		if(Calc(mid).second<=k) l=mid;
		else r=mid;
	}

	auto res=Calc(l);
	cout<<ll(res.first+k*l)<<endl;

	return 0;
}

[JOI Final 2026] Scarecrows 2

首先由于平面上每个点都必须覆盖至少 \(K\) 次,因此答案一定形如找出 \(K\) 组可以覆盖整个平面的半平面,相当于匹配 \(T_i=1,T_j=2,X_i\geq X_j\)\(T_i=3,T_j=4,Y_i\geq Y_j\),这样匹配 \(K\) 组。朴素方案显然费用流,然而费用流有凸性(每次选最小值斜率当然单调递增),因此先 wqs 二分把恰好 \(K\) 组的限制扔掉,接下来就是最小费用流。考虑模拟费用流,两维显然独立,以 \(X\) 轴为例,按 \(X_i\) 从小到大加入。如果加入的是前缀,则有匹配仍未匹配的权值最小的后缀,换掉之前某个前缀,以及什么都不干几个选项,按权值最小的操作即可。时间复杂度 \(O(N\log N\log V)\)

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
using ll=long long;
const int N=2e5+9;
const int C=2e9;

vector<int> o1,o2;
int t[N],x[N],y[N],c[N],n,k;
inline pair<ll,int> Calc(int k){
	ll sum=0;int cnt=0;
	for(auto &v:{o1,o2}){
		priority_queue<int> p,q;
		for(int i:v){
			if(~t[i]&1) p.push(-c[i]);
			else{
				int w1=p.size()?-p.top()+c[i]-k:INT_MAX;
				int w2=q.size()?c[i]-q.top():INT_MAX;
				if(min(w1,w2)>=0) continue ;
				if(w1<w2) p.pop(),q.push(c[i]),sum+=w1;
				else q.pop(),q.push(c[i]),sum+=w2;
			}
		}
		cnt+=q.size();
	}
	return {sum,cnt};
}

signed main(){
	cin.tie(0),cout.tie(0);
	ios::sync_with_stdio(0);

	cin>>n>>k;
	for(int i=1;i<=n;i++) cin>>t[i]>>x[i]>>y[i]>>c[i];

	for(int i=1;i<=n;i++){
		if(t[i]<=2) o1.push_back(i);
		else o2.push_back(i);
	}
	sort(o1.begin(),o1.end(),[](int i,int j){return x[i]!=x[j]?x[i]<x[j]:t[i]>t[j];});
	sort(o2.begin(),o2.end(),[](int i,int j){return y[i]!=y[j]?y[i]<y[j]:t[i]>t[j];});

	int l=-1,r=C+2;
	while(l+1<r){
		int mid=ll(l)+r>>1;
		if(Calc(mid).second<k) l=mid;
		else r=mid;
	}

	cout<<(l<=C?Calc(r).first+1ll*r*k:-1)<<endl;

	return 0;
}

[JOI Final 2026] Collecting Stamps 5

首先有长度限制想到点分治,考虑把路径拆成两半,对于起点那一半直接记录是否能敲章,对于终点的一半计算起点那一半要多长才能敲上章。

树状数组和双指针维护即可,时间复杂度 \(O(N\log^2 N)\)

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
const int N=4e5+9;

vector<int> e[N];
int t[N],ans[N],n,d;

int vis[N],siz[N];
inline void GetGrv(int x,int fa,int tot,int &grv){
	bool flag=1;
	siz[x]=1;
	for(int y:e[x]){
		if(y==fa) continue ;
		if(vis[y]) continue ;
		GetGrv(y,x,tot,grv);
		flag&=(siz[y]<=tot/2);
		siz[x]+=siz[y];
	}
	flag&=(tot-siz[x]<=tot/2);
	if(flag) grv=x;
}
vector<int> v[N];
int dep[N],up[N],down[N];
inline void DFS(int x,int fa,int u,int d){
	siz[x]=1;
	u=min(t[x],u-1),d=min(d,t[x]-dep[x]);
	up[x]=(u<=0),down[x]=d;
	for(int y:e[x]){
		if(y==fa) continue ;
		if(vis[y]) continue ;
		dep[y]=dep[x]+1;
		DFS(y,x,u,d);
		siz[x]+=siz[y];
	}
}
inline void GetNode(int x,vector<int> &v){
    queue<int> q;
    q.push(x);
    while(q.size()){
        int x=q.front();
        q.pop();
        v.push_back(x);
        for(int y:e[x]){
            if(vis[y]) continue ;
            if(dep[y]<dep[x]) continue ;
            q.push(y);
        }
    }
}
struct Fenwick{
	int tr[N];
	inline void Add(int x,int k){x++;while(x<=n+1) tr[x]+=k,x+=x&-x;}
	inline int Ask(int x){x++;int sum=0;while(x) sum+=tr[x],x&=x-1;return sum;}
	inline int Ask(int l,int r){return Ask(r)-Ask(l-1);}
}T;
inline void Conquer(int x,int tot){
	GetGrv(x,-1,tot,x);	

	vis[x]=1,dep[x]=0;
	DFS(x,-1,n+1,n+1);
	vector<int> node;
	for(int y:e[x]) if(!vis[y]) GetNode(y,v[y]);
	GetNode(x,node);
	auto Calc=[&](vector<int> &v,int w){
		sort(v.begin(),v.end(),[](int i,int j){return dep[i]<dep[j];});
		int i=v.size(),j=0;
		while(i--){
			while(j<v.size()&&dep[v[i]]+dep[v[j]]<=d) T.Add(max(down[v[j++]],0),1);
			if(!up[v[i]]) ans[v[i]]+=T.Ask(dep[v[i]])*w;
			else ans[v[i]]+=j*w;
		}
		for(int k=0;k<j;k++) T.Add(max(down[v[k]],0),-1);
	};
	Calc(node,1);
	for(int y:e[x]) if(!vis[y]) Calc(v[y],-1),v[y].clear();

	for(int y:e[x]) if(!vis[y]) Conquer(y,siz[y]);
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);

	cin>>n>>d;
	for(int i=1;i<=n;i++) cin>>t[i];
	for(int i=1,u,v;i<n;i++){
		cin>>u>>v;
		e[u].push_back(v);
		e[v].push_back(u);
	}

	Conquer(1,n);

	for(int i=1;i<=n;i++) cout<<ans[i]<<endl;

	return 0;
}

[JOI Final 2026] Baker

首先顾客 \(j\) 若可能在询问 \(i\) 中被满足,则一定有 \(B_i\leq T_j\leq B_i+L\),这意味着这样的 \(j\) 构成一个区间,假设是 \([l_i,r_i]\)

由于顾客和面包之间构成匹配关系,考虑 Hall 定理。令顾客为左部点,则左部点的领域只和左部点 \(T_j\) 最大值有关。根据匹配数公式 \(\displaystyle |S|-\max_{T\subseteq S} (|T|-|N(T)|)\),极端情形下左部点必然取满一个前缀。因此原式等价于 \(\displaystyle (r_i-l_i+1)-\max_{j=l_i}^{r_i}\left[(j-l_i+1)-\left\lfloor \dfrac{T_j+L-B_i}{A_i}\right\rfloor\right]\),化简后得到 \(\displaystyle (r_i-l_i+1)-\max_{j=l_i}^{r_i}\left\lceil \dfrac{jA_i-(T_j+L-B_i)}{A_i}\right\rceil\)

先忽略取整以及常数,则要求的就是 \(\displaystyle \max_{j=l_i}^{r_i} jx-T_j\),这显然可以用凸包维护。由于 \(r_i\)\(l_i\) 变大而变大,因此可以双指针。考虑双栈模拟队列维护凸包。每次操作形如将元素压入 A 栈以及从 B 栈弹出元素,若 B 栈为空将 A 栈元素全部倒序插入 B 栈中。考虑分别在 A 栈 B 栈各维护一个单调栈,若从 B 栈弹出栈顶,则加入之前被栈顶弹掉的直线。查询时分别查询 A 栈 B 栈的最大值即可。

时间复杂度 \(O(M+Q\log M)\)

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
using ll=long long;

const int N=2e6+9;
const int Q=4e5+9;

int ans[Q],m,q;
ll t[N],qa[Q],qb[Q],n,l;

struct Line{
	ll k,b;
	Line(){}
	Line(ll _k,ll _b){k=_k,b=_b;}
	inline ll operator ()(ll x){return k*x+b;}
}f[N];
inline ll FlDiv(ll x,ll y){return x/y-(x%y<0);}
inline ll ClDiv(ll x,ll y){return x/y+(x%y>0);}
inline ll Itsct(Line f,Line g){
	if(f.k>g.k) swap(f,g);
	return FlDiv(f.b-g.b,g.k-f.k);
}

vector<int> beat[N],s1,s2,v2;
inline void Push(int x){
	v2.push_back(x);
	while(s2.size()>1){
		ll p=Itsct(f[x],f[s2.end()[-1]]);
		ll q=Itsct(f[s2.end()[-1]],f[s2.end()[-2]]);
		if(p<=q) s2.pop_back();
		else break ;
	}
	s2.push_back(x);
}
inline void Pop(){
	if(!s1.size()){
		s2.clear();
		while(v2.size()){
			int x=v2.back();
			v2.pop_back();
			while(s1.size()>1){
				ll p=Itsct(f[x],f[s1.end()[-1]]);
				ll q=Itsct(f[s1.end()[-1]],f[s1.end()[-2]]);
				if(p>=q) beat[x].push_back(s1.back()),s1.pop_back();
				else break ;
			}
			s1.push_back(x);
		}
	}
	int x=s1.back();
	s1.pop_back();
	while(beat[x].size()) s1.push_back(beat[x].back()),beat[x].pop_back();
}
inline ll Query(ll x){
	ll ans=-1e18;
	if(s1.size()){
		int l=0,r=s1.size()-1;
		while(l<r){
			int mid=l+r>>1;
			if(x>Itsct(f[s1[mid]],f[s1[mid+1]])) r=mid;
			else l=mid+1;
		}
		ans=max(ans,f[s1[l]](x));
	}
	if(s2.size()){
		int l=0,r=s2.size()-1;
		while(l<r){
			int mid=l+r>>1;
			if(x<=Itsct(f[s2[mid]],f[s2[mid+1]])) r=mid;
			else l=mid+1;
		}
		ans=max(ans,f[s2[l]](x));
	}
	return ans;
}

signed main(){
	cin.tie(0),cout.tie(0);
	ios::sync_with_stdio(0);

	cin>>n>>m>>l>>q;
	for(int i=1;i<=m;i++) cin>>t[i],f[i]=Line(i,-t[i]);
	for(int i=1;i<=q;i++) cin>>qa[i]>>qb[i];

	int hd=1,tl=1;
	vector<int> o(q);
	iota(o.begin(),o.end(),1);
	sort(o.begin(),o.end(),[](int i,int j){return qb[i]<qb[j];});
	for(int i:o){
		while(hd<=m&&t[hd]<=qb[i]) Push(hd++);
		while(tl<=m&&t[tl]+l<qb[i]) Pop(),tl++;
		ll mx=-1e18;
		for(int j=tl;j<hd;j++) mx=max(mx,f[j](qa[i]));
		ans[i]=(hd-tl)-max(ClDiv(Query(qa[i])-l+qb[i],qa[i])-(tl-1),0ll);
	}

	for(int i=1;i<=q;i++) cout<<ans[i]<<endl;

	return 0;
}

[NordicOI 2026] Backrooms

先判断起点在原矩形内能不能走到终点在原矩形内对应的位置,以及需要在水平/垂直方向上跨过几个块。同时处理出一个点可以在水平/垂直方向上跨过几个块回到和子集在原矩形内对应相同的位置,这构成一个向量。显然如果两个点在同一个连通块内,它们的向量集是一样的。那么在扣掉硬性要求的移动之后,问题变为额外的移动距离能不能用起点的向量集表示出来。

找出这些向量是容易的,将原矩形内四联通的点之间分别连上权为 \([0,0]\) 的边,顶部和底部的点连权为 \([0,\pm1]\) 的边,左边和右边的点连上 \([\pm1,0]\) 的边,则向量集为所有简单环的权值,而简单环可以由一条非树边和树边构成的环线性组合而来,因此可以只考虑由一条非树边和树边构成的环的权值向量。

考虑把所有向量聚合成 \(\{[a,0],[b,c]\}\) 的形式,每次加入向量 \([x,y]\) 时先和 \([b,c]\) 辗转相除,得到 \([a',0]\)\([b',c']\),则新的向量集为 \(\{[\gcd(a,a'),0],[b',c']\}\),同时令 \(b'\)\(\gcd(a,a')\) 取模保证数据不会溢出。最后判断答案向量是否能被 \(\{[a,0],[b,c]\}\) 组成出来即可。

时间复杂度 \(O(RC\log V+Q)\),精细实现应该可以做到 \(O(RC+Q)\)

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
using ll=long long;
const int N=1e3+9;
const int M=1e6+9;
const int lgM=2e1;
const int dx[4]={1,0,-1,0};
const int dy[4]={0,1,0,-1};

struct Vec{
	ll x,y;
	Vec(){}
	Vec(ll _x,ll _y){x=_x,y=_y;}
	friend inline Vec operator +(Vec x,Vec y){return Vec(x.x+y.x,x.y+y.y);}
	friend inline Vec operator -(Vec x,Vec y){return Vec(x.x-y.x,x.y-y.y);}
	friend inline Vec operator *(ll k,Vec x){return Vec(x.x*k,x.y*k);}
};

char a[N][N];
Vec vdep[M],A[M],B[M];
vector<pair<int,Vec>> e[M];
int vis[M],rdep[M],bel[M],n,m,q;
inline int Id(int i,int j){return i*m+j+1;}
inline ll FlrDiv(ll x,ll y){return x/y+(x%y<0);}
inline void DFS(int x,int r){
	vis[x]=1,bel[x]=r;
	for(auto p:e[x]){
		int y=p.first;
		if(vis[y]){
			if(rdep[x]>=rdep[y]){
				Vec v=vdep[x]-vdep[y]+p.second;
				if(v.y<0) v=-1*v;
				while(v.y){
					B[r]=B[r]-(B[r].y/v.y)*v;
					swap(v,B[r]);
				}
				A[r].x=__gcd(A[r].x,v.x);
				if(A[r].x) B[r].x%=A[r].x;
			}
			continue ;
		}
		rdep[y]=rdep[x]+1;
		vdep[y]=vdep[x]+p.second;
		DFS(y,r);
	}
}
inline Vec Dist(int x,int y){return vdep[y]-vdep[x];}

signed main(){
	cin.tie(0),cout.tie(0);
	ios::sync_with_stdio(0);

	cin>>n>>m;
	for(int j=n-1;~j;j--){
		for(int i=0;i<m;i++) cin>>a[i][j];
	}

	for(int i=0;i<m;i++){
		for(int j=0;j<n;j++){
			if(a[i][j]=='#') continue ;
			for(int o:{0,1,2,3}){
				int ii=i+dx[o],jj=j+dy[o];
				if(ii<0||ii>=m||jj<0||jj>=n) continue ;
				if(a[ii][jj]=='#') continue ;
				e[Id(i,j)].push_back({Id(ii,jj),{0,0}});
			}
		}
	}
	for(int i=0;i<m;i++){
		if(a[i][0]=='#'||a[i][n-1]=='#') continue ;
		e[Id(i,0)].push_back({Id(i,n-1),{0,-1}});
		e[Id(i,n-1)].push_back({Id(i,0),{0,1}});
	}
	for(int i=0;i<n;i++){
		if(a[0][i]=='#'||a[m-1][i]=='#') continue ;
		e[Id(0,i)].push_back({Id(m-1,i),{-1,0}});
		e[Id(m-1,i)].push_back({Id(0,i),{1,0}});
	}

	for(int i=1;i<=n*m;i++) if(!vis[i]) vis[i]=1,DFS(i,i);

	cin>>q;
	while(q--){
		ll i,j,k,l;
		cin>>i>>j>>k>>l;

		Vec s(FlrDiv(i,m),FlrDiv(j,n)),t(FlrDiv(k,m),FlrDiv(l,n));
		int x=Id(i%=m,j%=n),y=Id(k%=m,l%=n);

		if(a[i][j]=='#'||a[k][l]=='#') assert(0);
		if(bel[x]!=bel[y]){
			cout<<"No"<<endl;
			continue ;
		}

		int z=bel[x];
		Vec p=(t-s)-Dist(x,y);

		if(!B[z].y){
			if(p.y){
				cout<<"No"<<endl;
				continue ;
			}
		}else{
			if(p.y%B[z].y){
				cout<<"No"<<endl;
				continue ;
			}else p.x-=p.y/B[z].y*B[z].x,p.y=0;
		}
		if(!A[z].x){
			if(p.x){
				cout<<"No"<<endl;
				continue ;
			}
		}else{
			if(p.x%A[z].x){
				cout<<"No"<<endl;
				continue ;
			}else p.x=0;
		}

		cout<<"Yes"<<endl;
	}

	return 0;
}

[NordicOI 2026] Catching Apples

定义 \(i \preceq j\) 满足当且仅当 \(t_i\leq t_j\wedge |x_j-x_i|\leq t_j-t_i\),则要求的就是 \(\preceq\) 的最小链覆盖。考虑用 Dilworth 定理转成最长反链,而 \(i\npreceq j\wedge j\npreceq i\) 等价于 \((x_i-t_i,x_i+t_i)\)\((x_j-t_j,x_j+t_j)\) 存在偏序关系,这又构成了另一个最长链问题,数据结构优化 DP 即可。构造方案考虑直接输出以 \(i\) 结尾的最长链长度即可,正确性显然。时间复杂度 \(O(N\log N)\)

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
const int N=2e5+9;

vector<int> o[N],xv,yv;
int x[N],y[N],f[N],n;

struct Fenwick{
	int tr[N];
	inline void Insert(int x,int k){while(x<=n) tr[x]=max(tr[x],k),x+=x&-x;}
	inline int Ask(int x){int res=0;while(x) res=max(res,tr[x]),x&=x-1;return res;}
}T;

signed main(){
	cin.tie(0),cout.tie(0);
	ios::sync_with_stdio(0);

	cin>>n;
	for(int i=1,t,p;i<=n;i++){
		cin>>t>>p;
		x[i]=p-t;
		y[i]=p+t;
	}

	xv=vector<int>(x+1,x+n+1),xv.push_back(INT_MIN),sort(xv.begin(),xv.end());
	yv=vector<int>(y+1,y+n+1),yv.push_back(INT_MIN),sort(yv.begin(),yv.end());
	xv.erase(unique(xv.begin(),xv.end()),xv.end());
	yv.erase(unique(yv.begin(),yv.end()),yv.end());
	for(int i=1;i<=n;i++){
		x[i]=lower_bound(xv.begin(),xv.end(),x[i])-xv.begin();
		y[i]=lower_bound(yv.begin(),yv.end(),y[i])-yv.begin();
		o[x[i]].push_back(i);
	}
	for(int i=1;i<xv.size();i++){
		for(int j:o[i]) f[j]=T.Ask(y[j]-1)+1;
		for(int j:o[i]) T.Insert(y[j],f[j]);
	}

	cout<<*max_element(f+1,f+n+1)<<endl;
	for(int i=1;i<=n;i++) cout<<f[i]<<' ';cout<<endl;

	return 0;
}
posted @ 2026-04-02 20:51  JoeyJiang  阅读(4)  评论(0)    收藏  举报