2025 省选做题记录(二)


By DaiRuichen007



Round #37 - 2024.12.12

A. [CF1997F] Chips on a Line

Problem Link

题目大意

给定 n 个棋子放在 1x 上,每次操作可以:

  • 把一个 i+1 上的棋子替换成两个分别在 i,i1 上的棋子,或执行逆操作。
  • 1/2 上的棋子移动到 2/1 上。

求有多少种放棋子的方案使得经过上述操作,能得到的棋子数量最小值是 m

数据范围:n,m1000,x10

思路分析

容易发现最优策略一定是把棋子都放在 1 上再推平。

一个位置 i 上的棋子对应 Fibi1 上的棋子。

假设最终在 1 上有 w 个棋子,那么得到的最小棋子数量就是 Fib 进制下 wpopcount

因此我们只关心最终 1 上有多少棋子。

fu,i,j 表示考虑位置 [1,u],填了 i 个棋子,对应最终 1 上的 j 个棋子。

统计答案的时候判断 popcount(j)=m 是否成立。

时间复杂度:O(n2xFibx)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1005,MAXV=65005,MOD=998244353;
inline void add(int &x,const int &y) { x=(x+y>=MOD)?x+y-MOD:x+y; }
int f[MAXV][MAXN],fib[50];
int pcnt(int x) {
	int s=0;
	for(int k=30;k>=1;--k) if(x>=fib[k]) x-=fib[k],++s;
	return s;
}
signed main() {
	fib[1]=fib[2]=1;
	for(int i=3;i<=30;++i) fib[i]=fib[i-1]+fib[i-2];
	int n,q,m;
	scanf("%d%d%d",&n,&q,&m);
	int up=n*fib[q];
	f[0][0]=1;
	for(int i=q;i;--i) {
		int z=fib[i];
		for(int j=0;j+z<=up;++j) for(int k=0;k<n&&k*z<=j;++k) {
			add(f[j+z][k+1],f[j][k]);
		}
	}
	int ans=0;
	for(int j=1;j<=up;++j) if(pcnt(j)==m) add(ans,f[j][n]);
	printf("%d\n",ans);
	return 0;
}



B. [CF2002F2] Court Blue

Problem Link

题目大意

给定两个变量 x,y 初始为 0,每次操作选择其中一个 +1,要求始终保证:x[0,n],y[0,m],gcd(x,y)1,求可能的最大 px+qy

数据范围:n,m2×107

思路分析

打表观察,可以发现当 x,y 较小的时候,几乎总是有合法的构造,因此可以假设 nx<Bmy<B(x,y) 总是能被生成。

然后直接 dp 即可,fi,j 表示 x=ni,y=mj 是否能得到。

时间复杂度 O(B2logV),取 B=120 能通过此题。

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int L=120;
bool f[L+5][L+5];
int bgcd(int x,int y) {
	if(!x||!y||x==y) return x|y;
	if(~x&1) return y&1?bgcd(x>>1,y):(bgcd(x>>1,y>>1)<<1);
	return y&1?(x<y?bgcd((y-x)>>1,x):bgcd((x-y)>>1,y)):bgcd(x,y>>1);
}
void solve() {
	int n,m,a,b;
	scanf("%d%d%d%d",&n,&m,&a,&b);
	memset(f,0,sizeof(f));
	int u=max(1,n-L),v=max(1,m-L);
	ll ans=0;
	for(int i=0;i<=n-u;++i) for(int j=0;j<=m-v;++j) {
		if(!i||!j) f[i][j]=1;
		else if(bgcd(i+u,j+v)<=1) f[i][j]=f[i-1][j]|f[i][j-1];
		if(f[i][j]) ans=max(ans,1ll*a*(i+u)+1ll*b*(j+v));
	}
	printf("%lld\n",ans);
}
signed main() {
	int T; scanf("%d",&T);
	while(T--) solve();
	return 0;
}



C. [CF1995E2] Let Me Teach You a Lesson

Problem Link

题目大意

给定 a1a2n,可以选择交换 ai,ai+n,最小化 max(a2i1+a2i)min(a2i1+a2i)

数据范围:n105

思路分析

最小化极差难以维护,可以考虑枚举 min(a2i1+a2i),目标变成最小化 max(a2i1,a2i)

有一个朴素的 dp:fi,0/1 表示考虑了 a[1,i],是否交换了 ai,ai+n 的方案。

转移为形如 (max,min) 矩阵乘法,而 min(a2i1+a2i) 相当于限定有一些转移是不可以选用的。

那么我们从小到大枚举可能的 min(a2i1+a2i),可以使用的转移越来越少,只有 O(n) 次修改,用线段树维护动态 dp 的过程即可。

时间复杂度 O(nlogn)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int inf=2e9+5;
struct Mat {
	array <array<int,2>,2> a;
	Mat() { a={inf,inf,inf,inf}; }
	inline friend Mat operator *(const Mat &u,const Mat &v) {
		Mat w;
		for(int i:{0,1}) for(int j:{0,1}) {
			w.a[i][j]=min(max(u.a[i][0],v.a[0][j]),max(u.a[i][1],v.a[1][j]));
		}
		return w;
	}
};
const int MAXN=2e5+5;
int n,a[MAXN][2];
Mat f[MAXN];
struct zKyGt1 {
	Mat tr[1<<19];
	int N;
	void init() {
		for(N=1;N<=n;N<<=1);
		for(int i=1;i<(N<<1);++i) tr[i].a={0,inf,inf,0};
		for(int i=1;i<=n;++i) tr[i+N]=f[i];
		for(int i=N-1;i;--i) tr[i]=tr[i<<1]*tr[i<<1|1];
	}
	void upd(int x) {
		for(tr[x+N]=f[x],x=(x+N)>>1;x;x>>=1) tr[x]=tr[x<<1]*tr[x<<1|1];
	}
}	T;
void solve() {
	scanf("%d",&n);
	for(int i=1;i<=n;++i) scanf("%d",&a[i][0]);
	for(int i=1;i<=n;++i) scanf("%d",&a[i][1]);
	vector <array<int,4>> op;
	if(n%2==0) {
		for(int i=1;i<=n;++i) {
			if(i%2==0) { f[i].a={0,0,0,0}; continue; }
			for(int x:{0,1}) for(int y:{0,1}) {
				int l=a[i][x]+a[i+1][y],r=a[i][x^1]+a[i+1][y^1];
				f[i].a[x][y]=max(l,r);
				op.push_back({min(l,r),i,x,y});
			}
		}
	} else {
		a[n+1][0]=a[1][1],a[n+1][1]=a[1][0];
		for(int i=1;i<=n;++i) {
			for(int x:{0,1}) for(int y:{0,1}) {
				if(i&1) f[i].a[x][y]=a[i][x]+a[i+1][y];
				else f[i].a[x][y]=a[i][x^1]+a[i+1][y^1];
				op.push_back({f[i].a[x][y],i,x,y});
			}
		}
	}
	T.init();
	int ans=inf;
	sort(op.begin(),op.end());
	for(auto it:op) {
		Mat &I=T.tr[1];
		int v=min(I.a[0][0],I.a[1][1]);
		if(v==inf) break;
		ans=min(ans,v-it[0]);
		f[it[1]].a[it[2]][it[3]]=inf;
		T.upd(it[1]);
	}
	printf("%d\n",ans);
}
signed main() {
	int _; scanf("%d",&_);
	while(_--) solve();
	return 0;
}



D. [CF2001E2] Deterministic Heap

Problem Link

题目大意

给定一棵高为 n 的满二叉树,进行 k 次操作,每次把一条到根的链上点权 +1,容易发现形成大根堆。偶

执行两次 pop 操作,要求每次操作访问到的非叶子节点的左右儿子权值不相等,求方案数。

数据范围:n100,k500

思路分析

称点权较大的儿子为重儿子,根的重儿子为重链。

能生成题目中的结构当且仅当 aials+ars,可以进行一次 pop 操作相当于每个点的重儿子左右子树权值不同。

那么可以 dp:hi,j 表示深度为 i 的堆,根节点权值为 j 的合法方案数,由于轻儿子中无限制,还要记录 gi,j 表示没有 pop 限制的方案数。

然后考虑第二次 pop,此时如果 u 在重链上,那么 au 变成其重儿子 v 的点权 av,而 av 变成原先 v 的重儿子 aw

我们的限定条件变成 awax,其中 xu 的轻儿子。

因此在 dp 状态中要记录重儿子的权值,fi,j,t 表示重儿子权值为 t 的方案数。

然后要讨论 aw,ax 的大小关系决定递归到哪个子问题中:

  • 如果 aw>ax,那么相当于 u 的重子树支持 pop 两次,从 fi1,av, 转移,而 u 的轻子树不需要 pop,从 gi1,ax 转移。
  • 否则左右子树都能 pop 一次,从 hi1,av,,hi1,ax, 转移。

转移使用前缀和优化。

时间复杂度 O(nk2)

代码呈现

#include<bits/stdc++.h>
using namespace std;
namespace FastMod {
typedef unsigned long long ull;
typedef __uint128_t uLL;
ull b,q,r; uLL m;
inline void init(const ull &B) { b=B,m=(uLL(1)<<64)/B; }
inline ull mod(const ull &a) {
	r=a-((m*a)>>64)*b;
	return r>=b?r-b:r;
}
}
#define o(x) FastMod::mod(x)
int n,k,MOD,f[505][505],g[505][505],s[505],h[505],u[505][505],t[505][505];
inline void add(int &x,const int &y) { x=(x+y>=MOD)?x+y-MOD:x+y; }
void solve() {
	memset(f,0,sizeof(f));
	memset(h,0,sizeof(h));
	scanf("%d%d%d",&n,&k,&MOD),FastMod::init(MOD);
	for(int i=1;i<=k;++i) for(int j=0;j<i&&i+j<=k;++j) f[i+j][i]+=2;
	for(int i=1;i<=k;++i) for(int j=0;j<=i;++j) add(f[i][j],f[i-1][j]),u[i][j]=f[i][j];
	for(int i=0;i<=k;++i) for(int j=0;i+j<=k;++j) ++h[i+j];
	for(int i=1;i<=k;++i) add(h[i],h[i-1]);
	for(int d=3;d<=n;++d) {
		memset(t,0,sizeof(t));
		for(int i=1;i<=k;++i) for(int j=0;j<=i;++j) {
			t[i][j]=u[i][j],add(t[i][j],j?t[i][j-1]:0);
		}
		
		memset(g,0,sizeof(g));
		for(int i=1;i<=k;++i) {
			memset(s,0,sizeof(s));
			for(int j=i;j>=0;--j) s[j]=f[i][j],add(s[j],s[j+1]);
			for(int j=0;j<i&&i+j<=k;++j) {
				g[i+j][i]=o(g[i+j][i]+2ll*h[j]*s[j+1]+2ll*t[i][j-1]*t[j][j]);
			}
		}
		for(int i=1;i<=k;++i) for(int j=0;j<=i;++j) add(g[i][j],g[i-1][j]);
		memcpy(f,g,sizeof(f));
		
		memset(g,0,sizeof(g));
		for(int i=1;i<=k;++i) for(int j=0;j<i&&i+j<=k;++j) {
			g[i+j][i]=o(g[i+j][i]+2ll*t[i][i]*h[j]);
		}
		for(int i=1;i<=k;++i) for(int j=0;j<=i;++j) add(g[i][j],g[i-1][j]);
		memcpy(u,g,sizeof(u));
		
		memset(s,0,sizeof(s));
		for(int i=0;i<=k;++i) for(int j=0;i+j<=k;++j) s[i+j]=o(s[i+j]+1ll*h[i]*h[j]);
		for(int i=1;i<=k;++i) add(s[i],s[i-1]);
		memcpy(h,s,sizeof(h));
	}
	int ans=0;
	for(int i=0;i<=k;++i) add(ans,f[k][i]);
	printf("%d\n",ans);
}
signed main() {
	int T; scanf("%d",&T);
	while(T--) solve();
	return 0;
}



