1 条题解
-
0
自动搬运
来自洛谷,原作者为

Miraik
看啊那只蝴蝶 飞过时间 落在心间搬运于
2025-08-24 22:47:38,当前版本为作者最后更新于2022-11-26 19:23:38,作者可能在搬运后再次修改,您可在原文处查看最新版自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多
以下是正文
首先考虑给定一个长度为 的括号序列,我们将它变成一个合法括号序列的最优策略是什么样的。
首先有两个性质:
- 我们只会做最多一次变换操作。
证明:显然,多次变换操作可以合为一次。
- 设这个序列有 个左括号, 个右括号,那么必然存在一种变换方案,使得变换后有 组括号被匹配。换句话说,就是左括号和右括号中个数较少的全部被匹配到。
证明:不妨设 ,把左括号看作 ,右括号看作 ,然后再得到一个前缀和数组 。然后选择数组中最小值所在位置 ,通过变换使得 这个位置移到新序列的末尾。
如果有右括号没匹配上左括号,就说明存在一个位置 使得 。
对于 之后的任意位置 都有 ,那么把 变换到序列开头,这部分必然使得整体的 数组增加。
而变换前 这部分在变换后的 值增加量是完全一致的(也就是变换前的 )。因此 (相当于变换后的 )依然是这部分的最小值。又因为 ,我们可以知道 此时就是 。
因此结论得证。 的情况是类似的,不再赘述。有了这两个性质,这题就可做了。因为我们发现如果一个序列的匹配数没有达到上界 的话,我们把它循环移位一下必然不劣。因此最后的答案就是 $\sum\limits_{l=1}^{n} \sum\limits_{r=l}^{n} \lvert A[l, r] - B[l, r]\rvert+g(l,r)$,其中 意义同上, 表示序列的匹配数是否达到上界(是为 ,不是为 )。
- ,
暴力枚举 ,再暴力计算,时间复杂度 。
- ,
暴力枚举 , 从 开始向后拓展,在拓展的过程中维护括号匹配情况以及 ,计算 的贡献。时间复杂度 。
- 特殊性质:所有括号都是左括号
此时容易发现就是求 。这就是 啊。如果您是数学带师的话还可以直接看出这个东西就是 (考虑组合意义)。
- 特殊性质:对于所有整数 ,有
所有可能的子串只有四种形态:,, 和 。其中只有第三种 ,容易做到线性计算。
- ,
此时我们再整理整理式子,让它变成 $\sum\limits_{l=1}^{n} \sum\limits_{r=l}^{n} |c_r-c_{l-1}| + \sum\limits_{l=1}^{n} \sum\limits_{r=l}^{n} g(l,r)$。
前面那个部分可以直接把 排序后快速计算 $\sum\limits_{r=0}^{n} \sum\limits_{l=0}^{r} c_r-c_l$ 即可。对于后面那个,我们考虑把 转化成更优秀的条件。
关键性质: 等价于:$\min\{c_l,c_{l+1},\cdots,c_{r-1}\} < \min(c_{l-1},c_r)$。
证明:
可以看作至少一个右括号失配。
可以看作至少一个左括号失配。
由之前得到的性质 2,容易知道这是 的充要条件。
直接硬莽 $\min\{c_l,c_{l+1},\cdots,c_{r-1}\} < \min(c_{l-1},c_r)$ 有点困难,我们将其转化为 和 $\min\{c_l,c_{l+1},\cdots,c_{r-1}\} < c_r \le c_{l-1}$ 两种情况的方案数之和。
一般化地,我们枚举 ,找到最小的 满足 ,再查询满足 且 的 个数,就可以算出满足 $\min\{c_{l+1},c_{l+2},\cdots,c_{r-1}\} < c_{l} < c_r$ 的方案数了。另一种情况同理。
使用单调栈求出 ,后面的部分本质上是二维偏序,可以离线树状数组,总时间复杂度 。
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=4000005; int n,m,sum[N],tmp[N],a[N],top,stc[N]; ll ts,ans; char str[N]; struct BIT{ int c[N]; #define lowbit(x) (x&(-x)) inline void clr(){ for(int i=1;i<=n*2+1;i++) c[i]=0; } inline void update(int x){ while(x<=n*2+1) c[x]++,x+=lowbit(x); } inline int query(int x){ int res=0; while(x) res+=c[x],x-=lowbit(x); return res; } }T; struct Q{ int x,val; bool operator < (const Q &a) const { return x<a.x; } }q[N]; inline void calc(int op){ // op=0:a[l]<=a[r],op=1:a[l]<a[r] 满足 min(a[l+1],...,a[r-1])<a[l] top=m=0; for(int i=n;i;i--){ while(top && a[i]<=a[stc[top]]) top--; if(top) q[++m]={stc[top],a[i]}; stc[++top]=i; } sort(q+1,q+m+1); for(int i=m,j=n;i;i--){ while(j>=q[i].x) T.update(a[j]+n+1),j--; ans+=n-j-T.query(q[i].val+n+op); } T.clr(); } inline void solve(){ scanf("%d%s",&n,str+1); for(int i=1;i<=n;i++) sum[i+1]=sum[i]+(str[i]=='('?1:-1); // 为了方便,我在这里直接把 c[0] 放在前面,后面统计贡献就全部是 1 下标了 ans=ts=0; n++; for(int i=1;i<=n;i++) tmp[i]=sum[i]; sort(tmp+1,tmp+n+1); for(int i=1;i<=n;i++) ans+=1ll*(i-1)*tmp[i]-ts,ts+=tmp[i]; // 求第一部分的贡献 for(int i=1;i<=n;i++) a[i]=sum[i]; calc(0); for(int i=1;i<=n;i++) a[i]=sum[n-i+1]; calc(1); printf("%lld\n",ans); } int main(){ int tc; scanf("%d",&tc); while(tc--) solve(); return 0; }- ,
的统计稍显麻烦,我们正难则反。
考虑 等价于 $\min\{c_l,c_{l+1},\cdots,c_{r-1}\} \ge \min(c_{l-1},c_r)$,我们不妨对 的情况计数。
那么这个式子等价于 或 。
发现一个重要的事实:在 时,我们上面的这个或式不可能同时满足!且 时,这两个式子必然同时满足或不满足。
另外我们还注意到:。
于是 可以直接等价于 !
那么容易发现对于一个 ,其向左和向右延伸的贡献为一段连续的区间,那么这个区间我们都不需要单调栈了(上面实际上也不用),用一个桶就可以轻松求出,且在 时,其贡献必然被 向右的区间和 向左的区间中的恰好一个统计到!
那么我们直接将所有贡献区间统计起来,就统计好了所有 的贡献,而 的贡献则被我们计算了两遍。
于是我们现在需要再减掉所有 的贡献。依旧是注意到 ,于是我们发现 有贡献当且仅当不存在 满足 且 。那么我们发现对于每一种 其出现的位置又可以依此划为一段一段,而每一段出现次数为 的贡献即为 。这个部分线性扫一遍即可完成,具体实现也可以参照代码。(代码比题解清楚.jpg)
至于前面的部分 中计算怎么做到线性? 的值域是 的,于是直接桶排序即可。
于是我们做到了时间复杂度 ,且实现非常简洁。
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=4000005; int n,m,sum[N],tmp[N],cnt[N],lst[N]; ll ts,ans; char str[N]; inline void solve(){ scanf("%d%s",&n,str+1); for(int i=1;i<=n;i++) sum[i+1]=sum[i]+(str[i]=='('?1:-1); // 为了方便,我在这里直接把 c[0] 放在前面,后面统计贡献就全部是 1 下标了 ans=ts=0; n++; for(int i=1;i<=n;i++) tmp[i]=sum[i]; for(int i=1;i<=n+n;i++) cnt[i]=0; for(int i=1;i<=n;i++) cnt[tmp[i]+n]++; for(int i=1,j=0;i<=n+n;i++) for(int t=1;t<=cnt[i];t++) ans+=1ll*j*(i-n)-ts,ts+=i-n,j++; // 求第一部分的贡献 ans+=1ll*n*(n-1)/2; for(int i=0;i<=n+n;i++) lst[i]=n+1; for(int i=n;i;i--){ int r=lst[sum[i]+n-1]-1; ans-=r-i; lst[sum[i]+n]=i; } for(int i=0;i<=n+n;i++) lst[i]=0; for(int i=1;i<=n;i++){ int l=lst[sum[i]+n-1]+1; ans-=i-l; lst[sum[i]+n]=i; } // 求第二部分的贡献 for(int i=1;i<=n+n;i++) cnt[i]=0; for(int i=1;i<=n;i++){ cnt[sum[i]+n]++; if(i==n||sum[i]>sum[i+1]){ ans+=1ll*cnt[sum[i]+n]*(cnt[sum[i]+n]-1)/2; cnt[sum[i]+n]=0; } } for(int i=1;i<=n+n;i++) if(cnt[i]) ans+=1ll*cnt[i]*(cnt[i]-1)/2; // 第二部分在 c[l]=c[r] 时会算重 printf("%lld\n",ans); } int main(){ int tc; scanf("%d",&tc); while(tc--) solve(); return 0; }顺带一提,这题的做法真的很多很多。
- 1
信息
- ID
- 7685
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 5
- 标签
- 递交数
- 0
- 已通过
- 0
- 上传者