1 条题解

  • 0
    @ 2025-8-24 22:47:38

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar Miraik
    看啊那只蝴蝶 飞过时间 落在心间

    搬运于2025-08-24 22:47:38,当前版本为作者最后更新于2022-11-26 19:23:38,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    首先考虑给定一个长度为 nn 的括号序列,我们将它变成一个合法括号序列的最优策略是什么样的。

    首先有两个性质:

    1. 我们只会做最多一次变换操作。

    证明:显然,多次变换操作可以合为一次。

    1. 设这个序列有 AA 个左括号,BB 个右括号,那么必然存在一种变换方案,使得变换后有 min(A,B)\min(A,B) 组括号被匹配。换句话说,就是左括号和右括号中个数较少的全部被匹配到。

    证明:不妨设 ABA \ge B,把左括号看作 11,右括号看作 1-1,然后再得到一个前缀和数组 cc。然后选择数组中最小值所在位置 xx,通过变换使得 xx 这个位置移到新序列的末尾。

    如果有右括号没匹配上左括号,就说明存在一个位置 ii 使得 ci<0c_i<0

    对于 xx 之后的任意位置 yy 都有 cx<cyc_x<c_y,那么把 [x+1,n][x+1,n] 变换到序列开头,这部分必然使得整体的 cc 数组增加。

    而变换前 [1,x][1,x] 这部分在变换后的 cc 值增加量是完全一致的(也就是变换前的 cncxc_n-c_x)。因此 cxc_x(相当于变换后的 cnc_n)依然是这部分的最小值。又因为 ABA \ge B,我们可以知道 cxc_x 此时就是 AB0A-B \ge 0

    因此结论得证。A<BA < B 的情况是类似的,不再赘述。有了这两个性质,这题就可做了。因为我们发现如果一个序列的匹配数没有达到上界 min(A,B)\min(A,B) 的话,我们把它循环移位一下必然不劣。因此最后的答案就是 $\sum\limits_{l=1}^{n} \sum\limits_{r=l}^{n} \lvert A[l, r] - B[l, r]\rvert+g(l,r)$,其中 A,BA,B 意义同上,g(l,r)g(l,r) 表示序列的匹配数是否达到上界(是为 00,不是为 11)。

    • n400n \leq 400n800\sum n \leq 800

    暴力枚举 l,rl,r,再暴力计算,时间复杂度 O(n3)O(n^3)

    • n2×103n \leq 2\times 10^3n4×103\sum n \leq 4\times 10^3

    暴力枚举 llrrll 开始向后拓展,在拓展的过程中维护括号匹配情况以及 A,BA,B,计算 [l,r][l,r] 的贡献。时间复杂度 O(n2)O(n^2)

    • 特殊性质:所有括号都是左括号

    此时容易发现就是求 l=1nr=lnrl+1\sum\limits_{l=1}^{n} \sum\limits_{r=l}^{n} r-l+1。这就是 i=1n(ni+1)×i\sum\limits_{i=1}^n (n-i+1) \times i 啊。如果您是数学带师的话还可以直接看出这个东西就是 (n+23)n+2 \choose 3(考虑组合意义)。

    • 特殊性质:对于所有整数 1i<n1\le i < n,有 sisi+1s_i \neq s_{i+1}

    所有可能的子串只有四种形态:()()()\texttt{()()} \cdots \texttt{()})()()\texttt{)()} \cdots \texttt{()})()()(\texttt{)()} \cdots \texttt{()(}()()()(\texttt{()()} \cdots \texttt{()(}。其中只有第三种 g(l,r)=1g(l,r)=1,容易做到线性计算。

    • n2×105n \leq 2\times 10^5n5×105\sum n \leq 5\times 10^5

    此时我们再整理整理式子,让它变成 $\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)$。

    前面那个部分可以直接把 cc 排序后快速计算 $\sum\limits_{r=0}^{n} \sum\limits_{l=0}^{r} c_r-c_l$ 即可。对于后面那个,我们考虑把 g(l,r)g(l,r) 转化成更优秀的条件。

    关键性质:g(l,r)=1g(l,r)=1 等价于:$\min\{c_l,c_{l+1},\cdots,c_{r-1}\} < \min(c_{l-1},c_r)$。

    证明:

    min{cl,cl+1,,cr1}<cl1\min\{c_l,c_{l+1},\cdots,c_{r-1}\} < c_{l-1} 可以看作至少一个右括号失配。

    min{cl,cl+1,,cr1}<cr\min\{c_l,c_{l+1},\cdots,c_{r-1}\} < c_{r} 可以看作至少一个左括号失配。

    由之前得到的性质 2,容易知道这是 g(l,r)=1g(l,r)=1 的充要条件。

    直接硬莽 $\min\{c_l,c_{l+1},\cdots,c_{r-1}\} < \min(c_{l-1},c_r)$ 有点困难,我们将其转化为 min{cl,cl+1,,cr1}<cl1<cr\min\{c_l,c_{l+1},\cdots,c_{r-1}\} < c_{l-1} < c_r 和 $\min\{c_l,c_{l+1},\cdots,c_{r-1}\} < c_r \le c_{l-1}$ 两种情况的方案数之和。

    一般化地,我们枚举 ll,找到最小的 poslpos_{l} 满足 min{cl+1,cl+2,,cposl}<cl\min\{c_{l+1},c_{l+2},\cdots,c_{pos_l}\} < c_{l},再查询满足 x>poslx>pos_lcx>clc_x>c_{l}xx 个数,就可以算出满足 $\min\{c_{l+1},c_{l+2},\cdots,c_{r-1}\} < c_{l} < c_r$ 的方案数了。另一种情况同理。

    使用单调栈求出 posipos_i,后面的部分本质上是二维偏序,可以离线树状数组,总时间复杂度 O(nlogn)O(n \log n)

    #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;
    }
    
    • 1n2×1061 \leq n \leq 2 \times 10^61n2×1071 \leq \sum n \leq 2 \times 10^7

    g(l,r)=1g(l,r)=1 的统计稍显麻烦,我们正难则反。

    考虑 g(l,r)=0g(l,r)=0 等价于 $\min\{c_l,c_{l+1},\cdots,c_{r-1}\} \ge \min(c_{l-1},c_r)$,我们不妨对 g(l,r)=0g(l,r)=0 的情况计数。

    那么这个式子等价于 cl1min{cl,cl+1,,cr}c_{l-1}\le \min\{c_l,c_{l+1},\cdots,c_{r}\}crmin{cl1,cl,,cr1}c_{r}\le \min\{c_{l-1},c_l,\cdots,c_{r-1}\}

    发现一个重要的事实:在 cl1crc_{l-1} \neq c_r 时,我们上面的这个或式不可能同时满足!且 cl1=crc_{l-1} = c_r 时,这两个式子必然同时满足或不满足。

    另外我们还注意到:ci+1=ci±1c_{i+1}=c_i \pm 1

    于是 clmin{cl+1,cl+2,,cr}c_l \le \min\{c_{l+1},c_{l+2},\cdots,c_{r}\} 可以直接等价于 (cl1){cl+1,cl+2,,cr}(c_l-1) \notin \{c_{l+1},c_{l+2},\cdots,c_{r}\}

    那么容易发现对于一个 cic_i,其向左和向右延伸的贡献为一段连续的区间,那么这个区间我们都不需要单调栈了(上面实际上也不用),用一个桶就可以轻松求出,且在 cl1crc_{l-1} \neq c_r 时,其贡献必然被 cl1c_{l-1} 向右的区间和 crc_r 向左的区间中的恰好一个统计到!

    那么我们直接将所有贡献区间统计起来,就统计好了所有 cl1crc_{l-1} \neq c_r 的贡献,而 cl1=crc_{l-1}=c_r 的贡献则被我们计算了两遍。

    于是我们现在需要再减掉所有 cl1=crc_{l-1}=c_r 的贡献。依旧是注意到 ci+1=ci±1c_{i+1}=c_i \pm 1,于是我们发现 cl1=crc_{l-1}=c_r 有贡献当且仅当不存在 ii 满足 lirl \le i \le rci=cl11c_i=c_{l-1}-1。那么我们发现对于每一种 cic_i 其出现的位置又可以依此划为一段一段,而每一段出现次数为 cntcnt 的贡献即为 (cnt2) cnt \choose 2。这个部分线性扫一遍即可完成,具体实现也可以参照代码。(代码比题解清楚.jpg)

    至于前面的部分 r=0nl=0rcrcl\sum_{r=0}^{n} \sum_{l=0}^{r} c_r-c_l 中计算怎么做到线性?cic_i 的值域是 [n,n][-n,n] 的,于是直接桶排序即可。

    于是我们做到了时间复杂度 O(n)O(n),且实现非常简洁。

    #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
    上传者