diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 340e69f..5c907aa 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -154,6 +154,7 @@ export default { {text: '22.线段树', link: '/数据结构/22.线段树'}, {text: '23.分块', link: '/数据结构/23.分块'}, {text: '笛卡尔树', link: '/数据结构/笛卡尔树'}, + {text: '并查集', link: '/数据结构/并查集'}, ] }, ], @@ -189,6 +190,8 @@ export default { {text: '18.图与树入门', link: '/图论/18.图与树入门'}, {text: '图论相关概念', link: '/图论/图论相关概念'}, {text: '二分图', link: '/图论/二分图'}, + {text: '最短路', link: '/图论/最短路'}, + {text: '生成树', link: '/图论/生成树'}, ] }, ], @@ -206,6 +209,8 @@ export default { items: [ {text: '对顶堆', link: '/杂项/对顶堆'}, {text: '对顶栈', link: '/杂项/对顶栈'}, + {text: 'WQS二分', link: '/杂项/WQS二分'}, + {text: '二进制分组', link: '/杂项/二进制分组'}, ] }, ], diff --git "a/docs/\345\233\276\350\256\272/\346\234\200\347\237\255\350\267\257.md" "b/docs/\345\233\276\350\256\272/\346\234\200\347\237\255\350\267\257.md" new file mode 100644 index 0000000..12de30d --- /dev/null +++ "b/docs/\345\233\276\350\256\272/\346\234\200\347\237\255\350\267\257.md" @@ -0,0 +1,244 @@ +--- +title: 最短路 +titleTemplate: 图论 +--- + +## 最短路 + +给定一张 $n$ 个点, $m$ 条边的图,每条边都有长度,问从一个点到另一个点的最短路长度。 + +如果起点固定,则称为单源最短路。 + +如果起点不固定,则称为多源最短路。 + +这张图必须保证没有长度为负数的环,否则在这个环上一直绕圈,长度会越来越小。 + +### BFS + +如果边的长度都是 $1$ 则之间广度优先搜索 + +### floyd + +假设边用邻接矩阵存储,此时 $eg[i][j]$ 是连接 $i$ 点和 $j$ 点的边的长度。 + +设 $f[k][i][j]$ 是只允许**途径** $1\sim k$ 的情况下,从 $i$ 点到 $j$ 点的最短路径长度。 + +则 $f[n][i][j]$ 就是最终的最短路。 + +设 $f[0][i][j]=eg[i][j]$,则有一个~~显然~~的转移方程 + +```cpp +f[k][i][j]=min(f[k-1][i][j],f[k-1][i][k]+f[k-1][k][j]); +``` + +用三重循环把转移完整地写出来: + +```cpp +for(int k=1;k<=n;++k) +{ + for(int i=1;i<=n;++i) + { + for(int j=1;j<=n;++j) + { + f[k][i][j]=min(f[k-1][i][k]+f[k-1][k][j]); + } + } +} +``` + +事实上第一维对转移没有影响 + +因为无论是从 $f[k-1][i][k]$ 转移还是从 $f[k][i][k]$ 转移,都不可能经过两次 $k$ 点,否则就会成环,而因为没有负环,所以代价一定会变大。 + +```cpp +for(int k=1;k<=n;++k) +{ + for(int i=1;i<=n;++i) + { + for(int j=1;j<=n;++j) + { + f[i][j]=min(f[i][k]+f[k][j]); + } + } +} +``` + +注意这里的 $k$ 的循环一定要在**最外面** + +当 $k>i>j$ 时 更新 $f[i][j]=min(f[i][k]+f[k][j])$, + +若 $k$ 不在最外层,则 $f[i][k]$ 还没有进行任何更新,说明 $f[i][k]$ 里面的值不是最小的,自然得到的 $f[i][j]$ 也不是最小值。 + +$floyd$ 的时间复杂度是 $O(n^3)$ ,空间复杂度是 $O(n^2)$ + +弗洛伊德得到的最短路是全源最短路,任意两个点之间的距离都能得到 + +[B3647 【模板】Floyd ](https://www.luogu.com.cn/problem/B3647) + +#### 最小环 + +假设图是简单图(没有重边和自环) + +最小环一定是一个简单环(不存在环套环) + +设环中编号最大的点是 $u$ 。 + +当得到 $f[u-1][x][y]$ 时,$f[u-1][x][y]$ 与 $(x,u)$ 和 $(y,u)$ 形成简单环,依次判断该最小值。 + +[P6175 无向图的最小环问题 ](https://www.luogu.com.cn/problem/P6175) + +#### 传递闭包 + +当某种关系具有传递性时(必须某两个点是否联通),可以用$floyd$ 将性质传递 + +$f[i][j]$ 表示 $i$ 到 $j$ 联通(或者有关系从 $i$ 指向 $j$ ) + +```cpp +f[i][j]|=f[i][k]&f[k][j]; +``` + +进一步可以用 bitset 优化到 $O(\frac{n^3}{w})$ + +[B3611 【模板】传递闭包 ](https://www.luogu.com.cn/problem/B3611) + +### bellman-ford + +该算法适用于求单源最短路,即从固定起点出发到其他点的最短路 + +设一个数组 $dis$ ,其中 $dis[u]$ 表示从出发点到 $i$ 点的最短路径长度。 + +如果存在一条 $u->t$ 且长度为 $v$ 的边,则可以用 `dis[t]=min(dis[t],dis[u]+v)` 来**松弛** $t$ 节点。 + +因为最短路最多经过 $n-1$ 条边,所以最多只用 $n-1$ 次松弛。 + +可以设计一个算法,每次用所有边进行依次松弛,最终一定能得到最短路。 + +```cpp +for(int i=1;it,长度为v的边。 + { + dis[t]=min(dis[t],dis[u]+v); + } + } +} +``` + +也可以进行 $n$ 轮松弛,而且如果第 $n$ 轮仍然能进行松弛,说明图中存在负环(总长度为负数的环)。 + +每轮松弛访问所有边一次,总时间复杂度 $O(nm)$ + +### spfa + +$bellman-ford$ 有很大的改进空间,比如某一轮松弛不可能所有点所有边都会用到。 + +第一次松弛,只有起点的距离是已知的,说明只有从起点出发的边是有用的。 + +第二次松弛,只有起点周围一圈的距离是已知的,说明只有这一圈点出发的边是有用的。 + +…… + +第 $k$ 次松弛,如果某一个点在第 $k-1$ 次松弛中,$dis$ 没有发生改变,那么以它为起点的边是没有必要松弛的,只有 $dis$ 发生改变的点才有必要松弛。 + + 开一个队列,将所有 $dis$ 更改过的点值更改过的点放进队列里,用这些点出发的边进行下一次松弛 + +```cpp +while(!q.empty()) +{ + int now=q.front(); + q.pop(); + //vis[now] 表示 now 是否在队列里,防止一个点占好几个位置 + vis[now]=0;//出队列 + for(auto [t,v]:eg[now]) + { + if(dis[t]>dis[now]+v) + { + dis[t]=dis[now]+v; + if(!vis[t]) q.push(t);//如果发生了改变,进队列 + } + } +} +``` + +可以给每个点记录一下 $dis$ 的修改次数,如果达到 $n$ 次则说明图中有负环。 + +它的最坏复杂度也是 $O(nm)$ ,但是平均期望复杂度是 $O(nlogn)$。 + +不推荐在平常情况下写这个算法,除非是判断负环或者边有负数。 + +[P3385 【模板】负环 ](https://www.luogu.com.cn/problem/P3385) + +### dijkstra + +当图中的边长度没有负数时求单源最短路: + +那么有一个神奇的事情,如果一个点的 $dis$ 是所有点中最小的那个,那么这个 $dis$ 一定是它的最短路。 + +举例: + +![](https://s3.bmp.ovh/imgs/2024/01/27/4e1c4b0b85f68a9a.png) + +如果没有边的长度是负数,那么此时 $1->2$ 的最短路径就是 $5$ 不会变得更短了。 + +如果有边的长度是负数,那么则不一定,有可能 $3->2$ 的长度是 $-8$ ,这时候如果先用 $2$ 更新其他点,再用 $3->2$这条边松弛 $2$ ,那 $2$ 之前的更新就都作废了。 + +建立在没有负数长度边的前提下,我们可以每次直接取出还没取过的 $dis$ 最小的那个点,因为它的答案确定了,所以它可以毫无压力地松弛别人。 + +它的本质可以说是贪心算法。 + +要取最小的,所以把队列改成堆(优先队列),存两个参数,第一个是当前点的 $dis$ ,第二个是节点的编号。 + +```cpp +while(!q.empty()) +{ + int now=q.top()[1];//[0] 是长度, [1] 是编号 + q.pop(); + if(vis[now]) continue;//如果这个点被选过就跳过 + vis[now]=1; + for(auto [t,v]:eg[now]) + { + if(dis[t]>dis[now]+v) + { + dis[t]=dis[now]+v; + q.push(pr{dis[t],t});//如果成功松弛,将松弛后的点加入优先队列。 + //因为优先队列中要存dis[t],而且每次松弛后dis[t] 长度不同,所以相同的点也必须都加进去,而不是像spfa只存一个。 + } + } +} +``` + +时间复杂度: + +最多 $m$ 次进堆和出堆,如果按每次时间复杂度 $logm$ 算,则时间复杂度是 $O(mlogm)$ + +[P4779 【模板】单源最短路径(标准版) ](https://www.luogu.com.cn/problem/P4779) + +### 分层图 + +[P4568 [JLOI2011] 飞行路线 ](https://www.luogu.com.cn/problem/P4568) + +在无负长度边情况下,求最短路径,且可以令任意 $k$ 条边的长度变为 $0$,求 $1$ 号点到 $n$ 号的最短路径。 + +将图视作一共 $k$ 层的分层图,每一层都是一整张图,而上一层的边可以通过 $0$ 代价走到下一层。 + +即如果本来存在 $u→v$ 长度为 $len$ 的边。 + +可以从 $(u,1)→(v,2)$ 长度为 $0$ 的边,第二个参数是层数。 + +层数增加是不可逆的,符合了建模的想法,最后 $(1,0)$ 到 $(n,k)$ 的距离就是最短路。 + +可以直接将状态设计成 $dis[u][k]$ 表示第 $k$ 层的节点 $u$ 。 + +### 练习题 + +[P1144 最短路计数 ](https://www.luogu.com.cn/problem/P1144) + +[P2865 [USACO06NOV] Roadblocks G ](https://www.luogu.com.cn/problem/P2865) + +[P1462 通往奥格瑞玛的道路 ](https://www.luogu.com.cn/problem/P1462) + +[P1119 灾后重建 ](https://www.luogu.com.cn/problem/P1119) + +[E - Skiing](https://atcoder.jp/contests/abc237/tasks/abc237_e) \ No newline at end of file diff --git "a/docs/\345\233\276\350\256\272/\347\224\237\346\210\220\346\240\221.md" "b/docs/\345\233\276\350\256\272/\347\224\237\346\210\220\346\240\221.md" new file mode 100644 index 0000000..7c3e2a2 --- /dev/null +++ "b/docs/\345\233\276\350\256\272/\347\224\237\346\210\220\346\240\221.md" @@ -0,0 +1,113 @@ +--- +title: 生成树 +titleTemplate: 图论 +--- + +## 生成树 + +前置知识:并查集,树与图 + +给定一张连通图,从图中选出数量最少的边,使得所有点仍然是连通的,那么选出的图一定是一棵树。 + +### 最小生成树 + +在生成树,边权之和最小的那一棵,称为最小生成树。 + +例如在市政规划时,管道要连接所有的节点,但是希望总长度最短等等 + +### prim + +把节点分为两个集合,一个是连通集合 $S$ ,另一个是未连通集合 $T$ 。 + +先随便选一个点作为 $S$ 的初始节点,然后重复以下步骤: + +选择连接 $S$ 和 $T$ 节点的权值最小的边,然后将这条边和端点都加入 $S$ 。 + +一共只有 $n-1$ 条边。 + +```cpp +bool vis[N];//是否在 S 集合 +int dis[N];//S 集合到该点的最短距离 +int sum=0; +vis[1]=1;dis[1]=0; +memset(dis,0x3f,sizeof(dis)); +for(int k=1;k<=n;++k) +{ + int pos=0; + for(int i=1;i<=n;++i) + { + if(!vis[i]&&dis[i] edge; +vector eg(m+1); +for(int i=1;i<=m;++i) +{ + cin>>e[i][0]>>a[i][1]>>a[i][2]; + //e[i][0]和e[i][1]之间有一条权值为e[i][2]的边。 +} +sort(e.begin()+1,e.end(),[&](edge a,edge b){ + return a[2] f(n+1);//并查集 +for(int i=1;i<=n;++i) f[i]=i; +int sum=0; +for(int i=1;i<=n;++i) +{ + int x=e[i][0],y=e[i][1]; + x=find(x),y=find(y); + if(x==y) continue; + sum+=e[i][2]; + f[x]=y; +} +``` + +这样复杂度是 $O(mlogm)$ ,在多数情况下都比 $prim$ 要好,除非是特别稠密的图。 + +[P3366 【模板】最小生成树 ](https://www.luogu.com.cn/problem/P3366) + +[P1194 买礼物 ](https://www.luogu.com.cn/problem/P1194) + +### Boruvka + +是一种多路增广的改进 $prim$ 算法。 + +开局有 $n$ 个节点,也就是 $n$ 个连通块,每轮操作给每个连通块找和其他连通块相连的长度最小的边,然后一起合并。 + +因为每一轮每个连通块至少节点数量翻倍,所以一共只有 $log$ 轮。 + +关键在于能否快速找到两个连通块之间的长度最小的边 + +例如:每个点有一个权值 $a_i$ ,两个点之间的边的权值是 $|a_i-a_j|$,在这张完全图上求最小生成树。 + +开一个 $multiset$ 存所有点的 $a_i$ ,当处理到某一个连通块的时候,先把这个连通块内的点删去,然后在 $set$ 里二分找最接近 $a_i$ 的点,找到后再添加回去。 + +每一轮的话要花 $O(nlogn)$ 时间,所以总时间是 $O(nlog^2n)$。 + +[CF1550F Jumping Around](https://codeforces.com/problemset/problem/1550/F) + +### 练习题 + +[P2330 [SCOI2005] 繁忙的都市 ](https://www.luogu.com.cn/problem/P2330) + +[Shichikuji and Power Grid ](https://www.luogu.com.cn/problem/CF1245D) + +[P2573 [SCOI2012] 滑雪 ](https://www.luogu.com.cn/problem/P2573) \ No newline at end of file diff --git "a/docs/\346\225\260\346\215\256\347\273\223\346\236\204/21.\346\240\221\347\212\266\346\225\260\347\273\204.md" "b/docs/\346\225\260\346\215\256\347\273\223\346\236\204/21.\346\240\221\347\212\266\346\225\260\347\273\204.md" index 5bc9cc8..ffc90bf 100644 --- "a/docs/\346\225\260\346\215\256\347\273\223\346\236\204/21.\346\240\221\347\212\266\346\225\260\347\273\204.md" +++ "b/docs/\346\225\260\346\215\256\347\273\223\346\236\204/21.\346\240\221\347\212\266\346\225\260\347\273\204.md" @@ -100,6 +100,30 @@ while(x<=n) s[x]+=k,x+=lowbit(x) 所以它也维护的信息具有可减性,或者说具有逆元。 +#### lowbit + +在这里补充说明以下如何快速求 $lowbit(x)$ ,即一个数的二进制最低位 + +计算机中存储负数时用的是**补码**(这个概念这里不介绍) + +因为补码是原本的二进制取反再 $+1$ 。 + +原本 $x$ 低位等于 $0$ 的位,在 $-x$ 的反码中这些位都是 $1$,知道遇到一个 $x$ 等于 $1$ 的位。 + +例如:$x=(10100)_2$ + +那么 $[-x]_反=(01011)_2$ + +再给反码 $+1$ ,那么就会让 $-x$ 的补码的最低位和 $x$ 的最低位对齐 + +$[x]_补=(10100)_2,[-x]_补=(01100)$ + +此时除了最低为,其他位均与 $x$ 不同。 + +所以 `lowbit(x)=x&-x`。 + +例题: + [P3374 【模板】树状数组 1 ](https://www.luogu.com.cn/problem/P3374) 动态前缀和 [P3368 【模板】树状数组 2 ](https://www.luogu.com.cn/problem/P3368) 动态差分 diff --git "a/docs/\346\225\260\346\215\256\347\273\223\346\236\204/\345\271\266\346\237\245\351\233\206.md" "b/docs/\346\225\260\346\215\256\347\273\223\346\236\204/\345\271\266\346\237\245\351\233\206.md" new file mode 100644 index 0000000..81e0922 --- /dev/null +++ "b/docs/\346\225\260\346\215\256\347\273\223\346\236\204/\345\271\266\346\237\245\351\233\206.md" @@ -0,0 +1,209 @@ +--- +title: 并查集 +titleTemplate: 数据结构 +--- + +### 并查集 + +$n$个点,有$m$次操作,每次操作如下: + +$1.$把 $x,y$ 连接起来。 + +$2.$ 问$x,y$ 是不是被直接或间接连接起来了。 + +怎么把 $x,y$ 连起来呢? + +哈哈,直接连双向边就行了。查询的时候从一个点开始$dfs$找另一个点。 + +但是这样,连接-查询是$O(1)-O(n)$的,查询复杂度太高了。 + +我们给每个点设置一个老大,一开始所有人的老大都是自己。(牢大,想你了) + +```cpp +for(int i=1;i<=n;++i) f[i]=i;//f[i]表示i的老大是谁。 +``` + + + +![](https://s2.loli.net/2023/07/30/Nm2bHRAtjiqzYnS.png) + +如果两个点要连边,那么就把其中一个人的老大设置成另一个人。 + +比如$1,2$连边,$1,3$连边: + +![](https://s2.loli.net/2023/07/30/wcQqkYvuSAfr49b.png) + +如果两个人的老大是同一个人,那么说明这两个人是一伙的。 + +这个老大要找的是最终的老大,因为有可能长这样: + +![](https://s2.loli.net/2023/07/30/PYcspiOAuVmk2Ra.png) + +这时候$2,4$也是一伙的,所以找老大要找到最终的老大为止。 + +```cpp +int find(int k) +{ + if(f[k]==k) return k;//如果老大是自己,返回自己。 + return find(f[k]);否则继续找老大的老大 +} +``` + +但是这样有问题,比如先连了$1,2$和$3,4$,这时候再连$2,3$,就会出现这种情况: + +![](https://s2.loli.net/2023/07/30/M2mFhcTEgfSHZn6.png) + +本来$4$已经收了$3$做小弟,这时候$3$偷偷跟着$2$跑了,这样就不对了(因为此时按理来说,$1,4$应该联通)。 + +这时候可以这样理解,$2,3$干了一架,$3$打输了,但是$3$不能直接投降,应该找自己的老大$4$给自己出气。 + +以此类推,其实这场架是双方的最终老大在干架,所以应该把双方的最终老大连起来。 + +![](https://s2.loli.net/2023/07/30/SAzCFwEheiZluK7.png) + +```cpp +void merge(int x,int y)//连接x,y +{ + if(find(x)==find(y)) return;//如果两个人其实是一伙的,跳过。 + f[find(x)]=find(y);//两个人的最终老大合并了。 +} +``` + +但是这样查询的复杂度还是$O(n)$,万一整个关系形成一条好长好长的链怎么办? + +我们有办法!这个图的形态要比一般的图简单很多,所以可以考虑一些优化方法。 + +#### 路径压缩 + +首先,我们判断某两个点是不是联通,只需要一个信息,就是点的最终老大是谁。 + +所以我们连这么长一条链是没有用的,不如在查询的时候做一点手脚: + +![](https://s2.loli.net/2023/07/30/gcdCA5rqstHXjio.png) + +我们将查询路径上遇到的点,全都直接去接到最终的老大下面。这样下次再查的时候直接$O(1)$出结果了,非常方便。 + +```cpp +int find(int k) +{ + if(f[k]==k) return k; + int t=find(f[k]); + return f[k]=t; +} +/* +更简单的写法 +int find(int k){return f[k]==k?k:f[k]=find(f[k]);} +*/ +``` + +复杂度分析:我也不太会 + +单纯的加上的路径压缩,单次查询复杂度上界是$O(logn)$ + +#### 按秩合并 + +(其实是一种启发式合并) + +还有另一个办法:如果我们不想破坏这个链的形态,也就是我们还关心每个点的直接老大是谁,那么可以用按秩序合并。 + +在合并两个最终老大的时候,之前没有去管哪个接在哪个下面,这里其实可以优化。 + +设两个最终老大$x,y$,$x$下面连了$k_1$层节点(最大深度为$k_1$),$y$下面连了$k_2$层节点(最大深度为$k_2$)。 + +不妨假设$k_1\leq k_2$。 + +如果$k_1r[y]) swap(x,y); + f[x]=y; + r[y]+=(r[x]==r[y]); +} +``` + +注意这里的想法类似启发式合并,但这里是按**深度**由小向大合并,因为在并查集中,复杂度是和深度有关的,而不是节点个数。 + +如果两个优化方法一起用,单次复杂度是 $O(\alpha)$ (反阿克曼函数),约等于线性 $O(1)$。 + +#### 删除 + +若要删除,则将自己的牢大重新指向自己。 + +这要求的原本的机构不被破坏,即只能用按秩合并,不能用路径压缩。 + +[P1551 亲戚 ](https://www.luogu.com.cn/problem/P1551) + +### 扩展域并查集 + +给每个点一个反义节点,用来维护不同种类的关系问题。 + +通常用法是判断一个图是不是二分图(即图中所有节点的可以分为两部分,同一部分的节点之间互相没有边,或者说每一条边的两边节点都属于不同的部分,最多有两个部分)也可以用并查集: + +假设有$n$个点,我们把这$n$个点每个点都复制一份,编号为$n+1\sim n+n$。 + +如果两个点 $x,y$ 之间有一条边,那么我们把 $(x,y+n)$ 和 $(x+n,y)$ 用并查集连起来。 + +这样就代表着,$x,y$ 在不同的部分里。 + +假如之后连边是这样的: + +$x,z$ 连边,$y,z$ 连边。 + +那么 $(x+n,z)$ 在一个集合,同时 $(x+n,y)$ 在一个集合 + +在试图把 $y,z$ 连边的时候,发现 $(y,z)$ 已经在一个集合里了,这说明 $(y,z)$ 属于二分图中的同一个部分,再连边就破坏了二分图的性质了。 + +[P1525 [NOIP2010 提高组] 关押罪犯 ](https://www.luogu.com.cn/problem/P1525) + +### 带权并查集 + +并查集向上的边也可以携带一些信息: + +[食物链](https://www.luogu.com.cn/problem/P2024) + +有三类生物,$A,B,C$,$A$吃$B$,$B$吃$C$,$C$吃$A$。 + +有两种关系:$x,y$是同一类生物,或者$x$吃$y$。 + +给定关系,如果新给出的关系和已经有了的关系冲突,就是假话,问有多少句假话。 + +我们把三种关系分别标号为$0,1,2$。设$x$的种类为$b(x)$ + +如果$(b(x)+1)\%3==b(y)$,那么$x$就会吃$y$。 + +那么可以在并查集连边的同时,维护一个信息$(0/1/2)$,如果$x->y$这条边的权值是$1$,那么表示$x$吃$y$,如果这条边权值是$2$,那么表示$y$吃$x$,如果权值是$0$,表示$x$和$y$是同类。 + +那么比如$x->y->z$这两条边的权值都是$1$,那么$x$吃$y$,$y$吃$z$,$z$吃$x$。 + +$z$吃$x$怎么表示出来呢,会发现$x$到$z$的路径和刚好是$2$,所以表示$z$吃$x$。 + +也就是两点之间的路径和对$3$的模数代表了这两个点之间种类的关系。 + +### 练习题 + +[P1621 集合 ](https://www.luogu.com.cn/problem/P1621) + +[P1197 [JSOI2008] 星球大战 ](https://www.luogu.com.cn/problem/P1197) + +[P1196 [NOI2002] 银河英雄传说 ](https://www.luogu.com.cn/problem/P1196) + +[P1955 [NOI2015] 程序自动分析 ](https://www.luogu.com.cn/problem/P1955) + +[P1682 过家家 ](https://www.luogu.com.cn/problem/P1682) \ No newline at end of file diff --git "a/docs/\346\235\202\351\241\271/WQS\344\272\214\345\210\206.md" "b/docs/\346\235\202\351\241\271/WQS\344\272\214\345\210\206.md" new file mode 100644 index 0000000..550df54 --- /dev/null +++ "b/docs/\346\235\202\351\241\271/WQS\344\272\214\345\210\206.md" @@ -0,0 +1,116 @@ +--- +title: WQS二分 +titleTemplate: 杂项 +--- + +### WQS二分 + +解决一类要求选择特定数量的某种物品前提下的最优化问题。 + +[P2619 [国家集训队] Tree I ](https://www.luogu.com.cn/problem/P2619) + +给你一个无向带权连通图,每条边是黑色或白色。让你求一棵最小权的恰好有 $m$ 条白色边的生成树。 + +设 $g(i)$ 是选 $i$ 条白色边的最小生成树。 + +则 $(i,g(i))$ 形成一个下凸包。 + +简易证明:若选 $x$ 条白色边恰好得到最小生成树,则每限制必须多选一条或者少选一条都会令代价增加。 + +![](https://s3.bmp.ovh/imgs/2024/01/29/56f4c7e4f356932c.png) + +若不限制数量,则是一个简单的最小生成树。 + +看见凸包则联想到线性规划,能否将问题转化成给定斜率求最小截距的问题? + +不妨设每选择一条白色边,就要多花费 $x$ 点代价,则新的总代价是 $f(i)=g(i)+i\times x$ + +转化一下 $g(i)=-i\times x+f(i)$ + +从几何角度看这个等于是一条过点 $(i,g(i))$ ,斜率为 $-x$ 的直线。 + +如果要截距 $f(i)$ 最小,在下凸包的范围内,肯定要切着下凸包的端点。 + +![](https://s3.bmp.ovh/imgs/2024/01/29/fac5c3d9d9ce7368.png) + + + +在这个基础上(每选择一条白色边,就要多花费 $x$ 点代价),只要做普通的最小生成树,顺便记录一下一共选了几条白色边,就可以得到最小截距 $f(i)$ 和边数 $i$ 了。 + +但是 $i$ 可能不等于 $m$ ,怎么办呢,可以进行调整。 + +从图上可以看出: + +如果 $im$ ,想要让相切的点向右移动,那么应该减小斜率 + +将这个斜率 $x$ 直接二分,然后根据每次选出的最小生成树的白色边数调整二分的范围。 + +最后由得到的 $g(m)=f(m)-m\times x$ 得到真实答案 + +特殊情况: + +![](https://s3.bmp.ovh/imgs/2024/01/29/8cfc51f5a1a7719c.png) + +注意这里,有可能存在并不能二分出一个斜率,让选中的边数恰好等于 $m$。 + +比如这个斜率会同时和几个点相切,假设中间那个点是 $m$ ,那么最小生成树求得的白边数量可能是 $m-1,m,m+1$。 + +如果你斜率恰好取到这个值,然后发现边的数量是 $m+1$ ,此时你把斜率减小,就会发现切不到 $i=m$ 了。 + +而如果你此时选出的边的数量是 $m-1$ ,把斜率增大,又会发现也切不到 $i=m$ 了。 + +最小生成树得到的边数不是 $m$ ,但是这个斜率既不能减小,也不能增大,否则都会错过正确答案,这怎么办呢。 + +针对这道题目,解决办法是当边的权值相同时,优先选择白色的边 + +当一个斜率切很多点时,会默认选择横坐标最大的位置上(这里是 $i=m+1$) + +然后每当得到的 $i\geq m$ 时,就将斜率记录下来,作为答案。 + +```cpp +while(l<=r) +{ + if(check(mid)>=m) ans=mid,r=mid-1; + else l=mid+1; +} +``` + +这样如果,当斜率同时切到 $m\sim m+k$ 时,即使再减小斜率,答案也已经记下来了,之后因为不会产生 $\geq m$ 的结果,所以此时就是最终答案。 + +当然这样最后得到的结果也不是减去 $i\times m$ 了,而是看具体选了几条白边。 + +这是下凸包的情况,如果是上凸包,你又优先选择了有数量规定的物品,因为上凸包横坐标左移是要斜率增大的,所以应该变成: + +```cpp +while(l<=r) +{ + if(check(mid)>=m) ans=mid,l=mid+1; + else r=mid-1; +} +``` + +这个要根据你二分的写法来定,希望能完全理解凸包、切点和斜率的关系才不会错。 + +如果自己搞不清楚也可以记下来: + +1、权值相同优先选有数量规定的物品 + +2、当选择的数量 $>=$ 规定数量时记录答案 + +3、如果是下凸包,$>=$ 时斜率减小 + +4、如果是上凸包,$>=$ 时斜率增大 + +当然还是希望你能完全理解其中含义。 + +总结:若问题根据物品选择的数量形成上凸包或者下凸包,当不限制数量时很好解决,就可以使用 $wqs$ 二分。 + +### 练习题 + +[跳石头,搭梯子 ](https://ac.nowcoder.com/acm/contest/59284/G) + +[Gosha is hunting ](https://www.luogu.com.cn/problem/CF739E) + +[P4983 忘情 ](https://www.luogu.com.cn/problem/P4983) \ No newline at end of file diff --git "a/docs/\346\235\202\351\241\271/\344\272\214\350\277\233\345\210\266\345\210\206\347\273\204.md" "b/docs/\346\235\202\351\241\271/\344\272\214\350\277\233\345\210\266\345\210\206\347\273\204.md" new file mode 100644 index 0000000..a3c12e0 --- /dev/null +++ "b/docs/\346\235\202\351\241\271/\344\272\214\350\277\233\345\210\266\345\210\206\347\273\204.md" @@ -0,0 +1,66 @@ +--- +title: 二进制分组 +titleTemplate: 杂项 +--- + +### 二进制分组 + +维护难以合并的信息,支持以下操作 + +1、加入一个信息 + +2、强制在线,查询全局信息 + +举个例子:[String Set Queries ](https://www.luogu.com.cn/problem/CF710F) + +1、往集合中加一个字符串 $s$ + +2、从集合中删除一个字符串 $s$ + +3、给一个字符串 $t$,问集合中的所有字符串在 $t$ 中出现过多少次。 + +强制在线,即得到上一个问题的答案才能知道下一次的真正操作。 + +#### 插入和查询 + +先假设没有操作 $2$ ,只有插入和查询 + +查询 $3$ 是一个模板 $AC$ 自动机,但是 $AC$ 自动机不支持插入新字符串。 + +考虑一个基于暴力重构的方法: + +(前面铺垫过很多次,这次就不提根号了) + +将新插入的字符串构建一个新的 $AC$ 自动机 + +如果相邻两个 $AC$ 自动机中的字符串数量相同,则暴力地把两个 $AC$ 自动机合并成一个。 + +什么情况呢,就是比如现在已经有了几个 $AC$ 自动机,里面的串数分别是 $32,4,2,1$。 + +现在新加入一个字符串,变成了 $32,4,2,1,1$。 + +发现最后两个 $AC$ 自动机的大小相同,则暴力地把两个 $AC$ 自动机合并成一个:$32,4,2,2$ + +递归下去得到 $32,8$ 两个 $AC$ 自动机。 + +容易证明任意时刻最多有 $logn$ 个 $AC$ 自动机。 + +当查询时,在 $logn$ 个 $AC$ 自动机分别跑一遍,把答案求和。 + +时间复杂度:每个字符串最多只被合并进新 $AC$ 自动机 $logn$ 次,插入总时间复杂度是 $O(|\sum |s_i||logn)$ + +每次查询最多只有 $logn$ 个 $AC$ 自动机,查询总时间复杂度是 $O(|\sum t_i|logn)$ + +#### 删除 + +删除需要信息满足可减性,如题目中的统计次数。 + +建立一个新的二进制分组,把所有删除的字符串放在新的二进制分组里。 + +依旧统计出现的字符串,在答案中减去这部分。 + +如果是求最值等不可减的信息,就不能用这个方法了。 + +### 练习题 + +[P3309 [SDOI2014]向量集](https://www.luogu.com.cn/problem/P3309) \ No newline at end of file