华为OD机试 - 云服务安全策略最优选择 - 深度优先搜索DFS(Java 新系统 200分)

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

专栏导读

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

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

一、题目描述

在云服务中,有 n 个安全策略,编号 1 到 n。每个策略有一个重要度权重(正整数)。某些策略对之间互斥,不能同时启用。

现在需要为某个实例恰好选择 k 个策略,要求:

  1. 选中的策略之间没有互斥关系(即构成一个独立集);

  2. 在满足条件1的所有大小为 k 的策略组合中,使得选中策略的权重之和最大(权重之和就是重要度权重数组中的权重的和)。

    请返回所有满足上述条件的最优策略组合(即权重和最大的所有合法组合)。

二、输入描述

• 第一行:整数 n(策略总数)

• 第二行:整数 k(需要选择的策略数量)

• 第三行:长度为 n 的整数数组 weights,其中 weightsi 表示策略 i + 1 的权重,字符串表示通过 , 分割

• 第四行:二维整数数组 conflicts,每个元素为 a, b,表示策略 a 和 b 互斥(无向,无重复边),通过字符串表示,a和b之间通过 , 分割,不同组通过空格分割

数据规模:

• 1 ≤ n ≤ 25

• 0 ≤ k ≤ n

• 1 ≤ weightsi ≤ 1000

• 0 ≤ conflicts.length ≤ n(n − 1)/2

三、输出描述

• 每个组合内的策略编号按升序排列;

• 所有组合按字典序排列(将每个组合视为一个数字序列);

• 如果没有合法组合(例如不存在大小为 k 的独立集),则返回空数组 \[\]。

注意

• 如果没有大小为 k 的独立集,则返回 空

四、测试用例

测试用例1:

1、输入

4

2

5,1,3,4

1,2 2,3

2、输出

1,4

3、说明

合法的两策略组合中,1,4 的权重为 5+4=9,为唯一最大值。

测试用例2:

1、输入

5

3

3,4,3,4,3

1,3 2,4 3,5

2、输出

1,2,5 1,4,5

3、说明

1,2,5 和 1,4,5 均不存在内部冲突,权重都为 10,并且已经按照字典序输出。

五、解题思路

把 n 个安全策略看成图中的 n 个顶点,互斥关系看成无向边,题目实际要求寻找"大小恰好为 k 的最大权独立集",并输出所有权重和相同的最优方案。

由于 n 最大只有 25,可以采用 DFS 回溯枚举组合。为了快速判断新选择的策略是否与已经选择的策略冲突,使用一个整数位掩码表示当前已选择策略,同时为每个策略维护 conflictMask:若 conflictMaski 的第 j 位为 1,说明策略 i 与策略 j 互斥。判断能否选择策略 i 只需执行 conflictMaski & selectedMask,复杂度为 O(1)。

单纯回溯最坏情况下需要枚举大量组合,因此增加一个 DP 乐观上界 upperic:表示从策略 i 到 n-1 中暂时忽略互斥关系、恰好再选 c 个策略时能够获得的最大权重。忽略冲突后的结果一定不小于真实可行结果,因此如果"当前权重 + upper"仍小于已经找到的最优权重,该分支一定不可能产生最优解,可以安全剪枝。

算法执行两遍 DFS。第一遍只计算最大权重 bestSum,不保存方案;第二遍再次按策略编号从小到大搜索,只输出权重恰好等于 bestSum 的方案。由于 DFS 始终按编号递增选择,所以每个组合内部天然升序,所有组合也天然按照字典序输出,无需额外排序。

预处理 upper 的复杂度为 O(nk),搜索最坏仍可能达到 O(2^n),这是一般图最大权独立集问题本身的性质;但 n≤25,并结合选取数量限制、冲突判断和上界剪枝后具有较好的实际性能。除递归路径和 upper 外不保存全部答案,额外空间约为 O(nk+n)。

六、Java算法源码

java 复制代码
public class OdTest {
    static int n, k;
    static int[] weights;

    // conflictMask[i]:与策略 i 互斥的所有策略的位集合
    static long[] conflictMask;

    /*
     * upper[i][c]:
     * 从 i ~ n-1 中忽略互斥关系,恰好选择 c 个策略时,
     * 可以获得的最大权重和。
     *
     * 因为忽略了互斥关系,所以它一定是实际可行解的"乐观上界",
     * 可以用来进行安全剪枝。
     */
    static long[][] upper;

