Blocking Elements(阻挡要素)
时间限制:4.0s 内存限制:256MB
【原题地址】
【问题描述】
给你一个由数字 a1,a2,…,an组成的数组。你的任务是封锁数组中的一些元素,以最小化成本。假设你封锁了索引为 1≤b1<b2<…<bm≤n的元素。那么数组的成本计算为以下各项的最大值:
阻塞元素的总和,即 ab1+ab2+…+abm。
移除阻塞元素后,数组被分割成的段的最大和。也就是说,以下( m+1)子数组的最大和:[ 1,b1−1 ]、[ b1+1,b2−1 ]、[ … ]、[ bm−1+1,bm−1 ]、[ bm+1,n ](形式为[ x,x−1 ]的子数组中的数字之和被认为是 0 )。
例如,如果 n=6 ,原始数组是[ 1,4,5,3,3,2 ],而你屏蔽了位于 2 和 5 位置的元素,那么数组的代价将是被屏蔽元素之和( 4+3=7 )和子数组之和( 1 , 5+3=8 , 2 )的最大值,也就是 max(7,1,8,2)=8 。
您需要输出阻塞后数组的最小代价。
【输入格式】
输入的第一行包含一个整数 t ( 1≤t≤30000 ) - 查询次数。
每个测试用例由两行组成。第一行包含一个整数 n ( 1≤n≤10^5 ) :数组的长度 a 。第二行包含 n 元素 a1,a2,…,an ( 1≤ai≤10^9 ) : 数组 a 。
保证所有测试用例的 n 之和不超过 10^5 。
【输出格式】
对于每个测试用例,输出一个数字 :阻塞数组的最小成本。
【样例输入】
3
6
1 4 5 3 3 2
5
1 2 3 4 5
6
4 1 6 3 10 7
【样例输出】
7
5
11
【样例说明】
第一个测试用例与语句中的数组相匹配。为了得到代价 7 ,需要屏蔽位置 2 和 4 的元素。在这种情况下,计算出的数组代价是以下值的最大值:
——被阻塞元素的总和,即 a2+a4=7 。
——移除阻塞元素后,数组被分割成的段的最大和,即 a1 、 a3 、 a5+a6=max(1,5,5)=5 的最大值。
因此成本为 max(7,5)=7 。
在第二个测试案例中,可以屏蔽位于 1 和 4 位置的元素。
在第三个测试用例中,要得到答案 11 ,可以屏蔽位置 2 和 5 的元素。还有其他方法可以得到这个答案,例如阻塞位置 4 和 6 。
【解题思路】
老汉使用到的是二分+单调队列优化+dp的解题方式
本题是求将整个长度为n的数组分割为多个数组,求每个分割部分各自的总和和与分割点的总和中的最大值,其中值最小的分割方案。
假设答案为 ans ,sum[i] 前 i 个数组元素的总和那么每块分割部分的总和以及分割点的总和都 <= ans ,该提中最小和为 0 ,最大值为 sum[n] ,则 0 <= ans <= sum[n] ,对 [ 0 , sum[n] ] 进行二分,不断比对,求出ans的值。
代码注释有详细过程
【代码】
package CF1918_D_BlockingElements;
import java.util.LinkedList;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
// 获取输入数据t
int t = scan.nextInt();
// 一共进行t次操作
while (t-- > 0) {
int n = scan.nextInt();
// 接收数组数据a_i
long[] a = new long[n + 2];
// 存放前n个数组的总和
long[] sum = new long[n + 1];
// 为数组a、sum赋值
for (int i = 1; i <= n; i++) {
a[i] = scan.nextLong();
sum[i] = sum[i - 1] + a[i];
}
// 运用二分,left为左端点,初始状态为最小值0,right为右端点初始状态为最大值sum[n]
long left = 0;
long right = sum[n];
// ans用来存放每次二分有效区间的中间值,最后一次二分取得最终结果
long ans = 0;
// dp[i]为选中i为阻塞元素,加上之前几个选中的阻塞元素的总和
// dp[n+1]即为所有阻塞元素的总和
long[] dp = new long[n + 2];
// 当左右两端重合或已达到不越过对方最接近值时,取得最终答案
while (left <= right) {
// 为中间值mid赋值
long mid = (left + right) / 2;
// bool判断最终结果ans是否位于[left,mid]之间,否则位于(mid,right]之间
boolean bool = true;
// 利用单调队列优化dp,使在该队列中dp单调递增,队列存放阻塞元素坐标
LinkedList<Integer> q = new LinkedList<Integer>();
// 初始化0位阻塞元素坐标,a[0]=0,不影响dp结果
q.add(0);
// 循环递归到dp[n+1]
for (int i = 1; i <= n + 1; i++) {
// 当有某个数组元素值大于mid时,表示ans不位于[left,mid]之间,退出循环
if (a[i] > mid) {
bool = false;
break;
}
// 踢出队列中阻塞元素与本次阻塞元素a[i]之间的数组元素和大于mid的阻塞元素下标
// 队列呈单调递增,阻塞元素下标越小,与本次阻塞元素距离远,之间的数组元素和越大
// 踢出不符合条件的元素下标后,目前队头和本次阻塞元素是之间数组元素和最大的
while (!q.isEmpty() && sum[i - 1] - sum[q.peek()] > mid) {
q.poll();
}
// 保存本次dp的值,表示设置i点为阻塞元素,在i左边最优的阻塞元素布置方案下,阻塞元素的和
dp[i] = dp[q.peek()] + a[i];
// 题出队尾大于本次存放的dp值的阻塞元素下标,保证队列单调性
while (!q.isEmpty() && dp[q.peekLast()] >= dp[i])
q.removeLast();
// 将本次阻塞元素下标入队
q.add(i);
}
// 如果已经求出ans不位于[left,mid]之间,无需再进行比对求bool
if (bool) {
// 当保证被分割后的数组元素和位于[left,mid]之间
// 如果最小阻塞元素和满足位于[left,mid]之间,则说明ans位于[left,mid]之间
bool = dp[n + 1] <= mid;
}
// 结果ans位于[left,mid]之间,则将范围缩短至[left,mid)进行二分,并保存ans值为本次mid
// 否则将范围缩短至(mid,right]进行二分,不对ans赋值
// 如果结果恰巧为mid,在范围[left,mid)之间取不到值
// 由于取这个范围之前保留了ans值,且之后不会进行对ans的赋值,结果依然正确
if (bool) {
right = mid - 1;
ans = mid;
} else {
left = mid + 1;
}
}
// 输出最终结果
System.out.println(ans);
}
scan.close();
}
}