E. [CF2002G] Lattice Optimizing

Problem Link

题目大意

给定 n×n 网格,边带权,定义路径权值为经过的边权 mex,求从左上走到右下的格路的最大权值。

数据范围:n20

思路分析

很显然无法优化维护 mex(S) 的过程,必须爆搜,因此需要 Meet-in-Middle 平衡复杂度。

那么思路就是选取一条副对角线,每条路径恰好过其中一个点,爆搜出从起点 / 终点到对角线上每个点的路径对应的权值集合。

求答案是否 k 相当于判断交点两侧路径权值集合进行 OR 卷积后,是否有元素包含二进制位 0k1

无法用 FWT 处理 OR 卷积状物,因此必须在一侧枚举子集。

那么设这条副对角线在 x+y=k 上,那么左侧枚举子集并插入哈希表,右侧每条格路直接在哈希表中查值即可判定答案是否 k,如果满足就更新 kk+1 继续判断。

时间复杂度 O(n(3k+22nk))

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
struct HshT {
	static const int MAXN=(1<<26)+5,P=3e7+1;
	int sz,hd[P],to[MAXN];
	ll w[MAXN];
	void ins(ll x) {
		int p=x%P;
		for(int i=hd[p];i;i=to[i]) if(w[i]==x) return ;
		w[++sz]=x,to[sz]=hd[p],hd[p]=sz;
	}
	bool qry(ll x) {
		int p=x%P;
		for(int i=hd[p];i;i=to[i]) if(w[i]==x) return true;
		return false;
	}
	void init() {
		for(int i=1;i<=sz;++i) hd[w[i]%P]=0,to[i]=w[i]=0;
		sz=0;
	}
}	H;
int n,k,z,a[45][45],b[45][45];
const ll B=1ll<<40;
void dfs(int x,int y,ll s) {
	if(x+y==k+2) {
		while(H.qry((x*B)|(((1ll<<z)-1)&(~s)))) ++z;
		return ;
	}
	if(x>1) dfs(x-1,y,s|1ll<<a[x-1][y]);
	if(y>1) dfs(x,y-1,s|1ll<<b[x][y-1]);
}
void solve() {
	scanf("%d",&n),H.init(),z=1;
	for(int i=1;i<n;++i) for(int j=1;j<=n;++j) scanf("%d",&a[i][j]);
	for(int i=1;i<=n;++i) for(int j=1;j<n;++j) scanf("%d",&b[i][j]);
	k=2*(n-1)/3;
	for(int p=0;p<(1<<k);++p) {
		int i=1,j=1; ll s=0;
		for(int o=0;o<k;++o) {
			if(p>>o&1) s|=1ll<<a[i++][j];
			else s|=1ll<<b[i][j++];
		}
		for(ll t=s;;t=(t-1)&s) {
			H.ins((i*B)|t);
			if(!t) break;
		}
	}
	dfs(n,n,0);
	printf("%d\n",z-1);
}
signed main() {
	int T; scanf("%d",&T);
	while(T--) solve();
	return 0;
}



*F. [CF2002H] Counting 101

Problem Link

题目大意

给定一个序列 a,一次操作可以将 ai1,ai,ai+1 替换成 max(ai1+1,ai,ai+1+1)

定义一个序列的权值为:保证 maxaim 的情况下能进行最多的操作次数。

给定 n,m,对于每个 k 求有多少序列 a1an 权值为 k

数据范围:n130,m30

思路分析

先刻画序列的权值,如果初始没有 ai=m 的元素,那么剩下的元素个数不超过 2

这是显然的,我们不断操作序列最中间的元素,此时最终的元素是 maxai+[imid]m

那么如果有 ai=m 的元素,很显然如果对这个元素操作,一定是以 ai 为中心进行操作。

观察相邻两个 m 之间的元素,我们发现最优解有两种情况:

  • 一种是先把以 m 为中心的操作全部做完,剩下 <m 的每一段元素用上述 maxai<m 的元素删剩下 12 个。
  • 如果想剩下 0 个元素,那么我们要先在区间内操作几次,在不产生 =m 的元素的情况下使得区间内的元素被两端 ai=m 的元素的操作覆盖。

因此我们只关心每个 ai=m 的位置上操作了几次,记为 gi,j,表示第 im 上操作了 j 次,最少能剩下几个元素。

转移就是 gi1,j1gi,j2,设两个 m 中间夹着的连续段长度为 L,转移系数就是这个 L 个元素最少能剩下几个:

  • 首先要求 j1+j2L

  • 如果 Lmod2=1,那么转移系数就是 +1

  • 否则考虑这一段区间内部,在值域 [1,m1] 的情况下最少剩几个元素,很显然这就是值域 m1 的子问题,设答案为 p

    如果 j1+j2p,系数是 0,否则系数是 1

然后解决原问题需要套一个 dp of dp,把 gi 压入状态。

gi 的信息量太大,需要压缩。

对于这种转移系数极差很小的情况,可以猜测 gi 的极差也很小。

因此定义 gi,j=gi,jminkgi,k,打表发现:gi 的形式和转移系数完全相似,都形如 1,2,1,0,1,2,1

即某种奇偶性的下表上全是 1,另一种上是若干个 2 加若干个 0 再加若干个 2,证明可以分讨归纳。

那么记录一个 gi,只需要 (mingi,l,r,len),表示长度为 len,取到 mingi 的下标是 [l,r] 中间和 l 同奇偶的位置。

此时状态仍然过大,进一步发现: >r 的元素意义较小,因为转移函数相对单调,较大的位置对 dp 转移几乎没有贡献。

事实上可以证明 (mingi,l,r,len)(mingi,l,r,),即这两个 gi 进行内层 dp 的结果相同,从而优化掉一维状态。

这还不够,我们发现同时记录 [l,r] 的复杂度难以承受,进一步观察可以发现 (mingi,l,r)(mingi,l,)+(mingi,lmod2,r)(mingi,lmod2,)

即最终内层 dp 的答案等价于这三个内层 dp 的答案相加减的结果。

感性理解就是用 [l,r] 转移得到的 gi,j[lmod2,r),[l,) 转移得到的 gi,j 中的一个相同,即最优的转移点不可能同时存在于 <l>r 的位置。

那么每次转移后立刻分拆状态,转移的时候分讨 g 形态变化即可。

时间复杂度 O(n5m)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int n=130,m=30,MOD=1e9+7;
inline void add(int &x,const int &y) { x=(x+y>=MOD)?x+y-MOD:x+y; }
int pw[35][135],dp[35][135][135],f[135][135][135],g[135][135][135];
//dp[v,i,j]: val[1,v], len=i, max del=j
//f[i,j,l],g[i,j,r]: len=i, g0=j, [L0,R0] = [l,inf]/[0,r]
signed main() {
	for(int i=0;i<=m;++i) for(int j=pw[i][0]=1;j<=n;++j) pw[i][j]=1ll*pw[i][j-1]*i%MOD;
	dp[0][0][0]=1;
	for(int v=1;v<=m;++v) {
		memset(f,0,sizeof(f));
		memset(g,0,sizeof(g));
		g[0][0][0]=1;
		for(int i=0;i<=n+1;++i) for(int j=0;j<=i;++j) {
			for(int l=0;l<=i;++l) if(f[i][j][l]) add(f[i][j][l&1],MOD-f[i][j][l]); //split -> [0,l]-[0,inf]
			for(int l=0;l<=i;++l) if(f[i][j][l]) {
				const int w=f[i][j][l];
				for(int k=0;i+k<=n;++k) {
					const int z=1ll*w*pw[v-1][k]%MOD;
					if(l>k) { //can't +0
						int nr=k-((l+1)&1); //trans +1, (a+l-k)%2=1
						if(nr>=0) add(g[i+k+1][j+2][nr],z); //max a = k-(l&1^1)
						else add(g[i+k+1][j+3][0],z); //can't +1
					} else add(g[i+k+1][j+1][k-l],z); //a+l<=k
				}
			}
			for(int r=0;r<=i;++r) if(g[i][j][r]) {
				const int w=g[i][j][r],l=r&1; //[l,r]
				for(int k=0;i+k<=n;++k) {
					const int z=1ll*w*pw[v-1][k]%MOD;
					if(l>k) { //can't +0 (same)
						int nr=k-((l+1)&1);
						if(nr>=0) add(g[i+k+1][j+2][nr],z);
						else add(g[i+k+1][j+3][0],z); 
					} else {
						//d<=a+r,a+l<=k -> a in [d-r,k-l]
						//split to [d-r,inf] + [0,k-l] - [0,inf]
						add(g[i+k+1][j+1][k-l],z);
						int *nw=f[i+k+1][j+1],*Z=dp[v-1][k];
						for(int d=r+(k-l)%2;d<=k;d+=2) if(Z[d]) {
							nw[d-r]=(nw[d-r]+1ll*w*Z[d])%MOD;
						}
					}
				}
			}
		}
		for(int i=1;i<=n+1;++i) for(int j=1;j<=i;++j) {
			//0-th & i-th element is virtual, j=g[0]
			for(int l=0;l<=i;++l) {
				add(dp[v][i-1][j-1+(!l?0:(l&1?1:2))],f[i][j][l]);
			}
			for(int r=0;r<=i;++r) {
				add(dp[v][i-1][j-1+(r&1)],g[i][j][r]);
			}
		}
	}
	int T; scanf("%d",&T);
	for(int N,M;T--;) {
		scanf("%d%d",&N,&M);
		for(int K=0;K<=(N-1)/2;++K) printf("%d ",dp[M][N][N-2*K]); puts("");
	}
	return 0;
}



