OSDN Git Service

Update mo-algo.md
authorcesonic <42774222+cesonic@users.noreply.github.com>
Wed, 29 Aug 2018 11:36:15 +0000 (19:36 +0800)
committerGitHub <noreply@github.com>
Wed, 29 Aug 2018 11:36:15 +0000 (19:36 +0800)
docs/misc/mo-algo.md

index 8115ea2..cab2d65 100644 (file)
@@ -8,7 +8,7 @@
 \r
 ### 形式\r
 \r
-对于序列上的区间询问问题,如果从 $[l,r]$ 的答案能够 $O(1)$ 扩展到 $[l-1,r],[l+1,r],[l,r+1],[l,r-1]$(即与 $[l,r]$ 相邻的区间)的答案,那么可以在 $O(n\sqrt{n})$ 的复杂度内求出所有询问的答案。\r
\81\87设n=mï¼\8cé\82£ä¹\88对äº\8eåº\8få\88\97ä¸\8aç\9a\84å\8cºé\97´è¯¢é\97®é\97®é¢\98ï¼\8cå¦\82æ\9e\9cä»\8e $[l,r]$ ç\9a\84ç­\94æ¡\88è\83½å¤\9f $O(1)$ æ\89©å±\95å\88° $[l-1,r],[l+1,r],[l,r+1],[l,r-1]$ï¼\88å\8d³ä¸\8e $[l,r]$ ç\9b¸é\82»ç\9a\84å\8cºé\97´ï¼\89ç\9a\84ç­\94æ¡\88ï¼\8cé\82£ä¹\88å\8f¯ä»¥å\9c¨ $O(n\sqrt{n})$ ç\9a\84å¤\8dæ\9d\82度å\86\85æ±\82å\87ºæ\89\80æ\9c\89询é\97®ç\9a\84ç­\94æ¡\88ã\80\82\r
 \r
 ### 实现\r
 \r
@@ -20,7 +20,7 @@
 \r
 ### 模板\r
 \r
-<pre><code class="cpp">int l = 0, r = 0, nowAns = 0;\r
+```cpp\r
 \r
 inline void move(int pos, int sign) {\r
     // update nowAns\r
@@ -38,10 +38,12 @@ void solve() {
         ans[q.id] = nowAns;\r
     }\r
 }\r
-</code></pre>\r
+```\r
 \r
 ### 复杂度分析\r
 \r
+以下的情况在n和m同阶的前提下讨论。\r
+\r
 首先是分块这一步,这一步的时间复杂度毫无疑问地是 $O(\sqrt{n}\sqrt{n}log\sqrt{n}+nlogn)=O(nlogn)$;\r
 \r
 接着就到了莫队算法的精髓了,下面我们用通俗易懂的初中方法来证明它的时间复杂度是$O(n\sqrt{n})$;\r
@@ -74,6 +76,12 @@ $$
 \r
 综上所述,莫队算法的时间复杂度为 $O(n\sqrt{n})$;\r
 \r
