华为OD机试 - 软件依赖树 - 广度优先搜索BFS(Java 新系统 200分)

在这里插入图片描述

华为OD机试 新系统 题库疯狂收录中,刷题点这里

专栏导读

本专栏收录于《华为OD机试(JAVA)真题》

刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新,全天CSDN在线答疑。

一、题目描述

在软件项目开发里涉及不同版本组件引用,组件依赖组件,形成一棵依赖树(从根节点深层展开);请编写一段代码,对当前项目的组件依赖关系进行分析,按项目组件依赖顺序输出最终有效的组件版本列表;
说明如下:

  1. 从根节点开始按依赖顺序输出组件列表,项目上层组件依赖先输出,如果同层依赖则父组件先出现的优先输出,父组件相同的按依赖出现顺序输出,如:项目依赖组件【b,a】,a依赖【c,d】,b依赖【e】,输出顺序为 b,a,e,c,d;
  2. 组件去重:依赖组件如多次出现,只保留第一次出现的顺序位置;
  3. 同一组件如有不同版本,以首次依赖的组件版本作为输出;

二、输入描述

  • 参数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)。

六、Java算法源码

public class OdTest {

    /**
     * 表示一条依赖关系。
     *
     * 例如:
     * a:b:v2:c,d
     *
     * 表示 a 依赖 b:v2,
     * 并且进入 b 的依赖子树以后,需要排除 c、d。
     */
    static class Edge {
        String childName;
        String version;
        Set<String> exclusions;

        Edge(String childName, String version, Set<String> exclusions) {
            this.childName = childName;
            this.version = version;
            this.exclusions = exclusions;
        }
    }

    /**
     * BFS 队列中的节点。
     *
     * activeExclusions 表示:
     * 从项目根节点到当前组件这条依赖路径上,
     * 当前仍然生效的所有排除项。
     */
    static class Node {
        String name;
        String version;
        Set<String> activeExclusions;