*G. [CF1994H] Fortnite

Problem Link

题目大意

交互器里有底数 p 和模数 m,每次可以询问字符串 s,得到哈希值 h(s)=c(si)pimodm,其中 c(a)=1,c(b)=2,,c(z)=26,在三次询问内求出 p,m

数据范围:26<p50,p+1<m2×109

思路分析

定义 v(s)=c(si)pi,即 h(s) 取模前的结果。

求出 p 是简单的,因为 h(aa)=p+1modm=p+1,因此可以直接求出 p

然后我们要通过两次询问确定 m,一个想法是:利用 mv(s)h(s) 挖掘性质,但这样显然难以做到两次询问。

那么假设 v(s)h(s)=km,如果能找到一个字符串 t 使得 v(t)h(t)=(k1)m,很显然能直接得到答案。

这就要求 v(t)[v(s)h(s)m,v(s)h(s)),尝试把限制中的 m 去掉,注意到 m<h(s),因此可以钦定 v(t)v(s)2h(s),依然满足条件。

转成左开右闭区间,就是 v(t)(v(s)12h(s),v(s)1h(s)]

我们要保证 v(s)1h(s)v(t)[0,h(s)]

v(s)1h(s) 表示成 p 进制数 a,b,逐位构造 v(t)p 进制表示 c

  • 如果 ai>bi,取 ci=aibi,此时 1ciai,且最终 abc 上这一位为 0

  • 否则向 ai+1 借位,取 ci=ai(借位前的),那么 abc 上这一位的贡献为 pbi

    由于 ai 至多被借一位,所以 bi>ai1,因此取 ai=26 时,bi>25>pbi,所以此时 pbi<bi

从而每一位的贡献都小于 b 在对应位的贡献。

那么 s 取全 z 串,并且保证 v(s)>m 即可。

时间复杂度 O(1)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
int a[15],b[15],c[15],p;
int hsh(string s) {
	cout<<"? "<<s<<endl;
	int o; cin>>o; return o;
}
ll val(string s) {
	reverse(s.begin(),s.end());
	ll z=0;
	for(char o:s) z=z*p+o-'a'+1;
	return z;
}
void solve() {
	p=hsh("aa")-1;
	string X="zzzzzzzzzz";
	int x=hsh(X);
	for(int i=0,w=x;i<10;++i) a[i]=(i?26:25),b[i]=w%p,w/=p;
	string s;
	for(int i=0;i<10;++i) {
		if(a[i]>b[i]) c[i]=a[i]-b[i];
		else --a[i+1],c[i]=a[i];
		s.push_back(c[i]-1+'a');
	}
	int y=hsh(s);
	cout<<"! "<<p<<" "<<(val(X)-x)-(val(s)-y)<<endl;
}
signed main() {
	int T; cin>>T;
	while(T--) solve();
	return 0;
}




Round #38 - 2024.12.16

A. [CF2006D] Iris and Adjacent Products

Problem Link

题目大意

定义一个数组是好的当且仅当可以重排该数组使得邻项乘积 k

给定 a1anq 次询问某个子区间至少要修改多少个元素才是好的。

数据范围:n,q105,k106

思路分析

先考虑什么样的数组是好的,对于所有 >k 的数只能和 k 的数放一起,因此对于每个 ii 的元素数量要超过 k/i 的元素数量。

注意 n=2t+1 的时候可以有 t+1k/i 的元素和 ti 的元素,因此出现次数要和 n/2min

用莫队或分块等方式维护区间中元素的出现次数。

每次修改一定是把 >k 的元素变成 1,求答案对每个 i 计算后求最大值。

时间复杂度 O(n+q(n+k))

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
int n,k,q,a[MAXN];
int lp[325],rp[325],bl[MAXN];
int x[325][1005],y[325][1005],c[1005],d[1005];
void solve() {
	cin>>n>>q>>k;
	for(int i=1;i<=n;++i) cin>>a[i];
	int B=sqrt(n),lim=sqrt(k);
	for(int i=1;(i-1)*B+1<=n;++i) {
		lp[i]=(i-1)*B+1,rp[i]=min(i*B,n);
		memcpy(x[i],x[i-1],sizeof(x[i]));
		memcpy(y[i],y[i-1],sizeof(y[i]));
		for(int j=lp[i];j<=rp[i];++j) {
			bl[j]=i;
			if(a[j]<=lim) ++x[i][a[j]];
			else ++y[i][k/a[j]];
		}
	}
	for(int l,r;q--;) {
		cin>>l>>r;
		memset(c,0,sizeof(c));
		memset(d,0,sizeof(d));
		if(bl[l]==bl[r]) {
			for(int j=l;j<=r;++j) {
				if(a[j]<=lim) ++c[a[j]];
				else ++d[k/a[j]];
			}
		} else {
			for(int j=l;j<=rp[bl[l]];++j) {
				if(a[j]<=lim) ++c[a[j]];
				else ++d[k/a[j]];
			}
			for(int j=lp[bl[r]];j<=r;++j) {
				if(a[j]<=lim) ++c[a[j]];
				else ++d[k/a[j]];
			}
			for(int j=1;j<=lim;++j) {
				c[j]+=x[bl[r]-1][j]-x[bl[l]][j];
				d[j]+=y[bl[r]-1][j]-y[bl[l]][j];
			}
		}
		int s=0;
		for(int j=1;j<=lim;++j) {
			c[j]+=c[j-1],d[j]+=d[j-1];
			s=max(s,min((d[j]-c[j]+1)/2,(r-l+1)/2-c[j]));
		}
		cout<<s<<" ";
	}
	cout<<"\n";
}
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



B. [CF2004F] Make a Palindrome

Problem Link

题目大意

对于数组 a,一次操作可以把 ai,ai+1 替换成 ai+ai+1 或进行逆操作。

定义数组权值和为:令数组回文的最小操作数,求 a 的每个子区间权值和。

数据范围:n2000

思路分析

用这样的方式刻画数组 b1bm:有 B=bi 个球,球中间有 m1 个隔板,第 i1 个和第 i 个隔板之间的距离是 bi

bi 前缀和为 si,那么 s1sm1 即为隔板位置。

那么一次操作就是插入或删除一个隔板,而数组回文当且仅当隔板序列回文。

那么每个隔板都要在对面操作一次,除非其对面的位置恰好有隔板,即 sj=Bsi 时对答案产生 2 贡献(如果 sj=si=B/2 答案 1)。

容易发现两个和相等的不相邻子区间恰好对答案产生 2 贡献,因此扫描线维护 a[1,i) 中每个区间和的出现次数。

注意相邻的两个子区间对答案产生 1 贡献。

时间复杂度 O(n2)

代码呈现

#include<bits/stdc++.h>
#include<ext/pb_ds/hash_policy.hpp>
#include<ext/pb_ds/assoc_container.hpp>
using namespace std;
using namespace __gnu_pbds;
const int MAXN=2005;
int a[MAXN];
void solve() {
	int n,ans=0;
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i];
	for(int i=1;i<=n;++i) ans+=(i-1)*(n-i+1);
	gp_hash_table <int,int> cnt;
	for(int i=1;i<=n;++i) {
		for(int j=i,s=0;j<=n;++j) ans-=cnt[s+=a[j]];
		for(int j=i,s=0;j>=1;--j) ++cnt[s+=a[j]];
		for(int j=i-1,s=0;j>=1;--j) ++cnt[s+=a[j]];
	}
	cout<<ans<<"\n";
}
signed main() {
	ios::sync_with_stdio(false);
	int T; cin>>T;
	while(T--) solve();
	return 0;
}



C. [CF2003F] Turtle and Three Sequences

Problem Link

题目大意

给定 n 元素,每个元素有高度、颜色、权值,选出 m 个元素满足高度递增,颜色不同,且权值和最大。

数据范围 n3000,m5

思路分析

这种选出不同颜色的元素的题可以考虑 random-coloring,即给每种颜色随机映射到 [1,m] 中,然后计算此问题的答案。

此时可以状压 dp,fi,s 表示选择了元素 i,出现过的颜色集合为 s 时的最大权值,树状数组优化转移。

对于最优解,可能被映射到 mm 中颜色序列中,其中两两颜色不同的序列有 m! 个,因此期望随机 mmm! 次即可。

时间复杂度 O(2mnlogn×mmm!)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=3005,inf=1e9,T=400;
mt19937 rnd(time(0));
int n,m,a[MAXN],b[MAXN],c[MAXN],o[MAXN];
struct FenwickTree {
	int tr[MAXN],s;
	void init() { memset(tr,-0x3f,sizeof(tr)); }
	void upd(int x,int v) { for(;x<=n;x+=x&-x) tr[x]=max(tr[x],v); }
	int qry(int x) { for(s=-inf;x;x&=x-1) s=max(s,tr[x]); return s; }
}	F[32];
signed main() {
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i) scanf("%d",&a[i]);
	for(int i=1;i<=n;++i) scanf("%d",&b[i]);
	for(int i=1;i<=n;++i) scanf("%d",&c[i]);
	int ans=-1;
	for(int _=0;_<T;++_) {
		for(int s=0;s<(1<<m);++s) F[s].init();
		F[0].upd(1,0);
		for(int i=1;i<=n;++i) o[i]=rnd()%m;
		for(int i=1;i<=n;++i) {
			for(int s=0;s<(1<<m);++s) if(!(s>>o[b[i]]&1)){
				int z=F[s].qry(a[i])+c[i];
				F[s|1<<o[b[i]]].upd(a[i],z);
			}
		}
		ans=max(ans,F[(1<<m)-1].qry(n));
	}
	printf("%d\n",ans);
	return 0;
}



D. [CF2006E] Iris's Full Binary Tree

Problem Link

题目大意

定义一棵树的权值为最小的 k 使得该树是高度为 k 的满二叉树的导出子图。

给一棵 n 个点的树,对于 i=1n,求 1i 点的导出子图的权值。

数据范围:n5×105

思路分析

刻画一棵树的权值,显然这棵树过二叉树的根,且不能有 4 度点。

我们发现只要根节点是 2 度点,其他点度数无限制。

显然根不可能是 1 度点因此一棵树的权值就是所有 2 度点到其他点距离最大值的最小值。

取出树的中心,容易发现最优的 2 度点就是离中心最近的一个。

那么在树上不断加点,中心每次至多移动 0.5 的距离。

