环计数问题是一类比较有意思的问题。
无向图三元环计数
先从最简单的三元环考虑起。
最暴力的做法是以每一个点为起点大力枚举,但这样每一个环会被算 6 次,时间复杂度也很高。下面介绍的是一种“优化过的暴力算法”。
首先给点定义大小关系,这里的大小关系定义为二元组 的大小关系,其中 表示编号为 的点的度。即如果 那么 。
按照这种大小关系可以构造一张 DAG,边 表示给定的无向图中 相连且按上述定义 。然后在这张图上找环。具体的,分为三步:
- 枚举点 。
- 枚举 在 DAG 上连接的所有点 ,将其标记上 。
- 标记完后枚举 连接的所有点 ,如果其已经被标记上 ,那么 构成一个环。
这种做法的原理在于,将无向图上的三元环 转换成为一个 DAG 上的结构 ,且通过定义某种“序”强制规定 比 都要小。这样只能通过枚举 统计这个环,且由于定了 的大小关系使得这个环只能被统计一次。
可以证明,这一算法的时间复杂度为 。
以本题为例,给出算法实现:
int n, m, du[100005] = {0};
int to[200005], nxt[200005], at[100005] = {0}, cnt = 0;
int to2[200005], nxt2[200005], at2[100005] = {0}, cnt2 = 0;
int mk[100005] = {0};
inline int cmp(int i, int j){
return (du[i] == du[j] ? i < j: du[i] < du[j]);
}
void init(){
// 构建 DAG 的过程
n = read(), m = read();
for (int i = 1; i <= m; ++i){
int u = read(), v = read();
to[++cnt] = v, nxt[cnt] = at[u], at[u] = cnt; // 只保存单向
++du[u], ++du[v];
}
for (int i = 1; i <= n; ++i)
for (int j = at[i]; j; j = nxt[j]){
if (cmp(i, to[j]))
to2[++cnt2] = to[j], nxt2[cnt2] = at2[i], at2[i] = cnt2;
else
to2[++cnt2] = i, nxt2[cnt2] = at2[to[j]], at2[to[j]] = cnt2;
}
}
void solve(){
// 正式求解
int ans = 0;
for (int i = 1; i <= n; ++i){
for (int j = at2[i]; j; j = nxt2[j])
mk[to2[j]] = i;
for (int j = at2[i]; j; j = nxt2[j]){
int v = to2[j];
for (int t = at2[v]; t; t = nxt2[t]){
if (mk[to2[t]] == i)
++ans;
}
}
}
printf("%d\n", ans);
}
个人猜测连边按 或者 复杂度都是对的,但不知道怎么证明。
无向图四元环计数
四元环计数可以套用三元环的思路,即也采用定序的方法。
假设我们希望找的四元环是 ,并且仍然要求这个环只能在对最小的 统计时被计算一次。那么在 DAG 上有 。然后会发现 和 的大小情况不明确,可以介于两者之间,也可以比两者都大或者小。这意味着应当对所有和 连接的无向边都予以考虑。
由此,我们可以使用和之前类似的方法,只不过这次相当于把路径拆成了 和 进行统计。需要一个辅助数组 记录以 为结尾的这样长度为 2 的路径的数目。
- 枚举点 。
- 枚举 在 DAG 上连接的所有点 ,枚举 在原图上连接的所有点 ,如果 小于 ,那么答案加上 ,然后令 自增 1。
- 对 统计完后把 清空。
可以证明,这一算法的时间复杂度为 。
以本题为例,示例代码如下:(这里边是按照 为 连的,如果按照 连接会 TLE)
int n, m;
int du[100005] = {0}, rk[100005];
int to[200005], nxt[200005], at[100005] = {0}, cnt = 0;
int to2[400005], nxt2[400005], at2[100005] = {0}, cnt2 = 0;
int pcnt[100005] = {0};
pair<int, int> pp[100005];
void init(){
n = read(), m = read();
for (int i = 1; i <= m; ++i){
int u = read(), v = read();
++du[u], ++du[v];
to2[++cnt2] = v, nxt2[cnt2] = at2[u], at2[u] = cnt2;
to2[++cnt2] = u, nxt2[cnt2] = at2[v], at2[v] = cnt2;
}
for (int i = 1; i <= n; ++i)
pp[i].first = -du[i], pp[i].second = -i; // 降序
sort(pp + 1, pp + n + 1);
for (int i = 1; i <= n; ++i)
rk[-pp[i].second] = i;
for (int i = 1; i <= n; ++i)
for (int j = at2[i]; j; j = nxt2[j])
if (rk[i] < rk[to2[j]])
to[++cnt] = to2[j], nxt[cnt] = at[i], at[i] = cnt;
}
void solve(){
ll ans = 0;
for (int i = 1; i <= n; ++i){
int id = -pp[i].second;
int v;
for (int j = at[id]; j; j = nxt[j]){
v = to[j];
for (int k = at2[v]; k; k = nxt2[k])
if (i < rk[to2[k]])
ans += pcnt[to2[k]]++;
}
for (int j = at[id]; j; j = nxt[j]){
v = to[j];
for (int k = at2[v]; k; k = nxt2[k])
pcnt[to2[k]] = 0;
}
}
printf("%lld\n", 8ll * ans);
}
有向图环计数
三元环和四元环和无向图类似,先按照无向图做,然后检查无向图中看到的环是不是在原有向图中有。
竞赛图三元环计数
可以用容斥。总共可能的三元环数目为 ,如果三个点无法构成三元环,那么必然有一个点指向另外两个点。
对每一个点 ,设其出度为 ,那么它作为这个指向两个点的点的方案数就是 。
于是答案就是 。
竞赛图三元环期望
如果竞赛图中,有的边会随机指定方向,应该如何处理?
先对每一个点算出确定的入度 和出度 ,那么没确定的边数为 。这些边可以和已有的出边一同取消一个环,也可以两两取消环。
因此答案为 。