三元环计数和四元环计数问题
- 无向图三元环计数问题
- 根号分治
- bitset
- 无向图四元环计数问题
- 有向图三四元环计数问题
无向图三元环计数问题
根号分治
记 di:id_i:idi:i 在原图中的度数。
按照以下规则改写无向图为有向图:
- 对于一条边 u,vu,vu,v。
- 如果两点度数不同,则度数大的指向度数小的。
- 如果两点度数相同,则编号小的指向编号大的。
「其实只需要定一个有序的规则即可,可以是度数小的指向大的,可以是编号大的指向小的,都不影响。」
显然,这个有向图同时也是个无环图。
证明:
假设存在一个环 (x0,x1,x2,...,xk)(x_0,x_1,x_2,...,x_k)(x0,x1,x2,...,xk)。
则有 dx0≥dx1≥dx2≥...≥dxkd_{x_0}\ge d_{x_1}\ge d_{x_2}\ge ...\ge d_{x_k}dx0≥dx1≥dx2≥...≥dxk。
即 dx0=...=dxkd_{x_0}=...=d_{x_k}dx0=...=dxk。
所以 x0<x1<x2<...<xk<x0x_0<x_1<x_2<...<x_k<x_0x0<x1<x2<...<xk<x0,矛盾。
所以三元环 (u,v,w)(u,v,w)(u,v,w) 在新图中的边一定是:u→v,u→w,v→wu\rightarrow v,u\rightarrow w,v\rightarrow wu→v,u→w,v→w。
考虑在 uuu 点记录这个三元环的贡献。
将所有与 uuu 直接相连的点打上标记,再从中枚举点作 vvv ,枚举与 vvv 直接相连的点 www ,检查 www 是不是 uuu 指向的点即可。
时间复杂度为 O(mm)O(m\sqrt{m})O(mm)。
证明:
将枚举 www 的复杂度记在 u→vu\rightarrow vu→v 边上,显然为 outvout_voutv 。「outv:vout_v:voutv:v 的出度」
则总复杂度为 ∑(u,v)∈Eoutv+∑uoutu\sum_{(u,v)\in E}out_v+\sum_{u}out_u∑(u,v)∈Eoutv+∑uoutu
若 outv<mout_v<\sqrt{m}outv<m,则复杂度为 O(mm)O(m\sqrt{m})O(mm)。
若 outv>mout_v>\sqrt{m}outv>m,因为有向图的连边规则,有 outu≥outv>mout_u\ge out_v>\sqrt{m}outu≥outv>m。
原无向图的总边数为 mmm,所以这样的 (u,v)(u,v)(u,v) 仅有 m\sqrt{m}m 个,复杂度为 OmO\sqrt{m}Om。
#include <bits/stdc++.h>
using namespace std;
#define maxn 100005
#define maxm 200005
vector < int > G[maxn];
int n, m;
int d[maxn], Eu[maxm], Ev[maxm], vis[maxn];bool cmp( int x, int y ) {return d[x] == d[y] ? x < y : d[x] > d[y];
}int main() {scanf( "%d %d", &n, &m );for( int i = 1;i <= m;i ++ ) {scanf( "%d %d", &Eu[i], &Ev[i] );d[Eu[i]] ++, d[Ev[i]] ++;}for( int i = 1;i <= m;i ++ )if( cmp( Eu[i], Ev[i] ) ) G[Eu[i]].push_back( Ev[i] );else G[Ev[i]].push_back( Eu[i] );int ans = 0;for( int i = 1;i <= n;i ++ ) {for( int j : G[i] ) vis[j] = i;for( int j : G[i] )for( int k : G[j] ) if( vis[k] == i ) ans ++;}printf( "%d\n", ans );return 0;
}
bitset
bitset
其实是一种二进制的优化。
只要想要的信息可以转化为被 0/10/10/1 表示出来,在空间不炸的情况下,bitset
自带 /ω/\omega/ω 的复杂度。
将与 iii 点直接相连的所有点对应在 iii 的 bitset
中位置置为 111,表示两点有边相连。
然后就直接枚举每条边,将边的两个端点的 bitset
取个 &
,就得到了与两点均直接相连的所有点集合。
空间复杂度为 O(n2)O(n^2)O(n2) ,来自 bitset
。
时间复杂度为 O(nmω)O(\frac{nm}{\omega})O(ωnm)。
如果有些题目空间比较卡,不能直接硬开。
其实是给每个点很大一段的 000 开完整 bitset
空间就会被大幅度浪费。
那么就可以设定一个阈值 BBB「分块的阈值」。
对于度数 >B>B>B 的点就开完整的 bitset
,这种点不会超过 min(n,mB)\min(n, \frac{m}{B})min(n,Bm) 个。
度数 ≤B\le B≤B 的点就直接暴力枚举连边,暴力判断。
所有度数 >B>B>B 的点最多会被 O(m)O(m)O(m) 条边枚举到,一次计算是 O(mω)O(\frac{m}{\omega})O(ωm),时间复杂度为 O(nmω)O(\frac{nm}{\omega})O(ωnm)。
度数 ≤B\le B≤B 的点的贡献是 d2d^2d2,则有 ∑d≤m,d≤B\sum_d\le m,d\le B∑d≤m,d≤B,总共为 ∑d2≤B⋅m\sum d^2\le B·m∑d2≤B⋅m。
时间复杂度为 O(nmω+B⋅m)O(\frac{nm}{\omega}+B·m)O(ωnm+B⋅m),空间复杂度就降为了 O(nmB)O(\frac{nm}{B})O(Bnm)。
「根据具体题目的时空灵活选取阈值 BBB,在不甚了解三元环的根号分治时,bitset
这种优美的暴力来的快也好写/doge」
无向图四元环计数问题
同样按照三元环计数问题的方法来建立有向图,并给每一个点分配唯一的 rankrankrank 排名。
则一个四元环在新图中至少有一个,至多有两个度数为 222 的点。
这时会发现原图的四元环 (u,v,w,x)(u,v,w,x)(u,v,w,x) 在新建的有向图中会有两种情况。
- u→v→w,u→x→wu\rightarrow v\rightarrow w,u\rightarrow x\rightarrow wu→v→w,u→x→w
- u→v→w→x,u→xu\rightarrow v\rightarrow w\rightarrow x,u\rightarrow xu→v→w→x,u→x
发现这两种形式都可以由 两条无向边+两条有向边 形式表示。
所以一个四元环唯一的表示为:u−v→w,u−x→wu-v\rightarrow w,u-x\rightarrow wu−v→w,u−x→w,且 rank(u)rank(u)rank(u) 最小,rank(w)rank(w)rank(w) 最大,这样才能保证一个四元环不被反复计算。
枚举点 uuu,枚举其在原图的出边 vvv,枚举 vvv 的在新图上所有出边 www,且要满足 rank(u)<rank(v)<rank(w)rank(u)<rank(v)<rank(w)rank(u)<rank(v)<rank(w)。
先 ans←cntwans\leftarrow cnt_wans←cntw,再 cntw++cnt_w++cntw++,最后 uuu 更换的时候,所有 cntcntcnt 都要清零。
在上述过程中,cntw:wcnt_w:wcntw:w 的入度,且 rank(w)rank(w)rank(w) 最大 rank(u)rank(u)rank(u) 最小,那么当有两组 (u,v1,w),(u,v2,w)(u,v_1,w),(u,v_2,w)(u,v1,w),(u,v2,w) 的时候代表找到了一个四元环。
虽然环有对称性,但这样处理后一个四元环只会从 uuu 算到 www 上。
时间复杂度为 O(mm)O(m\sqrt{m})O(mm)。证明与三元环一样。
#include <bits/stdc++.h>
using namespace std;
#define maxn 100005
vector < int > G[maxn], E[maxn];
int n, m;
int d[maxn], id[maxn], rnk[maxn], cnt[maxn];int main() {scanf( "%d %d", &n, &m );for( int i = 1, u, v;i <= m;i ++ ) {scanf( "%d %d", &u, &v );E[u].push_back( v );E[v].push_back( u );d[u] ++, d[v] ++, id[i] = i;}sort( id + 1, id + n + 1, []( int x, int y ) { return d[x] == d[y] ? x < y : d[x] > d[y]; } );for( int i = 1;i <= n;i ++ ) rnk[id[i]] = i;for( int i = 1;i <= n;i ++ ) for( int j : E[i] ) if( rnk[j] > rnk[i] ) G[i].push_back( j );int ans = 0;for( int i = 1;i <= n;i ++ ) {for( int j : E[i] ) for( int k : G[j] ) if( rnk[k] > rnk[i] ) ans += cnt[k], cnt[k] ++;for( int j : E[i] ) for( int k : G[j] ) cnt[k] = 0;} printf( "%d\n", ans );return 0;
}
有向图三四元环计数问题
按照无向图的方法做,求出一个元环后直接随便选一个点开始左右两个方向都走一圈,看能不能回到开始点,就知道无向图上的元环能否用原有向图上的边构造出来。