题目链接
题目描述
在一个基因调控网络(有向图)中,寻找一种名为“协同调控模体”的结构数量。
一个协同调控模体由四个不同的基因 组成,它们满足以下激活关系:
- 主调节基因
能直接激活两个中间基因
和
(即存在边
和
)。
- 这两个中间基因
和
又都能直接激活同一个目标基因
(即存在边
和
)。
简单来说,该结构是一个“菱形”:。
解题思路
这是一个在有向图中对特定子图进行计数的问题。暴力枚举所有四个节点的组合 () 显然会超时,我们需要一个更高效的算法。
1. 问题转换
协同调控模体的结构核心是:存在一个主调节基因 和一个目标基因
,并且至少有两条从
到
的、经过不同中间基因的、长度为 2 的路径。
例如,路径 和
就构成了这样一个模体。如果有三条这样的路径(例如,通过
),则可以构成三个模体(
组合,
组合,
组合)。
2. 高效计数算法
基于上述观察,我们可以设计一个通过计算“长度为 2 的路径”来解决问题的算法。
-
核心思想: 遍历每一个基因,把它当作模体中的主调节基因
。然后,对于这个固定的
,我们去计算它能通过不同的中间基因形成多少个模体。
-
算法步骤:
- 预处理:首先,根据输入构建图的邻接表
adj,其中adj[u]存储了所有基因u能直接激活的基因列表。 - 主循环:遍历每一个基因
a(从 1 到),将其视为潜在的主调节基因
。
- 寻找两步路径:对于当前的基因
a,我们需要找到所有可以从它出发、走两步到达的基因。- 我们使用一个哈希表 (Map)
paths2_count来统计。paths2_count[d]的值将记录从a到d的长度为 2 的路径有多少条。 - 遍历
a的所有邻居b(即a -> b)。 - 再遍历
b的所有邻居d(即b -> d)。 - 每找到一条路径
a -> b -> d,我们就将paths2_count[d]的计数值加 1。
- 我们使用一个哈希表 (Map)
- 计算组合:当遍历完
a的所有两步可达路径后,我们检查paths2_count这个哈希表。- 对于表中的每一个条目
(d, k),其中k是paths2_count[d]的值。 - 这个
k意味着,从a到d存在条不同的长度为 2 的路径,它们分别经过
个不同的中间基因。
- 从这
个中间基因中任意选择两个,就可以与
a和d构成一个协同调控模体。选择的方法数是组合数。
- 对于表中的每一个条目
- 累加结果:将计算出的组合数累加到总数
total_motifs中。 - 循环与返回:当外层循环遍历完所有可能的基因
a后,total_motifs就是最终的答案。
- 预处理:首先,根据输入构建图的邻接表
-
细节:节点不同 题目要求四个基因
互不相同。在我们的算法中:
是通过组合数
保证的(从
个不同中间基因中选两个)。
等关系在没有自环的图中是天然成立的。
- 唯一需要注意的是
不能等于
。因此,在找到路径
a -> b -> d后,我们需要确保a != d才进行计数。
代码
#include <iostream>
#include <vector>
#include <map>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(NULL);
int n, m;
cin >> n >> m;
// 使用邻接表表示图
vector<vector<int>> adj(n + 1);
for (int i = 0; i < m; ++i) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
}
long long total_motifs = 0;
// 遍历每个基因作为主调节基因 A
for (int a = 1; a <= n; ++a) {
// paths2_count[d] 记录从 a 到 d 长度为2的路径数量
map<int, int> paths2_count;
// 遍历 A 的下游基因 B
for (int b : adj[a]) {
// 遍历 B 的下游基因 D
for (int d : adj[b]) {
// 确保 A 和 D 不同
if (a != d) {
paths2_count[d]++;
}
}
}
// 计算组合数
for (auto const& [d, k] : paths2_count) {
if (k >= 2) {
total_motifs += (long long)k * (k - 1) / 2;
}
}
}
cout << total_motifs << "\n";
return 0;
}
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
// 使用邻接表表示图
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i <= n; i++) {
adj.add(new ArrayList<>());
}
for (int i = 0; i < m; i++) {
int u = sc.nextInt();
int v = sc.nextInt();
adj.get(u).add(v);
}
long totalMotifs = 0;
// 遍历每个基因作为主调节基因 A
for (int a = 1; a <= n; a++) {
// paths2Count.get(d) 记录从 a 到 d 长度为2的路径数量
Map<Integer, Integer> paths2Count = new HashMap<>();
// 遍历 A 的下游基因 B
for (int b : adj.get(a)) {
// 遍历 B 的下游基因 D
for (int d : adj.get(b)) {
// 确保 A 和 D 不同
if (a != d) {
paths2Count.put(d, paths2Count.getOrDefault(d, 0) + 1);
}
}
}
// 计算组合数
for (int k : paths2Count.values()) {
if (k >= 2) {
totalMotifs += (long) k * (k - 1) / 2;
}
}
}
System.out.println(totalMotifs);
}
}
import collections
def main():
n, m = map(int, input().split())
# 使用邻接表表示图
adj = collections.defaultdict(list)
for _ in range(m):
u, v = map(int, input().split())
adj[u].append(v)
total_motifs = 0
# 遍历每个基因作为主调节基因 A
for a in range(1, n + 1):
# paths2_count[d] 记录从 a 到 d 长度为2的路径数量
paths2_count = collections.defaultdict(int)
# 遍历 A 的下游基因 B
for b in adj[a]:
# 遍历 B 的下游基因 D
for d in adj[b]:
# 确保 A 和 D 不同
if a != d:
paths2_count[d] += 1
# 计算组合数
for k in paths2_count.values():
if k >= 2:
total_motifs += k * (k - 1) // 2
print(total_motifs)
if __name__ == "__main__":
main()
算法及复杂度
- 算法: 图遍历, 哈希表计数
- 时间复杂度:
,其中
是边的集合,out-degree(
) 是节点
的出度。该复杂度的本质是遍历图中所有长度为 2 的路径,对于稀疏图而言,这远优于
。
- 空间复杂度:
,主要用于存储图的邻接表。在每次主循环中,哈希表的空间消耗最多为
。

京公网安备 11010502036488号