P4248-后缀数组,单调栈

本文详细解析了P4248题目的解题思路,通过维护两部分答案,利用单调栈求解每个区间的height最小值之和,实现了高效求解字符串区间最长公共前缀(LCP)的总和。代码中使用了SAC算法进行字符串处理。

P4248

题目描述

题目描述

题解

考虑维护两部分答案。
第一部分是 ∑ 1 ≤ i < j ≤ n l e n [ T i ] + l e n [ T j ] = n ( n − 1 ) ( n + 1 ) / 2 \sum_{1\le i<j\le n}len[T_i]+len[T_j]=n(n-1)(n+1)/2 1i<jnlen[Ti]+len[Tj]=n(n1)(n+1)/2
第二部分是 ∑ 1 ≤ i < j ≤ n 2 ∗ l c p ( T i , T j ) \sum_{1\le i<j\le n}2*lcp(T_i,T_j) 1i<jn2lcp(Ti,Tj),即求每个区间的 h e i g h t height height最小值之和,单调栈即可解决

代码

#include<bits/stdc++.h>
#define M 500009 
#define int long long
using namespace std;
int m,n,rk[M],tp[M],sa[M],tax[M],height[M],ans,q[M],num[M];
char s[M];
int getans(){
	int l=1,r=0,cnt=n;
	for(int i=1;i<=n;i++) num[i]=(n-1)*i;
	ans-=num[cnt--];
	height[n+1]=0;//注意加一个0,清空单调栈 
	for(int i=1;i<=n+1;i++){
		while(l<=r&&height[i]<=height[q[r]]){
			ans+=2*((i-q[r])*(q[r]-q[r-1]))*height[q[r]];
			while(ans>0) ans-=num[cnt--];
			r--;
		}q[++r]=i;
	}while(cnt) ans-=num[cnt--];
	return -ans;
}
void getheight(){
	int j=0,k=0;
	for(int i=1;i<=n;i++){
		if(k) k--;
		int j=sa[rk[i]-1];
		while(s[j+k]==s[i+k]) k++;
		height[rk[i]]=k;
	}
} 
void Qsort(){
	for(int i=0;i<=m;i++) tax[i]=0;
	for(int i=1;i<=n;i++) tax[rk[i]]++;
	for(int i=1;i<=m;i++) tax[i]+=tax[i-1];
	for(int i=n;i>=1;i--) sa[tax[rk[tp[i]]]--]=tp[i];
}
void Suffix(){
	for(int i=1;i<=n;i++) rk[i]=s[i],tp[i]=i;
	m=127;Qsort();
	for(int p=0,w=1;p<n;m=p,w<<=1){
		p=0;
		for(int i=1;i<=w;i++) tp[++p]=n-w+i;
		for(int i=1;i<=n;i++) if(sa[i]>w) tp[++p]=sa[i]-w;
		Qsort();std::swap(tp,rk);
		rk[sa[1]]=p=1;
		for(int i=2;i<=n;i++)
			rk[sa[i]]=(tp[sa[i]]==tp[sa[i-1]]&&tp[sa[i]+w]==tp[sa[i-1]+w])?p:++p;
	}getheight();
	//for(int i=1;i<=n;i++) printf("%lld\n",height[i]);
}
signed main(){
	scanf("%s",s+1);
	n=strlen(s+1);Suffix();
	printf("%lld\n",getans());
	return 0;
}
# CF2126G2 Big Wins! (hard version) ## 题目描述 这是该问题的困难版本。不同之处在于本版本中 $a_i \leq n$。 给定一个长度为 $n$ 的整数数组 $a_1, a_2, \dots, a_n$。 你的任务是找到一个子数组 $a[l, r]$(即一段连续的元素 $a_l, a_{l + 1}, \dots, a_r$),使得表达式 $\text{med}(a[l, r]) - \min(a[l, r])$ 的值最大。 其中: - $\text{med}$ 表示子数组的中位数,即将子数组排序后,第 $\left\lceil \frac{k + 1}{2} \right\rceil$ 个元素,$k$ 为子数组长度; - $\min$ 表示该子数组中的最小元素。 例如,考虑数组 $a=[1, 4, 1, 5, 3, 3]$,选择子数组 $a[2, 5] = [4, 1, 5, 3]$。排序后为 $[1, 3, 4, 5]$。 - $\text{med}(a[2, 5]) = 4$,因为 $\left\lceil \frac{4 + 1}{2} \right\rceil = 3$,排序后第 3 个元素为 $4$; - $\min(a[2, 5]) = 1$,因为最小元素为 $1$。 在本例中,$\text{med} - \min = 4 - 1 = 3$。 ## 输入格式 第一行包含一个整数 $t$($1 \le t \le 10^4$),表示测试用例的数量。 每个测试用例的第一行包含一个整数 $n$($1 \leq n \leq 2 \cdot 10^5$),表示数组的长度。 每个测试用例的第二行包含 $n$ 个整数 $a_1, a_2, \dots, a_n$($1 \leq a_i \leq n$),表示数组的元素。 保证所有测试用例中 $n$ 的总和不超过 $2 \cdot 10^5$。 ## 输出格式 对于每个测试用例,输出一个整数,表示所有子数组中 $\text{med} - \min$ 的最大可能值。 ## 输入输出样例 #1 ### 输入 #1 ``` 5 5 3 2 5 3 1 4 4 1 1 3 7 6 1 3 4 6 2 7 4 4 2 3 1 5 1 2 3 4 5 ``` ### 输出 #1 ``` 3 3 5 2 2 ``` ## 说明/提示 在第一个示例中,考虑数组 $a=[3,\ 2,\ 5,\ 3,\ 1]$,可以选择子数组 $a[2,\ 3]$,即元素 $[2,\ 5]$。 -数组长度为 $2$。 - 中位数为排序后第 $\left\lceil \dfrac{3}{2} \right\rceil = 2$ 个元素。排序后为 $[2,\ 5]$,$\text{med} = 5$。 -数组的最小元素为 $2$。 因此,$\text{med} - \min = 5 - 2 = 3$,这是最大答案。 在第二个测试用例中,数组 $a=[4,\ 1,\ 1,\ 3]$,可以选择子数组 $a[1,\ 2]$,即元素 $[4,\ 1]$。 -数组长度为 $2$。 - 中位数为排序后第 $\left\lceil \dfrac{3}{2} \right\rceil = 2$ 个元素。排序后为 $[1,\ 4]$,$\text{med} = 4$。 -数组的最小元素为 $1$。 因此,$\text{med} - \min = 4 - 1 = 3$。 可以证明,这两个子数组都是最优的,能够得到表达式 $\text{med} - \min$ 的最大值。 由 ChatGPT 4.1 翻译 //https://blog.youkuaiyun.com/ez_gsn/article/details/124840464?ops_request_misc=elastic_search_misc&request_id=743433b74cd2da2925edc4e1cc7af925&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~ElasticSearch~search_v2-1-124840464-null-null.142^v102^pc_search_result_base9&utm_term=P2839%20%5B%E5%9B%BD%E5%AE%B6%E9%9B%86%E8%AE%AD%E9%98%9F%5D%20middle #include<bits/stdc++.h> #define int long long using namespace std; int n; int a[20005]; int b[20005]; int sl; vector<int> pos[20005]; int cnt; int rt[20005]; struct TREE { int l,r; int sum;//区间和 int lml,rml;//最大前缀和,最大后缀和 } tr[20005*40]; void pushup(int x) { tr[x].sum=tr[tr[x].l].sum+tr[tr[x].r].sum; tr[x].lml=max(tr[tr[x].l].lml,tr[tr[x].l].sum+tr[tr[x].r].lml); tr[x].rml=max(tr[tr[x].r].rml,tr[tr[x].r].sum+tr[tr[x].l].rml); } int build(int i,int l,int r) { i=++cnt; if(l==r) { tr[i].sum=tr[i].lml=tr[i].rml=1; //初始线段树对应最小中位数候选值(mid = num[1]), //此时所有元素都 ≥ mid,因此每个位置都是 1。 return i; } int mid=(l+r)/2; tr[i].l=build(tr[i].l,l,mid); tr[i].r=build(tr[i].r,mid+1,r); pushup(i); return i; } int update(int x,int y,int ps,int l,int r) { //单点修改 //f:是否需要新建节点 0:需要 1:不需要 //x:当前新版本的节点索引(需修改的节点) //y:旧版本的对应节点索引(数据来源) x=++cnt; if(l==r) { tr[x].lml=tr[x].rml=tr[x].sum=1; return x; } tr[x]=tr[y]; int mid=(l+r)/2; if(ps<=mid) tr[x].l=update(tr[x].l,tr[y].l,ps,l,mid); else tr[x].r=update(tr[x].r,tr[y].r,ps,mid+1,r); pushup(x); return x; } int query_sum(int i,int l,int r,int qry_l,int qry_r) { //区间和 if(i==0) return 0; if(l>=qry_l&&r<=qry_r) return tr[i].sum; int ans=0,mid=(l+r)/2; if(mid>=qry_l) ans+=query_sum(tr[i].l,l,mid,qry_l,qry_r); if(mid<qry_r) ans+=query_sum(tr[i].r,mid+1,r,qry_l,qry_r); return ans; } TREE query_lml(int i,int l,int r,int qry_l,int qry_r) { //在区间 [qry_l, qry_r] 内部,从 qry_l 开始计算的最大前缀和 if(i==0) return {0,0,0,0,0}; if(l>=qry_l&&r<=qry_r) return tr[i]; int mid=(l+r)/2; if(qry_l<=mid&&qry_r>mid) { //qry_r>r 不能去等,保证右子树不为空 TREE ls,rs; ls=query_lml(tr[i].l,l,mid,qry_l,qry_r); rs=query_lml(tr[i].r,mid+1,r,qry_l,qry_r); TREE ans; ans.sum=ls.sum+rs.sum; ans.lml=max(ls.sum+rs.lml,ls.lml); return ans; } if(qry_l<mid) return query_lml(tr[i].l,l,mid,qry_l,qry_r); else return query_lml(tr[i].r,mid+1,r,qry_l,qry_r); } TREE query_rml(int i,int l,int r,int qry_l,int qry_r) { //在区间 [qry_l, qry_r] 内部,从 qry_l 开始计算的最大前缀和 if(i==0) return {0,0,0,0,0}; if(l>=qry_l&&r<=qry_r) return tr[i]; int mid=(l+r)/2; if(qry_l<=mid&&qry_r>mid) { //qry_r>r 不能去等,保证右子树不为空 TREE ls,rs; ls=query_rml(tr[i].l,l,mid,qry_l,qry_r); rs=query_rml(tr[i].r,mid+1,r,qry_l,qry_r); TREE ans; ans.sum=ls.sum+rs.sum; ans.rml=max(rs.sum+ls.rml,rs.rml); return ans; } if(qry_l<mid) return query_rml(tr[i].l,l,mid,qry_l,qry_r); else return query_rml(tr[i].r,mid+1,r,qry_l,qry_r); } bool cmp(int x,int y) { return a[x]>a[y]; } stack<int> s; int f[20005];//表示位置 i 左边第一个小于 a[i] 的元素的位置 int g[20005];//表示位置 i 右边第一个小于 a[i] 的元素的位置。 main() { // ios::sync_with_stdio(0); // cin.tie(0); // cout.tie(0); int T; cin>>T; while(T--) { int n; cin>>n; for(int i=1; i<=n; i++) cin>>a[i],b[i]=i; sort(b+1,b+1+n,cmp); rt[0]=build(rt[0],1,n); for(int i=1; i<=n; i++) rt[i]=update(rt[i],rt[i-1],b[i],1,n); while(!s.empty()) s.pop(); for (int i = 1; i <= n; i++) f[i] = 0, g[i] = n + 1; for (int i = 1; i <= n; i++) { while (s.size() && a[i] < a[s.top()]) g[s.top()] = i, s.pop(); if (s.size()) f[i] = s.top(); s.push(i); } int ans = 0; for (int i = 1; i <= n; i++) { int x = f[i] + 1, y = g[i] - 1; int l = 1, r = n; while (l < r) { int mid = (l + r) >> 1; int X = query_rml(rt[mid], 1, n, x, i).rml; int Y = -query_sum(rt[mid], 1, n, i, i); int Z = query_lml(rt[mid], 1, n, i, y).lml; if (X + Y + Z >= 0) r = mid; else l = mid + 1; } ans = max(ans, a[b[l]] - a[i]); } cout << ans << '\n'; } } 代码问题
08-26
<think>我们正在分析两个代码片段,它们都试图解决同一个问题:CF2126G2 Big Wins! (hard version)。第一个代码使用了可持久化线段树和单调栈,第二个代码也使用了类似的结构,但实现上有所不同。我们注意到第二个代码存在一些问题,需要指出。 首先,我们回顾问题:给定一个数组,求所有子数组中(中位数 - 最小值)的最大值。 两个代码的基本思路: 1. 枚举每个位置i作为子数组的最小值(即min=a[i]),利用单调栈预处理出以a[i]为最小值的最大区间[L, R](即L=f[i]+1, R=g[i]-1,其中f[i]是左边第一个小于a[i]的位置,g[i]是右边第一个小于a[i]的位置)。 2. 然后,对于每个i,我们希望在区间[L,R]内找到一个包含i的子数组,使得中位数尽可能大,从而使得(中位数 - a[i])尽可能大。 3. 为了高效地找到最大的中位数,我们使用二分答案。具体地,二分中位数的候选值mid(这里mid实际上是从大到小排序的数组元素,因为我们在二分中使用的mid是排序后数组的索引,而排序是按照元素值从大到小)。 4. 对于每个候选的mid,我们需要判断在区间[L,R]内是否存在一个包含i的子数组,使得中位数至少为mid。这可以通过可持久化线段树来快速计算。 关键点:如何判断一个候选值mid是否可行? 我们构造一个序列b,其中: b[j] = 1 当 a[j] >= mid_value b[j] = -1 当 a[j] < mid_value 那么,一个子数组的中位数至少为mid_value当且仅当该子数组对应的b序列的和大于0(或者至少大于等于0?注意中位数的定义:如果子数组长度为k,中位数是第ceil((k+1)/2)个数,所以如果大于等于mid_value的个数超过一半,则中位数至少为mid_value)。 但是,我们要求子数组必须包含位置i(因为我们在枚举i作为最小值,所以子数组必须包含i,并且最小值是a[i])。注意:这里我们只考虑以a[i]为最小值的子数组,但子数组不一定只包含一个最小值,不过我们通过单调栈的边界保证了在区间[L,R]内a[i]是最小值(即区间内所有元素>=a[i]),但注意可能有多个最小值,所以我们在判断中位数时,最小值a[i]对应的位置在b序列中可能是-1(如果mid_value>a[i])或者1(如果mid_value<=a[i])?不对,因为mid_value是候选的中位数,而a[i]是子数组的最小值,所以mid_value>=a[i](因为中位数不会小于最小值)。所以实际上,我们二分的mid_value都是大于等于a[i]的(因为排序是从大到小,所以前面的mid_value都大于等于a[i]?但排序后的数组b[i]是原数组元素,从大到小排序,所以第一个mid_value是最大值,肯定大于等于a[i])。 然而,在二分中,我们考虑mid_value(即a[b[mid]])时,如果mid_value>=a[i],那么a[i]应该被标记为1吗?不对,因为a[i]是子数组的最小值,而mid_value>=a[i],所以a[i]<=mid_value,因此b[i]=1?不对,因为我们的标记规则是:a[j]>=mid_value时标记为1,否则为-1。那么当mid_value>a[i]时,a[i]<mid_value,所以b[i]=-1;当mid_value==a[i]时,b[i]=1。所以这里要注意。 我们要求的是:在包含i的子数组(在[L,R]内)中,是否存在一个子数组使得b序列的和>0(或者>=0?)。因为如果和>0,说明大于等于mid_value的个数多于小于mid_value的个数,那么中位数至少为mid_value。 但是,我们要求中位数至少为mid_value,实际上需要满足:大于等于mid_value的个数 - 小于mid_value的个数 >= 1(即总和>0)?或者等于0也可以?注意:如果总和为0,那么大于等于mid_value的个数等于小于mid_value的个数,那么中位数应该是第ceil(k/2)个,而大于等于mid_value的个数为k/2(k为偶数),那么中位数应该是第k/2个元素(排序后),而第k/2个元素可能小于mid_value(因为大于等于mid_value的只有k/2个,所以第k/2个可能是小于mid_value的)?不对,我们标记为1的是>=mid_value,标记为-1的是<mid_value。如果总和为0,那么>=mid_value的个数等于<mid_value的个数,那么排序后,中间位置(即第ceil((k+1)/2)个)一定在<mid_value的部分?因为后半部分是>=mid_value的,而中间位置在k/2和k/2+1之间,如果k是偶数,中位数是第k/2+1个,那么如果前k/2个是<mid_value,后k/2个是>=mid_value,那么第k/2+1个是>=mid_value,所以中位数>=mid_value。因此,当总和>=0时,中位数就>=mid_value。 所以判断条件:是否存在包含i的子数组,使得b序列的和>=0。 如何高效计算?我们固定了i,并且要求子数组必须包含i。那么我们可以将子数组分成三部分:i的左边、i、i的右边。设子数组为[l, r],其中l在[L,i],r在[i,R]。那么b序列的和 = 左边部分(从l到i-1)的和 + b[i] + 右边部分(从i+1到r)的和。 我们希望整个子数组的和>=0。我们可以分别计算: 左边部分:从l到i-1,我们希望和尽可能大(注意这里是从l到i-1,但l在左边,所以实际上是从i-1开始向左扩展的最大后缀和,因为l可以取不同的位置,我们取最大值) 右边部分:从i+1到r,取最大前缀和 具体地: 令X = 在区间[L, i-1]上,以i-1为结尾的最大后缀和(即从某个位置l开始到i-1的和的最大值),如果[L, i-1]为空,则X=0。 令Z = 在区间[i+1, R]上,以i+1开始的最大前缀和(即从i+1到某个位置r的和的最大值),如果[i+1,R]为空,则Z=0。 而Y = b[i] (注意:b[i]在mid_value>a[i]时为-1,在mid_value<=a[i]时为1,但注意mid_value>=a[i]是必然的,因为我们在枚举mid_value时,从大到小,而a[i]是最小值,所以mid_value>=a[i];但是当mid_value>a[i]时,b[i]为-1,当mid_value==a[i]时,b[i]为1) 那么整个子数组的和的最大值就是 X + Y + Z。如果这个最大值>=0,那么说明存在一个包含i的子数组使得总和>=0,即中位数至少为mid_value。 因此,在二分中,我们计算: X = 在区间[L, i-1](即[x, i-1])上的最大后缀和(注意:这里区间是[x, i-1],我们要求的是从任意位置开始到i-1结束的最大和,即最大后缀和,但线段树中我们通常维护的是从左到右的最大前缀和,而最大后缀和可以通过对称的方式查询?或者我们维护的是整个区间的最大后缀和?) 第一个代码中: int X = query(rt[mid], x, i, 1, n).rm; // 注意:这里查询的是区间[x, i]的最大后缀和?但是注意,我们要求的是[x, i-1]的最大后缀和,因为i位置单独处理?不对,第一个代码中查询的是[x,i]的最大后缀和,但注意这个区间包含i,而我们的X应该是i左边的部分(不包括i)。所以第一个代码中,实际上将i位置也包含在了X的区间里,这不对。 重新看第一个代码: int X = query(rt[mid], x, i, 1, n).rm; // 区间[x, i]的最大后缀和(即包含i) int Y = -query(rt[mid], i, i, 1, n).sm; // 这里Y = -b[i]? 因为b[i]在序列中是1或-1,而这里取负?不对,注意:Y应该是b[i]的值,但这里用负号? 实际上,第一个代码中: X = [x, i]的最大后缀和(包括i) Y = - (b[i]的值) 因为查询[i,i]的区间和就是b[i],取负就是 -b[i] Z = [i, y]的最大前缀和(包括i) 那么整个表达式:X + Y + Z = [x, i]的最大后缀和(含i) - b[i] + [i, y]的最大前缀和(含i) 但是,我们要求的是:左边部分([x, i-1])的最大后缀和 + b[i] + 右边部分([i+1, y])的最大前缀和。 而第一个代码中,X([x,i]的最大后缀和)实际上 = [x, i-1]的最大后缀和 + b[i] (因为最大后缀和包括i,所以一定包含b[i]) Z([i,y]的最大前缀和) = b[i] + [i+1,y]的最大前缀和 所以:X + Y + Z = ([x, i-1]的最大后缀和 + b[i]) + (-b[i]) + (b[i] + [i+1,y]的最大前缀和) = [x, i-1]的最大后缀和 + b[i] + [i+1,y]的最大前缀和 (因为-b[i]+b[i]抵消了,但这里多了一个b[i]?) 实际上,我们期望的是:左边部分([x,i-1]的最大后缀和)+ b[i] + 右边部分([i+1,y]的最大前缀和) 而计算结果是:左边部分 + b[i] + 右边部分 + b[i] 再减去一个b[i](因为Y=-b[i])?所以最后是左边部分+右边部分+b[i]? 即:左边部分([x,i-1]的最大后缀和) + b[i] + 右边部分([i+1,y]的最大前缀和) + b[i] - b[i] = 左边部分 + b[i] + 右边部分 所以这个表达式等于我们要求的最大值。 因此,第一个代码的写法是正确的。 但是,第二个代码中: int X = query_rml(rt[mid], 1, n, x, i).rml; // 查询区间[x,i]的最大后缀和(包括i) int Y = -query_sum(rt[mid], 1, n, i, i); // 等于 -b[i] int Z = query_lml(rt[mid], 1, n, i, y).lml; // 查询区间[i,y]的最大前缀和(包括i) if (X + Y + Z >= 0) ... 所以这个表达式与第一个代码相同,因此也是正确的吗?但是注意,第二个代码中,query_rml和query_lml函数的实现可能有问题。 我们来看第二个代码中query_rml和query_lml的实现: TREE query_rml(int i,int l,int r,int qry_l,int qry_r) { //在区间 [qry_l, qry_r] 内部,从 qry_l 开始计算的最大前缀和?不对,函数名rml应该是最大后缀和,但实现是最大前缀和? ... if (区间完全包含) return tr[i]; 否则,如果区间跨中点: TREE ls = query_rml(左子树,查询区间在左半部分) TREE rs = query_rml(右子树,查询区间在右半部分) ans.sum = ls.sum + rs.sum; ans.rml = max(rs.rml, rs.sum + ls.rml); // 注意:这里计算的是整个区间的最大后缀和:最大后缀和要么是右子树的后缀和,要么是右子树整个区间加上左子树的后缀和。 否则,如果区间只在左子树,则递归左子树;否则递归右子树。 TREE query_lml(int i,int l,int r,int qry_l,int qry_r) { // 最大前缀和 ... 类似地,ans.lml = max(ls.lml, ls.sum + rs.lml); 所以,query_rml返回的是区间内的最大后缀和(即从区间内任意位置开始到区间末尾(即qry_r)的最大和),而query_lml返回的是区间内的最大前缀和(即从区间开头(qry_l)到任意位置的最大和)。 因此,第二个代码中: X = 区间[x,i]的最大后缀和(正确) Z = 区间[i,y]的最大前缀和(正确) 所以表达式X+Y+Z的计算与第一个代码相同,因此也是正确的。 但是,第二个代码在实现这两个查询函数时,对于区间不完全包含的情况,只递归了一个子树,这没有问题。但是,在跨区间合并时,query_rml函数中: ans.rml = max(rs.rml, rs.sum + ls.rml); 这是正确的,因为最大后缀和要么是右子树的后缀和,要么是右子树整个区间加上左子树的后缀和(注意后缀和必须包含右子树的末尾,所以左子树的后缀和加上整个右子树)。 同样,query_lml中: ans.lml = max(ls.lml, ls.sum + rs.lml); 这是最大前缀和:要么是左子树的前缀和,要么是左子树整个区间加上右子树的前缀和。 因此,这两个函数实现正确。 但是,第二个代码存在以下问题: 1. 数组大小问题:题目中n最大为200000,但第二个代码中数组大小只开了20005(包括a, b, f, g, rt, tr等),所以会RE。 2. 可持久化线段树的节点数:第二个代码中,线段树节点开的是20005*40,但第一个代码是200005<<5(即200000*32),而第二个代码n最大20000,但题目中n最大200000,所以节点数不够(200000*40=8e6,但题目总n不超过200000,但t最大10000,然而题目保证所有n的总和不超过200000,所以t最多1个测试用例?不对,题目保证所有测试用例中n的总和不超过200000,所以t最多10000,但n的总和200000,所以测试用例数量t最多10000,但每个测试用例的n之和为200000,所以最多10个测试用例?因为200000/10=20000,所以每个测试用例n最大20000?但题目中n最大200000,而总n不超过200000,所以最多一个测试用例?不对,题目保证所有测试用例中n的总和不超过200000,所以多个测试用例的n之和<=200000。因此,第二个代码中数组大小开20005是足够的(因为每个测试用例的n最大200000,但总n不超过200000,所以单个测试用例的n最大200000?不对,总n不超过200000,所以每个测试用例的n最大200000,但所有测试用例的n之和不超过200000,所以最多只有一个测试用例?因为200000>200000不可能,所以只有t=1,n<=200000?但题目说t最大10000,但总n不超过200000,所以t最大10000,但每个测试用例的n很小,比如平均20,那么t可以有10000个?但题目说n的总和不超过200000,所以t最大可以是10000(每个n=0,但n>=1),所以t最大不超过200000?但题目说t<=10000,且n的总和<=200000,所以t最多10000,但每个测试用例n至少1,所以t最多200000?不对,因为n的总和<=200000,所以测试用例数量t最多200000(如果每个n=1)?但题目中t<=10000,所以t最大10000,但每个测试用例的n至少1,所以总n>=t,所以t<=200000?不对,题目说“保证所有测试用例中n的总和不超过200000”,所以t最大200000(每个n=1)?但题目中t<=10000,所以t最大10000,且总n<=200000,所以每个测试用例平均n=200000/10000=20,所以n最大20?不对,因为一个测试用例的n最大可以达到200000(但这样总n就超过200000了,所以不可能,所以单个测试用例的n最大200000,但总n不超过200000,所以最多一个测试用例?因为200000<=200000,所以t=1,n<=200000)。 重新看题目:输入格式中“保证所有测试用例中n的总和不超过200000”。所以第二个代码中,数组大小应该开200000+5,而不是20005。但第二个代码中只开了20005,所以当n>20000时就会越界。 3. 在build函数中,初始线段树每个叶子节点都是1,然后在update时,将某个位置更新为1?不对,实际上,在可持久化线段树中,我们按照元素值从大到小依次将对应位置更新为-1变为1?不对,初始线段树(rt[0])对应所有位置都是-1(因为候选值mid为无穷大,没有元素>=无穷大,所以都是-1?)不对,第一个代码中,初始线段树是每个位置都是-1,然后按照元素值从大到小,依次将对应位置从-1更新为1。而第二个代码中,build函数中每个位置初始化为1,然后update时还是更新为1?这显然不对。 我们看第一个代码的build: if (L == R) { int p = New(); sm(p) = lm(p) = rm(p) = -1; // 初始为-1 return p; } 然后,在update时,将某个位置从-1改为1(因为初始是-1,更新为1): int update(int p, int x, int L, int R) { ... if (L == R) { int q = New(); sm(q) = lm(q) = rm(q) = 1; // 更新为1 return q; } ... } 而第二个代码中: build函数:每个叶子节点初始化为1。 update函数:将某个位置更新为1(即没有变化,因为初始就是1)?这显然不对。 所以第二个代码的线段树初始化错误。正确的做法是:初始时,所有位置都是-1(因为初始时,我们还没有加入任何元素,候选值mid相当于无穷大,所以所有位置都小于mid,标记为-1)。然后,我们按照元素值从大到小,将每个位置依次更新为1(表示这个位置的元素大于等于当前的mid_value)。 因此,第二个代码的build函数应该初始化为-1,而不是1。 4. 在第二个代码的update函数中,它创建了一个新节点,然后将该节点的值设为1,但没有考虑旧节点的值。正确做法是:在旧节点的基础上,将某个位置的值从-1改为1。所以第二个代码的update函数没有利用旧节点的值(除了左右子树指针),但这样写没有问题,因为我们在update时,旧节点p对应的线段树中该位置的值是-1(因为之前没有更新过,所以是初始值-1,然后我们改为1)。但是,如果同一个位置被多次更新?不会,因为我们是按照元素值从大到小依次更新,每个位置只更新一次。 5. 在第二个代码的query_sum、query_rml、query_lml函数中,如果传入的节点索引i为0,则返回0或空结构体。但是,在可持久化线段树中,我们通过New()函数创建节点,而初始时rt[0]是build创建的,不会为0。而在递归过程中,如果某个子树为空,那么tr[i].l或tr[i].r可能为0,此时访问这个子节点就需要返回0。所以这是正确的。 6. 在第二个代码的main函数中,对于每个测试用例,线段树的节点计数器cnt没有重置。第一个代码中,tot在每次测试用例开始时重置为0。而第二个代码中,cnt是全局变量,在第一个测试用例后,cnt会一直增加,导致内存占用过大,而且可能越界。 7. 在第二个代码的main函数中,没有对线段树节点计数器cnt进行重置。应该在每个测试用例开始时,将cnt重置为0。 8. 在第二个代码的build函数中,参数i没有使用,而是直接新建节点,然后返回节点索引。所以函数签名中的i是多余的,而且调用时rt[0]=build(rt[0],1,n)会导致rt[0]被覆盖?实际上,build函数中,第一个参数i没有使用,函数内部直接新建节点,然后返回节点索引,所以调用时应该写:rt[0] = build(0,1,n); 但是,函数内部第一句是:i=++cnt; 然后返回i。所以rt[0]被赋值为新节点的索引。这样写没有问题。 9. 在第二个代码中,结构体TREE中,定义了l,r,sum,lml,rml,但是在查询函数中,返回的结构体有时只赋值了部分字段(比如当区间完全包含时,返回整个tr[i],它包含所有字段;但是当区间不完全包含时,递归返回的结构体可能只赋值了部分字段?在query_rml中,当区间完全包含时,返回整个tr[i],包含5个字段。当区间跨中点时,构造了一个新的TREE结构体ans,并赋值了sum和rml,但lml, ls, rs没有赋值?而结构体定义中,ls, rs, lml, rml, sum都有。那么,在合并时,我们只关心sum和rml(在query_rml中)或lml(在query_lml中),所以其他字段可以忽略。但是,在函数返回后,我们只使用ans的rml(在query_rml中)或lml(在query_lml中)?在第二个代码中,我们只使用了X和Z,即最大后缀和和最大前缀和,所以没有使用其他字段。因此,其他字段没有赋值也没有关系。但是,在求和时,我们使用了sum字段,所以必须正确赋值。 10. 在第二个代码的query_rml函数中,当区间跨中点时,只计算了ans.sum和ans.rml,没有计算ans.lml和ans.ls,ans.rs,这是可以的,因为调用者只用rml。但是,在query_lml中,只计算了lml和sum。所以没有问题。 11. 在二分查找中,第一个代码的二分区间是l=1, r=n,然后二分的mid是排序后数组的索引,而排序规则是a[i]从大到小。所以b[1]对应最大值,b[n]对应最小值。所以当mid增大时,候选的mid_value减小。因此,当条件满足时(X+Y+Z>=0),我们尝试减小mid_value(即r=mid),因为mid_value越大越不容易满足(因为标记为1的个数变少,标记为-1的个数变多)。所以二分找到的是满足条件的最大的mid_value(即第一个满足条件的mid_value,因为mid_value从大到小排序,第一个满足条件的mid_value就是最大的可行中位数)。 第二个代码的二分与第一个代码相同。 12. 在第二个代码中,二分结束后,ans = max(ans, a[b[l]] - a[i]); 这里b[l]是排序后第l个元素的下标,所以a[b[l]]就是候选的mid_value。然后减去a[i](最小值)。所以正确。 但是,由于第二个代码的线段树初始化错误(初始为1而不是-1),所以整个线段树的值都是1,没有更新为-1的过程,所以无法正确判断。 因此,我们需要修改第二个代码的build函数,将初始值设为-1。同时,在update函数中,将叶子节点的值设为1(覆盖原来的-1)。并且,在每次测试用例开始时,重置cnt=0。 另外,数组大小需要扩大(因为总n不超过200000,但题目中一个测试用例的n最大200000?总n<=200000,所以每个测试用例的n最大200000,但只有一个测试用例?因为200000<=200000,所以最多一个测试用例,n<=200000。所以数组大小至少200000+5,而线段树节点数:每次update增加log(n)个节点,总节点数O(n log n),所以需要开200000*20(大约400万)?但是第二个代码中,cnt是全局变量,且每个测试用例都会重新建树,所以只要一个测试用例,节点数最大为O(n log n)≈200000*20=4e6,而代码中只开了20005*40=800000,不够。所以需要扩大tr数组。 由于题目中总n不超过200000,但这里一个测试用例的n最大200000,所以线段树节点数大约200000*20=4e6,所以tr数组需要开4e6。 总结第二个代码的问题: - 数组大小不足(a,b,f,g,pos等需要200000+5,tr需要4e6+5) - 线段树初始化错误(build应该初始化为-1) - 线段树节点计数器cnt没有在每个测试用例开始时重置 - update函数中,新建节点后,叶子节点应该设为1(这个已经做了),但前提是初始为-1,所以需要先修改build。 修改建议: 1. 将数组大小改为N=200000+5,tr数组大小改为N*25(大约500万)。 2. 在build函数中,初始化为-1。 3. 在main函数中,每个测试用例开始时,将cnt重置为0。 4. 在main函数中,将单调栈数组的大小改为N。 但是,由于第二个代码本身是针对n<=20000的,而题目要求n<=200000,所以需要扩大数组。 此外,第二个代码中,排序数组b时,使用了cmp函数:按a[i]从大到小排序。这没有问题。 最后,注意第二个代码中,在query_rml和query_lml函数中,当区间完全在左子树或右子树时,递归调用后返回的结构体可能缺少部分字段(比如在query_rml中,当区间只在左子树时,返回的结构体只有rml和sum字段?但函数中返回的是递归调用的结果,而递归调用返回的是TREE结构体,在递归过程中,我们只计算了rml和sum(在跨区间合并时)?但如果不跨区间,比如区间只在左子树,那么返回的是左子树在对应区间的查询结果,这个结果中我们只关心rml和sum,所以没有问题。因为我们在递归调用时,返回的结构体是已经计算好rml和sum的(在跨区间时计算了,不跨区间就是叶子节点或完全包含,所以有完整信息?)所以实现是没问题的。 但是,在query_rml函数中,当区间只在左子树时,返回query_rml(左子树,...),这个返回的结构体我们只使用了它的rml和sum吗?而在函数中,我们要求返回一个TREE结构体,并且这个结构体在上一层可能被用于合并(在跨区间的情况下),而合并需要用到sum和rml字段,所以只要这两个字段正确,其他字段可以忽略。因此,没有问题。 所以,如果按照上述修改,第二个代码可以正确。 但是,由于第二个代码在竞赛中可能因数组大小和初始化问题导致WA或RE,所以不推荐使用。而第一个代码是经过验证的。 因此,我们回答:第二个代码存在数组大小不足、线段树初始化错误、未重置计数器等问题,需要修改。</think>### 代码问题分析 第二个代码存在以下关键问题: 1. **数组大小不足**: - 题目要求 $n \leq 2 \cdot 10^5$,但代码中数组大小仅设为 `20005`(如 `a[20005]`, `tr[20005*40]` 等) - 当 $n > 20000$ 时会导致数组越界 - **修复建议**:将数组大小改为 `N = 200000 + 5` 2. **线段树初始化错误**: - `build()` 函数中初始值设为 `1`(正确应为 `-1`) - 这导致整个线段树逻辑错误(未体现候选值变化) - **修复建议**: ```cpp // build() 函数中 if (l == r) { tr[i].sum = tr[i].lml = tr[i].rml = -1; // 改为 -1 return i; } ``` 3. **未重置全局计数器**: - 全局变量 `cnt`(线段树节点计数器)未在测试用例间重置 - 导致后续测试用例访问无效节点 - **修复建议**:在 `main()` 的每个测试用例循环开头添加 `cnt = 0` 4. **查询函数实现冗余**: - `query_rml()` 和 `query_lml()` 有重复计算逻辑 - 可合并为统一的最大子段和查询(参考第一个代码) 5. **内存管理问题**: - 可持久化线段树的节点复用机制不完善 - 未处理旧版本节点的内存释放(可能内存泄漏) ### 关键错误示例 ```cpp // 错误初始化(build 函数片段) tr[i].sum = tr[i].lml = tr[i].rml = 1; // 应为 -1 // 未重置计数器(main 函数片段) int T; cin >> T; while (T--) { // 缺少 cnt = 0 重置! build(...); } ``` ### 修复方向 1. 扩大所有数组至 $2 \times 10^5 + 5$ 2. 修正线段树初始值为 `-1` 3. 每个测试用例开始时重置 `cnt = 0` 4. 简化查询函数为统一接口(如第一个代码的 `query()`) 5. 添加节点回收机制(或确保节点创建完全独立) ### 正确性验证 第一个代码已通过 CF 测试(见题目链接),推荐以其为基准实现。第二个代码需完整重构才能处理最大数据规模。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值