每次中心移动都是对某个子树内或子树外的距离 ±1,可以 dfn 序上线段树维护,对于未加入的点以及 3 度点,直接将距离设成 ,加入该点的时候直接求出距离即可。

此时答案就是线段树上的最小值。

时间复杂度 O(nlogn)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+5,inf=1e9;
int n;
struct SegmentTree {
	int tr[MAXN<<2],tg[MAXN<<2];
	void psu(int p) { tr[p]=min(tr[p<<1],tr[p<<1|1]); }
	void adt(int p,int k) { tg[p]+=k,tr[p]+=k; }
	void psd(int p) { adt(p<<1,tg[p]),adt(p<<1|1,tg[p]),tg[p]=0; }
	void init(int l=1,int r=n,int p=1) {
		tr[p]=inf,tg[p]=0;
		if(l==r) return ;
		int mid=(l+r)>>1;
		init(l,mid,p<<1),init(mid+1,r,p<<1|1);
	}
	void set(int u,int k,int l=1,int r=n,int p=1) {
		if(l==r) return tr[p]=k,void();
		int mid=(l+r)>>1; psd(p);
		u<=mid?set(u,k,l,mid,p<<1):set(u,k,mid+1,r,p<<1|1);
		psu(p);
	}
	void add(int ul,int ur,int k,int l=1,int r=n,int p=1) {
		if(ul>ur) return ;
		if(ul<=l&&r<=ur) return adt(p,k);
		int mid=(l+r)>>1; psd(p);
		if(ul<=mid) add(ul,ur,k,l,mid,p<<1);
		if(mid<ur) add(ul,ur,k,mid+1,r,p<<1|1);
		psu(p);
	}
}	T;
vector <int> G[MAXN];
int fa[MAXN],L[MAXN],R[MAXN],dep[MAXN],st[MAXN][20],dcnt;
void dfs(int u) {
	dep[u]=dep[fa[u]]+1,L[u]=++dcnt,st[dcnt][0]=fa[u];
	for(int v:G[u]) dfs(v);
	R[u]=dcnt;
}
int bit(int x) { return 1<<x; }
int cmp(int x,int y) { return L[x]<L[y]?x:y; }
int LCA(int x,int y) {
	if(x==y) return x;
	int l=min(L[x],L[y])+1,r=max(L[x],L[y]),k=__lg(r-l+1);
	return cmp(st[l][k],st[r-bit(k)+1][k]);
}
int dis(int x,int y) { return dep[x]+dep[y]-2*dep[LCA(x,y)]; }
int nxt(int x,int y) {
	if(L[x]<=L[y]&&L[y]<=R[x]) {
		return *--upper_bound(G[x].begin(),G[x].end(),y,[&](int i,int j){ return L[i]<L[j]; });
	}
	return fa[x];
}
int u,v,x,y,deg[MAXN];
void solve() {
	cin>>n;
	for(int i=1;i<=n;++i) G[i].clear(),deg[i]=0;
	for(int i=2;i<=n;++i) cin>>fa[i],G[fa[i]].push_back(i);
	dcnt=0,dfs(1);
	for(int k=1;k<20;++k) for(int i=1;i+bit(k)-1<=n;++i) {
		st[i][k]=cmp(st[i][k-1],st[i+bit(k-1)][k-1]);
	}
	u=v=x=y=1,T.init(),T.set(1,0),cout<<"1 ";
	for(int p=2;p<=n;++p) {
		++deg[fa[p]],++deg[p];
		T.set(L[p],min(dis(p,x),dis(p,y)));
		if(deg[fa[p]]>3) {
			for(int i=p;i<=n;++i) cout<<"-1 ";
			cout<<"\n";
			return ;
		}
		if(deg[fa[p]]==3) T.set(L[fa[p]],inf);
		if(dis(u,p)<dis(v,p)) swap(u,v),swap(x,y);
		if(dis(u,p)>dis(u,v)) {
			if(x!=y) {
				if(fa[x]==y) T.add(L[x],R[x],1);
				else T.add(1,L[y]-1,1),T.add(R[y]+1,n,1);
				x=y;
			} else {
				y=nxt(y,p);
				if(fa[y]==x) T.add(L[y],R[y],-1);
				else T.add(1,L[x]-1,-1),T.add(R[x]+1,n,-1);
			}
			v=p;
		}
		cout<<T.tr[1]+(dis(u,v)+1)/2+1<<" ";
	}
	cout<<"\n";
}
signed main() {
	ios::sync_with_stdio(false);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



*E. [CF1870G] MEXanization

Problem Link

题目大意

定义一个多重集 S 的权值为:每次选取一个 S 的子集 T,删除 T 并加入 mex(T),所能得到的最大元素。

给定 a1an,求出 a 的每个前缀的权值。

数据范围:n2×105

思路分析

发现当 i>1 的时候,a[1,i] 的权值单调递增,可以双指针,因此只要判定 S 的权值是否 k

这要求我们有 0k1 至少 1 个。

然后考虑 k1,如果没有 k1,我们就需要 0k2 至少两个。

然后重复类似的过程,最后判断 0 的个数够不够。

形式化的来说,记 i 的出现次数为 ci,动态维护当前需要的变量个数 x

初始 x=1ik1 枚举到 1:如果 ci<k,那么 kk+(kci),如果 k<ci,那么多出来的 cik 个元素可以一次操作后变成 0,最后判断是否有 c0k

注意到如果 ci<k,那么我们至少的元素数量至少增加 i1,因此想要合法,ci<ki 至多 O(n) 个。

可以线段树优化找元素的过程,做到 O(nnlogn)

但实际上能做到更优,考虑在线段树上一边 dfs 一边处理,如果 [l,r] 中最小值 k 就跳过,如果 r×k>n 就返回。

此时第 i 层至多访问 n2d 个节点,求和后得到访问总结点量是 O(n) 的。

时间复杂度 O(nn)

代码呈现

#include<bits/stdc++.h> 
using namespace std;
const int MAXN=2e5+5,inf=2e9;
int n,a[MAXN];
struct SegmentTree {
	int su[MAXN<<2],mn[MAXN<<2];
	void add(int u,int l=0,int r=n,int p=1) {
		++su[p];
		if(l==r) return ++mn[p],void();
		int mid=(l+r)>>1;
		u<=mid?add(u,l,mid,p<<1):add(u,mid+1,r,p<<1|1);
		mn[p]=min(mn[p<<1],mn[p<<1|1]);
	}
	void qry(int ul,int ur,int &k,int &z,int s,int l=0,int r=n,int p=1) {
		if(ul<=l&&r<=ur) {
			if(k>s/r) return k=inf,void();
			if(mn[p]>=k) return z+=su[p]-(r-l+1)*k,void();
			if(l==r) {
				if(k<mn[p]) z+=mn[p]-k;
				else k+=k-mn[p];
				return ;
			}
		}
		int mid=(l+r)>>1;
		if(mid<ur) qry(ul,ur,k,z,s,mid+1,r,p<<1|1);
		if(ul<=mid) qry(ul,ur,k,z,s,l,mid,p<<1);
	}
	int qs(int ul,int ur,int l=0,int r=n,int p=1) {
		if(ul<=l&&r<=ur) return su[p];
		int mid=(l+r)>>1,s=0;
		if(ul<=mid) s+=qs(ul,ur,l,mid,p<<1);
		if(mid<ur) s+=qs(ul,ur,mid+1,r,p<<1|1);
		return s;
	}
}	T;
void solve() {
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i];
	int x=0;
	for(int i=1;i<=n;++i) {
		T.add(min(a[i],n));
		for(;x<i;++x) {
			int k=1,z=T.qs(x+1,n)+T.qs(0,0);
			if(x) T.qry(1,x,k,z,i);
			if(z<k) break;
		}
		cout<<(i==1?max(a[i],x):x)<<" ";
	}
	cout<<"\n";
	for(int i=1;i<=(n+1)*4;++i) T.su[i]=T.mn[i]=0;
}
signed main() {
	ios::sync_with_stdio(false);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



*F. [CF1896H2] Cyclic Hamming

Problem Link

题目大意

给定 k,定义两个长度为 2k+1 的 01 串 a,b 是好的当且仅当:

  • a,b 分别含有 2k1
  • 对于每个 b 的循环移位 bdis(a,b)2kdis 表示海明距离。

给定含有未确定字符的字符串 a,b,求所有填充方式中有多少使得 (a,b) 是好的。

数据范围:k12

思路分析

n=2k

首先 dis(a,b)=2k+12k,而 b 恰好有 2k+1 个,因此题目限制等价于 dis(a,b)=2k

用卷积刻画循环移位的海明距离,首先由于 ai=bi=0 的位置数量和 ai=bi=1 的位置数量相等,因此海明距离 =2k 等价于 ai=bi=1 的位置有 2k1 个。

然后构造两个多项式 A=aixi,B=b2nx1xi,每个循环移位的海明距离就是 A×Bmod(x2n1) 的每一项。

因此题目限制就是 A×Bmod(x2n1)=2k1(1++x2n1)

用减法代替多项式取模,即 \existC 使得 A×B=C(x2n1)+2k1(1++x2n1)

注意到右式有公因式:A×B=((x1)C+2k1)(1++x2n1)=((x1)C+2k1)i=0k(1+x2i)

先要求 (1+x2i)A×B,然后就是 A×B(1+x2i)=(x1)C+2k1,即 A×B(1+x2i)mod(x1)=2k1

相当于要求带入 x=1 后,A(1)×B(1)2k+1=2k1,由于 A(1)=B(1)=2k,这是显然成立的。

因此合法的充要条件就是所有 1+x2iA1+x2iB

刻画这个条件,对 A 的系数按 mod2i 的余数分类,每部分独立,且都要求是 1+x 的倍数。

也就是说,把 A 的系数按 mod2i 分类,得到的每个多项式带入 x=1 结果为 0

用 Trie 树分类,把 ap=1p 二进制位倒着插入一棵 Trie,mod2i 分类相当于第 i 层的每棵子树 u

带入 x=1 等价于及奇数项和等于偶数项和,注意到奇数项为 u1 儿子,偶数项为 u0 儿子。

因此 1+x2iA 就要求 Trie 树上深度为 i 的每个节点左右子树中叶子个数相等。

那么对于 A,B 分别枚举整除哪些 1+x2i,在 Trie 树上自下而上 dp,fu,s,i 表示 Trie 上 u 子树中整除 s 里的多项式,包含 i 个叶子的方案数。

暴力转移复杂度 O(8k),且状态数过高。

用观察优化,设 u 子树深度为 d,那么 s 只有 2d 种可能,且注意到 |s|1 的时候 j 一定是偶数,进一步,我们知道 2|s|j,因此 d 只有 2d|s| 种。

那么枚举 |s|=i,状态数 d2kdid(di)2i=O(3k),转移的时候要对左右子树大小卷积,复杂度 d2kdid(di)22i=O(5k)

如果用 NTT 优化左右子树的卷积,复杂度可以做到 O(k3k)

时间复杂度 O(5k)

代码呈现

#include<bits/stdc++.h>
#define pc __builtin_popcount
#define ll long long
using namespace std;
const int MOD=998244353,MAXN=(1<<13)+5;
vector<vector<int>> f[14][MAXN];
//f[i,s,u,j]: dep = i, ok dig = s, node u, size = j*2^|s|
int n,k,rv[MAXN];
inline void add(int &x,const ll &y) { x=(x+y)%MOD; }
void DP(char *str,int *dp) {
	for(int i=1;i<n;++i) rv[i]=(rv[i>>1]>>1)|(i&1?n>>1:0);
	for(int i=0;i<n;++i) if(rv[i]>i) swap(str[i],str[rv[i]]);
	for(int i=0;i<=k+1;++i) for(int s=0;s<(1<<i);++s) {
		int U=1<<(k+1-i),J=1<<(i-pc(s));
		f[i][s]=vector<vector<int>>(U,vector<int>(J+1,0));
	}
	for(int i=0;i<n;++i) {
		f[0][0][i][0]=(str[i]!='1');
		f[0][0][i][1]=(str[i]!='0');
	}
	for(int i=1;i<=k+1;++i) for(int s=0;s<(1<<(i-1));++s) {
		int U=1<<(k+1-i),J=1<<(i-pc(s)-1);
		for(int u=0;u<U;++u) {
			auto &fl=f[i-1][s][u<<1],&fr=f[i-1][s][u<<1|1];
			for(int x=0;x<=J;++x) {
				add(f[i][s|(1<<(i-1))][u][x],1ll*fl[x]*fr[x]);
			}
			auto &fi=f[i][s][u];
			for(int x=0;x<=J;++x) if(fl[x]) for(int y=0;y<=J;++y) if(fr[y]) {
				add(fi[x+y],1ll*fl[x]*fr[y]);
			}
		}
	}
	for(int s=0;s<n-1;++s) dp[s]=f[k+1][s][0][1<<(k-pc(s))];
}
char S[MAXN],T[MAXN];
int fs[MAXN],ft[MAXN];
signed main() {
	scanf("%d%s%s",&k,S,T),n=1<<(k+1);
	reverse(T,T+n);
	DP(S,fs),DP(T,ft);
	for(int i=0;i<=k;++i) for(int s=0;s<n;++s) if(!(s>>i&1)) fs[s]=(fs[s]+MOD-fs[s|1<<i])%MOD;
	int ans=0;
	for(int s=0;s<n;++s) add(ans,1ll*fs[s]*ft[(n-1)^s]);
	printf("%d\n",ans);
	return 0;
}



Round #39 - 2024.12.18

A. [CF2009G3] Yunli's Subarray Queries

Problem Link

题目大意

给定 n,k,定义序列权值为至少要修改几个位置才能使得其中存在一个长度 k 的公差为 1 的等差数列。

给定 a1anq 次询问 $$求这个值的和。

数据范围:n2×105

思路分析

先计算每个长度为 k 的区间的答案,相当于 aii 的众数,计算是简单的。

bi=a[i,i+k1] 的答案,那么一个区间的答案就是 bi 的区间最小值。

因此答案就是 bi 每个子区间最小值的和,这是经典问题,扫描线单调栈,配合历史和线段树维护之即可。

时间复杂度 O((n+q)logn)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
int n,m,q,a[MAXN];
struct SegmentTree {
	int len[MAXN<<2];
	ll sum[MAXN<<2],sumh[MAXN<<2],ad[MAXN<<2],ti[MAXN<<2],sd[MAXN<<2];
	void trs(int p) {
		sumh[p]+=sum[p],sd[p]+=ad[p],++ti[p];
	}
	void adt(int p,ll k) {
		sum[p]+=k*len[p],ad[p]+=k;
	}
	void psd(int p) {
		for(int c:{p<<1,p<<1|1}) {
			sumh[c]+=sum[c]*ti[p]+len[c]*sd[p];
			sd[c]+=ad[c]*ti[p]+sd[p];
			sum[c]+=ad[p]*len[c],ad[c]+=ad[p],ti[c]+=ti[p];
		}
		ad[p]=ti[p]=sd[p]=0;
	}
	void psu(int p) { sum[p]=sum[p<<1]+sum[p<<1|1],sumh[p]=sumh[p<<1]+sumh[p<<1|1]; }
	void init(int l=1,int r=n,int p=1) {
		len[p]=r-l+1,sum[p]=sumh[p]=ad[p]=ti[p]=sd[p]=0;
		if(l==r) return ;
		int mid=(l+r)>>1; init(l,mid,p<<1),init(mid+1,r,p<<1|1);
	}
	void add(int ul,int ur,ll k,int l=1,int r=n,int p=1) {
		if(ul<=l&&r<=ur) return adt(p,k);
		int mid=(l+r)>>1; psd(p);
		if(ul<=mid) add(ul,ur,k,l,mid,p<<1);
		if(mid<ur) add(ul,ur,k,mid+1,r,p<<1|1);
		psu(p);
	}
	ll qry(int ul,int ur,int l=1,int r=n,int p=1) {
		if(ul<=l&&r<=ur) return sumh[p];
		int mid=(l+r)>>1; ll ans=0; psd(p); 
		if(ul<=mid) ans+=qry(ul,ur,l,mid,p<<1);
		if(mid<ur) ans+=qry(ul,ur,mid+1,r,p<<1|1);
		return ans;
	}
}	T;
int c[MAXN*2],w[MAXN],mx,v[MAXN];
void add(int x) {
	--w[c[x]],++w[++c[x]];
	if(w[mx+1]) ++mx;
}
void del(int x) {
	--w[c[x]],++w[--c[x]];
	if(!w[mx]) --mx;
}
int stk[MAXN];
ll ans[MAXN];
vector <array<int,2>> Q[MAXN];
void solve() {
	cin>>n>>m>>q,mx=0,w[0]=n;
	for(int i=1;i<=n;++i) cin>>a[i],a[i]=a[i]-i+n;
	for(int i=1;i<=m;++i) add(a[i]);
	v[1]=m-mx;
	for(int i=2;i<=n-m+1;++i) {
		del(a[i-1]),add(a[i+m-1]),v[i]=m-mx;
	}
	for(int i=1,l,r;i<=q;++i) {
		cin>>l>>r,Q[r-m+1].push_back({l,i});
	}
	T.init();
	for(int i=1,tp=0;i<=n-m+1;++i) {
		for(;tp&&v[stk[tp]]>=v[i];--tp) T.add(stk[tp-1]+1,stk[tp],-v[stk[tp]]);
		T.add(stk[tp]+1,i,v[i]),stk[++tp]=i;
		T.trs(1);
		for(auto o:Q[i]) ans[o[1]]=T.qry(o[0],i);
	}
	for(int i=1;i<=q;++i) cout<<ans[i]<<"\n";
	for(int i=0;i<=n*2;++i) c[i]=0;
	for(int i=0;i<=n;++i) w[i]=0,Q[i].clear();
}
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



B. [CF2013F2] Digital Village

Problem Link

题目大意

给定 n 个点的树,先手从 1 出发,后手从 x 出发,两个人轮流在树上游走,不能重复经过点,对于 uv 上的每个 x,求谁有必胜策略。

数据范围:n2×105

思路分析

假设 1x 路径是 p1pm,两个人的策略就是在 p 上运动,直到某个时刻必胜的时候离开路径。

di 表示从 pi 处离开路径,最多能走多少步。

假设先手在 pl 处离开路径必胜,这就要求:maxi=l+1ml+1mi+di<dl+l1

假设后手在 pr 处离开路径必胜,这就要求:maxi=mr+2r1ai+i1ar+mr

暴力枚举每个 l,r,考虑第一个合法的 l 和第一个合法的 r,比较 l1,nr 即可知道谁必胜,现在要优化这部分的分之道。

定义 fl=dl+l1,gr=ar+mr,表示先手 / 后手从 pl,pr 处离开路径后最多能走多远。

我们要找 fl>maxg[l+1,ml+1] 的第一个 l,我们发现随着 l 的增加,maxg 的范围越来越小,因此 i<j,fi<fj 时,i 必胜能推出 j 必胜 。

注意到 l 变化的时候 maxg 实际上不太容易变化,可以找到 g[mid+1,m] 中的最大值 gr,只有 l1>nr 的时候才会产生变化。

找到 f[1,mid] 的最大值 fl,不妨设 l1nr,比较 fl,gr 的关系:

  • 如果 flgr,那么 i[1,l] 的点都不可能合法。
  • 否则 i[nl+1,n] 的点都不可能合法,我们只要判断 i[1,l] 中有没有合法点,很显然最容易合法的就是 fl,只需要判断 fl 是否合法即可。

因此我们只需要判定 fl 是否合法,然后就把问题缩小成了 [l,nl+1] 的子问题。

不断这样的递归,我们会需要判定的 l 一定是 f[1,mid] 的后缀最大值,需要判定的 r 一定是 g[mid+1,r] 的前缀最大值。

考虑 i<j,fifj 的两个点,此时一定有 di>dj,因此 f[1,mid] 的后缀最大值的 d 单调递增。

由于 di=O(n),因此访问到的 l,r 总数是 O(n) 级别的。

在 dfs 的时候动态维护栈中节点的 fi,gi,需要一个数据结构支持修改末尾元素,快速查询区间最大值。

可以用 st 表,si,k 维护 [i2k+1,i] 范围内的最小值,修改末尾元素 O(logn),查询最大值 O(1)

此时我们能得到后手从每个 x 出发是否必胜,容易回答询问。

时间复杂度 O(nn)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5;
inline int bit(int x) { return 1<<x; }
struct ST {
	int v[MAXN],f[MAXN][18];
	int cmp(int x,int y) { return v[x]>v[y]?x:y; }
	void upd(int x,int z) {
		v[x]=z,f[x][0]=x;
		for(int k=1;bit(k)<=x;++k) f[x][k]=cmp(f[x][k-1],f[x-bit(k-1)][k-1]);
	}
	int qry(int l,int r) {
		int k=__lg(r-l+1);
		return cmp(f[l+bit(k)-1][k],f[r][k]);
	}
}	A,B;
void upd(int d,int w) { A.upd(d,w+d-1),B.upd(d,w-d); }
bool sol(int n) {
	int mid=(n+1)>>1;
	for(int l=A.qry(1,mid),r=B.qry(mid+1,n);;) {
		if(l-1<=n-r) {
			if(A.v[l]>B.v[B.qry(l+1,n-l+1)]+n) return 1;
			if(l>=mid) return 0;
			l=A.qry(l+1,mid);
		} else {
			if(B.v[r]+n>=A.v[A.qry(n-r+2,r-1)]) return 0;
			if(r<=mid+1) return 1;
			r=B.qry(mid+1,r-1);
		}
	}
}
vector <int> G[MAXN];
int n,d[MAXN],f[MAXN],fa[MAXN];
bool ans[MAXN];
void dfs1(int u,int fz) {
	d[u]=d[fz]+1,f[u]=0,fa[u]=fz;
	for(int v:G[u]) if(v^fz) dfs1(v,u),f[u]=max(f[u],f[v]+1);
}
void dfs2(int u) {
	int mx=0,sx=0;
	for(int v:G[u]) if(v^fa[u]) {
		if(f[v]+1>mx) sx=mx,mx=f[v]+1;
		else sx=max(sx,f[v]+1);
	}
	if(u>1) upd(d[u],mx),ans[u]=sol(d[u]);
	for(int v:G[u]) if(v^fa[u]) upd(d[u],f[v]+1==mx?sx:mx),dfs2(v);
}
void prt(int u) { cout<<(ans[u]?"Alice":"Bob")<<"\n"; }
void out(int u,int v) {
	if(u==v) return prt(u);
	if(d[u]>d[v]) prt(u),out(fa[u],v);
	else out(u,fa[v]),prt(v);
}
void solve() {
	cin>>n;
	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);
	dfs1(1,0),dfs2(1);
	int s,t;
	cin>>s>>t,out(s,t);
}
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



