牛客多校第二场j题

给你一段含1和-1的序列,序列长度固定为 1 0 9 10^9 109,现在给你n个区间,区间内全为1,然后问你有多少个区间和大于0;
其中 l i &lt; r i l_i&lt;r_i li<ri
r i + 1 &lt; l i + 1 r_i+1&lt;l_{i+1} ri+1<li+1 for 0 ≤ i &lt; n − 1 \leq i &lt; n - 1 i<n1
0 ≤ l 0 0\leq l0 0l0
l i ≤ r i l_i\leq r_i liri
r n − 1 &lt; 1 0 9 r_{n-1}&lt;10^9 rn1<109
∑ ( l i − r i + 1 ) ≤ 1 0 7 \sum (l_i-r_i+1)\leq10^7 (liri+1)107
这题看了大佬的题解之后还是疯狂wa,调试了一个下午,补题之路非常漫长
因为 ∑ ( l i − r i + 1 ) ≤ 1 0 7 \sum (l_i-r_i+1)\leq10^7 (liri+1)107可知1的个数最多只有 1 0 7 10^7 107,也就是说有绝大部分的-1在无效区域,dp好像不行,因为两段可能连起来为有效区域,你每一段1必须考虑后面所有的1段和前面所有的1段是不是可以连接起来,但考虑所有的1段实在是太耗时,那么有没有办法给缩小一下范围呢?
显然可以,我们想办法把整一段序列分成相互隔绝的几段独立序列,然后分别考虑,代码详解:

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e7+20;
typedef long long ll;
int l[maxn/10],r[maxn/10];
int rlt[maxn/10],lrt[maxn/10],sum[maxn*3],num[maxn*3],lnum[maxn*3];
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d%d",&l[i],&r[i]);
    }
    //l,r记录每段1段的左端点和右端点
   rlt[1]=r[1]-l[1]+1;
   //rlt[i]记录从第i段的右端点往左的最大前缀和,显然第一段就是第一段1的长度
	for(int i=2;i<=n;i++)
	{
		rlt[i]=r[i]-l[i]+1;
		if(rlt[i-1]>(l[i]-r[i-1]-1))
		rlt[i]+=rlt[i-1]-(l[i]-r[i-1]-1);
	}
	//如果rlt[i-1]大于第i段和第i-1段1之间的-1,则rlt[i-1]对rlt[i]常生了贡献
	lrt[n]=r[n]-l[n]+1;
	//lrt[i]记录从第i段左端点往右的最大前缀和,显然第n段就是第n段1的长度
	for(int i=n-1;i>=1;i--)
	{
		lrt[i]=r[i]-l[i]+1;
		if(lrt[i+1]-(l[i+1]-r[i]-1)>0)
		lrt[i]+=lrt[i+1]-(l[i+1]-r[i]-1);
	}
	//同理
    int x=1;
    ll ans=0;//记录结果
    int bas=1e7;
    //
    while(x<=n)
    {
        int i=x;
        int j=i+1;
        while(j<=n&&lrt[j]+rlt[j-1]>=l[j]-r[j-1]-1)
        {
            j++;
        }
        //这里便是来分割独立序列了,因为如果从第j段左端点往右的最大前缀和加上从第j-1段右端点往左的最大前缀和都不足以抵消第j段和第j-1段之间的-1,这第j段必然无法去前面连接起来;
        j--;
        int end=1e9-1;
        int mx=0,mi=2e9;
        int begins=max(0,l[i]-lrt[i]);
        int ends=min(end,rlt[j]+r[j]);
        sum[0]=0;//记录独立序列段的前缀和;
        for(int k=begins;k<=ends;k++)
        {
            int a=k-begins+1;
            sum[a]=sum[a-1];
            if(k<l[i]||k>r[i])
            sum[a]--;
            else sum[a]++;
            if(k==r[i])
            i++;
            mx=max(mx,sum[a]+bas);//加上bas的好处是防止有负数
            mi=min(mi,sum[a]+bas);
            num[sum[a]+bas]++;//记录前缀和为sum[a]+bas的数量
        }
        for(int k=mx-1;k>=mi;k--)
        {
            num[k]+=num[k+1];//num[k]记录大于k-1的前缀和有多少个点
        }
        ans+=num[bas+1];//跟后面区间无关的要加上
        for(int k=begins;k<=ends;k++)
        {
            int a=sum[k-begins+1]+bas;//a为k位置的前缀和加上bas;
            num[a+1]-=lnum[a+1];
            //num[a+1]为大于a的前缀和的个数,lnum[a+1]为k位置往左大于a的前缀和的个数,两者相减即得k往左右多少个前缀和大于a,那每个大于a的前缀和减a必然大于零,即从k为置到前缀和大于a的区间满足条件
            lnum[a]+=lnum[a+1]+1;
            lnum[a+1]=0;//因为num[a+1]在k位置已经减掉在k位置的lnum[k+1]的值了,所以这里直接置零
            ans+=num[a+1];
        }
        for(int k=mi;k<=mx;k++)
            num[k]=0,lnum[k]=0;
        x=j+1;
    }
    cout<<ans<<endl;
}在这里插入代码片
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值