From 7c52f05b42b04af39c93d70e08e9670b7307426d Mon Sep 17 00:00:00 2001 From: cesonic <42774222+cesonic@users.noreply.github.com> Date: Wed, 29 Aug 2018 19:36:15 +0800 Subject: [PATCH] Update mo-algo.md --- docs/misc/mo-algo.md | 276 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 249 insertions(+), 27 deletions(-) diff --git a/docs/misc/mo-algo.md b/docs/misc/mo-algo.md index 8115ea28..cab2d654 100644 --- a/docs/misc/mo-algo.md +++ b/docs/misc/mo-algo.md @@ -8,7 +8,7 @@ ### 形式 -对于序列上的区间询问问题,如果从 $[l,r]$ 的答案能够 $O(1)$ 扩展到 $[l-1,r],[l+1,r],[l,r+1],[l,r-1]$(即与 $[l,r]$ 相邻的区间)的答案,那么可以在 $O(n\sqrt{n})$ 的复杂度内求出所有询问的答案。 +假设n=m,那么对于序列上的区间询问问题,如果从 $[l,r]$ 的答案能够 $O(1)$ 扩展到 $[l-1,r],[l+1,r],[l,r+1],[l,r-1]$(即与 $[l,r]$ 相邻的区间)的答案,那么可以在 $O(n\sqrt{n})$ 的复杂度内求出所有询问的答案。 ### 实现 @@ -20,7 +20,7 @@ ### 模板 -
int l = 0, r = 0, nowAns = 0;
+```cpp
 
 inline void move(int pos, int sign) {
     // update nowAns
@@ -38,10 +38,12 @@ void solve() {
         ans[q.id] = nowAns;
     }
 }
-
+``` ### 复杂度分析 +以下的情况在n和m同阶的前提下讨论。 + 首先是分块这一步,这一步的时间复杂度毫无疑问地是 $O(\sqrt{n}\sqrt{n}log\sqrt{n}+nlogn)=O(nlogn)$; 接着就到了莫队算法的精髓了,下面我们用通俗易懂的初中方法来证明它的时间复杂度是$O(n\sqrt{n})$; @@ -74,6 +76,12 @@ $$ 综上所述,莫队算法的时间复杂度为 $O(n\sqrt{n})$; +但是对于m的其他取值,如m @@ -223,7 +226,7 @@ int main() using namespace std; templateinline void IN(_Tp&dig) { - char c;dig=0; + char c;dig=0; while(c=getchar(),!isdigit(c)); while(isdigit(c))dig=dig*10+c-'0',c=getchar(); } @@ -278,3 +281,222 @@ int main() return 0; } ``` + +### 树上莫队 + +莫队只能处理线性问题,我们要把树强行压成序列 + +我们可以将树的括号序跑下来,把括号序分块,在括号序上跑莫队 + +具体怎么做呢? + +dfs一棵树,然后如果dfs到x点,就push_back(x),dfs完x点,就直接push_back(-x),然后我们在挪动指针的时候 + +- 新加入的值是x ---> add(x) +- 新加入的值是-x ---> del(x) +- 新删除的值是x ---> del(x) +- 新删除的值是-x ---> add(x) + +这样的话,我们就把一棵树处理成了序列。 + +例题是[[WC2013]糖果公园](https://www.luogu.org/problemnew/show/P4074),这题是带修改树上莫队 + +题意是给你一棵树,每个点有颜色,每次询问 + +$\sum_{c}val_c\sum_{i=1}^{cnt_c}w_i$ + +val表示该颜色的价值 + +cnt表示颜色出现的次数 + +w表示该颜色出现i次后的价值 + +先把树变成序列,然后每次添加/删除一个点,这个点的对答案的的贡献是可以在$O(1)$时间内获得的,即$val_c\times w_{cnt_{c+1}}$ + +发现因为他会把起点的子树也扫了一遍,产生多余的贡献,怎么办呢? + +因为扫的过程中起点的子树里的点肯定会被扫两次,但贡献为0 + +所以可以开一个vis数组,每次扫到点x,就把$vis_x$异或上1 + +如果$vis_x=0$,那这个点的贡献就可以不计 + +所以可以用树上莫队来求 + +修改的话,加上一维时间维即可,变成带修改树上莫队 + +然后因为所包含的区间内可能没有LCA,对于没有的情况要将多余的贡献删除,然后就完事了 + +code: +```cpp +#include +#include +#include +#include + +#define DEBUG printf("line:%d func:%s\n",__LINE__,__FUNCTION__); + +using namespace std; + +const int maxn=200010; + +int f[maxn],g[maxn],id[maxn],head[maxn],cnt,last[maxn],dep[maxn],fa[maxn][22],v[maxn],w[maxn]; +int block,index,n,m,q; +int pos[maxn],col[maxn],app[maxn]; +bool vis[maxn]; +long long ans[maxn],cur; + +struct edge { + int to,nxt; +} e[maxn]; +int cnt1=0,cnt2=0;//时间戳 + +struct query { + int l,r,t,id; + bool operator <(const query &b)const { + return (pos[l]=0; i--) + if(dis>=(1<=0; i--) { + if(fa[x][i]!=fa[y][i]) + x=fa[x][i],y=fa[y][i]; + } + return fa[x][0]; +} + +inline void add(int x) { + if(vis[x]) + cur-=(long long )v[col[x]]*w[app[col[x]]--]; + else + cur+=(long long )v[col[x]]*w[++app[col[x]]]; + vis[x]^=1; +} + +inline void modify(int x,int t) { + if(vis[x]) { + add(x); + col[x]=t; + add(x); + } else col[x]=t; +}//在时间维上移动 + +int main() { + scanf("%d%d%d",&n,&m,&q); + for(int i=1; i<=m; i++) + scanf("%d",&v[i]); + for(int i=1; i<=n; i++) + scanf("%d",&w[i]); + for(int i=1; if[y]) + swap(x,y); + a[++cnt1]=(query) { + lca(x,y)==x?f[x]:g[x],f[y],cnt2,cnt1 + }; + } + } + sort(a+1,a+cnt1+1); + int L,R,T;//指针坐标 + L=R=0; + T=1; + for(int i=1; i<=cnt1; i++) { + while(T<=a[i].t) { + modify(b[T].l,b[T].t); + T++; + } + while(T>a[i].t) { + modify(b[T].l,b[T].r); + T--; + } + while(L>a[i].l) { + L--; + add(id[L]); + } + while(La[i].r) { + add(id[R]); + R--; + } + while(R