莫队学习笔记
大抵是太久没碰过了,有点生疏了。
以下仅为本人理解,如有失误还请指出。
可以看看这位大蛇的 juju的莫队练习题
普通莫队
先来知晓一下莫队是干嘛的。
一个经典例题是多次询问区间颜色种类。
如果设计一档询问 \(l_{i-1} <= l_i,r_{i-1} <= r_i\),相信大家都会做吧,直接双指针移动就好了。
可是一般情况我们双指针移动耗时可能较大。
考虑先将询问离线,然后考虑将询问序列排序使得复杂度达到可行速度。
莫队的思想就是控制部分维度,比如让一部分的 \(r_i\) 单调递增,这样 \(R\) 指针的总移动次数就是最多 \(n\) 次了。
考虑平衡一下,假设分 \(B\) 块,那么 \(R\) 移动次数为 \(nB\),\(L\) 移动次数为 \(\frac{n}{B}\times m\),这是因为每个 \(l\) 都在一个块内的话移动次数是不超过 \(\frac{n}{B}\) 次的,当然,这里还漏了一个复杂度,就是每一段第一个要计算出来,复杂度也是 \(nB\) 的。
考虑平衡,\(B\) 取 \(\sqrt{m/2}\) 时理论复杂度最优,但由于上面有些是跑不满的,所以上下浮动也是没错的,那每块长度大概就是 \(\frac{n}{\sqrt{m}}\)。
奇偶性排序
本来这个复杂度已经不错了,不过我们发现就是每一段第一个要计算出来,考虑继续沿用前面的,直接奇数块从前往后,偶数快从后往前,好好利用 \(R\) 指针让它少跑一点。
#include<bits/stdc++.h>
using namespace std;
int n,q,a[100010],p,l,r,sum,c[100010],t[100010],ml,mr,ans[100010];
struct w
{
int l,r,id,ans,k;
}b[100010];
bool cmp(w x,w y)
{
if(c[x.l] != c[y.l]) return x.l < y.l;
if(c[x.l] & 1) return x.r < y.r;
return x.r > y.r;
}
int main()
{
scanf("%d%d",&n,&q); p = n/sqrt(q);
for(int i = 1;i <= n;i++) scanf("%d",&a[i]),c[i] = (i-1)/p+1;
for(int i = 1;i <= q;i++) scanf("%d%d",&b[i].l,&b[i].r),b[i].id = i;
sort(b + 1,b + 1 + q,cmp);
ml = b[1].r + 1;
mr = b[1].r;
for(int i = 1;i <= q;i++)
{
while(ml < b[i].l)
{
t[a[ml]]--;
if(!t[a[ml]]) sum--;
ml++;
}
while(mr > b[i].r)
{
t[a[mr]]--;
if(!t[a[mr]]) sum--;
mr--;
}
while(mr < b[i].r)
{
mr++;
if(!t[a[mr]]) sum++;
t[a[mr]]++;
}
while(ml > b[i].l)
{
ml--;
if(!t[a[ml]]) sum++;
t[a[ml]]++;
}
if(b[i].r - b[i].l + 1 == sum) ans[b[i].id] = 1;
}
for(int i = 1;i <= q;i++)
{
if(ans[i] == 1) printf("Yes\n");
else printf("No\n");
}
return 0;
}
练习题:P12598 参数要吉祥,考虑用莫队求出每种出现次数有几种数,然后值域分块,此题有一些小细节,为了方便读者自行思考我把细节放这里面了->code,如果你本来就习惯好就不会出错(显然作者习惯不好盯了好一会才恍然大悟)。
带修莫队
普通莫队是不支持修改的,于是有神人继续研究发现了带修莫队。
待修莫队本质上就是多带了一维,所以这个修改必须要方便添加撤销才行。
继续考虑莫队做法,固定 \(Z\) 指针从左往右,另外两维不知道。
复杂度分析跟上面类似,大概是 \(\sqrt[3]{n^2}\times n\),具体分析可以看这里
#include<bits/stdc++.h>
using namespace std;
int n,q,a[200010],p,l,r,sum,t[1000010],ml,mr,ans[200010],o,o1,x,y,k[200010];
char oo;
struct w
{
int l,r,id,z,k;
}b[200010],c[200010];
inline int read() {
int f = 0, t = 0;
char c = getchar();
while (!isdigit(c)) t |= (c == '-'), c = getchar();
while (isdigit(c)) f = (f << 3) + (f << 1) + c - 48, c = getchar();
return t ? -f : f;
}
void write(int x) {
if (x < 0) putchar('-'), x = -x;
if (x > 9) write(x / 10);
putchar('0' + x % 10);
}
bool cmp(w x,w y)
{
if(k[x.l] == k[y.l])
{
if(k[x.r] == k[y.r]) return x.id < y.id;
return x.r < y.r;
}
return k[x.l] < k[y.l];
}
int main()
{
n = read(),q = read(); p = pow(n,0.666) + 1;
for(int i = 1;i <= n;i++) a[i] = read(),k[i] = (i - 1) / p + 1;
for(int i = 1;i <= q;i++)
{
cin >> oo,x = read(),y = read();
if(oo == 'Q') b[++o].l = x,b[o].r = y,b[o].id = o1,b[o].z = i;
else c[++o1].l = x,c[o1].r = y,c[o1].id = i;
}
sort(b + 1,b + 1 + o,cmp);
ml = b[1].r + 1;
mr = b[1].r; x = 0;
for(int i = 1;i <= o;i++)
{
while(ml < b[i].l)
{
t[a[ml]]--;
if(!t[a[ml]]) sum--;
ml++;
}
while(mr > b[i].r)
{
t[a[mr]]--;
if(!t[a[mr]]) sum--;
mr--;
}
while(mr < b[i].r)
{
mr++;
if(!t[a[mr]]) sum++;
t[a[mr]]++;
}
while(ml > b[i].l)
{
ml--;
if(!t[a[ml]]) sum++;
t[a[ml]]++;
}
while(x < b[i].id)
{
x++;
if(b[i].l <= c[x].l && c[x].l <= b[i].r)
{
t[c[x].r]++,t[a[c[x].l]]--;
if(t[c[x].r] == 1) sum++;
if(!t[a[c[x].l]]) sum--;
}
swap(a[c[x].l],c[x].r);
}
while(x > b[i].id)
{
if(b[i].l <= c[x].l && c[x].l <= b[i].r)
{
t[c[x].r]++,t[a[c[x].l]]--;
if(t[c[x].r] == 1) sum++;
if(!t[a[c[x].l]]) sum--;
}
swap(a[c[x].l],c[x].r);
x--;
}
ans[b[i].z] = sum;
}
for(int i = 1;i <= q;i++) if(ans[i]) write(ans[i]),puts("");
return 0;
}
树上莫队
大概是这个名字吧?
莫队本来是处理序列的,我们知道一棵树有多种序列表达方式,考虑能不能转成序列做。
十分厉害的题,本质上是带修树上莫队。
DFS序、时间戳和欧拉序,讲的还不赖,可以复习一下怎么把树转成序列。
具体思路可以看看代码最下面写的,懒得再写一遍了(
#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace IO
{
template<typename T>
void read(T &_x){_x=0;int _f=1;char ch=getchar();while(!isdigit(ch)) _f=(ch=='-'?-1:_f),ch=getchar();while(isdigit(ch)) _x=_x*10+(ch^48),ch=getchar();_x*=_f;}
template<typename T,typename... Args>
void read(T &_x,Args&...others){Read(_x);Read(others...);}
const int BUF=20000000;char buf[BUF],top,stk[32];int plen;
#define pc(x) buf[plen++]=x
#define flush(); fwrite(buf,1,plen,stdout),plen=0;
template<typename T>inline void print(T x){if(!x){pc(48);return;}if(x<0) x=-x,pc('-');for(;x;x/=10) stk[++top]=48+x%10;while(top) pc(stk[top--]);}
}
using namespace IO;
const int N = 2e5+10;
int B,n,m,q,head[N],cnt,V[N],W[N],v1[N],C[N],K[N],dfn[N],dep[N],las[N],ans[N],cnt1,cnt2,a[N],x,y,z,k,l,r,lg[N],fa[N][20];
int v[N],sum;
struct w
{
int to,nxt;
}b[N<<1];
struct w1
{
int x,y,l,r,lca,id,id1;
}d[N];//询问人从l到r,在进行了id次操作后的答案
struct w2
{
int x,y;
}c[N];//修改操作,将C[x]变为y
inline void add(int x,int y)
{
b[++cnt].nxt = head[x];
b[cnt].to = y;
head[x] = cnt;
}
void dfs(int x,int y)
{
dfn[x] = ++cnt,a[cnt] = x; dep[x] = dep[y]+1,fa[x][0] = y;
for(int i = 1;i <= lg[dep[x]];i++) fa[x][i] = fa[fa[x][i-1]][i-1];
for(int i = head[x];i;i = b[i].nxt)
if(b[i].to != y)
dfs(b[i].to,x);
las[x] = ++cnt,a[cnt] = x;
}
inline int lca(int x,int y)
{
if(dep[x] < dep[y]) swap(x,y);
while(dep[x] != dep[y]) x = fa[x][lg[dep[x]-dep[y]]];
if(x == y) return x;
for(int i = lg[dep[x]];i >= 0;i--)
if(fa[x][i] != fa[y][i])
x = fa[x][i],y = fa[y][i];
return fa[x][0];
}
inline bool cmp(w1 x,w1 y)
{
if(K[x.l] != K[y.l]) return x.l < y.l;
if(K[x.r] != K[y.r]) return x.r < y.r;
return x.id < y.id;
}
inline void sol(int x)
{
v1[x] ^= 1;
if(v1[x] == 1) v[C[x]]++,sum += V[C[x]]*W[v[C[x]]];
else sum -= V[C[x]]*W[v[C[x]]],v[C[x]]--;
}
signed main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
read(n),read(m),read(q); B = pow(2*n,0.666);
for(int i = 1;i <= m;i++) read(V[i]);
for(int i = 1;i <= n;i++) read(W[i]);
for(int i = 1;i <= 2*n;i++) K[i] = (i-1)/B+1;
for(int i = 2;i <= n;i++) lg[i] = lg[i/2]+1;
for(int i = 1;i < n;i++) read(x),read(y),add(x,y),add(y,x);
cnt = 0; dfs(1,0);
for(int i = 1;i <= n;i++) read(C[i]);
for(int i = 1;i <= q;i++)
{
read(x),read(y),read(z);
if(x == 1)
{
d[++cnt1].lca = lca(y,z),d[cnt1].x = y,d[cnt1].y = z,d[cnt1].l = dfn[y],d[cnt1].r = dfn[z],d[cnt1].id = cnt2,d[cnt1].id1 = cnt1;
if(d[cnt1].l > d[cnt1].r) swap(d[cnt1].l,d[cnt1].r),swap(d[cnt1].x,d[cnt1].y);
}
else c[++cnt2].x = y,c[cnt2].y = z;
}
sort(d+1,d+1+cnt1,cmp);
for(int i = 1;i <= d[1].id;i++) swap(C[c[i].x],c[i].y);
z = d[1].id,l = d[1].l,r = d[1].r;
for(int i = d[1].l;i <= d[1].r;i++) sol(a[i]);
for(int i = 1;i <= cnt1;i++)
{
while(l < d[i].l) sol(a[l]),l++;
while(l > d[i].l) l--,sol(a[l]);
while(r < d[i].r) r++,sol(a[r]);
while(r > d[i].r) sol(a[r]),r--;
while(z < d[i].id)
{
z++;
if(l <= dfn[c[z].x] && dfn[c[z].x] <= r) sol(c[z].x);
if(l <= las[c[z].x] && las[c[z].x] <= r) sol(c[z].x);
swap(C[c[z].x],c[z].y);
if(l <= dfn[c[z].x] && dfn[c[z].x] <= r) sol(c[z].x);
if(l <= las[c[z].x] && las[c[z].x] <= r) sol(c[z].x);
}
while(z > d[i].id)
{
if(l <= dfn[c[z].x] && dfn[c[z].x] <= r) sol(c[z].x);
if(l <= las[c[z].x] && las[c[z].x] <= r) sol(c[z].x);
swap(C[c[z].x],c[z].y);
if(l <= dfn[c[z].x] && dfn[c[z].x] <= r) sol(c[z].x);
if(l <= las[c[z].x] && las[c[z].x] <= r) sol(c[z].x);
z--;
}
if(d[i].x != d[i].lca)
{
sol(d[i].x),sol(d[i].lca);
ans[d[i].id1] = sum;
sol(d[i].x),sol(d[i].lca);
}
else ans[d[i].id1] = sum;
}
for(int i = 1;i <= cnt1;i++) print(ans[i]),pc('\n');
flush();
return 0;
}
/*
带修莫队
先考虑把树转成序列,经典的有欧拉序和dfs序
先考虑dfs序,因为每个点恰好出现两次,这样可以理解为第一次进入加,第二次减,用一个桶异或就好了
若询问的两个点有一个是它们的lca,直接dfn[lca]到dfn[x+y-lca]都跑一下就好了
这样的话其它子树的点都会跑两次或没跑,就消了
这个很显然,可以理解为我目前在x了,访问儿子如果访问错了就一直走知道这里删除掉
否则访问对了就不去其它管未访问的x的儿子了,所以没问题。
但凡学了其它算法就会发现,x,y中没有lca多半会有点问题
发现若x,y中没有lca,考虑从x走到y,发现恰好漏掉了x和lca,加上就好了
这个到很好证明,因为lca包含了他们两个,而便利完他们两个后就停止了,所以缺少了lca
然后就是左端点因为访问了两次所以漏了,而它到lca的点因为比它先经过所以只会访问一次,所以就它漏了
所以一共就漏了这两个
考虑如果转成序列怎么做,左右指针移动
操作1(x):若新加的为x,则v[x]++,加上V_x*W_v[x]
操作2(x):若新减的为x,则减去V_x*W_v[x],然后v[x]--
至于遇到修改操作,先看修改的点是否加入了
1.加入了,就减去原来的贡献,然后加上新的贡献
2.未加入,直接更改那个点的值就行了
*/
树上莫队练习题:
SP10707 COT2 - Count on a tree II
P12001 在小小的奶龙山里面挖呀挖呀挖 很有趣的题,可以树上莫队直接做。
P12003 在小小的奶龙山里面挖呀挖呀挖(加强版) 需要根号分治,根据自身喜好写一个其它算法。
浙公网安备 33010602011771号