
华为OD机试 新系统 题库疯狂收录中,刷题++点这里++
专栏导读
本专栏收录于《华为OD机试(JAVA)真题》。
刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新,全天CSDN在线答疑。
一、题目描述
在云服务中,有 n 个安全策略,编号 1 到 n。每个策略有一个重要度权重(正整数)。某些策略对之间互斥,不能同时启用。
现在需要为某个实例恰好选择 k 个策略,要求:
-
选中的策略之间没有互斥关系(即构成一个独立集);
-
在满足条件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在线答疑。
