首页 > 技术文章 > [Ynoi2013]对数据结构的爱

zjjws 2020-09-26 19:55 原文

\(\texttt {data-2020-10-24}\)

修正了一些语言描述上的错误,以及对于一些地方使用了更好的表述。


\(\texttt {data-2020-9-17}\)

修正了一些公式和语言描述上的错误。


对于当前要经过的一段区间 \(x\),若给定不同初始值的 \(sum\) ,其减 \(p\) 的次数 \(k\) 也不同,那么 \(k\)\(sum\) 是否满足什么关系呢?

意淫一下感觉是非递减,看能不能写清楚。

考虑增大 \(sum\) ,此时虽然模 \(p\) 的位置并不确定,但是 \(k\) 要么不变,要么增大,理解一下这个过程中取模位置和次数的变化:

先扫过去,对于 \(i\in[r_1,r_2)\) 这段范围内的数,过程量 \(sum_i\) 都增加 \(num\)(增大的量),原来满足 \(sum_i<p\) 这个条件的有:\(sum_i+num<p\)

然后 \(r_2\) 这个位置满足:\(sum_{r_2}<p\)\(sum_{r_2}+num\ge p\),此时 \(r_2\) 这个位置取模。

这时有两种情况:

\(1.\ num-p\ge 0\)

对于 \(i\in[r_2,r_3)\) 这段范围内的数,过程量 \(sum_i\) 都增加 \(num-p\),后同上。

\(2.\ num-p<0\)

实际上这个子问题就是 \(num<0\) 的实现。

此时后面一段的过程量减小,直到一个原本应当进行一次减 \(p\) 操作的位置不减了,而这个情况可以看做两个位置是否取模的真假(true or false)交换,可以保证 \(k\) 不变。然后后面的又是变成跟原来一样的子问题。

但也有可能后面没有这个真变假(原本取模变成不取模)的位置,也就是减 \(p\) 次数变为 \(k+1\)

综上可得:对于每个使 \(k+1\) (对 \(k\) 产生了 \(1\) 的贡献)的位置,最多有一个使 \(k-1\) 的位置与其对应,所以 \(k\)\(sum\) 非严格单调递增。

接下去就是考虑实现了:

\(k\)\(sum\) 非严格单调递增,同时有 \(0\le k\le lens\),其中 \(lens\) 为块长。

更严格来说,设原来的取模次数为 \(sl\),有:

\[\begin {cases} sl\le k\le lens,sum>0\\ 0\le k\le sl,sum<0\end {cases} \]

这里 \(k\)\(sum\) 的关系十分相似于函数 \(y=\left\lfloor x \right\rfloor\)

那么如果二分 \(sum\) 的话,扫过这个块模拟,就可以得到这个 \(sum\) 对应的 \(k\)

也就是说可以预处理出对于每个 \(k\),最小的 \(sum\) 值,设这个东西为 \(f(k)\),特殊的,\(f(0)=-\infty\)

那么如果前面的值处理好了传给当前区间,就可以求出对应的 \(k\) 然后加上这段区间的和,减去取模次数乘模数。

分块的话:

预处理复杂度:\(\operatorname O(\log(2e9\times lens)\times n)\) 乘上一个常数。

查询的复杂度:\(\operatorname O(m\times \frac{n}{lens}\times \log lens)\)

最优的极限复杂度为:\(1.3\times 10^{10}\)

分块打了,炸了,一分都骗不到


看了题解,不是傻傻地每一块单独算,而是“合并”左右子树的信息。

处理好自己左子树和右子树的信息以后,设当前节点为 \(i\),左儿子为 \(l\),右儿子为 \(r\)