        Node(String name, String version, Set<String> activeExclusions) {
            this.name = name;
            this.version = version;
            this.activeExclusions = activeExclusions;
        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        // 第一行:项目直属依赖 directDeps
        String directLine = scanner.hasNextLine()
                ? scanner.nextLine().trim()
                : "";

        // 第二行:全局组件依赖 depRules
        // 没有依赖规则时第二行可以不存在
        String ruleLine = scanner.hasNextLine()
                ? scanner.nextLine().trim()
                : "";

        /*
         * 保存全局依赖规则。
         *
         * List 必须严格保留输入顺序。
         * 例如:
         * a:c:v1: a:d:v1:
         *
         * 必须保证处理 a 时先处理 c,再处理 d。
         */
        Map<String, List<Edge>> rules = new HashMap<>();

        if (!ruleLine.isEmpty()) {
            String[] ruleItems = ruleLine.split("\\s+");

            for (String item : ruleItems) {

                /*
                 * split(":", -1) 中的 -1 非常重要。
                 *
                 * a:c:v1:
                 *
                 * 最后一个排除字段为空,
                 * 如果不使用 -1,Java 会自动丢弃末尾空字符串。
                 */
                String[] parts = item.split(":", -1);

                String parent = parts[0];
                String child = parts[1];
                String version = parts[2];

                Set<String> exclusions =
                        parseExclusions(parts.length > 3 ? parts[3] : "");

                rules.computeIfAbsent(parent, k -> new ArrayList<>())
                        .add(new Edge(child, version, exclusions));
            }
        }

        /*
         * LinkedHashMap 有两个作用:
         *
         * 1. 判断组件是否已经出现,实现全局去重;
         * 2. 保留组件第一次出现的顺序。
         *
         * value 保存第一次出现时的版本。
         * 后面即使遇到同组件的其他版本,也不能覆盖。
         */
        Map<String, String> selected = new LinkedHashMap<>();

        // BFS 队列
        Queue<Node> queue = new ArrayDeque<>();

        /*
         * 先按照输入顺序加入所有直属依赖。
         *
         * 直属依赖属于依赖树的第一层,
         * 所以必须全部先输出,再处理它们的下一层依赖。
         */
        if (!directLine.isEmpty()) {

            String[] directItems = directLine.split("\\s+");

            for (String item : directItems) {
                String[] parts = item.split(":", -1);

                String name = parts[0];
                String version = parts[1];

                Set<String> exclusions =
                        parseExclusions(parts.length > 2 ? parts[2] : "");

                // 组件只保留第一次出现的位置和版本
                if (!selected.containsKey(name)) {
                    selected.put(name, version);

                    /*
                     * 直属依赖中的排除项不排除直属组件本身,
                     * 而是从该组件的依赖子树开始生效。
                     */
                    queue.offer(
                            new Node(name, version, exclusions)
                    );
                }
            }
        }

        /*
         * BFS 分层遍历。
         *
         * 为什么不能 DFS?
         *
         * 假设:
         * 项目依赖 b、a
         * b -> e
         * a -> c、d
         *
         * 题目要求:
         * b a e c d
         *
         * BFS 正好得到这个顺序。
         */
        while (!queue.isEmpty()) {

            Node parent = queue.poll();

            List<Edge> children =
                    rules.getOrDefault(
                            parent.name,
                            Collections.emptyList()
                    );

            for (Edge edge : children) {

                /*
                 * 如果当前依赖路径已经要求排除这个组件,
                 * 那么这个组件以及它下面整棵依赖子树都不能进入。
                 */
                if (parent.activeExclusions.contains(edge.childName)) {
                    continue;
                }

                /*
                 * 全局组件去重。
                 *
                 * 第一次出现的位置和版本已经确定,
                 * 后面再次遇到时全部忽略。
                 *
                 * 这也可以避免依赖关系中存在环时产生死循环。
                 */
                if (selected.containsKey(edge.childName)) {
                    continue;
                }

                // 第一次出现,确定最终版本
                selected.put(edge.childName, edge.version);

                /*
                 * 子节点的排除集合 =
                 * 祖先路径已经生效的排除项
                 * +
                 * 当前依赖边新增的排除项。
                 *
                 * 例如:
                 * a:b:v1:c
                 *
                 * 当进入 b 后,c 开始被排除。
                 */
                Set<String> childExclusions =
                        new HashSet<>(parent.activeExclusions);

                childExclusions.addAll(edge.exclusions);

                queue.offer(
                        new Node(
                                edge.childName,
                                edge.version,
                                childExclusions
                        )
                );
            }
        }

        // 按第一次出现的顺序输出
        StringJoiner result = new StringJoiner(" ");

        for (Map.Entry<String, String> entry : selected.entrySet()) {
            result.add(
                    entry.getKey() + ":" + entry.getValue()
            );
        }

        System.out.println(result);
    }

    /**
     * 解析排除组件列表。
     *
     * ""      -> {}
     * "a"     -> {a}
     * "a,b,c" -> {a,b,c}
     */
    private static Set<String> parseExclusions(String text) {

        Set<String> set = new HashSet<>();

        if (text == null || text.isEmpty()) {
            return set;
        }

        String[] names = text.split(",");

        Collections.addAll(set, names);

        return set;
    }
}

七、效果展示

1、输入

b:v1: a:v1:
a:c:v1: a:d:v1: b:e:v1:

2、输出

b:v1 a:v1 e:v1 c:v1 d:v1

3、说明

第1层:

b a

第2层:

b -> e
a -> c
a -> d

所以顺序必须是:b a e c d

而不能是 DFS 产生的:b e a c d

在这里插入图片描述


🏆下一篇:华为OD机试 - 简易内存池 - 逻辑分析(Java 新系统 200分)

🏆本专栏收录于《华为OD机试(JAVA)真题》

刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新,全天CSDN在线答疑。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哪 吒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值