降维技巧

前缀和

前缀和用于解决连续询问区间和,并且中途不插入新数的一系列问题。

一维前缀和

以下是前缀和的预处理和查询:

预处理:

for(int i=1;i<=n;i++) sum[i]=sum[i-1]+a[i];

查询 \(l\sim r\) 的和:

sum[r]-sum[l-1];

可以发现,预处理的时间复杂度是 \(O(n)\),查询的时间复杂度是 \(O(1)\)

例题:

上面讲的是一维前缀和,其实二维前缀和也是同理。

二维前缀和

\(sum_{i,j}\) 表示以 \((1,1)\) 为左上角,\((i,j)\) 为右下角的矩形内所有数字的和,那么有容斥原理,有如下关系:

\[sum_{i,j}=sum_{i-1,j}+sum_{i,j-1}-sum_{i-1,j-1}+a_{i,j} \]

照上式,可以完成预处理。

查询也有如下关系:

int query(int x,int y,int c,int d) {//左上角(x,y),右下角(c,d)
	return sum[c][d]-sum[x-1][d]-sum[c][y-1]+sum[x-1][y-1];
}

同理,预处理 \(O(nm)\),查询 \(O(1)\)

例题:

异或前缀和

快速查询区间异或和。

预处理:

for(int i=1;i<=n;i++) sum[i]=(sum[i-1]^a[i]);

查询:

sum[r]^sum[l-1];

提一嘴树上前缀和,其实这种东西通常与树上差分一起,单独很少出现。

树上点权前缀和

\(sum_i\) 表示根节点到 \(i\) 的前缀和,预处理跑一遍 \(dfs\) 即可,查询如下:

int query(int x,int y) {
    return sum[x]+sum[y]-sum[lca]-sum[fa[lca]];
}

树上边权前缀和

首先将边权下放,除了查询其他同上。查询如下:

int query(int x,int y) {
    return sum[x]+sum[y]-2*sum[lca];
}

差分

差分是前缀和的逆运算,即差分数组的前缀和就是原数组,差分数组可用于快速处理区间加/减,然后一起查询,如果要边加边查询,就要借助树状数组等数据结构。

一维差分

差分数组的定义如下:

对于数组 \(a\)\(b\) 为其差分数组:

\[b_1=a_1\\ b_i=a_i-a_{i-1}(i>2) \]

例题:

关于二次差分,指在差分数组上再进行一次差分。可以处理在区间上加等差数列的问题。

由于等差数列由首项,项数,公差组成,不难发现,在一次差分数组上加上首项,在二次差分数组上加上公差,最后从下往上进行前缀和即可计算。

题目:三步必杀

二维差分

快速处理矩形加/减。

将左上角是 \((x,y)\) 右下角是 \((c,d)\) 的矩形加上 \(k\).

b[x][y]+=k;
b[c+1][d+1]+=k;
b[c+1][y]-=k;
b[x][d+1]-=k;

单点修改也是同理。

例题:

异或差分

\(l\sim r\) 同时异或上一个数字 \(x\).

diff[l]^=x;
diff[r+1]^=x;

单点修改同理。

例题:

下面说说树上问题。

树上点权差分

\(x,y\) 的最短路径上,将所有点加上 \(k\).

diff[x]+=k;
diff[y]+=k;
diff[lca(x,y)]-=k;
diff[fa[lca(x,y)]]-=k;

在查询时做一遍树上前缀和即可。

例题:

树上边权差分

同样,将边权下放至点,再差分。

\(x\sim y\) 上的所有边加 \(k\)

diff[x]+=k;
diff[y]+=k;
diff[lca(x,y)]-=2*k;

例题:

双指针

双指针能解决的问题一定是能用两个 \(for\) 循环嵌套解决的,所以发现题目能用两个 \(for\) 循环解决的时,考虑双指针。

双指针维护一个前指针 \(l\) 和一个后指针 \(r\),当题目满足 \(r\) 右移,\(l\) 要么不动要么向右移时,说明双指针时可行的,也称作符合单调性。

代码模板:

  • 越短越容易OK。

    int l=1;//左端点
    for(int i=1;i<=n;i++) {//右端点
        加上当前i的影响
        while(l<=i&&不满足条件) {
            将之前的l影响消除
            l++;
        }
        if(满足条件) 更新答案
    }
    

    唯一的雪花 Unique Snowflakes 中,便使用了上面的代码模板。

    int l=1,ans=0;
    for(int i=1;i<=n;i++) {
        b[a[i]]++;
        while(l+1<=i&&b[a[i]]>1) {
            b[a[l]]--;
            l++;
        }
        ans=max(ans,i-l+1);
    }
    
  • 越长越容易OK。

    int r=0;//右端点
    for(int i=1;i<=n;i++) {//左端点
        if(i!=1) 减去i-1的影响
        while(r+1<=n&&不符合要求) {
            r++;
            加上r的贡献
        }
        if(满足要求) 更新答案
    }
    

    [NOIP2011 提高组] 选择客栈 中,使用上述模板。

    int cnt=0;
    LL ans=0;
    for(int i=1,j=0;i<=n;i++) {
        if(i!=1&&b[i-1]<=p) cnt--;
        while(j+1<=n&&(cnt==0||i==j)) {
        	j++;
    		if(b[j]<=p) cnt++;
        }
        if(i!=j&&cnt!=0) ans+=(LL)s[j][a[i]];
    }
    cout<<ans;
    

单调队列

维护与 \(i\) 保持固定距离范围的最值,维护最大值用单调递减,维护最小值用单调递增。

如果可以用单调队列维护,一定要满足,“比你小还比你强,你就退役”的说法。对于\(\max_{i-x_j,i-y_j} a\) ,显然是不能用一个单调队列维护的,因为 \(x,y\) 不止一个,所以距离 \(i\) 的范围不等,不能用单调队列维护,但是可以对于每一个 \(x,y\) ,开一个单调队列,这样就是可维护的。

通常用于 \(dp\) 优化。

代码模板:

int l=1,r=0;
int n,m;
for(int i=1;i<=n;i++) {
    while(l<=r&&i-q[l]+1>m) l++; //将左端点不符合距离范围的去掉
    while(l<=r&&calc(i)>calc(q[r])) r--; //将末尾不符合单调性的去掉
    //单增还是单减,取决于上一句代码的 "<" 还是 ">"
    q[++r]=i;
    if(符合条件) 更新答案;
}

题目:

单调栈

单调栈常用于求一个数左/右边第一个比他大/小的是谁。

可以用于维护每个点作为 \(\max\)\(\min\) 所占据的区间,从而处理区间内关于 \(\min,\max\) 的相关问题。

也可以处理子矩形问题,只需向左右找到比自己小的,便可计算出包含自己且高度是自己的矩形宽。

常见处理套路:对于左边求不包括等于的,对于右边求包括等于的,这样可以避免重复计算。

求左边还是右边,将for循环倒序即可。若求大于自己,维护单减序列;若求小于自己,维护单增序列。

代码模板(下面是求右边比自己大的下标):

st.push(1);
for(int i=2;i<=n;i++) {
    while(st.size()&&a[i]>a[st.top()]) {
        b[st.top()]=i;
        st.pop();
    }
    st.push(i);
}
posted @ 2023-07-18 14:05  2017BeiJiang  阅读(6)  评论(0编辑  收藏  举报