\[f_{i,x+y}=\min(f_{i,x+y},\max(f_{l,x},f_{r,y}+p\times x-add_l) \]

其中,\(add_l\) 表示 \(l\) 这段区间的和,\(x,y\) 分别为左右区间取模的次数。

关于@81179332_ 提到的 \(c_{x+1}-c_x\ge p\) 的证明(在我这里写成 \(f_{x+1}-f_x\ge p\)),我的理解是:

因为 \(f_x\) 对应的是取模 \(x\)\(sum\) 的最小值,那么当 \(sum=f_x-1\),有一个本来要取模的位置不取模了,所以只可能是某个过程量在 \(sum=f_x\) 的时候刚好等于 \(0\)(即这步操作减 \(p\) 以后等于 \(0\))。

\(sum=f_x+p-1\) 时,可以用前面 \(\operatorname A\) 的处理过程结合上一段来理解,\(k\) 一定等于 \(x\),所以相邻两个值的差至少为 \(p\)


那么对于上面的式子来说,有:

\[\begin {cases}f_{r,y}+p\times x-add_l\ge f_{r,y-1}+ p\times (x+1)-add_l \\ f_{l,x}+p\le f_{l,x+1}\end {cases} \]

并且一个合法的二元组 \((x,y)\) 需满足:\(f_{l,x+1}-1+add_l-p\times x\ge f_{r,y}\)

对于一个常数 \(k=x+y\),若 \(k\) 已经被 \((x,y)\) 更新过了,那么 \((x+1,y-1)\) 还有存在的必要吗?

由前面可以得到:

\[f_{r,y-1}+p\times x-add_l\le f_{r,y}+p\times x-add_l\le f_{l,x+1}-1 \]

那么此时对于二元组 \((x+1,y-1)\)\(\max(f_{l,x+1},f_{r,y-1}+p\times x-add_l)\) 取得一定是前面的那项。

前面那项和 \(y\) 又没有任何关系,所以有:

如果存在合法二元组 \((x,y)\),则 \((x+1,y-1)\) 对答案无贡献。(当然,要考虑 \(x\) 的贡献。即如果 \((x,|r|)\) 符合二元组,为了让 \(x+1\) 的贡献能够更新 \(f_{i,x+|r|+1}\),之后 \((x+1,|r|)\) 也得考虑)

注:这里的 \(|r|\) 指区间 \(r\) 的长度,即 \(y\) 的最大取值。

那么就可以双指针使得复杂度正确了。

#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int MAN=1e6+3;
const LL MAX=1e18;
struct milk
{
    int l,r;
    int ls,rs;
    int lens;
    LL sum;
    vector <long long> q;
}t[MAN<<1];
int nam;
int n,m;
LL ans=0;
LL p;
int rin()
{
    int s=0;
    char c=getchar();
    bool bj=0;
    for(;(c>'9'||c<'0')&&c!='-';c=getchar());
    if(c=='-')c=getchar(),bj=true;
    for(;c>='0'&&c<='9';c=getchar())s=(s<<1)+(s<<3)+(c^'0');
    if(bj)return -s;
    return s;
}
inline void make_tree(int l,int r,int i)
{
    t[i].l=l;t[i].r=r;
    t[i].lens=t[i].r-t[i].l+1;
    t[i].ls=t[i].rs=0;
    t[i].q.push_back(-MAX);//f[0]为负无穷
    if(l==r)
    {
        t[i].sum=rin();
        t[i].q.push_back(p-t[i].sum);
        t[i].q.push_back(MAX);
        return;
    }
    int mid=(l+r)>>1;
    t[i].ls=++nam;make_tree(l,mid,nam);
    t[i].rs=++nam;make_tree(mid+1,r,nam);
    t[i].sum=t[t[i].ls].sum+t[t[i].rs].sum;
    for(int _=l;_<=r+3;_++)t[i].q.push_back(MAX);
    l=t[i].ls;r=t[i].rs;
    int x,y;
    x=y=0;
    for(;x<=t[l].lens;x++)
    {
        if(y>t[r].lens)y--;
        for(;y<=t[r].lens;y++)
        {
            if(t[l].q[x+1]-1-p*x+t[l].sum<t[r].q[y]){if(y!=0)y--;break;}//当前的 x 无法满足后面减 y 次的需求
            t[i].q[x+y]=min(t[i].q[x+y],max(t[l].q[x],t[r].q[y]+p*x-t[l].sum));
        }
    }
    return;
}
inline void cheak(int l,int r,int i)
{
    if(l<=t[i].l&&t[i].r<=r)
    {
        int left=1,right=t[i].r-t[i].l+1;
        int last=0;
        for(;left<=right;)
        {
            int mid=(left+right)>>1;
            if(ans>=t[i].q[mid])last=mid,left=mid+1;
            else right=mid-1;
        }
        ans+=t[i].sum;
        ans-=p*last;
        return;
    }
    if(t[i].ls==0)return;
    if(t[t[i].ls].r>=l)cheak(l,r,t[i].ls);
    if(t[t[i].rs].l<=r)cheak(l,r,t[i].rs);
    return;
}
int main()
{
    int i,j;
    n=rin();m=rin();p=rin();
    nam=1;
    make_tree(1,n,1);
    for(i=1;i<=m;i++)
    {
        int l,r;
        l=rin();r=rin();
        ans=0;
        cheak(l,r,1);
        printf("%lld\n",ans);
    }
    return 0;
}

推荐阅读