环计数问题
普通环计数
解题思路
考虑状态压缩动态规划。记
对于状态
这样会把二元环(即重边)也算上,并且每个非二元环会被计算两次(因为固定起点可以向两个方向走),所以答案为
示例代码
#include <iostream>
using namespace std;
int n, m;
struct Edge {
int to, nxt;
} edge[500];
int cntEdge, head[20];
void addEdge(int u, int v) {
edge[++cntEdge] = {v, head[u]}, head[u] = cntEdge;
}
long long answer, dp[1 << 19][20];
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
addEdge(u, v);
addEdge(v, u);
}
for (int i = 1; i <= n; i++) dp[1 << (i - 1)][i] = 1;
for (int s = 1; s < (1 << n); s++)
for (int i = 1; i <= n; i++) {
if (!dp[s][i]) continue;
for (int j = head[i]; j; j = edge[j].nxt) {
int u = i, v = edge[j].to;
if ((s & -s) > (1 << (v - 1))) continue;
if (s & (1 << (v - 1))) {
if ((s & -s) == (1 << (v - 1))) answer += dp[s][u];
} else
dp[s | (1 << (v - 1))][v] += dp[s][u];
}
}
cout << (answer - m) / 2 << '\n';
return 0;
}
三元环计数
三元环 指的是一个简单图
首先给所有边定向。我们规定从度数小的点指向度数大的点,度数相同就从编号小的点指向编号大的点。那么此时此图是一张有向无环图(DAG)。
该图没有环的证明
反证法,假设存在环,那么环中的点度数一个比一个大,要形成环,所有点的度数必须相等,但是编号必定不同,矛盾。
所以定向后图肯定不存在环。
枚举
这个算法的时间复杂度为
时间复杂度证明
对于定向部分,遍历了所有的边,时间复杂度
对于每一对
若
若
总时间复杂度为
事实上,如果定向时从度数大的点指向度数小的点,复杂度也正确,只需要交换
示例代码(洛谷 P1989 无向图三元环计数)
#include <iostream>
using namespace std;
int n, m, total;
int deg[200001], u[200001], v[200001];
bool vis[200001];
struct Edge {
int to, nxt;
} edge[200001];
int cntEdge, head[100001];
void addEdge(int u, int v) {
edge[++cntEdge] = {v, head[u]}, head[u] = cntEdge;
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n >> m;
for (int i = 1; i <= m; i++) cin >> u[i] >> v[i], deg[u[i]]++, deg[v[i]]++;
for (int i = 1; i <= m; i++) {
if ((deg[u[i]] == deg[v[i]] && u[i] > v[i]) || deg[u[i]] < deg[v[i]])
swap(u[i], v[i]);
addEdge(u[i], v[i]);
}
for (int u = 1; u <= n; u++) {
for (int i = head[u]; i; i = edge[i].nxt) vis[edge[i].to] = true;
for (int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to;
for (int j = head[v]; j; j = edge[j].nxt) {
int w = edge[j].to;
if (vis[w]) total++;
}
}
for (int i = head[u]; i; i = edge[i].nxt) vis[edge[i].to] = false;
}
cout << total << '\n';
return 0;
}
例题 2
解题思路
这个图形是两个三元环共用了一条边形成的。所以我们先跑一遍三元环计数,统计出一条边上三元环的数量,然后枚举共用的那条边,设有
时间复杂度
示例代码
#include <cstring>
#include <iostream>
using namespace std;
int n, m, total;
int deg[200001], u[200001], v[200001];
int edgeId[200001], cnt[200001];
struct Edge {
int to, nxt;
} edge[200001];
int cntEdge, head[100001];
void addEdge(int u, int v) {
edge[++cntEdge] = {v, head[u]}, head[u] = cntEdge;
}
int main() {
while (cin >> n >> m) {
cntEdge = total = 0;
memset(deg, 0, sizeof deg);
memset(head, 0, sizeof head);
for (int i = 1; i <= m; i++) cin >> u[i] >> v[i], deg[u[i]]++, deg[v[i]]++;
for (int i = 1; i <= m; i++) {
if ((deg[u[i]] == deg[v[i]] && u[i] > v[i]) || deg[u[i]] < deg[v[i]])
swap(u[i], v[i]);
addEdge(u[i], v[i]);
}
for (int u = 1; u <= n; u++) {
for (int i = head[u]; i; i = edge[i].nxt) edgeId[edge[i].to] = i;
for (int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to;
for (int j = head[v]; j; j = edge[j].nxt) {
int w = edge[j].to;
if (edgeId[w]) cnt[i]++, cnt[j]++, cnt[edgeId[w]]++;
}
}
for (int i = head[u]; i; i = edge[i].nxt) edgeId[edge[i].to] = 0;
}
for (int i = 1; i <= m; i++) total += cnt[i] * (cnt[i] - 1) / 2, cnt[i] = 0;
cout << total << '\n';
}
return 0;
}
四元环计数
类似地,四元环 就是指四个点
考虑先对点进行排序。度数小的排在前面,度数大的排在后面。
考虑枚举排在最后面的点
注意到我们枚举的复杂度本质上与枚举三元环等价,所以时间复杂度也是
值得注意的是,
另外,度数相同的结点的排名将不相同,并且需要注意判断
示例代码(LibreOJ P191 无向图四元环计数)
#include <iostream>
#include <vector>
using namespace std;
int n, m, deg[100001], cnt[100001];
vector<int> E[100001], E1[100001];
long long total;
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
E[u].push_back(v);
E[v].push_back(u);
deg[u]++, deg[v]++;
}
for (int u = 1; u <= n; u++)
for (int v : E[u])
if (deg[u] > deg[v] || (deg[u] == deg[v] && u > v)) E1[u].push_back(v);
for (int a = 1; a <= n; a++) {
for (int b : E1[a])
for (int c : E[b]) {
if (deg[a] < deg[c] || (deg[a] == deg[c] && a <= c)) continue;
total += cnt[c]++;
}
for (int b : E1[a])
for (int c : E[b]) cnt[c] = 0;
}
cout << total << '\n';
return 0;
}
例题 3
解题思路
容易把情况分为五种:菊花图、四元环、三元环上一个点连出一条边、四个点构成的链中间一个点连出一条边以及五个点构成的链。
菊花图直接枚举点的度数,用组合数解决即可。四元环可以直接按照上述算法求得。三元环部分只需枚举三元环
下面考虑第四种情况。考虑枚举度数为
对于最后一种情况,先枚举中间的点
同样地,这其中有多算的部分。设
与 重合,但是 与 不重合时,等价于第三种情况; 与 重合,但是 与 不重合时,同样等价于第三种情况; 与 , 与 都重合时,等价于一个三元环; 与 重合时,等价于一个四元环(第二种情况)。
考虑到第三种情况中两个度数
于是我们就得出了所有情况的算法,时间复杂度为
示例代码
#include <iostream>
#include <vector>
using namespace std;
constexpr int mod = 1000000007;
constexpr long long power(long long a, long long n = mod - 2) {
long long res = 1;
while (n) {
if (n & 1) res = res * a % mod;
a = a * a % mod;
n >>= 1;
}
return res;
}
constexpr long long power24 = power(24), power2 = power(2);
int n, m;
vector<int> E[100001], E1[100001], E2[100001];
bool vis[100001];
int cnt1[100001], cnt2[100001];
long long solve1();
long long solve2();
long long solve3();
long long solve4();
long long solve5();
long long ans[6];
long long solve1() {
if (ans[1] != -1) return ans[1];
ans[1] = 0;
for (int i = 1; i <= n; i++) {
int x = E[i].size();
ans[1] +=
1ll * x * (x - 1) % mod * (x - 2) % mod * (x - 3) % mod * power24 % mod;
}
return ans[1] %= mod;
}
long long solve2() {
if (ans[2] != -1) return ans[2];
ans[2] = 0;
for (int i = 1; i <= n; i++) {
for (int j : E1[i])
for (int k : E1[j]) cnt1[k]++;
for (int j : E2[i])
for (int k : E1[j])
if (k != i) cnt2[k]++;
for (int j : E1[i])
for (int k : E1[j])
ans[2] +=
(1ll * cnt1[k] * (cnt1[k] - 1) / 2 + 1ll * cnt1[k] * cnt2[k]) % mod,
cnt1[k] = 0;
for (int j : E2[i])
for (int k : E1[j])
if (k != i)
ans[2] += 1ll * cnt2[k] * (cnt2[k] - 1) / 2 % mod * power2 % mod,
cnt2[k] = 0;
}
return ans[2];
}
long long solve3() {
if (ans[3] != -1) return ans[3];
ans[0] = ans[3] = 0;
for (int i = 1; i <= n; i++) {
for (int j : E1[i]) vis[j] = true;
for (int j : E1[i])
for (int k : E1[j])
if (vis[k])
ans[3] =
(1ll * ans[3] + E[i].size() + E[j].size() + E[k].size() - 6) %
mod,
ans[0]++;
for (int j : E1[i]) vis[j] = false;
}
return ans[3];
}
long long solve4() {
if (ans[4] != -1) return ans[4];
ans[4] = 0;
for (int i = 1; i <= n; i++)
for (int j : E[i])
(ans[4] += 1ll * (E[j].size() - 1) * (E[j].size() - 2) / 2 *
(E[i].size() - 1) % mod) %= mod;
return ans[4] = (ans[4] - 2 * solve3()) % mod;
}
long long solve5() {
if (ans[5] != -1) return ans[5];
ans[5] = 0;
for (int i = 1; i <= n; i++) {
long long sum = 0;
for (int j : E[i]) {
ans[5] += sum * (E[j].size() - 1) % mod;
sum += E[j].size() - 1;
}
}
solve3();
return ans[5] =
(ans[5] % mod - 2 * solve3() - 4 * solve2() - 3 * ans[0]) % mod;
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
int T;
cin >> T;
while (T--) {
ans[5] = ans[1] = ans[2] = ans[4] = ans[3] = -1;
cin >> n >> m;
for (int i = 1; i <= n; i++) E[i].clear(), E1[i].clear(), E2[i].clear();
while (m--) {
int x, y;
cin >> x >> y;
E[x].push_back(y), E[y].push_back(x);
}
for (int i = 1; i <= n; i++)
for (int j : E[i]) {
if (make_pair(E[i].size(), i) < make_pair(E[j].size(), j))
E1[i].push_back(j);
else
E2[i].push_back(j);
}
cout << ((solve5() + solve1() + solve2() + solve4() + solve3()) % mod +
mod) %
mod
<< '\n';
}
return 0;
}
习题
洛谷 P3547 [POI2013] CEN-Price List
CodeForces 985G Team Players(容斥原理)
创建日期: 2023年9月22日