    static long bestSum = Long.MIN_VALUE;

    // 表示 upper 中"不可能恰好选择指定数量"的状态
    static final long NEG = Long.MIN_VALUE / 4;

    // 第二遍 DFS 输出时,用于控制不同组合之间的空格
    static boolean firstOutput = true;

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

        n = Integer.parseInt(sc.nextLine().trim());
        k = Integer.parseInt(sc.nextLine().trim());

        String[] weightParts = sc.nextLine().trim().split(",");
        weights = new int[n];

        for (int i = 0; i < n; i++) {
            weights[i] = Integer.parseInt(weightParts[i].trim());
        }

        /*
         * n <= 25,一个 long 足够表示所有策略。
         * 第 j 位为 1 表示与策略 j 存在互斥关系。
         */
        conflictMask = new long[n];

        String conflictLine = sc.hasNextLine()
                ? sc.nextLine().trim()
                : "";

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

            for (String pair : pairs) {
                String[] ab = pair.split(",");

                int a = Integer.parseInt(ab[0].trim()) - 1;
                int b = Integer.parseInt(ab[1].trim()) - 1;

                // 互斥关系是无向边,因此两个方向都需要记录
                conflictMask[a] |= 1L << b;
                conflictMask[b] |= 1L << a;
            }
        }

        buildUpperBound();

        /*
         * 第一遍 DFS:
         * 只寻找最大权重 bestSum,不保存具体组合。
         *
         * 这样即使最优组合数量非常多,也不会在这里消耗大量内存。
         */
        dfsBest(0, k, 0L, 0L);

        BufferedWriter out =
                new BufferedWriter(new OutputStreamWriter(System.out));

        /*
         * 不存在大小为 k 的独立集时输出 []。
         *
         * k = 0 时数学上存在一个"空组合",这里按照题目的数组输出形式
         * 同样使用 [] 表示。
         */
        if (bestSum == Long.MIN_VALUE || k == 0) {
            out.write("[]");
            out.newLine();
            out.flush();
            return;
        }

        /*
         * 第二遍 DFS:
         * 重新搜索,只输出权重和等于 bestSum 的方案。
         *
         * DFS 按编号升序搜索,因此无需额外排序。
         */
        dfsPrint(
                0,
                k,
                0L,
                0L,
                new ArrayList<>(),
                out
        );

        out.newLine();
        out.flush();
    }

    /**
     * 构造 DFS 使用的乐观上界。
     */
    static void buildUpperBound() {
        upper = new long[n + 1][k + 1];

        Arrays.fill(upper[n], NEG);
        upper[n][0] = 0;

        /*
         * 类似 0/1 背包:
         *
         * upper[i][need] 有两种情况:
         * 1. 不选择策略 i
         * 2. 选择策略 i
         *
         * 注意这里故意不考虑互斥关系,因为我们需要的是一个
         * "一定不会低估真实最大值"的乐观上界。
         */
        for (int i = n - 1; i >= 0; i--) {
            Arrays.fill(upper[i], NEG);
            upper[i][0] = 0;

            for (int need = 1; need <= k; need++) {
                long skip = upper[i + 1][need];

                long take =
                        upper[i + 1][need - 1] == NEG
                                ? NEG
                                : weights[i]
                                + upper[i + 1][need - 1];

                upper[i][need] = Math.max(skip, take);
            }
        }
    }

    /**
     * 第一遍 DFS:寻找最大的权重和。
     *
     * @param start        下一次可以选择的最小策略下标
     * @param need         还需要选择多少个策略
     * @param selectedMask 当前已经选择的策略集合
     * @param sum          当前权重和
     */
    static void dfsBest(
            int start,
            int need,
            long selectedMask,
            long sum
    ) {

        // 已经恰好选择 k 个策略
        if (need == 0) {
            bestSum = Math.max(bestSum, sum);
            return;
        }

        // 剩余策略数量不足,不可能选满 need 个
        if (n - start < need) {
            return;
        }

        /*
         * upper[start][need] 是忽略所有冲突后的理论最大增量。
         *
         * 如果:
         * 当前权重 + 理论最大增量 < 当前已经找到的最优值
         *
         * 那么这个分支无论如何都不可能成为最优方案,可以直接剪枝。
         */
        if (upper[start][need] == NEG
                || (bestSum != Long.MIN_VALUE
                && sum + upper[start][need] < bestSum)) {
            return;
        }

        /*
         * 始终按照策略编号由小到大选择。
         *
         * i 最大只能到 n - need,
         * 否则后面剩余策略数量不足以选满 need 个。
         */
        for (int i = start; i <= n - need; i++) {

            /*
             * conflictMask[i] & selectedMask != 0
             *
             * 表示策略 i 与当前已经选择的至少一个策略互斥,
             * 因而不能加入当前组合。
             */
            if ((conflictMask[i] & selectedMask) != 0) {
                continue;
            }

            dfsBest(
                    i + 1,
                    need - 1,
                    selectedMask | (1L << i),
                    sum + weights[i]
            );
        }
    }

    /**
     * 第二遍 DFS:输出所有权重等于 bestSum 的方案。
     */
    static void dfsPrint(
            int start,
            int need,
            long selectedMask,
            long sum,
            List<Integer> path,
            BufferedWriter out
    ) throws IOException {

        if (need == 0) {
            if (sum == bestSum) {

                if (!firstOutput) {
                    out.write(' ');
                }

                firstOutput = false;

                for (int i = 0; i < path.size(); i++) {
                    if (i > 0) {
                        out.write(',');
                    }

                    out.write(String.valueOf(path.get(i)));
                }
            }

            return;
        }

        // 剩余数量不够
        if (n - start < need) {
            return;
        }

        /*
         * 第二遍已经知道目标 bestSum。
         *
         * 如果该分支在最乐观情况下都达不到 bestSum,
         * 则不可能包含任何最优组合。
         */
        if (upper[start][need] == NEG
                || sum + upper[start][need] < bestSum) {
            return;
        }

        /*
         * 按编号递增枚举:
         * 1. 单个组合内部一定升序;
         * 2. 不同组合天然按照字典序产生。
         */
        for (int i = start; i <= n - need; i++) {

            if ((conflictMask[i] & selectedMask) != 0) {
                continue;
            }

            path.add(i + 1);

            dfsPrint(
                    i + 1,
                    need - 1,
                    selectedMask | (1L << i),
                    sum + weights[i],
                    path,
                    out
            );

            // 回溯,撤销刚才选择的策略
            path.remove(path.size() - 1);
        }
    }
}