C. [CF2020F] Count Leaves

Problem Link

题目大意

给定 n,d,构造一棵深度为 d 的有根树,根节点的标号为 n,标号为 x 的节点有 σ0(x) 个儿子,标号为分别为 x 的每个因数。

f(n,d) 为这棵树的叶子个数,求 i=1nf(ik,d)

数据范围:n109,k,d105

思路分析

先刻画 f(n,d),观察 n 的某个约数 w 出现次数,相当于 n 的每个质因子的质数逐层递减,计数路径条数,

容易发现 f(n,d) 是积性函数,因此只要考虑 f(pc,d),其中 p 是质数,很显然 f(pc,d) 就是 (c+dd)

因此 w(i)=f(ik,d)=pc(ck+dd),要算的就是这个积性函数的答案。

从小大大爆搜每个质因数,枚举 i 除以最大质因子的结果,如果最大质因子出现次数 =1,贡献确定,只要数质数个数,相当于数 1nt 的质数个数,Min25 筛预处理。

而最大质因子出现次数 2,此时这个质因子 n,在爆搜的时候特判即可。

时间复杂度 O(n3/4logn)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5,MOD=1e9+7,MAXV=3.2e6+5;
ll fac[MAXV],ifac[MAXV];
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 C(int x,int y) {
	if(x<0||y<0||y>x) return 0;
	return fac[x]*ifac[y]%MOD*ifac[x-y]%MOD;
}
int n,K,D,B,val[MAXN];
bool isc[MAXN];
int p[MAXN],tot,m,idx1[MAXN],idx2[MAXN],g[MAXN];
inline int idx(int v) {
	return (v<=B)?idx1[v]:idx2[n/v];
}
ll f[32],ans;
void dfs(int i,int N,ll dp) {
	if(g[idx(N)]>i) ans=(ans+dp*f[1]%MOD*(g[idx(N)]-i))%MOD;
	for(int j=i+1;j<=tot&&p[j]<=N/p[j];++j) {
		for(int c=1,M=N/p[j];M>=p[j];++c,M/=p[j]) {
			ans=(ans+dp*f[c+1])%MOD;
			dfs(j,M,dp*f[c]%MOD);
		}
	}
}
void solve() {
	scanf("%d%d%d",&n,&K,&D),B=sqrt(n),tot=m=0;
	for(int i=2;i<=B;++i) {
		if(!isc[i]) p[++tot]=i;
		for(int j=1;j<=tot&&i*p[j]<=B;++j) {
			isc[i*p[j]]=true;
			if(i%p[j]==0) break;
		}
	}
	for(ll l=1,r;l<=n;l=r+1) {
		r=n/(n/l),val[++m]=n/l;
		if(val[m]<=B) idx1[val[m]]=m;
		else idx2[n/val[m]]=m;
		g[m]=val[m]-1;
	}
	for(int k=1;k<=tot;++k) {
		for(int i=1;i<=m&&1ll*p[k]*p[k]<=val[i];++i) {
			g[i]-=g[idx(val[i]/p[k])]-(k-1);
		}
	}
	for(int i=1;i<=30;++i) f[i]=C(K*i+D,D);
	ans=1,dfs(0,n,1);
	printf("%lld\n",ans);
	for(int i=1;i<=m;++i) val[i]=g[i]=0;
	for(int i=1;i<=B;++i) idx1[i]=idx2[i]=0,isc[i]=false;
}
signed main() {
	for(int i=fac[0]=1;i<MAXV;++i) fac[i]=fac[i-1]*i%MOD;
	ifac[MAXV-1]=ksm(fac[MAXV-1]);
	for(int i=MAXV-1;i;--i) ifac[i-1]=ifac[i]*i%MOD;
	int _; scanf("%d",&_);
	while(_--) solve();
	return 0;
}



