
华为OD机试 新系统 题库疯狂收录中,刷题点这里
专栏导读
本专栏收录于《华为OD机试(JAVA)真题》。
刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新,全天优快云在线答疑。
一、题目描述
在一个远古修士的洞穴中,探宝者发现了一排共 N 棵灵草,每棵灵草有一个灵力值 A,灵力值可为正、负或零。
为避免灵草被探宝者一扫而空,远古修士设置了禁制,要求探宝者必须选择一段连续的灵草序列带回,且必须满足以下规则:
规则一:选中的连续序列中,灵力值为正的数量不少于 K 棵。
规则二:在满足规则一的前提下,探宝者还可以从选中序列中额外挑选最多 M 棵灵力值为负的灵草丢弃,以提升总收益。
求所有合法选择方案中,能够走灵草的灵力值总和的最大值。如果无法满足规则一,输出 -1。
数据范围:
1 <= N <= 10^41 <= K <= N0 <= M <= 5-10^9 <= A <= 10^9
二、输入描述
输入格式:
N K M
a1 a2 a3,... aN
其中:
N表示灵草数量K表示至少需要选择的正数灵草数量M表示最多可丢弃的负数灵草数量a1 a2 ... aN表示灵草灵力值数组
三、输出描述
输出一个整数,表示最大灵力值总和;若不存在满足条件的连续子序列,则输出 -1。
四、测试用例
测试用例1:
1、输入
8 3 2
3 -5 4 -1 2 -8 6 -2
2、输出
14
3、说明
可以选择区间 [1,7]:
3 -5 4 -1 2 -8 6
其中有 4 个正数,满足至少 3 个正数。删除 -5 和 -8 后:
3 + 4 - 1 + 2 + 6 = 14
测试用例2:
1、输入
5 4 1
1 -2 3 -4 5
2、输出
-1
3、说明
整个数组只有 1、3、5 共 3 个正数,小于 K=4,不存在合法方案。
五、解题思路
本题要求选择一个连续区间,区间内至少包含 K 个正数,并允许最多丢弃 M 个负数。由于 M <= 5,可以围绕“删除负数次数”设计小状态动态规划。
枚举区间右端点 r。假设从数组开头到 r 已经出现至少 K 个正数,令 p 为“从 r 向左数第 K 个正数”的位置。任何以 r 为右端点、满足至少 K 个正数的合法区间,其左端点一定不超过 p。因此一个合法区间可以拆成两部分:[l,p-1] 和固定区间 [p,r]。
对于左半部分,预处理 leftBest[i][d]:选择一个以 i 结尾的连续后缀,并最多丢弃 d 个负数时能够得到的最大收益,同时允许空后缀,其收益为 0。这个过程类似 Kadane 最大子段和,只是在遇到负数时多出“保留当前负数”和“消耗一次机会丢弃当前负数”两种选择,时间复杂度 O(NM)。
对于固定区间 [p,r],其普通区间和可以使用前缀和 O(1) 得到。若允许删除负数,显然应该优先删除绝对值最大的负数。因此建立线段树,每个节点只保存区间内绝对值最大的 M 个负数。一次区间查询即可得到 [p,r] 最值得删除的最多 M 个负数,查询复杂度 O(M log N)。
最后枚举 d=0…M,表示左半部分最多使用 d 次删除机会,则固定区间最多使用 M-d 次,两部分收益相加更新答案。若整个数组中的正数数量不足 K,则直接输出 -1。
总时间复杂度为 O(NMlogN),空间复杂度为 O(N*M)。由于 M 最大只有 5,在 N=10^4 的数据范围内效率很高。
六、Java算法源码
public class OdTest {
static int N;
static int M;
static long[] a;
static long[] prefixSum;
static long[][] tree;
/**
* 合并两个节点中“丢弃负数能够增加的收益”。
* 每个数组按从大到小保存最多 M 个收益。
* 不足 M 个负数时使用 0 补位。
*/
static long[] mergeTop(long[] left, long[] right) {
long[] result = new long[M];
int i = 0;
int j = 0;
for (int k = 0; k < M; k++) {
if (i < M && (j >= M || left[i] >= right[j])) {
result[k] = left[i++];
} else if (j < M) {
result[k] = right[j++];
}
}
return result;
}
/**
* 构建线段树。
* 每个节点保存当前区间内绝对值最大的 M 个负数。
*
* 例如负数 -8,丢弃它以后区间总和会增加 8,
* 因此在线段树中保存的是收益 8。
*/
static void build(int node, int l, int r) {
if (l == r) {
if (M > 0 && a[l] < 0) {
tree[node][0] = -a[l];
}
return;
}
int mid = (l + r) >>> 1;
build(node << 1, l, mid);
build(node << 1 | 1, mid + 1, r);
tree[node] = mergeTop(
tree[node << 1],
tree[node << 1 | 1]
);
}
/**
* 查询区间 [ql, qr] 中绝对值最大的 M 个负数,
* 返回对应的删除收益,并按照从大到小排列。
*/
static long[] query(int node, int l, int r, int ql, int qr) {
if (ql <= l && r <= qr) {
return tree[node];
}
int mid = (l + r) >>> 1;
if (qr <= mid) {
return query(node << 1, l, mid, ql, qr);
}
if (ql > mid) {
return query(node << 1 | 1, mid + 1, r, ql, qr);
}
return mergeTop(
query(node << 1, l, mid, ql, qr),
query(node << 1 | 1, mid + 1, r, ql, qr)
);
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 同时兼容空格、换行以及英文逗号分隔。
scanner.useDelimiter("[\\s,]+");
N = scanner.nextInt();
int K = scanner.nextInt();
M = scanner.nextInt();
a = new long[N + 1];
prefixSum = new long[N + 1];
// 保存所有正数所在的位置。
int[] positivePositions = new int[N];
int positiveCount = 0;
for (int i = 1; i <= N; i++) {
a[i] = scanner.nextLong();
// 前缀和用于快速得到任意固定区间 [l,r] 的原始总和。
prefixSum[i] = prefixSum[i - 1] + a[i];
if (a[i] > 0) {
positivePositions[positiveCount++] = i;
}
}
/*
* 如果整个数组的正数总数量都不足 K,
* 那么任何连续区间都不可能满足规则一。
*/
if (positiveCount < K) {
System.out.println(-1);
return;
}
/*
* leftBest[i][d]:
*
* 在前 i 个位置中,选择一个“以 i 作为右边界的连续后缀”,
* 并允许最多丢弃 d 个负数时能够取得的最大收益。
*
* 同时允许选择空后缀,收益记为 0。
*
* 这个状态类似于 Kadane 最大子段和,
* 但遇到负数时可以选择消耗一次机会把它丢掉。
*/
long[][] leftBest = new long[N + 1][M + 1];
for (int i = 1; i <= N; i++) {
for (int d = 0; d <= M; d++) {
if (a[i] >= 0) {
/*
* 当前数非负。
* 保留当前数一定不会让答案变差,
* 直接连接在上一个最优连续后缀后面。
*/
leftBest[i][d] =
leftBest[i - 1][d] + a[i];
} else {
/*
* 方案1:保留当前负数。
*/
long keep =
leftBest[i - 1][d] + a[i];
/*
* 方案2:如果还有删除额度,
* 丢弃当前负数。
*
* 丢弃以后当前负数对总和的贡献为 0。
*/
long discard = d > 0
? leftBest[i - 1][d - 1]
: Long.MIN_VALUE / 4;
/*
* 方案3:之前的区间收益太差,
* 直接重置为空区间 0,
* 之后重新开始选择连续区间。
*/
leftBest[i][d] =
Math.max(0L, Math.max(keep, discard));
}
}
}
/*
* 线段树用于快速查询任意区间中
* 最值得丢弃的最多 M 个负数。
*/
tree = new long[4 * N + 5][M];
build(1, 1, N);
long answer = Long.MIN_VALUE;
int seenPositive = 0;
/*
* 枚举最终连续区间的右端点 r。
*/
for (int r = 1; r <= N; r++) {
if (a[r] > 0) {
seenPositive++;
}
// 正数数量不足 K,此时还无法构成合法区间。
if (seenPositive < K) {
continue;
}
/*
* p 表示截至位置 r,
* 从右向左数第 K 个正数的位置。
*
* 对于任何以 r 为右端点且至少有 K 个正数的区间,
* 左端点一定 <= p。
*
* 因此区间可以拆成:
*
* [l, p-1] + [p, r]
*/
int p = positivePositions[seenPositive - K];
/*
* 固定区间 [p,r] 尚未删除负数时的原始总和。
*/
long baseSum =
prefixSum[r] - prefixSum[p - 1];
/*
* 查询 [p,r] 中最值得丢弃的 M 个负数。
*/
long[] topNegativeGains =
query(1, 1, N, p, r);
/*
* removeGain[e]:
* 在固定区间 [p,r] 中,
* 最多丢弃 e 个负数能够额外增加多少收益。
*/
long[] removeGain = new long[M + 1];
for (int e = 1; e <= M; e++) {
removeGain[e] =
removeGain[e - 1]
+ topNegativeGains[e - 1];
}
/*
* 将最多 M 次删除机会分配给左右两部分。
*
* 左侧最多使用 d 次,
* 那么固定区间 [p,r] 最多使用 M-d 次。
*/
for (int d = 0; d <= M; d++) {
long candidate =
leftBest[p - 1][d]
+ baseSum
+ removeGain[M - d];
answer = Math.max(answer, candidate);
}
}
System.out.println(answer);
}
}
七、效果展示
1、输入
3 2 0
1 -100 2
2、输出
-97
3、说明
需要至少两个正数,只能选择整个区间:
1 -100 2
又因为 M=0 不能删除负数,所以:
1 - 100 + 2 = -97
这个测试非常重要,因为不能简单地把答案初始化为 -1。

🏆下一篇:华为OD机试 - 简易内存池 - 逻辑分析(Java 新系统 200分)
🏆本专栏收录于《华为OD机试(JAVA)真题》。
刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新,全天优快云在线答疑。

28万+



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



