Boruvka
每一轮操作,对于每个点来说,让他和“最近的与他有连边且还未连通的点”相连。
最多 \(\log n\) 轮,每轮 \(O(n\cdot p)\),\(p\) 为找“最近的与他有连边且还未连通的点”的复杂度。
\(O(np\log n)\)
Kruskal 重构树
设从小到大加边,性质:
- 二叉树,\(2n-1\) 个点(数组开两倍)
- 原图的节点对应树中的叶子
- 若忽略叶子,重构树满足堆的性质,即父亲 \(\ge\) 儿子
- \(u,v\) 之间所有路径的“经过边权最大值”的最小值,是 \(u,v\) 在重构树上 LCA 的权值
- 原图 \(u\) 点,其只经过 \(\le w\) 的边能到达的点,为 \(u\) 在树上最高的权值 \(\le w\) 的祖先的子树内所有叶子,找到这个点可倍增。
他的性质比原图最小生成树的性质,要好得多。
同余最短路
问题:给定一些数 \(a_i\),求某范围内的数有多少数可用 \(a_i\) 完全背包得出。
若 \(r\) 能被表示出,则 \(r+k\cdot a_i\) 也能被表示出,故固定一个 \(a_t=m\) 作为模数,对 \(x\in[0..m-1]\) 算最小的能被凑出的 \(p\equiv x\pmod m\) 是多少。
建图:对所有 \(i\),\(x\to(x+a_i)\bmod m\) 连权值为 \(a_i\) 的边,跑最短路即可得出所求。
转圈做法:考虑模 \(m\) 意义下的完全背包,考虑新加入一个 \(a_i\),显然他在长度为 \(m\) 的环上形成 \(\gcd(a_i,m)=d\) 个子环,对每个子环分别顺序转移两圈即可;或者从子环上权值最小的点开始顺序转移一圈即可(写起来不如前者简洁)。\(O(nm)\)
个人觉得转圈做法更自然。
Johnson
多源最短路,支持负边,\(O(nm\log m)\)
考虑对每个点 \(i\) 赋 \(h_i\),令 \(w'(u,v)\gets w(u,v)+h_u-h_v\),求出新图 \(s\to t\) 最短路后减去 \(h_s-h_t\) 即可求出原图最短路。
列出条件:\(w(u,v)+h_u-h_v\ge 0\),即 \(h_u+w(u,v)\ge h_v\),发现令 \(h_u\) 为 \(d_u\) 即可。
故令 \(w(S,i)=0\),跑 SPFA(顺便判负环),改边权得新图后跑 \(n\) 次 Dijkstra 即可。
最短路树
若 \(v\) 的最短路是由 \((u,v)\) 松弛而来,则在生成树中保留 \((u,v)\)。
性质:树上 \(s\to v\) 的路径长度为 \(s\to v\) 的最短路。
最短路图
若 \((u,v,w)\) 满足 \(dis_u+w=dis_v\),则保留这条边。
相当于对每个点 \(v\) 保留图上所有 \(s\to v\) 的最短路。
性质:
- 按 \(dis\) 定向后是个 DAG
- 他的所有生成树都是最短路树
若 \(dis(s,u)+w+dis(v,t)=dis(s,t)\),则 \((u,v,w)\) 可以在 \(s\to t\) 的最短路上。
欧拉回路
看代码就懂了。
int tp, stk[N];
void Eul(int u) {
for (int &i = cur[u]; i < (int)G[u].size(); ) Eul(G[u][i++]);
stk[++tp] = u;
}
Eul(s);
while (tp) cout << stk[tp--] << ' ';
cout << '\n';
- 欧拉路径:一笔画
- 欧拉回路:一笔画,起点等于终点
- 欧拉图:存在欧拉回路
- 半欧拉图:存在欧拉路径,但不是欧拉图
欧拉图判定:
- 有向图:弱连通,入度等于出度
- 无向图:连通,每个点度数为偶数
半欧拉图判定:
- 有向图:弱连通,存在两个点入度分别等于出度加一、出度减一,其余点入度等于出度
- 无向图:连通,恰好两个点度数为奇数
连通性
\(\text{low}(u)\):\(u\) 子树内能一步走到的最小 dfn
- 边双:割边,每个点恰属于一个边双连通分量
- 点双:割点,每条边恰属于一个点双连通分量
Menger 定理:
- 边形式:使 \(u,v\) 不连通所需删去的边数最小值,等于 \(u,v\) 之间边不交的路径的数量的最大值。
- 点形式:使 \(u,v\)(不相邻)不连通所需删去的点数最小值,等于 \(u,v\) 之间点不交(不含 \(u,v\))的路径的数量的最大值。
证明可以看 Alex-Wei 的。这个定理与最大流-最小割定理类似。
于是我们可以定义:
- 局部边连通度:使 \(u,v\) 不连通所需删去的边数最小值;
- 局部点连通度:使 \(u,v\) 不连通所需删去的点数最小值(\(u,v\) 不相邻);
- \(k\)-边连通:\(u,v\) 边连通度 \(\ge k\);
- \(k\)-点连通:\(u,v\) 点连通度 \(\ge k\)。
圆方树
普通圆方树是只支持仙人掌的;这里默认的圆方树是广义圆方树。
建法:若 \(\text{low}(v)\ge\text{dfn}(u)\),说明 \(v\) 子树在栈内的部分与 \(u\) 形成了点双,新建方点并连边、弹栈直到 \(v\) 被弹出即可,注意两倍空间。
圆方树完整保留了原图的必经性。
支配树
对于有向图,若 \(s\to y\) 的所有路径均经过 \(x\),则称 \(x\) 支配 \(y\)。
由于支配关系具有
- 传递性:若 \(x\) 支配 \(y\)、\(y\) 支配 \(z\),则 \(x\) 支配 \(z\)。
- 自反性:\(x\) 支配 \(x\)。
- 反对称性:若 \(x\) 支配 \(y\),\(y\) 支配 \(x\),则 \(x=y\)。
这保证了支配是偏序关系,这让他能建出 DAG,但不够优秀。
性质:若 \(x,y\) 均支配 \(z\),则 \(x,y\) 之间也有支配关系。
注意到这样就可以建树了:考虑支配 \(y\) 的所有点 \(D_y=\{x_1,x_2,\ldots,x_k\}\),则 \(D_y\) 内所有元素形成了全序关系,即任意两个元素之间均可比较,所有 \(x_i\) 的关系可以用一条链描述,这就是 \(y\) 的祖先链。
令 \(f(i)\) 表示 \(D_i\) 当中被其他所有点支配的 \(x_j\),在 \(i\) 和 \(f(i)\) 之间连边,这就是支配树。
支配树用于描述有向图在给定起点时节点的必经性。
对于 DAG,我们可以 \(O((n+m)\log n)\) 求出其支配树:
按拓扑序处理,若 \(x\) 有入边 \(y_i\to x\),则
\[ f(x)=LCA(y_1,y_2,\ldots,y_k) \]动态更新倍增数组即可。
DAG 链剖分
若原图有若干无入度点,新建 \(1\) 向他们连边,这样只有 \(1\) 没有入度。
设 \(f(i)\) 为从 \(i\) 出发的路径条数。定义重儿子为 \(f\) 值最大的后继,这样对于任意轻边 \(i\to j\),\(2f(j)\le f(i)\)。这可以解决部分题目,但不够优秀,我们想要 \(i\to son(i)\) 形成的是链而不是树。
设 \(g(i)\) 为 \(1\to i\) 的路径条数。设 \(i\to j\) 为重边当且仅当 \(j\) 为 \(i\) 的 \(f\) 值最大的后继,且 \(i\) 为 \(j\) 的 \(g\) 值最大的前驱。这样形成了若干条链。走一条轻边,要么 \(f\) 减半,要么 \(g\) 翻倍,故一条路径上的轻边条数为 \(\log V\) 级别,\(V\) 为总路径条数。这说明只有当路径条数不大时 DAG 链剖分才适用。
DAG 链剖分常与 SAM 结合,因为此时路径条数为 \(s\) 的本质不同子串数,为 \(O(n^2)\) 级别,且剖分后可以快速定位字符串查询相关信息。更重要的是可以通过 \(\log\) 条时间戳连续的链描述 \(s[l,r_1]\) 到 \(s[l,r_2]\) 的路径,非常有用。
Dilworth 定理
一个偏序集的最大反链大小等于其最小链覆盖。
考虑一个长度为 \(n\) 的排列 \(p\)。以下两个结论至少有一个成立:
- 存在一个长度为 \(\sqrt n\) 的递增序列
- 存在一个长度为 \(\sqrt n\) 的递减序列
Hall 定理
设一个二分图的两部点数为 \((x,y),x<y\),其的一个完美匹配定义为左部 \(x\) 个点成为匹配点。
该二分图存在完美匹配的充要条件是,对于左部点的大小为 \(k\) 的任意子集 \(S\),这些点在右部点连到的点集(也被称为 \(S\) 的邻域,记为 \(N(S)\))大小不小于 \(k\)。
推论:一个任意二分图 \((V,E)\) 的最大匹配为
\[ |V|-\max_{S\subseteq V}\{|S|-|N(S)|\} \]习题会添加在这里面,附带简要题解。
题目选做
1. CF1965F Conference
Statement
二分图匹配,但是左边每个人连向的都是一段区间,\(\forall k\in[1..n_1]\),输出从右边点选任意长度为 \(k\) 的区间和左边所有人做匹配,匹配数能达到 \(k\) 的区间的个数,\(n_1,n_2\le2\cdot10^5\).
Solution
判断右边点单个区间 \([l,r]\),考虑 Hall 定理,设 \(N(S)\) 为右边点 \(S\) 对应的左边点集大小,\(S\) 合法当且仅当 \(|N(S)|\ge|S|\),那么 \([l,r]\) 合法当且仅当 \(\forall S\subseteq[l,r],S\) 合法.
猜测只用判断成一段区间的 \(S\),但是会被以下数据叉掉:
3
1 3
2 2
2 2
对于 \([1,3]\),只有 \(\{1,3\}\) 不合法.
于是考虑处理初始的左边点的线段,发现对于 \([a,b],[a,c](b\le c)\),把他们换成 \([a,b],[a+1,c]\) 不影响结果(\(a=b=c\) 时相当于删掉这条线段),因为对于 \(a\) 点,为了最优,他一定会选 \([a,b]\) 而非 \([a,c]\).
处理后每个人的左端点互不相同,此时找性质,发现:
- 可以只判成一段区间的 \(S\),因为若一个不合法的 \(S\) 不是一段连续的区间,考虑将其补全成一段区间,此时他还是不合法,因为每补全一个数,\(|N(S)|\) 至多增加 \(1\),此时有只包含这个数的区间.
- 看单调性,发现若 \([l,r]\) 不合法,\([l,r+1],[l-1,r]\) 也不合法,因为 \([l,r]\) 合法当且仅当 \(\forall S\subseteq[l,r],S\) 合法.
于是可以双指针求,对每个 \(l\) 最大的 \(r\) 使 \([l,r]\) 合法,最后差分统计即可.
Code
// LUOGU_RID: 160078999
// by:Joey_c / sto hunction orz
#include <bits/stdc++.h>
using namespace std;
#define rep(i,j,k) for(int i=j;i<=k;i++)
#define reo(i,j,k) for(int i=j;i>=k;i--)
#define debug fprintf(stderr,"Running %s on line %d...\n",__FUNCTION__,__LINE__)
#define fi first
#define se second
#define pb emplace_back
#define mp make_pair
typedef long long ll;
constexpr int N=2e5+10;
int main() {
ios::sync_with_stdio(false), cin.tie(nullptr);
int n,m=0,nn;cin>>n,nn=n;vector<pair<int,int>>a(n+1);vector<int>buc[N];rep(i,1,n)cin>>a[i].fi>>a[i].se,m=max(m,a[i].se),buc[a[i].fi].pb(a[i].se);
n=0;priority_queue<int,vector<int>,greater<int>>q;
rep(i,1,m){
for(int j:buc[i])q.push(j);while(!q.empty()&&q.top()<i)q.pop();if(!q.empty())a[++n]=mp(i,q.top()),q.pop();
}
vector<int>b(m+2,0),c(m+2,0),ans(max(m,nn)+2,0);rep(i,1,n)b[a[i].fi]=1,++c[a[i].se];
for(int s=0,l=m,r=m;l;--l){
s+=c[l];while(r>=l&&s<r-l+1)s-=b[r--];if(r<=m)++ans[r-l+1];
}reo(i,max(m,nn),1)ans[i]+=ans[i+1];rep(i,1,nn)cout<<ans[i]<<"\n";
return 0;
}