
专栏导读
本专栏收录于《华为OD机试真题(Python/JS/C/C++)》。
刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新。
一、题目描述
在软件项目开发里涉及不同版本组件引用,组件依赖组件,形成一棵依赖树(从根节点深层展开);请编写一段代码,对当前项目的组件依赖关系进行分析,按项目组件依赖顺序输出最终有效的组件版本列表;
说明如下:
- 从根节点开始按依赖顺序输出组件列表,项目上层组件依赖先输出,如果同层依赖则父组件先出现的优先输出,父组件相同的按依赖出现顺序输出,如:项目依赖组件【b,a】,a依赖【c,d】,b依赖【e】,输出顺序为 b,a,e,c,d;
- 组件去重:依赖组件如多次出现,只保留第一次出现的顺序位置;
- 同一组件如有不同版本,以首次依赖的组件版本作为输出;
二、输入描述
-
参数1:项目直属依赖
directDeps,格式:组件名:版本号:排除组件列表,如:a:v1:表示项目依赖a组件,版本为v1,无排除组件; -
参数2:全局组件依赖
depRules,格式:组件名:依赖组件名:依赖组件版本号:排除组件列表",如:a:b:v2:c,表示a组件依赖b组件v2版本,须排除b组件依赖的c组件子树;
输入列表顺序为依赖声明顺序;组件名、版本号由字母数字组成,非空;输入保证同一组件不同版本依赖相同;排除组件列表以半角逗号分隔,格式如:“a,b”,排除列表可为空,输入保证排除列表不会出现",b"或"a,"格式;
三、输出描述
符合依赖顺序要求的项目依赖组件列表,格式:组件名:版本号
四、测试用例
测试用例1:
1、输入
a:v1:
2、输出
a:v1
3、说明
没有任何间接依赖,所以直接输出 a:v1。
测试用例2:
1、输入
a:v1: b:v1:
a:c:v1: b:d:v1: d:c:v2:
2、输出
a:v1 b:v1 c:v1 d:v1
3、说明
第1层:
a:v1 b:v1
第2层:
a -> c:v1
b -> d:v1
得到:a b c d
之后:d -> c:v2
但 c 已经以 v1 第一次出现,所以忽略 c:v2。
五、解题思路
本题的核心是按照依赖树的层级顺序输出组件,因此采用 BFS(广度优先搜索) 遍历依赖关系,而不是 DFS。先把项目的直属依赖按输入顺序加入队列,再逐层处理每个组件的子依赖,这样可以保证“上层组件优先、同层按父组件出现顺序、同一父组件按依赖声明顺序”这一输出要求。
主要采用了以下数据结构:
邻接表 Map<String, List>:保存每个组件对应的依赖组件列表。List 可以保留依赖声明的原始顺序,适合表示组件依赖关系。
队列 Queue:用于 BFS 层序遍历,保证先处理上层组件,再处理下一层组件。
LinkedHashMap:保存组件第一次出现时的版本及顺序。它既可以实现组件去重,又能保留第一次插入顺序,因此可以满足“同一组件只保留第一次出现的位置和版本”的要求。
HashSet:保存当前依赖路径上生效的排除组件,查询某组件是否被排除的效率较高。
算法处理时,每遇到一个新的组件,先判断它是否被当前依赖路径排除;如果没有被排除,再判断是否已经出现过。只有第一次出现的组件才加入结果和 BFS 队列,并将当前依赖边上的排除项合并后继续向其子树传递。
整体上可以理解为:
构建依赖邻接表
↓
直属依赖按顺序加入 BFS 队列
↓
逐层遍历依赖关系
↓
排除当前路径不允许的组件
↓
LinkedHashMap 去重并保留首次版本
↓
按首次出现顺序输出
在不考虑排除集合复制开销的情况下,主体遍历的时间复杂度约为 O(V + E),其中 V 是组件数量,E 是依赖关系数量;空间复杂度约为 O(V + E)。
六、Python算法源码
from collections import defaultdict, deque
import sys
def parse_exclusions(text):
"""
把逗号分隔的排除列表转换成集合。
"" -> set()
"a,b" -> {"a", "b"}
"""
return set(text.split(',')) if text else set()
def main():
lines = sys.stdin.read().splitlines()
# 第一行直属依赖,第二行全局依赖规则
direct_line = lines[0].strip() if len(lines) >= 1 else ""
rule_line = lines[1].strip() if len(lines) >= 2 else ""
# rules[parent] 按声明顺序保存子依赖
rules = defaultdict(list)
if rule_line:
for item in rule_line.split():
parts = item.split(':')
parent = parts[0]
child = parts[1]
version = parts[2]
exclusions = parse_exclusions(
parts[3] if len(parts) > 3 else ""
)
rules[parent].append(
(child, version, exclusions)
)
"""
Python 3.7+ 的 dict 保证插入顺序。
所以 selected 同时负责:
1. 全局组件去重;
2. 保存第一次出现的版本;
3. 保存最终输出顺序。
"""
selected = {}
# BFS 队列
queue = deque()
if direct_line:
for item in direct_line.split():
parts = item.split(':')
name = parts[0]
version = parts[1]
exclusions = parse_exclusions(
parts[2] if len(parts) > 2 else ""
)
if name not in selected:
selected[name] = version
queue.append(
(name, exclusions)
)
"""
BFS 保证:
上层组件优先;
同层按照父组件出现顺序;
同父组件按照依赖声明顺序。
"""
while queue:
parent, active_exclusions = queue.popleft()
for child, version, edge_exclusions in rules.get(parent, []):
# 当前依赖路径已经排除 child,则整个 child 子树跳过
if child in active_exclusions:
continue
# 第一次出现的版本和位置已经确定
if child in selected:
continue
selected[child] = version
# 将当前依赖边新增的排除项继续传递到 child 子树
child_exclusions = (
active_exclusions | edge_exclusions
)
queue.append(
(child, child_exclusions)
)
print(
' '.join(
f'{name}:{version}'
for name, version in selected.items()
)
)
if __name__ == '__main__':
main()
七、JavaScript算法源码
const fs = require('fs');
function parseExclusions(text) {
return text
? new Set(text.split(','))
: new Set();
}
const lines = fs
.readFileSync(0, 'utf8')
.split(/\r?\n/);
const directLine = (lines[0] || '').trim();
const ruleLine = (lines[1] || '').trim();
/*
* rules:
*
* parent -> [
* { child, version, exclusions },
* ...
* ]
*
* 数组能够保证同一父组件的依赖声明顺序。
*/
const rules = new Map();
if (ruleLine) {
for (const item of ruleLine.split(/\s+/)) {
const parts = item.split(':');
const parent = parts[0];
const child = parts[1];
const version = parts[2];
const exclusions =
parseExclusions(parts[3] || '');
if (!rules.has(parent)) {
rules.set(parent, []);
}
rules.get(parent).push({
child,
version,
exclusions
});
}
}
/*
* JavaScript 的 Map 保证插入顺序。
*
* selected 同时负责:
* 1. 去重;
* 2. 保存第一次出现版本;
* 3. 保存输出顺序。
*/
const selected = new Map();
/*
* 使用数组 + head 实现队列。
*
* 不直接使用 shift(),
* 避免频繁移动数组元素造成额外性能开销。
*/
const queue = [];
let head = 0;
if (directLine) {
for (const item of directLine.split(/\s+/)) {
const parts = item.split(':');
const name = parts[0];
const version = parts[1];
const exclusions =
parseExclusions(parts[2] || '');
if (!selected.has(name)) {
selected.set(name, version);
queue.push({
name,
activeExclusions: exclusions
});
}
}
}
// BFS 分层遍历依赖关系
while (head < queue.length) {
const {
name: parent,
activeExclusions
} = queue[head++];
const children =
rules.get(parent) || [];
for (const edge of children) {
/*
* 当前路径已经排除该组件,
* 则整个组件子树都不能进入。
*/
if (activeExclusions.has(edge.child)) {
continue;
}
/*
* 全局去重:
* 第一次出现决定最终位置和版本。
*/
if (selected.has(edge.child)) {
continue;
}
selected.set(
edge.child,
edge.version
);
/*
* 子节点继承祖先的排除规则,
* 并加入当前依赖边新增的排除规则。
*/
const childExclusions =
new Set(activeExclusions);
for (const x of edge.exclusions) {
childExclusions.add(x);
}
queue.push({
name: edge.child,
activeExclusions: childExclusions
});
}
}
const result = [];
for (const [name, version] of selected) {
result.push(`${name}:${version}`);
}
console.log(result.join(' '));
八、C算法源码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
typedef struct {
char **data;
int size;
int cap;
} StrSet;
typedef struct {
char *child;
char *version;
StrSet exclusions;
} Edge;
typedef struct {
char *parent;
Edge *edges;
int size;
int cap;
} RuleGroup;
typedef struct {
char *name;
char *version;
} Selected;
typedef struct {
char *name;
// 当前依赖路径累计生效的排除组件
StrSet activeExclusions;
} Node;
/* 自己实现字符串复制,避免依赖非标准 strdup。 */
static char *dupStr(const char *s) {
size_t n = strlen(s) + 1;
char *p = (char *) malloc(n);
if (p != NULL) {
memcpy(p, s, n);
}
return p;
}
/*
* 标准 C 动态读取一整行。
* 不依赖 POSIX getline。
*/
static char *readLine(void) {
int cap = 128;
int len = 0;
char *s =
(char *) malloc(
(size_t) cap
);
int ch;
if (s == NULL) {
exit(1);
}
while ((ch = getchar()) != EOF
&& ch != '\n') {
if (ch == '\r') {
continue;
}
if (len + 1 >= cap) {
cap *= 2;
s = (char *) realloc(
s,
(size_t) cap
);
if (s == NULL) {
exit(1);
}
}
s[len++] = (char) ch;
}
s[len] = '\0';
return s;
}
/*
* 获取当前行中的下一个空白分隔字符串。
*
* 例如:
* a:v1: b:v1:
*
* 依次取得:
* a:v1:
* b:v1:
*/
static char *nextToken(char **cursor) {
char *p = *cursor;
while (*p
&& isspace(
(unsigned char) *p
)) {
++p;
}
if (!*p) {
*cursor = p;
return NULL;
}
char *start = p;
while (*p
&& !isspace(
(unsigned char) *p
)) {
++p;
}
if (*p) {
*p++ = '\0';
}
*cursor = p;
return start;
}
static void setInit(StrSet *s) {
s->data = NULL;
s->size = 0;
s->cap = 0;
}
static int setContains(
const StrSet *s,
const char *x) {
int i;
for (i = 0; i < s->size; ++i) {
if (strcmp(
s->data[i],
x
) == 0) {
return 1;
}
}
return 0;
}
static void setAdd(
StrSet *s,
const char *x) {
if (setContains(s, x)) {
return;
}
if (s->size == s->cap) {
s->cap =
s->cap == 0
? 4
: s->cap * 2;
s->data =
(char **) realloc(
s->data,
sizeof(char *)
* (size_t) s->cap
);
if (s->data == NULL) {
exit(1);
}
}
s->data[s->size++] =
dupStr(x);
}
static StrSet setCopy(
const StrSet *src) {
StrSet dst;
int i;
setInit(&dst);
for (i = 0;
i < src->size;
++i) {
setAdd(
&dst,
src->data[i]
);
}
return dst;
}
static void setMerge(
StrSet *dst,
const StrSet *src) {
int i;
for (i = 0;
i < src->size;
++i) {
setAdd(
dst,
src->data[i]
);
}
}
static void setFree(StrSet *s) {
int i;
for (i = 0;
i < s->size;
++i) {
free(s->data[i]);
}
free(s->data);
}
/*
* 把:
*
* a,b,c
*
* 解析成排除集合。
*/
static StrSet parseExclusions(
const char *text) {
StrSet result;
char *copy;
char *p;
char *start;
setInit(&result);
if (text == NULL
|| *text == '\0') {
return result;
}
copy = dupStr(text);
p = copy;
start = p;
while (1) {
if (*p == ','
|| *p == '\0') {
char saved = *p;
*p = '\0';
setAdd(
&result,
start
);
if (saved == '\0') {
break;
}
start = p + 1;
}
++p;
}
free(copy);
return result;
}
/*
* 按固定字段数量切分 ':'。
*
* 必须保留末尾空字段:
*
* a:v1:
*
* -> a
* -> v1
* -> ""
*/
static void splitFields(
char *item,
char **parts,
int count) {
int i;
int j;
char *p = item;
for (i = 0;
i < count - 1;
++i) {
char *colon;
parts[i] = p;
colon = strchr(p, ':');
if (colon == NULL) {
p += strlen(p);
for (j = i + 1;
j < count;
++j) {
parts[j] = p;
}
return;
}
*colon = '\0';
p = colon + 1;
}
parts[count - 1] = p;
}
static RuleGroup *findGroup(
RuleGroup *groups,
int groupCount,
const char *parent) {
int i;
for (i = 0;
i < groupCount;
++i) {
if (strcmp(
groups[i].parent,
parent
) == 0) {
return &groups[i];
}
}
return NULL;
}
static int selectedIndex(
Selected *selected,
int count,
const char *name) {
int i;
for (i = 0;
i < count;
++i) {
if (strcmp(
selected[i].name,
name
) == 0) {
return i;
}
}
return -1;
}
int main(void) {
char *directLine = readLine();
char *ruleLine = readLine();
RuleGroup *groups = NULL;
int groupCount = 0;
int groupCap = 0;
char *cursor = ruleLine;
char *item;
/*
* 构建依赖邻接表。
* 每个父组件的 edges 数组保持输入声明顺序。
*/
while ((item = nextToken(
&cursor)) != NULL) {
char *parts[4];
RuleGroup *g;
Edge *e;
splitFields(
item,
parts,
4
);
g = findGroup(
groups,
groupCount,
parts[0]
);
if (g == NULL) {
if (groupCount
== groupCap) {
groupCap =
groupCap == 0
? 4
: groupCap * 2;
groups =
(RuleGroup *)
realloc(
groups,
sizeof(RuleGroup)
* (size_t) groupCap
);
if (groups == NULL) {
exit(1);
}
}
g = &groups[groupCount++];
g->parent =
dupStr(parts[0]);
g->edges = NULL;
g->size = 0;
g->cap = 0;
}
if (g->size == g->cap) {
g->cap =
g->cap == 0
? 4
: g->cap * 2;
g->edges =
(Edge *) realloc(
g->edges,
sizeof(Edge)
* (size_t) g->cap
);
if (g->edges == NULL) {
exit(1);
}
}
e = &g->edges[g->size++];
e->child =
dupStr(parts[1]);
e->version =
dupStr(parts[2]);
e->exclusions =
parseExclusions(
parts[3]
);
}
Selected *selected = NULL;
int selCount = 0;
int selCap = 0;
/*
* 动态数组实现 BFS 队列。
* head 表示当前队首位置。
*/
Node *queue = NULL;
int qSize = 0;
int qCap = 0;
int head = 0;
cursor = directLine;
while ((item = nextToken(
&cursor)) != NULL) {
char *parts[3];
splitFields(
item,
parts,
3
);
if (selectedIndex(
selected,
selCount,
parts[0]
) < 0) {
if (selCount
== selCap) {
selCap =
selCap == 0
? 4
: selCap * 2;
selected =
(Selected *)
realloc(
selected,
sizeof(Selected)
* (size_t) selCap
);
if (selected == NULL) {
exit(1);
}
}
selected[selCount].name =
dupStr(parts[0]);
selected[selCount].version =
dupStr(parts[1]);
++selCount;
if (qSize == qCap) {
qCap =
qCap == 0
? 4
: qCap * 2;
queue =
(Node *) realloc(
queue,
sizeof(Node)
* (size_t) qCap
);
if (queue == NULL) {
exit(1);
}
}
queue[qSize].name =
dupStr(parts[0]);
queue[qSize]
.activeExclusions =
parseExclusions(
parts[2]
);
++qSize;
}
}
/*
* BFS 分层遍历。
*/
while (head < qSize) {
Node *parent =
&queue[head++];
RuleGroup *g =
findGroup(
groups,
groupCount,
parent->name
);
int i;
if (g == NULL) {
continue;
}
for (i = 0;
i < g->size;
++i) {
Edge *e =
&g->edges[i];
/*
* 当前路径已经排除这个组件,
* 则它以及整个子树都跳过。
*/
if (setContains(
&parent->activeExclusions,
e->child)) {
continue;
}
/*
* 全局只保留第一次出现。
*/
if (selectedIndex(
selected,
selCount,
e->child
) >= 0) {
continue;
}
if (selCount
== selCap) {
selCap =
selCap == 0
? 4
: selCap * 2;
selected =
(Selected *)
realloc(
selected,
sizeof(Selected)
* (size_t) selCap
);
if (selected == NULL) {
exit(1);
}
}
selected[selCount].name =
dupStr(e->child);
selected[selCount].version =
dupStr(e->version);
++selCount;
if (qSize == qCap) {
qCap =
qCap == 0
? 4
: qCap * 2;
queue =
(Node *) realloc(
queue,
sizeof(Node)
* (size_t) qCap
);
if (queue == NULL) {
exit(1);
}
}
queue[qSize].name =
dupStr(e->child);
/*
* 子节点继承父节点当前生效的排除规则,
* 再合并当前依赖边自己的排除项。
*
* 排除只沿当前依赖子树传播,
* 不影响其他独立路径。
*/
queue[qSize]
.activeExclusions =
setCopy(
&parent->activeExclusions
);
setMerge(
&queue[qSize]
.activeExclusions,
&e->exclusions
);
++qSize;
}
}
/*
* selected 的插入顺序
* 就是最终输出顺序。
*/
{
int i;
for (i = 0;
i < selCount;
++i) {
if (i > 0) {
putchar(' ');
}
printf(
"%s:%s",
selected[i].name,
selected[i].version
);
}
putchar('\n');
}
/* 释放动态内存 */
{
int i;
int j;
for (i = 0;
i < groupCount;
++i) {
free(groups[i].parent);
for (j = 0;
j < groups[i].size;
++j) {
free(
groups[i]
.edges[j]
.child
);
free(
groups[i]
.edges[j]
.version
);
setFree(
&groups[i]
.edges[j]
.exclusions
);
}
free(groups[i].edges);
}
free(groups);
for (i = 0;
i < selCount;
++i) {
free(selected[i].name);
free(selected[i].version);
}
free(selected);
for (i = 0;
i < qSize;
++i) {
free(queue[i].name);
setFree(
&queue[i]
.activeExclusions
);
}
free(queue);
}
free(directLine);
free(ruleLine);
return 0;
}
九、C++算法源码
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <queue>
#include <unordered_map>
#include <unordered_set>
using namespace std;
struct Edge {
string child;
string version;
unordered_set<string> exclusions;
};
struct Node {
string name;
// 当前依赖路径中累计生效的排除组件
unordered_set<string> activeExclusions;
};
/**
* 手动切分字符串,并且保留末尾空字符串。
*
* 例如:
* a:v1:
*
* 应得到:
* a
* v1
* ""
*/
vector<string> splitKeepEmpty(
const string& s,
char delimiter) {
vector<string> result;
string current;
for (char c : s) {
if (c == delimiter) {
result.push_back(current);
current.clear();
} else {
current += c;
}
}
result.push_back(current);
return result;
}
unordered_set<string> parseExclusions(
const string& text) {
unordered_set<string> result;
if (text.empty()) {
return result;
}
vector<string> names =
splitKeepEmpty(text, ',');
for (const string& name : names) {
result.insert(name);
}
return result;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string directLine;
string ruleLine;
getline(cin, directLine);
if (!getline(cin, ruleLine)) {
ruleLine = "";
}
/*
* 邻接表。
*
* vector 保证一个父组件的多个依赖
* 严格按照输入顺序保存。
*/
unordered_map<string, vector<Edge>> rules;
if (!ruleLine.empty()) {
stringstream ss(ruleLine);
string item;
while (ss >> item) {
vector<string> parts =
splitKeepEmpty(item, ':');
string parent = parts[0];
string child = parts[1];
string version = parts[2];
string exclusionText =
parts.size() > 3
? parts[3]
: "";
rules[parent].push_back({
child,
version,
parseExclusions(exclusionText)
});
}
}
/*
* unordered_map 用于 O(1) 判断组件是否已经出现。
*
* 因为 unordered_map 本身没有输出顺序,
* 所以另外使用 order 保存第一次出现顺序。
*/
unordered_map<string, string> selected;
vector<pair<string, string>> order;
queue<Node> q;
if (!directLine.empty()) {
stringstream ss(directLine);
string item;
while (ss >> item) {
vector<string> parts =
splitKeepEmpty(item, ':');
string name = parts[0];
string version = parts[1];
string exclusionText =
parts.size() > 2
? parts[2]
: "";
if (!selected.count(name)) {
selected[name] = version;
order.push_back({
name,
version
});
q.push({
name,
parseExclusions(exclusionText)
});
}
}
}
/*
* BFS 保证先处理依赖树上层组件,
* 再处理下一层。
*/
while (!q.empty()) {
Node parent =
std::move(q.front());
q.pop();
auto it = rules.find(parent.name);
if (it == rules.end()) {
continue;
}
for (const Edge& edge : it->second) {
/*
* 如果当前路径已经排除 child,
* 则 child 以及它的子树全部跳过。
*/
if (parent.activeExclusions.count(
edge.child)) {
continue;
}
/*
* 第一次出现决定最终位置和版本。
*/
if (selected.count(edge.child)) {
continue;
}
selected[edge.child] =
edge.version;
order.push_back({
edge.child,
edge.version
});
/*
* 将父节点排除项复制下来,
* 再加入当前依赖边新增的排除项。
*/
auto childExclusions =
parent.activeExclusions;
childExclusions.insert(
edge.exclusions.begin(),
edge.exclusions.end()
);
q.push({
edge.child,
std::move(childExclusions)
});
}
}
for (size_t i = 0;
i < order.size();
++i) {
if (i > 0) {
cout << ' ';
}
cout << order[i].first
<< ':'
<< order[i].second;
}
cout << '\n';
return 0;
}
🏆下一篇:华为OD机试真题 - 简易内存池(Python/JS/C/C++ 新系统 200分)
🏆本文收录于,华为OD机试真题(Python/JS/C/C++)
刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新。

1597

被折叠的 条评论
为什么被折叠?



