#P2329. 第3题-微服务群组
-
1000ms
Tried: 650
Accepted: 75
Difficulty: 7
所属公司 :
华为
时间 :2024年4月24日-暑期实习
-
算法标签>强连通分量
第3题-微服务群组
题面描述:
在这道题中,我们需要处理一个包含 n 个微服务的调用关系数组 edges,其中每个微服务通过数组值指向另一个微服务。我们的目标是识别所有的微服务环群组,计算每个环的内聚值 H = L - V(L 为环中微服务的数量,V 为可以访问该环的其他微服务数量),并根据 H 值进行排序(当 H 值相同时,按环中最大微服务编号排序)。最后,输出 H 值最大的环,确保输出顺序是从环中编号最小的微服务开始。输入包含一个整数 n 和一个长度为 n 的数组 edges,输出为满足条件的微服务编号,以空格分隔。
思路:BFS遍历
1,6,3组成了微服务群组(环) a1,L1值为3,编号为4、9的2个微服务可以访问到a1,因此√1值为2,H1为L1V1 =1;
0,2,10,5组成了微服务群组 (环) a2,L2值为4,编号为7、8、11的3个微服务可以访问到2,因此v2值为3,H2为L2-V2=1;
先对比H值,H1=H2,H值相等;
再对比环中序号最大值,a1中最大数为6。a2中最大数为10,a2排前面,因此输出答案为:0 2 10 5
题解
本题要求识别微服务调用中的环(强连通分量),并计算它们的内聚值。具体步骤如下:
-
输入数据结构:首先,我们需要一个整数
n表示微服务数量,以及一个数组edges,其中edges[i]表示微服务i调用的目标微服务。 -
图的表示:将微服务调用关系表示为有向图。每个微服务通过调用关系指向其他微服务。
-
强连通分量:利用 Tarjan 算法找出所有的强连通分量(SCC),每个强连通分量内的节点可以互相到达。
-
计算内聚值:
- 对于每个强连通分量,计算环内微服务数量 L 和可访问该环的其他微服务数量 V,从而得到内聚值 H = L - V。
- H 值越大,表示环的内聚性越强。
-
排序和输出:根据 H 值从大到小排序,若 H 值相等,则选择环中编号最大的微服务进行比较。最终输出 H 值最大的环,且按照环中最小编号的微服务开始输出。
代码
C++
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 10; // 最大微服务数量
int n, m; // n: 微服务数量, m: 边的数量(暂未使用)
int a[maxn]; // 存储微服务调用关系
int ver[maxn << 1], Next[maxn << 1], head[maxn], tot = 1; // 图的边信息
int low[maxn], dfn[maxn], num; // Tarjan算法中的访问标记和低点
int Stack[maxn], top, ins[maxn], c[maxn]; // Tarjan算法的栈和标记
int cnt, id, ans = INT_MIN; // cnt: 强连通分量数量, id: 当前最大H值的强连通分量编号
vector<int> scc[maxn]; // 存储所有强连通分量
// 添加边到邻接表
inline void add(int x, int y) {
ver[++tot] = y;
Next[tot] = head[x];
head[x] = tot;
}
// Tarjan算法实现
void tarjan(int x) {
dfn[x] = low[x] = ++num; // 设置访问时间和最低点
Stack[++top] = x; // 将当前节点入栈
ins[x] = 1; // 标记当前节点在栈中
for (int i = head[x]; i; i = Next[i]) {
int y = ver[i]; // 访问邻接点
if (!dfn[y]) { // 如果y未被访问
tarjan(y); // 递归访问
low[x] = min(low[x], low[y]); // 更新最低点
} else if (ins[y]) {
low[x] = min(low[x], dfn[y]); // 更新最低点
}
}
// 强连通分量的根节点
if (low[x] == dfn[x]) {
++cnt; // 新的强连通分量
int y;
do {
y = Stack[top--]; // 出栈
ins[y] = 0; // 标记出栈
c[y] = cnt; // 记录所属强连通分量
scc[cnt].push_back(y); // 存储强连通分量
} while (x != y); // 直到回到根节点
}
}
// 获取强连通分量中的最大编号
int getMax(int id) {
int res = 0;
for (int x : scc[id]) {
res = max(res, x); // 比较取最大
}
return res;
}
// 计算访问到达某个强连通分量的数量
int dfs(int x, int fa) {
int tmp = 0;
for (int i = head[x], y; i; i = Next[i]) {
y = ver[i];
if (y == fa) continue; // 不回到父节点
tmp += dfs(y, x); // 递归访问
}
return tmp + 1; // 返回访问到的节点数
}
int main() {
std::ios::sync_with_stdio(false); // 加速输入输出
cin >> n; // 输入微服务数量
// 构建图
for (int i = 0; i < n; ++i) {
cin >> a[i]; // 输入每个微服务的调用关系
add(i, a[i]); // 添加边
}
// 使用Tarjan算法求解强连通分量
for (int i = 1; i <= n; ++i) {
if (!dfn[i]) {
tarjan(i);
}
}
// 新建反图的构建
for (int i = 0; i < n; ++i) {
if (scc[c[i]].size() == 1) { // 该强连通分量只有一个点
add(c[a[i]] + n, c[i] + n); // 只有不在环中的点才需要建立边
}
}
// 遍历所有强连通分量
for (int i = 1; i <= cnt; ++i) {
if (scc[i].size() != 1) { // 说明为环
int V = dfs(i + n, -1) - 1; // 计算可访问的节点数量
int H = scc[i].size() - V; // 计算内聚值
if (H > ans) { // 更新最大内聚值
id = i;
ans = H;
} else if (H == ans && getMax(id) < getMax(i)) { // H相等时,比较最大编号
id = i;
}
}
}
// 找到该强连通分量中编号最小的点的位置
int pos = 0;
for (int i = 0; i < scc[id].size(); ++i) {
if (scc[id][i] < scc[id][pos]) {
pos = i; // 更新最小编号的位置
}
}
// 求scc时,编号按遍历顺序倒着加入进scc中,所以倒序输出
for (int i = pos; i >= 0; --i) {
cout << scc[id][i] << " "; // 输出环中节点
}
for (int i = scc[id].size() - 1; i > pos; --i) {
cout << scc[id][i] << " "; // 输出环中节点
}
return 0;
}
python
import sys
sys.setrecursionlimit(10**7)
from collections import deque
def main():
data = sys.stdin.read().split()
if not data:
return
it = iter(data)
n = int(next(it))
to = [int(next(it)) for _ in range(n)]
# 构建反向图用于后续计算 V
rev = [[] for _ in range(n)]
for u, v in enumerate(to):
rev[v].append(u)
# Tarjan 算法寻找所有强连通分量
dfn = [0] * n
low = [0] * n
in_stack = [False] * n
stack = []
idx = 0
comp_id = [0] * n
sccs = []
def tarjan(u):
nonlocal idx
idx += 1
dfn[u] = low[u] = idx
stack.append(u)
in_stack[u] = True
v = to[u]
if dfn[v] == 0:
tarjan(v)
low[u] = min(low[u], low[v])
elif in_stack[v]:
low[u] = min(low[u], dfn[v])
# 如果是 SCC 根节点
if low[u] == dfn[u]:
comp = []
while True:
x = stack.pop()
in_stack[x] = False
comp_id[x] = len(sccs)
comp.append(x)
if x == u:
break
sccs.append(comp)
for i in range(n):
if dfn[i] == 0:
tarjan(i)
# 计算每个非单点 SCC(环)的 H 值并选最好
best = None # (H, max_node, comp)
for comp in sccs:
if len(comp) <= 1:
continue # 单点不构成环
L = len(comp)
# 统计可访问该环的其他节点数 V
visited = set(comp)
dq = deque(comp)
V = 0
while dq:
u = dq.popleft()
for w in rev[u]:
if w not in visited:
visited.add(w)
dq.append(w)
V += 1
H = L - V
m = max(comp)
if best is None or (H, m) > (best[0], best[1]):
best = (H, m, comp)
# 输出最佳环,从最小编号开始,按调用顺序
_, _, comp = best
start = min(comp)
order = [start]
nxt = to[start]
while nxt != start:
order.append(nxt)
nxt = to[nxt]
print(" ".join(map(str, order)))
if __name__ == "__main__":
main()
Java
import java.util.*;
public class Main {
static int n;
static int[] edges;
static int[] nums; // 每个节点的子节点数目
static int[] inDegree;
public static void main(String[] args) {
// Input
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
edges = new int[n]; // 存储每个节点指向的下一个节点的数组
inDegree = new int[n]; // 存储每个节点的入度数组
nums = new int[n]; // 每个节点的子节点数目
// 读取每个节点的下一个节点,并更新入度数组
for (int i = 0; i < n; ++i) {
edges[i] = sc.nextInt(); // 读取每个节点的下一个节点
inDegree[edges[i]]++;
}
Queue<Integer> q = new LinkedList<>(); // 创建队列以存储所有入度为0的节点
// 将所有入度为0的节点加入队列
for (int i = 0; i < n; ++i) {
if (inDegree[i] == 0) {
q.offer(i);
}
}
// BFS遍历
while (!q.isEmpty()) {
int qSize = q.size();
for (int i = 0; i < qSize; i++) {
int curIdx = q.poll();
int curNextIdx = edges[curIdx];
inDegree[curNextIdx]--; // 当前节点指向的结点的入度减1
// nums 存储每个节点的子节点数目
nums[curNextIdx] += nums[curIdx] + 1;
if (inDegree[curNextIdx] == 0) {
q.offer(curNextIdx);
}
}
}
List<List<Integer>> circles = new ArrayList<>(); // 存储所有环的列表
List<Integer> hVal = new ArrayList<>(); // 存储每个环的内聚值H = L(环内节点数量) - V(指向环的节点数量)
List<Integer> maxIdxInCircles = new ArrayList<>(); // 存储每个环中的最大节点索引
// 找环
for (int i = 0; i < n; i++) {
if (inDegree[i] == 0) {
continue;
}
int curIdx = i, v = 0, maxIdx = i;
List<Integer> curCircle = new ArrayList<>();
while (inDegree[curIdx] > 0) {
// nums 存储每个节点的子节点数目
v += nums[curIdx];
curCircle.add(curIdx);
inDegree[curIdx] = 0;
curIdx = edges[curIdx];
maxIdx = Math.max(maxIdx, curIdx);
}
circles.add(curCircle);
maxIdxInCircles.add(maxIdx);
// 计算内聚值,并加入列表
// 每个环的内聚值H = L(环内节点数量) - V(指向环的节点数量)
hVal.add(curCircle.size() - v);
}
Integer[] idx = new Integer[hVal.size()]; // 创建索引数组
for (int i = 0; i < idx.length; ++i) idx[i] = i; // 初始化索引数组
// 对索引数组进行排序,优先根据环的内聚值排序,其次看最大节点索引
Arrays.sort(idx, (a, b) -> {
int valCompare = hVal.get(b).compareTo(hVal.get(a));
if (valCompare != 0) return valCompare;
return Integer.compare(maxIdxInCircles.get(b), maxIdxInCircles.get(a));
});
List<Integer> resCircle = circles.get(idx[0]);
int startIdx = Collections.min(resCircle);
for (int i = 0; i < resCircle.size(); i++) {
System.out.print(startIdx);
startIdx = edges[startIdx];
if (i != resCircle.size() - 1) {
System.out.print(" ");
}
}
}
}
javascript
const rl = require("readline").createInterface({ input: process.stdin });
var iter = rl[Symbol.asyncIterator]();
const readline = async () => (await iter.next()).value;
void (async function () {
let n = parseInt(await readline()); // 读取节点数量
let edges = new Array(n).fill(0); // 存储每个节点指向的下一个节点
let inDegree = new Array(n).fill(0); // 存储每个节点的入度
let nums = new Array(n).fill(0); // 统计每个节点的子节点数目
// 读取边,并更新入度数组
let input = (await readline()).split(" ").map(Number);
for (let i = 0; i < n; i++) {
edges[i] = input[i];
inDegree[edges[i]]++; // 统计入度
}
let queue = []; // BFS 处理入度为 0 的节点
// 将所有入度为 0 的节点加入队列
for (let i = 0; i < n; i++) {
if (inDegree[i] === 0) queue.push(i);
}
// BFS 遍历
while (queue.length > 0) {
let curIdx = queue.shift();
let curNextIdx = edges[curIdx];
inDegree[curNextIdx]--; // 当前节点指向的结点的入度减 1
nums[curNextIdx] += nums[curIdx] + 1; // 更新子节点数
if (inDegree[curNextIdx] === 0) queue.push(curNextIdx);
}
let circles = []; // 存储所有环的列表
let hVal = []; // 存储每个环的内聚值 H = L(环内节点数量) - V(指向环的节点数量)
let maxIdxInCircles = []; // 存储每个环的最大节点索引
// 找环
for (let i = 0; i < n; i++) {
if (inDegree[i] === 0) continue;
let curIdx = i, v = 0, maxIdx = i;
let curCircle = [];
while (inDegree[curIdx] > 0) {
v += nums[curIdx];
curCircle.push(curIdx);
inDegree[curIdx] = 0; // 标记为访问过
maxIdx = Math.max(maxIdx, curIdx);
curIdx = edges[curIdx];
}
circles.push(curCircle);
maxIdxInCircles.push(maxIdx);
hVal.push(curCircle.length - v); // 计算内聚值
}
// 创建索引数组,并排序
let idx = Array.from({ length: hVal.length }, (_, i) => i);
idx.sort((a, b) => {
if (hVal[b] !== hVal[a]) return hVal[b] - hVal[a]; // 按 H 值降序
return maxIdxInCircles[b] - maxIdxInCircles[a]; // 按最大节点索引降序
});
let resCircle = circles[idx[0]]; // 选择排序后第一个环
// 输出环的最小起始节点
let startIdx = Math.min(...resCircle);
let result = [];
for (let i = 0; i < resCircle.length; i++) {
result.push(startIdx);
startIdx = edges[startIdx];
}
console.log(result.join(" "));
rl.close();
})();
题目描述
上期我们聊到,你为了帮助小明维护云服务器而大打出手。如今,云服务器已经平稳运行并给用户们提供了一系列不错的服务,但是小明仍花了很多时间在运维和数据分析上。
更形式的,小明为了调研微服务调用情况,对n个微服务调用数据进行了采集分析,微服务使用数字0至n−1进行编号,给你一个下标从0开始的数组edges,其中edges[i]表示存在一条从微服务i到微服务edges[i]的接口调用。
为了更好的维护,小明将形成1个环的多个微服务称为微服务群组,一个微服务群组的所有微服务数量为L,能够访问到该微服务群组的微服务数量为V,这个微服务群组的内聚值H=L−V。
已知提供的数据中有1个或多个微服务群组,请按照内聚值H的结果从大到小的顺序对所有微服务群组(H相等时,取环中最大的数进行比较)排序,输出排在第一的做服务群组,输出时每个微服务群组输出的起始编号为环中最小的数。
输入描述
第一行输入n,表示有n个微服务
第二行为数组edges,其中edges[i]表示存在一条从微服务i到微服务edges[i]的接口调用,数字以空格分隔。
数据范围:
n == edges.length
2≤n≤105
0≤edges[i]≤n−1
输入保证edges[i]=i
输出描述
输出排在第一的微服务群组的编号数组,按照环的访问顺序输出,起始编号为环中最小的数,数字以空格分隔。
样例一
输入
4
3 3 0 2
输出
0 3 2
解释
0,3,2组成了微服务群组 (环)a,他的L值为3,对于a来说,只有编号为1的1个微服务可以访问到a,因此a的为1答案输出微服务群组为0 3 2
样例二
输入
12
2 6 10 1 6 0 3 0 5 4 5 8
输出
0 2 10 5
解释
1,6,3组成了微服务群组(环) a1,L1值为3,编号为4、9的2个微服务可以访问到a1,因此√1值为2,H1为L1V1 =1;
0,2,10,5组成了微服务群组 (环) a2,L2值为4,编号为7、8、11的3个微服务可以访问到2,因此v2值为3,H2为L2-V2=1;
先对比H值,H1=H2,H值相等;
再对比环中序号最大值,a1中最大数为6。a2中最大数为10,a2排前面,因此输出答案为:0 2 10 5
Limitation
1s, 1024KiB for each test case.
Related
In following contests: