Codeforces1918 D. Blocking Elements

Blocking Elements(阻挡要素)

时间限制:4.0s 内存限制:256MB

【原题地址】

点击此处跳转至原题

【问题描述】

给你一个由数字 a1,a2,…,an组成的数组。你的任务是封锁数组中的一些元素,以最小化成本。假设你封锁了索引为 1≤b1

阻塞元素的总和,即 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();
	}
}

你可能感兴趣的:(Java算法题解,算法,动态规划,java)