*D. [CF2018E2] Complex Segments

Problem Link

题目大意

给定 n 个区间,选出若干个区间,使得其可以分割成若干个大小相同的子集,且两个区间相交当且仅当他们属于同一个子集,最大化选出区间个数。

数据范围:n3×105

思路分析

一个子集中的线段两两有交,相当于有一个公共点。

很自然的想法是枚举每个子集的大小 k,算出能分出多少个组 fk

一种求 fk 的方法如下:按右端点从小到大考虑每条线段,数据结构维护每个点被覆盖的次数,如果存在一个点被覆盖 k 次,那么就把 ljri 的线段全部删除。

此时我们得到了 O(nlogn) 求单个 fk 的算法。

注意到 fknk 并且 fk 单调递减,于是可以整体二分,在 fl=fr 的时候直接得到返回。

此时考虑第 d 层的递归次数:前 2d/2 个区间每个查询一次,剩余的区间左端点至少是 n2d×2d/2,那么 fi2d/2,这部分的查询次数也是 2d/2 的。

那么总的查询次数就是 2d/2=O(n)

直接计算可以做到 o(nnlogn)

需要优化计算 fk 的复杂度,需要解决的问题就是扫描线的时候后缀加,查询全局最大值,很显然我们只关心后缀最大值。

首先我们可以离散化,使得所有区间的左右端点互不重合,那么后缀最大值每次变化量为 {0,1}

[l,r] 后缀加的时候就是找 <l 的最后一个后缀最大值 +1 的位置,并把把差分值改成 +0

如果 +1 不存在说明全局最大值 +1,用并查集就能维护这个过程。

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

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=6e5+5;
array<int,2> a[MAXN];
int n,dsu[MAXN],ans;
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
int f(int k) {
	iota(dsu,dsu+2*n+1,0);
	int s=0,lim=0,lst=0,cnt=0;
	for(int i=1;i<=n;++i) {
		int l=a[i][0],r=a[i][1];
		if(l<=lim) continue;
		for(int u=lst+1;u<r;++u) dsu[u]=lst;
		int p=find(l); lst=r;
		if(p<=lim) ++cnt;
		else dsu[p]=p-1;
		if(cnt>=k) cnt=0,lim=r,++s;
	}
	ans=max(ans,s*k);
	return s;
}
void cdq(int l,int r,int lo,int hi) {
	if(l>r||lo==hi) return ;
	int mid=(l+r)>>1,z=f(mid);
	cdq(l,mid-1,lo,z),cdq(mid+1,r,z,hi);
}
void solve() {
	cin>>n;
	vector <array<int,2>> st;
	for(int i=1;i<=n;++i) cin>>a[i][0],st.push_back({a[i][0],-i});
	for(int i=1;i<=n;++i) cin>>a[i][1],st.push_back({a[i][1],i});
	sort(st.begin(),st.end());
	for(int i=0;i<2*n;++i) {
		if(st[i][1]<0) a[-st[i][1]][0]=i+1;
		else a[st[i][1]][1]=i+1;
	}
	sort(a+1,a+n+1,[&](auto x,auto y){ return x[1]<y[1]; });
	ans=0,cdq(2,n-1,f(1),f(n));
	cout<<ans<<"\n";
}
signed main() {
	ios::sync_with_stdio(false);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



*E. [CF2013F2] Game in Tree

Problem Link

题目大意

给定 n 个点的树,先手从 1 出发,后手从 x 出发,两个人轮流在树上游走,不能重复经过点,对于 uv 上的每个 x,求谁有必胜策略。

数据范围:n2×105

思路分析

假设 1x 路径是 p1pm,两个人的策略就是在 p 上运动,直到某个时刻必胜的时候离开路径。

di 表示从 pi 处离开路径,最多能走多少步。

假设先手在 pl 处离开路径必胜,这就要求:maxi=l+1ml+1mi+di<dl+l1

假设后手在 pr 处离开路径必胜,这就要求:maxi=mr+2r1ai+i1ar+mr

暴力枚举每个 l,r,考虑第一个合法的 l 和第一个合法的 r,比较 l1,nr 即可知道谁必胜,现在要优化这部分的分之道。

定义 fl=dl+l1,gr=ar+mr,表示先手 / 后手从 pl,pr 处离开路径后最多能走多远。

我们要找 fl>maxg[l+1,ml+1] 的第一个 l,我们发现随着 l 的增加,maxg 的范围越来越小,因此 i<j,fi<fj 时,i 必胜能推出 j 必胜 。

注意到 l 变化的时候 maxg 实际上不太容易变化,可以找到 g[mid+1,m] 中的最大值 gr,只有 l1>nr 的时候才会产生变化。

找到 f[1,mid] 的最大值 fl,不妨设 l1nr,比较 fl,gr 的关系:

  • 如果 flgr,那么 i[1,l] 的点都不可能合法。
  • 否则 i[nl+1,n] 的点都不可能合法,我们只要判断 i[1,l] 中有没有合法点,很显然最容易合法的就是 fl,只需要判断 fl 是否合法即可。

因此我们只需要判定 fl 是否合法,然后就把问题缩小成了 [l,nl+1] 的子问题。

不断这样的递归,我们会需要判定的 l 一定是 f[1,mid] 的后缀最大值,需要判定的 r 一定是 g[mid+1,r] 的前缀最大值。

考虑 i<j,fifj 的两个点,此时一定有 di>dj,因此 f[1,mid] 的后缀最大值的 d 单调递增。

由于 di=O(n),因此访问到的 l,r 总数是 O(n) 级别的。

在 dfs 的时候动态维护栈中节点的 fi,gi,需要一个数据结构支持修改末尾元素,快速查询区间最大值。

可以用 st 表,si,k 维护 [i2k+1,i] 范围内的最小值,修改末尾元素 O(logn),查询最大值 O(1)

此时我们能得到后手从每个 x 出发是否必胜,容易回答询问。

时间复杂度 O(nn)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5;
inline int bit(int x) { return 1<<x; }
struct ST {
	int v[MAXN],f[MAXN][18];
	int cmp(int x,int y) { return v[x]>v[y]?x:y; }
	void upd(int x,int z) {
		v[x]=z,f[x][0]=x;
		for(int k=1;bit(k)<=x;++k) f[x][k]=cmp(f[x][k-1],f[x-bit(k-1)][k-1]);
	}
	int qry(int l,int r) {
		int k=__lg(r-l+1);
		return cmp(f[l+bit(k)-1][k],f[r][k]);
	}
}	A,B;
void upd(int d,int w) { A.upd(d,w+d-1),B.upd(d,w-d); }
bool sol(int n) {
	int mid=(n+1)>>1;
	for(int l=A.qry(1,mid),r=B.qry(mid+1,n);;) {
		if(l-1<=n-r) {
			if(A.v[l]>B.v[B.qry(l+1,n-l+1)]+n) return 1;
			if(l>=mid) return 0;
			l=A.qry(l+1,mid);
		} else {
			if(B.v[r]+n>=A.v[A.qry(n-r+2,r-1)]) return 0;
			if(r<=mid+1) return 1;
			r=B.qry(mid+1,r-1);
		}
	}
}
vector <int> G[MAXN];
int n,d[MAXN],f[MAXN],fa[MAXN];
bool ans[MAXN];
void dfs1(int u,int fz) {
	d[u]=d[fz]+1,f[u]=0,fa[u]=fz;
	for(int v:G[u]) if(v^fz) dfs1(v,u),f[u]=max(f[u],f[v]+1);
}
void dfs2(int u) {
	int mx=0,sx=0;
	for(int v:G[u]) if(v^fa[u]) {
		if(f[v]+1>mx) sx=mx,mx=f[v]+1;
		else sx=max(sx,f[v]+1);
	}
	if(u>1) upd(d[u],mx),ans[u]=sol(d[u]);
	for(int v:G[u]) if(v^fa[u]) upd(d[u],f[v]+1==mx?sx:mx),dfs2(v);
}
void prt(int u) { cout<<(ans[u]?"Alice":"Bob")<<"\n"; }
void out(int u,int v) {
	if(u==v) return prt(u);
	if(d[u]>d[v]) prt(u),out(fa[u],v);
	else out(u,fa[v]),prt(v);
}
void solve() {
	cin>>n;
	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);
	dfs1(1,0),dfs2(1);
	int s,t;
	cin>>s>>t,out(s,t);
}
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}




Round #40 - 2024.12.19

A. [CF2032F] Peanuts

Problem Link

题目大意

给定 a1an,先手先将 a1an 分成若干个连续段,然后两个人从左到右依次在每个连续段内部做 Nim 游戏,求有多少分段策略使得先手必胜。

数据范围:n106

思路分析

考虑如何判定一个局面是先手必胜还是后手必胜,从最后一个连续段开始,先手肯定要赢下这个游戏。

那么这个游戏如果先手必胜,则先手要输掉前面那个游戏,使得他能在这个游戏中成为先手,否则先手要赢下前面那个游戏。

因此从后往前就能推断出想要获胜的人要在每个游戏中赢还是输。

于是可以 dp,fi,0/1 表示 a[i,n] 的分段已经确定,想获胜要输 / 赢 ai1 所处的游戏。

转移系数就是 a[i,j) 构成的游戏先手有没有必胜 / 必败策略。

先手有必胜策略就是 Nim 博弈,即异或和 0

先手有必败策略则是 Anti-Nim 博弈,判定条件是 maxai>1 时异或和 0,或者所有 ai=1 时异或和 =0

因此暴力 dp 可以做到 O(n2)

优化转移只需要对异或前缀和开桶,特殊讨论 maxa[i,j)=1 的情况。

时间复杂度 O(nlogn)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+5,MOD=998244353;
int n,a[MAXN],s[MAXN];
ll f[MAXN][2],g[MAXN][2];
//f[i][0/1]: fill [i,n], lose/win game at i-1
void solve() {
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i];
	vector <int> vals{0};
	for(int i=n;i>=1;--i) vals.push_back(s[i]=s[i+1]^a[i]);
	sort(vals.begin(),vals.end()),vals.erase(unique(vals.begin(),vals.end()),vals.end());
	for(int i=1;i<=n+1;++i) s[i]=lower_bound(vals.begin(),vals.end(),s[i])-vals.begin()+1;
	g[s[n+1]][1]=g[0][1]=f[n+1][1]=1;
	int h[2]={0,0};
	for(int i=n,j=n+1;i>=1;--i) {
		if(a[i]>1) {
			h[0]=h[1]=0;
			for(;j>i;--j) {
				g[s[j]][0]=(g[s[j]][0]+f[j][0])%MOD;
				g[0][0]=(g[0][0]+f[j][0])%MOD;
			}
		}
		f[i][1]=(f[i][1]+g[s[i]][1])%MOD;
		f[i][0]=(f[i][0]+g[0][1]+MOD-g[s[i]][1])%MOD;
		f[i][1]=(f[i][1]+g[s[i]][0])%MOD;
		f[i][0]=(f[i][0]+g[0][0]+MOD-g[s[i]][0])%MOD;
		f[i][1]=(f[i][1]+h[(i+1)&1])%MOD;
		f[i][0]=(f[i][0]+h[i&1])%MOD;
		g[s[i]][1]=(g[s[i]][1]+f[i][1])%MOD;
		g[0][1]=(g[0][1]+f[i][1])%MOD;
		h[i&1]=(h[i&1]+f[i][0])%MOD;
	}
	cout<<f[1][0]<<"\n";
	for(int i=0;i<=n+1;++i) {
		a[i]=s[i]=f[i][0]=f[i][1]=g[i][0]=g[i][1]=0;
	}
}
signed main() {
	ios::sync_with_stdio(false);
	int T; cin>>T;
	while(T--) solve();
	return 0;
}



B. [CF2022D2] Asesino

Problem Link

题目大意

给定 n 个人,每个人是骑士或骗子,骑士总说真话,骗子总说假话,但现在有一个骗子进行了伪装,使得其他人都认为他是骑士。

你可以询问第 i 个人是否认为第 j 个人是骑士,在最少次数内确定伪装的骗子。

数据范围:n105

思路分析

询问 i,j 相当于查询 i,j 身份是否相同,因此询问 (i,j),(j,i) 的答案相同时,说明这两人中没有伪装者,反之说明其中恰有一个伪装者。

如果 n 为偶数,n2 次询问可以确定伪装者在哪两人之间,再询问两次即可确定答案。

否则需要 n+1 次,尝试优化,我们发现如果询问一个环,环上没有伪装者当且仅当有偶数个人回答否。

因此先询问一个三元环,即可变成 n 为偶数的情况,如果伪装者在三元环中,询问两条反向边即可确定答案。

此时在 n>3 时都可以做到 n 次询问。

可以证明不能更小,否则我们可以把欺骗者安排到没有入度的点或没有出度的点上。

时间复杂度 O(n)

代码呈现

 #include<bits/stdc++.h>
using namespace std;
bool qry(int x,int y) {
	cout<<"? "<<x<<" "<<y<<endl;
	int z; cin>>z; return z^1;
}
void solve() {
	int n;
	cin>>n;
	if(n&1) {
		if(n==3) {
			if(qry(1,2)!=qry(2,1)) {
				bool o=qry(1,3)^qry(3,1);
				cout<<"! "<<(o?1:2)<<endl;
			} else cout<<"! "<<3<<endl;
			return ;
		}
		int a=qry(n,n-1),b=qry(n-1,n-2),c=qry(n-2,n);
		if((a+b+c)&1) {
			a^=qry(n-1,n),b^=qry(n-2,n-1);
			cout<<"! "<<(a&&b?n-1:(a?n:n-2))<<endl;
			return ;
		} else n-=3;
	}
	int p=1,q=3;
	for(int i=3;i<n;i+=2) if(qry(i,i+1)^qry(i+1,i)) {
		p=i,q=1; break;
	}
	bool o=qry(p,q)^qry(q,p);
	cout<<"! "<<(o?p:p+1)<<endl;
}
signed main() {
	int T; cin>>T;
	while(T--) solve();
	return 0;
}



C. [CF2023D] Many Games

Problem Link

题目大意

给定 n 个物品 (pi,wi),选出若干个元素,最大化 piwi,其中 pi=1%100%

数据范围:n2×105,wi×pi2000

思路分析

P=pi,S=wi

pi=p 的元素选择了 k 个,那么删去最小值后的权值不优于当前方案,故 pkS>pk1k1kS,从而 k<11p

因此可以把元素个数优化到 PlnP 级别,其中 P=100

从最优解中删除一个元素,不可能更优,故:PS>Ppi(Swi),从而 S<wi1pi,由于 wipi2000

因此 wi2000(1pi)pi,在 pi=1% 时取到最小值,大约为 O(VP) 级别,其中 B=2000

然后按 S 暴力背包即可。

时间复杂度 O(nlogn+VP2)

代码呈现

#include<bits/stdc++.h>
#define ld long double
using namespace std;
const int MAXN=2e5+5,Q=100;
vector <int> A[105];
int n,p[MAXN],w[MAXN];
ld f[MAXN];
signed main() {
	scanf("%d",&n);
	for(int i=1;i<=n;++i) scanf("%d%d",&p[i],&w[i]),A[p[i]].push_back(i);
	vector <int> it;
	for(int p=1;p<Q;++p) {
		sort(A[p].begin(),A[p].end(),[&](int i,int j){ return w[i]>w[j]; });
		int c=min((int)A[p].size(),Q/(Q-p)+1);
		for(int i=0;i<c;++i) it.push_back(A[p][i]);
	}
	f[0]=1;
	for(int i:it) for(int j=MAXN-1;j>=w[i];--j) {
		f[j]=max(f[j],f[j-w[i]]*p[i]/100);
	}
	ld s=0,z=0;
	for(int i:A[Q]) s+=w[i];
	for(int j=0;j<MAXN;++j) z=max(z,(j+s)*f[j]);
	printf("%.20Lf",z);
	return 0;
}



