(v4 更新)0x60 图论
0x61 图论 Misc
三元环计数、四元环计数
无向图三元环计数:
- 将所有点按照度数从小到大排序(度数相同时任意排序),将所有边按照排名小指向排名大的规则定向,形成一个 DAG。
- 此时所有三元环的边向情况,只有 \(x \to y\),\(y \to z\),\(x \to z\) 这一种情况。
- 枚举点 \(x\)。第一轮枚举 \(x\) 在 DAG 中的出边 \(z\),将 \(z\) 打上标记;第二轮枚举 \(x\) 在 DAG 中的出边 \(y\),再枚举 \(y\) 在 DAG 中的出边 \(z\)。如果 \(z\) 存在标记,则 \(x, y, z\) 构成三元环。
无向图三元环计数 时间复杂度:\(\mathcal{O}(m\sqrt{m})\)。
- 记 \(\mathrm{deg}_u\) 为原图中点 \(u\) 的度,\(\mathrm{out}_u\) 为 DAG 中点 \(u\) 的出度。可以证明 \(\mathrm{out}_u\) 为 \(\mathcal{O}(\sqrt{m})\) 级别。
- 若 \(\mathrm{deg}_u \leq \sqrt{m}\),则 \(\mathrm{out}_u \leq \sqrt{m}\)。
- 若 \(\mathrm{deg}_u > \sqrt{m}\),由于 \(u\) 在 DAG 中指向的点的度数大于 \(\sqrt{m}\),这样的点不超过 \(\sqrt{m}\) 个,故 \(\mathrm{out}_u \leq \sqrt{m}\)。
- 上述枚举的时间复杂度,相当于对 DAG 中每条边 \((u, v)\) 的 \(\mathrm{out}_v\) 求和。每条边的贡献为 \(\mathcal{O}(\sqrt{m})\),故时间复杂度为 \(\mathcal{O}(m\sqrt{m})\)。
无向图三元环计数的推论:无向图的三元环个数为 \(\mathcal{O}(m\sqrt{m})\) 级别。
0x61 无向图三元环计数.cpp:
// 无向图三元环计数
void ring3() {
std::vector<int> deg(n + 1);
for (auto [x, y] : edges) {
deg[x] ++, deg[y] ++;
}
std::vector< std::vector<int> > out(n + 1);
for (auto &[x, y] : edges) {
if (deg[x] > deg[y] || (deg[x] == deg[y] && x > y)) {
std::swap(x, y);
}
out[x].push_back(y);
}
std::vector<int> vis(n + 1);
for (int x = 1; x <= n; x ++) {
for (int z : out[x]) {
vis[z] = 1;
}
for (int y : out[x]) {
for (int z : out[y]) {
if (vis[z]) {
// 找到一个三元环 x, y, z
}
}
}
for (int z : out[x]) {
vis[z] = 0;
}
}
}
无向图四元环计数:
- 将所有点按照度数从小到大排序(度数相同时任意排序),将所有边按照排名小指向排名大的规则定向,形成一个 DAG。
- 我们认为一个四元环的贡献,仅在排名最大的那个点 \(z\) 处被统计到。此时所有四元环的边向情况,都可以认为是两条形如 \(x \leftrightarrow y, y \to z\) 的路径拼起来的情况(其中 \(x \leftrightarrow y\) 是原图中的边,\(y \to z\) 是 DAG 中的边,并且 \(z\) 是四个点中排名最大的点)。
- 枚举点 \(x\)。再枚举 \(x\) 在原图中的边 \(y\),再枚举 \(y\) 在 DAG 中的边 \(z\)(此时我们应该保证 \(\mathrm{rk}_x < \mathrm{rk}_z\)),则先前枚举的 \(x \leftrightarrow y, y \to z\) 路径均与当前枚举的路径构成四元环。
无向图四元环计数 时间复杂度:\(\mathcal{O}(m\sqrt{m})\),分析基本同无向图三元环计数。
0x61 无向图四元环计数.cpp:
// 无向图四元环计数
void ring4() {
std::vector<int> deg(n + 1);
for (auto [x, y] : edges) {
deg[x] ++, deg[y] ++;
}
std::vector< std::vector<int> > out(n + 1);
for (auto &[x, y] : edges) {
if (deg[x] > deg[y] || (deg[x] == deg[y] && x > y)) {
std::swap(x, y);
}
out[x].push_back(y);
}
i64 ans = 0;
std::vector<int> cnt(n + 1);
for (int x = 1; x <= n; x ++) {
for (int y : G[x]) {
for (int z : out[y]) {
if (deg[x] < deg[z] || (deg[x] == deg[z] && x < z)) {
// 找到一条 x<->y, y->z 的路径
ans += cnt[z] ++;
}
}
}
for (int y : G[x]) {
for (int z : out[y]) {
if (deg[x] < deg[z] || (deg[x] == deg[z] && x < z)) {
cnt[z] = 0;
}
}
}
}
}
有向图三(四)元环计数:转换为无向图三(四)元环计数,找到环时判断方向即可。
竞赛图三元环计数:记点 \(i\) 的出度为 \(\mathrm{out}_i\),则竞赛图的三元环个数为
\[\binom{n}{3} - \sum_{i = 1}^n \binom{\mathrm{out}_i}{2}
\]
Prufer 序列
Prufer 序列:一个包含 \(n - 2\) 个取值范围在 \([1, n]\) 中的正整数序列。可以理解为有标号完全图的生成树与数列的双射。
Prufer 序列构造:每次选择一个编号最小的叶子然后删掉它,并且在序列中记录与它相连的那个节点。重复 \(n - 2\) 次以后,只剩下两个节点时停止。
Prufer 序列性质:
- Prufer 序列构造完毕后会剩下两个节点,其中一个必定是编号最大的 \(n\)。而另一个则是 \(n\) 到 \(n - 1\) 路径上的第一个点。
- 每个节点在 Prufer 序列中的出现次数,等于其度数减 \(1\)。
Cayley 公式:对于包含 \(n\) 个点的完全图,共有 \(n^{n - 2}\) 棵生成树。
扩展 Cayley 公式 1:对于包含 \(n\) 个点 \(m\) 条边的森林,设其有 \(k\) 个连通块,大小分别为 \(s_1, s_2, \cdots, s_k\),则可以使得森林变成树的加边方式共有
\[n^{k - 2}\prod_{i = 1}^k s_i
\]
扩展 Cayley 公式 2:对于 \(n\) 个有标号节点,形成恰好包含 \(s\) 棵树的森林,如果实现指定了 \(s\) 个节点,并要求这 \(s\) 个节点两两不在同一棵树中,则满足该条件的森林方案总数共有
\[sn^{n - s - 1}
\]
泛滥河水将我冲向你的心头,不停流 ......

浙公网安备 33010602011771号