七、效果展示

1、输入

6

3

5,5,5,5,1,1

1,2 3,4

2、输出

1,3,5 1,3,6 1,4,5 1,4,6 2,3,5 2,3,6 2,4,5 2,4,6

3、说明

策略 1、2 不能同时选,3、4 不能同时选。因此最优方案必须分别从 {1,2}、{3,4} 中选一个,再从 {5,6} 中选一个,共有 2×2×2=8 个并列最优解。

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

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

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

相关推荐
拒绝内耗。1 小时前
程序员学架构(一):一张图看懂 Java 后端架构:一个请求怎样从手机到数据库?
java·智能手机·架构
棣廷1 小时前
初识Python面向对象
java·开发语言
用户3126874877201 小时前
ThreadLocal 到底会不会内存泄漏?源码级拆解 + 线程池陷阱
java
珍珠先生1 小时前
P11 · MySQL 驱动版本过旧:Client does not support authentication protocol
java
~木雨1 小时前
Java 并发编程架构全景:并发层的五域设计 —— 线程模型、线程池隔离、并发安全到问题治理(全体系汇总)
java·安全·架构·线程池·并发编程·高并发架构
极小狐2 小时前
从复制粘贴到可复用组件:极狐GitLab CI/CD 组件目录实战
java·ci/cd·gitlab·devops·组件化
麻瓜code2 小时前
【Agent】Spring AI RAG 实战:本地向量库 + 云端知识库
java·人工智能·spring
小刘在重生~2 小时前
Java 集合|Collection、List、ArrayList、LinkedList、泛型、Collections 工具类
java·数据结构·list
我不会起名字3222 小时前
一天一道算法题(29):单调栈
java·数据结构·python·算法·leetcode·golang·单调栈