莫队算法总结&专题训练
回顾:
一个莫队,六种方法(普通莫队、带修莫队、树上莫队、树上带修莫队、回滚莫队/不删除莫队、莫队二次离线/第十四分块(前体)),连续写了三篇博文来讲述。本篇博文是最后一篇,将会讲述最后两种莫队:[回滚莫队/不删除莫队] [莫队二次离线/第十四分快(前体)],同时将会总结六种莫队算法。
3.练习题
题单:(前面的就略去了)
- 回滚莫队/不删除莫队
- AT1219 歴史の研究
- 莫队二次离线/第十四分块(前体)
- P4887 【模板】莫队二次离线(第十四分块(前体))
回滚莫队/不删除莫队
学到了这里,各位会发现:莫队支持查询,修改,也支持删除(当然不是直接删除一个数)。但是,对于一些题目,普通莫队就过不去了,因为你会发现:del 函数非常难写,可能写着写着就变成了 O ( n ) O(n) O(n) 暴力删除,这是莫队所不能容忍的。还不如直接 O ( n 2 ) O(n^2) O(n2) 暴力呢?于是,针对这种问题,回滚莫队/不删除莫队就出现了。
事实上,我个人认为这道题更像回滚莫队/不删除莫队的模板题,因此我拿它来做讲解。
这题的 add 函数很好打,但是 del 函数很不好打,因此我们要另寻他法。
我们回顾一下莫队的三个优化:
优化一:使用 c n t cnt cnt 数组省略值域。貌似没有什么用处qwq。
优化二:使用 l , r l,r l,r 指针移来移去。带修莫队已经用过了。
优化三:应用分块思想进行排序,不使用奇偶性排序的情况下左端点在同一块的询问右端点按照从小到大排序。唉等等,这个优化好像还没有用过,我们是不是可以使用这个优化写出对回滚莫队/不删除莫队呢?当然可以!
考虑分块思想。设 b l o c k block block 表示块长, b n u m bnum bnum 表示块的数量,那么对于每一块块内的询问,直接暴力出奇迹即可。
那么针对于那些跨越块数的呢?
想想当初分块的时候,我们顺序处理整块,那么这里我们也顺序处理一下:
对于第 i i i 快,我们指定 l = i ∗ b l o c k + 1 , r = l − 1 l=i*block+1,r=l-1 l=i∗block+1,r=l−1 。根据上面所述, r r r 只需要往右边移动即可,但是不能保证 l l l 只往左移动,因此我们还得想办法解决 l l l 的问题。
这里就是一个指针移动顺序的问题了。
我们有四个顺序: l + + , l − − , r − − , r + + l++,l--,r--,r++ l++,l−−,r−−,r++ ,前面四种莫队随便乱搞都能过,但是回滚莫队/不删除莫队不能随便乱搞。首先, r − − r-- r−− 不可能。其次,为了保证 “不删除” ,我们需要规定 r + + r++ r++ 第一个操作(因为 r r r 不可能回去)。将当前操作完的答案临时存在 t m p tmp tmp 里面。然后, l − − l-- l−− ,算出答案 t o t a l total total 。在算完答案之后,我们需要 l + + l++ l++ ,并且让 t o t a l = t m p total=tmp total=tmp 。 啥意思?
因为这里我们要保证 “不删除”,所以我们不能在 l + + l++ l++ 的时候维护答案。最好的办法就是将 l l l 移回到 i ∗ b l o c k + 1 i*block+1 i∗block+1 ,同时由于我们先处理了 r + + r++ r++ ,因此我们可以保证 t m p tmp tmp 是 [ i ∗ b l o c k + 1 , r ] [i*block+1,r] [i∗block+1,r] 之间的答案。因此,将 l l l 移回去,同时路上消除 c n t cnt cnt 的影响(要统计个数),而 t o t a l = t m p total=tmp total=tmp 能够保证在 “不删除” 的情况下存储答案。
以上就是回滚莫队/不删除莫队的主要思路。
代码里面需要注意:暴力小块时用的 c n t cnt cnt 不能与莫队大块时用的 c n t cnt cnt 一起,否则很容易导致 WA。同时绝对不能使用奇偶性排序!
说每暴力一次都 memset 的人可以看一看这组数据:
n=10000,block=sqrt(n)=100;
询问:[1,2][2,3]······
询问 9999 个,每一次都memset一遍,时间复杂度直接飙升到 O(n^2)
TLE 在向你招手!
不要忘记 long long ,离散化。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+10;
typedef long long LL;
int n,m,a[MAXN],b[MAXN],block,ys[MAXN],lastn,bnum;
LL total,ans[MAXN];
map<int,int>cnt,cnt2;//懒得写离散化,用 map 代替
struct node
{
int l,r,id;
}q[MAXN];
int read()
{
int sum=0;char ch=getchar();
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9') {
sum=(sum<<3)+(sum<<1)+ch-'0';ch=getchar();}
return sum;
}
bool cmp(const node &fir,const node &sec)
{
if(ys[fir.l]^ys[sec.l]) return ys[fir.l]<ys[sec.l];
return fir.r<sec.r;
}
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++) cnt[a[i]=read()]=0;
block=sqrt(n);bnum=ceil((double)n/block);
for(int i=1;i<=n;i++) ys[i]=(i-1)/block+1;
for(int i=1;i<=m;i++) {
q[i].l=read();q[i].r=read();q[i].id=i;}
sort(q+1,q+m+1,cmp);
int j=1;
for(int i=1;i<=bnum;i++)
{
cnt.clear();
int l=i*block+1,r=l-1;
LL tmp=0;total=0;
for(;ys[q[j].l]==i;j++)
{
if(ys[q[j].l]==ys[q[j].r])
{
cnt2.clear();tmp=0;
for(int k=q[j].l;k<=q[j].r;k++) {
cnt2[a[k]]++;tmp=max(tmp,1ll*a[k]*cnt2[a[k]]);}
ans[q[j].id]=tmp;
continue;
}
while(r<q[j].r) {
++r;cnt[a[r]]++;total=max(total,1ll*a[r]*cnt[a[r]]);}
tmp=total;
while(l>q[j].l) {
--l;cnt[a[l]]++;total=max(total,1ll*a[l]*cnt[a[l]]);}
ans[q[j].id]=total;
while(l<i*block+1) {
cnt[a[l]]--;l++;}
total=tmp;
}
}
for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
return 0;
}
莫队二次离线/第十四分块(前体)
其实看到 “第十四分块(前体)”,已经有一种要被 lxl 吊打的感觉了。
确定这玩意我会做?先咕咕咕~
update 2020/12/16:终于在似懂非懂的情况下 AC 了 莫队二次离线/第十四分块(前体)
现在就开始讲解吧~
开端:先推一波柿子
接下来有亿堆柿子,请做好准备。
我们规定: f ( i , [ l , r ] ) f(i,[l,r]) f(i,[l,r]) 表示 a i a_i ai 对 [ l , r ] [l,r] [l,r] 区间所产生的贡献,即 [ l , r ] [l,r] [l,r] 中有几个数与 a i a_i ai 做异或操作后二进制下恰好有 k k k 个 1。
那么,根据推导,我们可以得出如下一个式子:
f ( i , [ l , r ] ) = f ( i , [ 1 , r ] ) − f ( i , [ 1 , l − 1 ] ) f(i,[l,r]) = f(i,[1,r]) - f(i,[1,l-1]) f(i,[l,r])=f(i,[1,r])−f(i,[1,l−1])
但是这个式子很不好处理。
由于莫队移动的时候 l , r l,r l,r 是连续移动的,所以我们可以将这个式子转化成:
f ( r + 1 , [ l , r ] ) = f ( r + 1 , [ 1 , r ] ) − f ( r + 1 , [ 1 , l − 1 ] ) f(r + 1, [l, r]) = f(r + 1, [1, r]) - f(r + 1, [1, l - 1]) f(r+1,[l,r])=f(r+1,[1,r])−f(r+1,[1,l−1])
观察上述式子的右边,我们发现:这个式子变成了一个 类前缀和 的式子。
因此这道题,第一个结合算法就产生了:类前缀和思想。
首先 f ( r + 1 , [ 1 , r ] ) f(r+1,[1,r]) f(r+1,[1,r]) 显然可以直接预处理出来,我们将 f ( r + 1 , [ 1 , r ] ) f(r+1,[1,r]) f(r+1,[1,r]) 存在 p r p_r pr 当中。但是我们发现 f ( r + 1 , [ 1 , l − 1 ] ) f(r+1,[1,l-1]) f(r+1,[1,l−1]) 这个东西长得太恶心了,要想办法解决它。
而这就是我们需要用到的第二个思想:扫描线做法。而扫描线也就是二次离线的体现。
这里在讲如何二次离线之前,先讲一讲如何预处理 p i p_i pi :
- 预处理出 [ 0 , 16384 ) [0,16384) [0,16384) 中所有二进制下有 k k k 个 1 的数,存在 b b b 数组里面。
- 然后,对于每一个 a i a_i ai ,我们开一个 t t t 数组,作用见后面。令 p i = t a i p_i=t_{a_i} pi=tai 。为什么?还是见后面。
- 根据 a ⊕ b = c a \oplus b = c a⊕b=c 等价于 a ⊕ c = b a \oplus c = b a⊕c=b,我们可以枚举 b b b 数组,将;里面的每一个数全部异或上 a i a_i ai ,然后对于每一个结果 x x x , t x + + t_x++ tx++。所以 t t t 就是用来统计用的。
精华:如何二次离线?
现在回归我们的标题:莫队二次离线。
二次离线,就肯定要离线二次。而第一次离线就是莫队的基础离线。
这个想必不用我多说。
不过:这里在统计答案的时候跟我们之前的写法不一样!我们需要在结构体当中新建一个变量 a n s ans ans, 表示这个询问的答案。而且这里也不需要 t o t a l total total 来存临时答案!
那么根据上述话语可以发现,其实我们每一次求的是 答案的变化量。
因此最后不要忘记做前缀和。
友善的作者友情提醒您:
道路千万条, long long 第一条。
结果存 int ,爆零两行泪。
道路千万条, %lld 第一条。
输出用 %d, 爆零两行泪。
而 扫描线就是我们的第二次离线。
考虑在第一次离线的时候,我们预先存下所有 f ( r , [ 1 , l − 1 ] ) f(r,[1,l-1]) f(r,[1,l−1]) 的所有询问。
怎么存?开个 vector 存!
在一次离线的过程中,我们需要对每一个询问 q i q_i qi 首先存下 f ( q i . r , [ 1 , q i . r − 1 ] ) f(q_i.r,[1,q_i.r-1]) f(qi.r,[1,qi.r−1]) ,同时我们考虑在莫队的过程中将我们遇到的 f ( q i . r , [ 1 , l ] ) f(q_i.r,[1,l]) f(qi.r,[1,l]) 先以 ( q i . r , l , r , i , 1 / − 1 ) (q_i.r,l,r,i,1/-1) (qi.r,l,r,i,1/−1) 的形式存下来。最后的 1/-1 是干什么用的?
因为我们在莫队的时候不知道 l , r l,r l,r 是在 q i . l , q i . r q_i.l,q_i.r qi.l,qi.r 的左边还是右边,所以如果 l , r l,r l,r 在对应询问的左右段点的右边的时候,我们这个答案反过来是要加回去的,而反之答案要减掉。因此我们需要打一个标记来表明这个 f ( q i . r , [ 1 , l ] ) f(q_i.r,[1,l]) f(qi.r,[1,l]) 是加是减。然后跑一遍扫描线即可。
就这样,我们完成了二次离线的所有操作!撒花!
好像也不是特别难?
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int MAXN = 1e5 + 10;
int n, m, k, a[MAXN], t[MAXN], p[MAXN], block, ys[MAXN];
LL ans[MAXN];
struct node
{
int l, r, id;
LL ans;
}q[MAXN];
vector <int> b;
vector < tuple <int, int, int, int> > v[MAXN];//这里不能用普通 C++ 编译,推荐 C++17
int read()
{
int sum = 0; char ch = getchar();
while (ch < '0' || ch > '9') ch = getchar();
while (ch >= '0' && ch <= '9') {
sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
return sum;
}
bool cmp(const node &fir, const node &sec)
{
if (ys[fir.l] ^ ys[sec.l]) return ys[fir.l] < ys[sec.l];
return fir.r < sec.r;
}//最好不要用奇偶性排序
int Count(int x)
{
int sum = 0;
for (; x; x >>= 1)
if (x & 1) sum++;
return sum;
}
int main()
{
n = read(); m = read(); k = read(); block = ceil(n / sqrt(m));
if (k > 14)
{
for (int i = 1; i <= m; ++i) printf("0\n");
return 0;
}//当心有坑!
for (int i = 1; i <= n; ++i) {
a[i] = read(); ys[i] = (i - 1) / block + 1;}
for (int i = 1; i <= m; ++i) {
q[i].l = read(); q[i].r = read(); q[i].id = i;}
sort(q + 1, q + m + 1, cmp);
for (int i = 0; i < 16384; ++i)
if (Count(i) == k) b.push_back(i);
for (int i = 1; i <= n; ++i)
{
p[i] = t[a[i]];
for (int j = 0; j < b.size(); ++j) t[a[i] ^ b[j]]++;
}//预处理
memset(t, 0, sizeof(t));
int l = 1, r = 0;
for (int i = 1; i <= m; ++i)
{
if (l > q[i].l) v[r].emplace_back(q[i].l, l - 1, i, 1);
while (l > q[i].l) q[i].ans -= p[--l];
if (r < q[i].r) v[l - 1].emplace_back(r + 1, q[i].r, i, -1);
while (r < q[i].r) q[i].ans += p[++r];
if (l < q[i].l) v[r].emplace_back(l, q[i].l - 1, i, -1);
while (l < q[i].l) q[i].ans += p[l++];
if (r > q[i].r) v[l - 1].emplace_back(q[i].r + 1, r, i, 1);
while (r > q[i].r) q[i].ans -= p[r--];
}//一次离线
for (int i = 1; i <= n; ++i)
{
for (int j = 0; j < b.size(); ++j) ++t[a[i] ^ b[j]];
for (int j = 0; j < v[i].size(); ++j)
{
tuple x = v[i][j];
for (int zzh = get<0>(x); zzh <= get<1>(x); ++zzh)
{
if (zzh <= i && k == 0) q[get<2>(x)].ans += get<3>(x) * (t[a[zzh]] - 1);
else q[get<2>(x)].ans += get<3>(x) * t[a[zzh]];
}
}
}//二次离线
for (int i = 1; i <= m; ++i) q[i].ans += q[i-1].ans;
for (int i = 1; i <= m; ++i) ans[q[i].id] = q[i].ans;
for (int i = 1; i <= m; ++i) printf("%lld\n", ans[i]);
return 0;
}
不过,莫队二次离线的使用条件有一定的前提:必须要满足区间可减性。也就是能表示成前缀和的形式。
4.总结
终于到总结了qwq。
如果真的掌握了莫队,莫队其实还是很简单的。
这里再放一放 6 种莫队的主要思路吧!
- 普通莫队:两个指针 l , r l,r l,r 在序列上动,排序以左端点所在块为第一关键字,右端点为第二关键字排序,可以使用奇偶性优化加快排序。
- 带修莫队:加一维指针 t t t ,让第三个指针在时间轴上动。
- 树上莫队:用欧拉序将树上问题转变成区间问题
- 树上带修莫队:前两者的结合,用欧拉序转变成区间问题后使用带修莫队的套路。
- 回滚莫队/不删除莫队:借助分块思路,块内暴力,块外指针移动,同时记录 [ i ∗ b l o c k + 1 , r ] [i*block+1,r] [i∗block+1,r] 的答案,但是绝对不能使用奇偶性排序。
- 莫队二次离线/第十四分块(前体):使用扫描线将 f ( r , [ 1 , l ] ) f(r,[1,l]) f(r,[1,l]) 类的询问全部离线解决。
最后再放一下三篇博文的链接:
莫队算法总结&专题训练1:普通莫队。
莫队算法总结&专题训练2:带修莫队,树上莫队,树上带修莫队。
莫队算法总结&专题训练3:回滚莫队/不删除莫队,莫队二次离线/第十四分块(前体)。
有兴趣的读者也可以看一下 洛谷日报 #183 期:你以为莫队只能离线?莫队的在线化改造,增强对莫队的理解与自己的水平。(所以以后很多树套树的题目都能用在线莫队切掉了?)