降维技巧
前缀和
前缀和用于解决连续询问区间和,并且中途不插入新数的一系列问题。
一维前缀和
以下是前缀和的预处理和查询:
预处理:
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)\)。
例题:
- [NOIP2011 提高组] 聪明的质监员
- 观察到式子中有 \(\sum_{i=l}^{r}\) 的形式,在 \(check\) 中使用前缀和先预处理,就可以 \(O(n)\) \(check\),总时间复杂度 \(O(n\log n)\)
- 最大子段和
- 最大加权矩形
- 将二维压缩成一维,即可按照最大子段和的方式做。
- [USACO11MAR] Brownie Slicing G
上面讲的是一维前缀和,其实二维前缀和也是同理。
二维前缀和
设 \(sum_{i,j}\) 表示以 \((1,1)\) 为左上角,\((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\) 为其差分数组:
例题:
- [NOIP2012 提高组] 借教室
- [USACO07MAR] Face The Right Way G
- 需要将 \(0,1\) 转化为奇偶,便于维护。
关于二次差分,指在差分数组上再进行一次差分。可以处理在区间上加等差数列的问题。
由于等差数列由首项,项数,公差组成,不难发现,在一次差分数组上加上首项,在二次差分数组上加上公差,最后从下往上进行前缀和即可计算。
题目:三步必杀
二维差分
快速处理矩形加/减。
将左上角是 \((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;
单点修改同理。
例题:
- [USACO07MAR] Face The Right Way G
- 奶牛转向,即可看做区间异或 \(1\)。
下面说说树上问题。
树上点权差分
在 \(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);
}