D. [CF2025G] Variable Damage

Problem Link

题目大意

游戏里有 n 个人,每个人可以携带一件装备,假设共有 m 件装备,则每个人和装备每秒都会受到 1n+m 的伤害。

每个人和装备都有血量,血量为 0 时会消失,一个人消失的时候他的装备也会消失。

q 次操作,每次加入一个装备或人,动态维护游戏至多能进行多久。

数据范围:q3×105

思路分析

我们只要统计每个东西能承受多少伤害,一个血量为 x 的人显然能受 x 点伤害,而他的装备血量为 y,则能承受 min(x,y) 点伤害。

那么我们要求的就是最大匹配,先假设每个装备的贡献就是 y,然后要求有多少贡献实际达不到,相当于对于每个 k,如果设血量 k 的人数为 ak,装备数为 bk,那么要减去的贡献就是 max(0,bkak)

因此我们就把这个问题转化为了数据结构问题:前缀加 ±1,查询全局正数和。

感觉上 polylog 做法是困难的,因此考虑分块,对于散块修改可以直接重构,整块时打标记。

查询的时候,设当前块标记为 x,我们要求的就是 bkakxbkak+x,重构时对 bkak 开桶,由于每次 x 的变化只有 ±1,因此动态维护后缀和是简单的。

时间复杂度 O(qq)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=3e5+5;
int n,m,a[MAXN],op[MAXN],st[MAXN],w[MAXN];
ll ans[MAXN],f[MAXN<<1],g[MAXN<<1];
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	cin>>m;
	for(int i=1;i<=m;++i) cin>>op[i]>>a[i],st[++n]=a[i];
	sort(st+1,st+n+1),n=unique(st+1,st+n+1)-st-1;
	for(int i=1;i<=m;++i) a[i]=lower_bound(st+1,st+n+1,a[i])-st;
	for(int i=1;i<=m;++i) ans[i]=ans[i-1]+st[a[i]];
	for(int l=1,r,B=sqrt(n);l<=n;l=r+1) {
		r=min(n,l+B-1);
		memset(f,0,sizeof(f));
		memset(g,0,sizeof(g));
		for(int j=l;j<=r;++j) {
			f[m-w[j]]+=st[j]-st[j-1];
			g[m-w[j]]+=1ll*(st[j]-st[j-1])*w[j];
		}
		ll cnt=f[m],sum=g[m];
		for(int i=1,tg=0;i<=m;++i) {
			int x=a[i],c=(op[i]==1?-1:1);
			if(x>=r) {
				if(c==1) ++tg,cnt+=f[m+tg],sum+=g[m+tg];
				else cnt-=f[m+tg],sum-=g[m+tg],--tg;
			} else if(x>=l) {
				cnt=0,sum=0;
				for(int j=l;j<=r;++j) f[m-w[j]]=g[m-w[j]]=0;
				for(int j=l;j<=r;++j) {
					w[j]+=tg+(j<=x?c:0);
					if(w[j]>=0) {
						cnt+=st[j]-st[j-1];
						sum+=1ll*(st[j]-st[j-1])*w[j];
					}
					f[m-w[j]]+=st[j]-st[j-1];
					g[m-w[j]]+=1ll*(st[j]-st[j-1])*w[j];
				}
				tg=0;
			}
			ans[i]-=sum+tg*cnt;
		}
	}
	for(int i=1;i<=m;++i) cout<<ans[i]<<"\n";
	return 0;
}



*E. [CF2023E] Tree of Life

Problem Link

题目大意

给定 n 棵点的树,选出最少的简单路径,使得每对相邻的边都至少在一条路径中相邻出现过。

数据范围:n5×105

思路分析

先选出所有长度为 2 的路径,然后可以合并端点相同的一些。

对于每个子树 u,我们求出子树内的结果后,应该在保证子树内答案不变的情况下,向上的路径尽可能多,记为 fu

那么我们的思路就是,对于 u 的每个儿子 v,我们可以把 min(fv,degu1)vu 的路径合并起来。

如果 fv>degu1,那么剩余的所有路径提出来,传递到 fu 中即可。

但是此时我们发现这个做法不一定最优,因为当我们处理完 f1 后,多余的路径全部浪费了,因此对于有一些路径,在 u 处合并可能更优。

但我们无法在 u 处确定祖先需要多少条链,因此我们先在 u 处合并,然后在祖先的有需要的时候拆开若干条,使得 fvdegu1

很显然在 u 处合并后,还能随意在祖先处拆开,因此我们只要最大化子树中这样合并的路径个数 gu

我们已经知道每个子树还有多少条剩余的路径,只要两两匹配即可,当且仅当这些路径数不存在绝对众数时有解。

但如果存在绝对众数,我们依然可以通过拆掉其他子树内的 gv,获得新的两条路径,和绝对众数匹配,那么求出其他子树的 gv 之和,拆掉最少的链,使得绝对众数不存在即可。

时间复杂度 O(n)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=5e5+5;
vector <int> G[MAXN];
ll f[MAXN],g[MAXN],ans;
void dfs(int u,int fz) {
	int s=G[u].size()-1;
	ans+=1ll*s*(s+1)/2;
	array <ll,2> mx={0,0};
	ll p=0;
	for(int v:G[u]) if(v^fz) {
		dfs(v,u);
		if(f[v]<s) {
			ll z=min(g[v],(s-f[v]+1)/2);
			g[v]-=z,f[v]+=2*z;
		}
		if(f[v]>s) {
			ans-=s,f[v]-=s,p+=f[v];
			mx=max(mx,{f[v],g[v]});
		} else ans-=f[v];
		g[u]+=g[v];
	}
	if(p<mx[0]*2) {
		ll z=min(g[u]-mx[1],(mx[0]*2-p+1)/2);
		g[u]-=z,p+=2*z;
	}
	ll z=min(p/2,p-mx[0]);
	f[u]=s+p-2*z,g[u]+=z;
}
void solve() {
	int n;
	scanf("%d",&n),ans=0;
	for(int i=1;i<=n;++i) f[i]=g[i]=0,G[i].clear();
	for(int i=1,u,v;i<n;++i) scanf("%d%d",&u,&v),G[u].push_back(v),G[v].push_back(u);
	dfs(1,0),ans-=g[1],printf("%lld\n",ans);
}
signed main() {
	int T; scanf("%d",&T);
	while(T--) solve();
	return 0;
}



*F. [CF2023F] Hills and Pits

Problem Link

题目大意

给定 n 个沙坑,高度为 a1an,有正有负。

你可以在 [1,n] 数轴上左右游走,你手中初始没有沙子,经过 ai>0 的沙坑可以取走 [0,ai] 个沙子,经过 ai<0 的沙坑可以放入 [0,ai] 个沙子,要求每个沙坑上恰好有 0 个沙子,求最小步数。

q 次询问,对 alar 求答案。

数据范围:n,q3×105

思路分析

首先分析答案下界,如果 ai<0 显然不可能,否则答案肯定不超过 2n,因为我们可以从左往右取走所有正数,然后从右往左填平所有负数。

如果想要答案更小,我们就要避免每个位置都经过两次,不妨假设路径是从 sts<t,由于 [l,s)(t,r] 都已经走过了,那么我们的目标就是不在 [s,t] 中走回头路。

此时我们对路径是 slrt,因此一个 [s,t] 中的 ai<0 的位置,如果 j=liaj0 时,我们能直接填完 ai,否则需要找到 >i 的第一个前缀和 0 的位置,然后把这一段负的前缀和全部填成正的。

因此此时的权值是 i=st1[vi0][vi<0],其中 vi 表示 a[l,i] 的前缀和。

所以答案就是最大子段和,但 v 的计算和 l 有关,故不能直接线段树维护。

Si 为全局前缀和,注意到 vi0SiSl1,因此我们把询问按 Sl1 从小到大排序后扫描线,就只需要 O(n) 次修改,线段树维护最大子段和即可。

t>s 的情况是对称的,翻转 a 重复上述过程。

时间复杂度 O((n+q)logn)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=3e5+5; 
struct info {
	int su,mx,lx,rx;
	inline friend info operator +(const info &u,const info &v) {
		return {u.su+v.su,max({u.mx,v.mx,u.rx+v.lx}),max(u.lx,u.su+v.lx),max(v.rx,v.su+u.rx)};
	}
};
struct zKyGt1 {
	info tr[1<<20];
	int N;
	void init(int n) {
		for(N=1;N<=n;N<<=1);
		for(int i=1;i<2*N;++i) tr[i]={0,0,0,0};
		for(int i=1;i<=n;++i) tr[i+N]={-1,0,-1,-1};
		for(int i=N-1;i;--i) tr[i]=tr[i<<1]+tr[i<<1|1];
	}
	void upd(int x) {
		for(tr[x+=N]={1,1,1,1},x>>=1;x;x>>=1) tr[x]=tr[x<<1]+tr[x<<1|1];
	}
	int qry(int l,int r) {
		info sl={0,0,0,0},sr={0,0,0,0};
		for(l+=N-1,r+=N+1;l^r^1;l>>=1,r>>=1) {
			if(~l&1) sl=sl+tr[l^1];
			if(r&1) sr=tr[r^1]+sr;
		}
		return (sl+sr).mx;
	}
}	T;
ll a[MAXN],s[MAXN];
int n,m,l[MAXN],r[MAXN],ans[MAXN];
vector <int> qy[MAXN];
void sol() {
	T.init(n);
	vector <int> id;
	for(int i=1;i<=n;++i) qy[i].clear(),s[i]=s[i-1]+a[i];
	for(int i=1;i<=m;++i) if(l[i]<r[i]) qy[l[i]].push_back(i);
	for(int i=0;i<=n;++i) id.push_back(i);
	sort(id.begin(),id.end(),[&](int i,int j){ return s[i]^s[j]?s[i]>s[j]:i>j; });
	for(int i:id) {
		if(i>0) T.upd(i);
		for(auto q:qy[i+1]) {
			ans[q]=min(ans[q],2*(r[q]-l[q])-T.qry(l[q],r[q]-1));
		}
	}
}
void solve() {
	cin>>n>>m;
	for(int i=1;i<=n;++i) cin>>a[i];
	for(int i=1;i<=m;++i) cin>>l[i]>>r[i],ans[i]=2*(r[i]-l[i]);
	sol();
	reverse(a+1,a+n+1);
	for(int i=1;i<=m;++i) swap(l[i],r[i]),l[i]=n-l[i]+1,r[i]=n-r[i]+1;
	sol();
	for(int i=1;i<=m;++i) cout<<(s[r[i]]<s[l[i]-1]?-1:ans[i])<<"\n";
}
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}
posted @   DaiRuiChen007  阅读(18)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示