+但是对于m的其他取值,如m<n,分块方式需要改变才能变的更优\r
+\r
+怎么分块呢?\r
+\r
+我们设块长度为$S$,那么对于任意多个在同一块内的询问,挪动的距离就是$n$,一共$\frac{n}{S}$个块,移动的总次数就是$\frac{n^2}{S}$,移动可能跨越块,所以还要加上一个$mS$的复杂度,总复杂度为$O(\frac{n^2}{S}+mS)$,我们要让这个值尽量小,那么就要将这两个项尽量相等,发现$S$取$\frac{n}{\sqrt{m}}$是最优的,此时复杂度为$O(\frac{n^2}{\frac{n}{\sqrt{m}}}+m(\frac{n}{\sqrt{m}}))=O(n\sqrt{m})$\r
+\r
 ### 例题&代码\r
 \r
 [小Z的袜子](https://www.lydsy.com/JudgeOnline/problem.php?id=2038)\r
@@ -150,39 +158,36 @@ int main()
 \r
 ### 特点\r
 \r
-- 用于离线处理区间问题\r
-\r
-- 仅含单点修改\r
-\r
-- 能 $O(1)$ 转移区间(和普通莫队一样)\r
-\r
-- 分块的每一块的大小是 $n^\frac{2}{3}$\r
+普通莫队是不能带修改的\r
 \r
-- 复杂度 $O(n^\frac{5}{3})$\r
+我们可以强行让它可以修改,就像DP一样,可以强行加上一维**时间维**,表示这次操作的时间。\r
 \r
-带修改的莫队的询问排序方法为:\r
+时间维表示经历的修改次数。\r
 \r
-- 第一关键字:左端点所在块编号,从小到大排序。\r
+即把询问$[l,r]$变成$[l,r,time]$\r
 \r
-- 第二关键字:右端点所在块编号,从小到大排序。\r
+那么我们的坐标也可以在时间维上移动,即$[l,r,time]$多了一维可以移动的方向,可以变成:\r
 \r
-- 第三关键字:**经历的修改次数**。也可以说是询问的先后,先询问的排前面。\r
+- $[l-1,r,time]$\r
+- $[l+1,r,time]$\r
+- $[l,r-1,time]$\r
+- $[l,r+1,time]$\r
+- $[l,r,time-1]$\r
+- $[l,r,time+1]$\r
 \r
-对于前后两个区间的转移:\r
+这样的转移也是$O(1)$的,但是我们排序又多了一个关键字,再搞搞就行了\r
 \r
-设当前询问为 $a$,下一个询问为 $b$,我们已知 $a$,要求 $b$。\r
+可以用和普通莫队类似的方法排序转移,做到$O(n^{\frac{5}{3}})$\r
 \r
-首先我们像普通莫队一样转移左右端点\r
+这一次我们排序的方式是以$n^{\frac{2}{3}}$为一块,分成了$n^{\frac{1}{3}}$块,第一关键字是左端点所在块,第二关键字是右端点所在块,第三关键字是时间\r
 \r
-è¿\99æ\97¶å\80\99æ\88\91们å\8f¯è\83½ä¼\9aå\8f\91ç\8e°**$a$ å\92\8c $b$ ç\9a\84ç»\8få\8e\86ç\9a\84ä¿®æ\94¹æ¬¡æ\95°ä¸\8då\90\8c**ï¼\81\r
+è¿\98æ\98¯æ\9d¥è¯\81æ\98\8eä¸\80ä¸\8bæ\97¶é\97´å¤\8dæ\9d\82度ï¼\88é»\98认å\9d\97大å°\8f为$\sqrt{n}$ï¼\89ï¼\9a\r
 \r
-怎么办呢?\r
+- 左右端点所在块不变,时间在排序后单调向右移,这样的复杂度是$O(n)$\r
 \r
-然而,莫队就是个优雅的暴力。\r
+- 若左右端点所在块改变,时间一次最多会移动n个格子,时间复杂度$O(n)$\r
 \r
-假如 $a$ 较 $b$ 少修改了 $p$ 次,那我们就把这 $p$ 次修改一个一个 **从前往后** 暴力地加上去。假如 $a$ 较 $b$ 多修改了 $q$ 次,那我们就把这 $q$ 次修改**从后往前**还原掉。\r
-\r
-具体怎么做呢?我们来看一道例题。\r
+- 左端点所在块一共有$n^{\frac{1}{3}}$中,右端点也是$n^{\frac{1}{3}}$种,一共${n^{\frac{1}{3}}}\times{n^{\frac{1}{3}}}=n^{\frac{2}{3}}$种,每种乘上移动的复杂度$O(n)$,总复杂度$O(n^{\frac{5}{3}})$\r
 \r
 ### 例题\r
 [数颜色 BZOJ - 2120](https://www.lydsy.com/JudgeOnline/problem.php?id=2120)\r
@@ -214,8 +219,6 @@ int main()
 \r
 因此这道题就这样用带修改莫队轻松解决啦!\r
 \r
-记得前面说的一些普通莫队与带修改莫队不同的地方就行了,比如分块的每一块的大小是 $n^\frac{2}{3}$。这个很重要。。。\r
-\r
 代码:\r
 ```cpp\r
 #include <bits/stdc++.h>\r
@@ -223,7 +226,7 @@ int main()
 using namespace std;\r
 template<typename _Tp>inline void IN(_Tp&dig)\r
 {\r
-       char c;dig=0;\r
+    char c;dig=0;\r
        while(c=getchar(),!isdigit(c));\r
        while(isdigit(c))dig=dig*10+c-'0',c=getchar();\r
 }\r
@@ -278,3 +281,222 @@ int main()
        return 0;\r
 }\r
 ```\r
+\r
+### 树上莫队\r
+\r
+莫队只能处理线性问题,我们要把树强行压成序列\r
+\r
+我们可以将树的括号序跑下来,把括号序分块,在括号序上跑莫队\r
+\r
+具体怎么做呢?\r
+\r
+dfs一棵树,然后如果dfs到x点,就push_back(x),dfs完x点,就直接push_back(-x),然后我们在挪动指针的时候\r
+\r
+- 新加入的值是x  ---> add(x)\r
+- 新加入的值是-x ---> del(x)\r
+- 新删除的值是x  ---> del(x)\r
+- 新删除的值是-x ---> add(x)\r
+\r
+这样的话,我们就把一棵树处理成了序列。\r
+\r
+例题是[[WC2013]糖果公园](https://www.luogu.org/problemnew/show/P4074),这题是带修改树上莫队\r
+\r
+题意是给你一棵树,每个点有颜色,每次询问\r
+\r
+$\sum_{c}val_c\sum_{i=1}^{cnt_c}w_i$\r
+\r
+val表示该颜色的价值\r
+\r
+cnt表示颜色出现的次数\r
+\r
+w表示该颜色出现i次后的价值\r
+\r
+先把树变成序列,然后每次添加/删除一个点,这个点的对答案的的贡献是可以在$O(1)$时间内获得的,即$val_c\times w_{cnt_{c+1}}$\r
+\r
+发现因为他会把起点的子树也扫了一遍,产生多余的贡献,怎么办呢?\r
+\r
+因为扫的过程中起点的子树里的点肯定会被扫两次,但贡献为0\r
+\r
+所以可以开一个vis数组,每次扫到点x,就把$vis_x$异或上1\r
+\r
+如果$vis_x=0$,那这个点的贡献就可以不计\r
+\r
+所以可以用树上莫队来求\r
+\r
+修改的话,加上一维时间维即可,变成带修改树上莫队\r
+\r
+然后因为所包含的区间内可能没有LCA,对于没有的情况要将多余的贡献删除,然后就完事了\r
+\r
+code:\r
+```cpp\r
+#include<algorithm>\r
+#include<iostream>\r
+#include<cstdio>\r
+#include<cmath>\r
+\r
+#define DEBUG printf("line:%d func:%s\n",__LINE__,__FUNCTION__);\r
+\r
+using namespace std;\r
+\r
+const int maxn=200010;\r
+\r
+int f[maxn],g[maxn],id[maxn],head[maxn],cnt,last[maxn],dep[maxn],fa[maxn][22],v[maxn],w[maxn];\r
+int block,index,n,m,q;\r
+int pos[maxn],col[maxn],app[maxn];\r
+bool vis[maxn];\r
+long long ans[maxn],cur;\r
+\r
+struct edge {\r
+    int to,nxt;\r
+} e[maxn];\r
+int cnt1=0,cnt2=0;//时间戳\r
+\r
+struct query {\r
+    int l,r,t,id;\r
+    bool operator <(const query &b)const {\r
+        return (pos[l]<pos[b.l])||(pos[l]==pos[b.l]&&pos[r]<pos[b.r])||(pos[l]==pos[b.l]&&pos[r]==pos[b.r]&&t<b.t);\r
+    }\r
+} a[maxn],b[maxn];\r
+\r
+inline void addedge(int x, int y) {\r
+    e[++cnt]=(edge) {\r
+        y,head[x]\r
+    };\r
+    head[x]=cnt;\r
+}\r
+\r
+\r
+void dfs(int x) {\r
+    id[f[x]=++index]=x;\r
+    for(int i=head[x]; i; i=e[i].nxt) {\r
+        if(e[i].to!=fa[x][0]) {\r
+            fa[e[i].to][0]=x;\r
+            dep[e[i].to]=dep[x]+1;\r
+            dfs(e[i].to);\r
+        }\r
+    }\r
+    id[g[x]=++index]=x;//括号序\r
+}\r
+\r
+inline void swap(int &x,int &y) {\r
+    int t;\r
+    t=x;\r
+    x=y;\r
+    y=t;\r
+}\r
+\r
+inline int lca(int x,int y) {\r
+    if(dep[x]<dep[y])\r
+        swap(x,y);\r
+    if(dep[x]!=dep[y]) {\r
+        int dis=dep[x]-dep[y];\r
+        for(int i=20; i>=0; i--)\r
+            if(dis>=(1<<i))\r
+                dis-=1<<i,x=fa[x][i];\r
+    }//爬到同一高度 \r
+    if(x==y) return x;\r
+    for(int i=20; i>=0; i--) {\r
+        if(fa[x][i]!=fa[y][i])\r
+            x=fa[x][i],y=fa[y][i];\r
+    }\r
+    return fa[x][0];\r
+}\r
+\r
+inline void add(int x) {\r
+    if(vis[x])\r
+        cur-=(long long )v[col[x]]*w[app[col[x]]--];\r
+    else\r
+        cur+=(long long )v[col[x]]*w[++app[col[x]]];\r
+    vis[x]^=1;\r
+}\r
+\r
+inline void modify(int x,int t) {\r
+    if(vis[x]) {\r
+        add(x);\r
+        col[x]=t;\r
+        add(x);\r
+    } else col[x]=t;\r
+}//在时间维上移动\r
+\r
+int main() {\r
+    scanf("%d%d%d",&n,&m,&q);\r
+    for(int i=1; i<=m; i++)\r
+        scanf("%d",&v[i]);\r
+    for(int i=1; i<=n; i++)\r
+        scanf("%d",&w[i]);\r
+    for(int i=1; i<n; i++) {\r
+        int x,y;\r
+        scanf("%d%d",&x,&y);\r
+        addedge(x,y);\r
+        addedge(y,x);\r
+    }\r
+    for(int i=1; i<=n; i++) {\r
+        scanf("%d",&last[i]);\r
+        col[i]=last[i];\r
+    }\r
+    dfs(1);\r
+    for(int j=1; j<=20; j++)\r
+        for(int i=1; i<=n; i++)\r
+            fa[i][j]=fa[fa[i][j-1]][j-1];//预处理祖先 \r
+    int block=pow(index,2.0/3);\r
+    for(int i=1; i<=index; i++) {\r
+        pos[i]=(i-1)/block;\r
+    }\r
+    while(q--) {\r
+        int opt,x,y;\r
+        scanf("%d%d%d",&opt,&x,&y);\r
+        if(opt==0) {\r
+            b[++cnt2].l=x;\r
+            b[cnt2].r=last[x];\r
+            last[x]=b[cnt2].t=y;\r
+        } else {\r
+            if(f[x]>f[y])\r
+                swap(x,y);\r
+            a[++cnt1]=(query) {\r
+                lca(x,y)==x?f[x]:g[x],f[y],cnt2,cnt1\r
+            };\r
+        }\r
+    }\r
+    sort(a+1,a+cnt1+1);\r
+    int L,R,T;//指针坐标\r
+    L=R=0;\r
+    T=1;\r
+    for(int i=1; i<=cnt1; i++) {\r
+        while(T<=a[i].t) {\r
+            modify(b[T].l,b[T].t);\r
+            T++;\r
+        }\r
+        while(T>a[i].t) {\r
+            modify(b[T].l,b[T].r);\r
+            T--;\r
+        }\r
+        while(L>a[i].l) {\r
+            L--;\r
+            add(id[L]);\r
+        }\r
+        while(L<a[i].l) {\r
+            add(id[L]);\r
+            L++;\r
+        }\r
+        while(R>a[i].r) {\r
+            add(id[R]);\r
+            R--;\r
+        }\r
+        while(R<a[i].r) {\r
+            R++;\r
+            add(id[R]);\r
+        }\r
+        int x=id[L],y=id[R];\r
+        int llca=lca(x,y);\r
+        if(x!=llca&&y!=llca) {\r
+            add(llca);\r
+            ans[a[i].id]=cur;\r
+            add(llca);\r
+        } else ans[a[i].id]=cur;\r
+    }\r
+    for(int i=1; i<=cnt1; i++) {\r
+        printf("%lld\n",ans[i]);\r
+    }\r
+    return 0;\r
+}\r
+```\r