CF1918 D. Blocking Elements [二分+数据结构优化dp]

传送门:CF

[前题提要]:二分+数据结构优化dp,赛时想到了二分,想到了dp,想到了应该是某种双log的做法,但是硬是想不出正确的dp的定义,看了讲解感觉dp方程的定义还是很典的,dp题写的少是这样的…


题目要求我们输出满足所有去掉的数字和以及区间段和的最大值的最小值.不难想到使用二分答案.

考虑二分答案,此时我们的问题变成了,判断当前是否存在方案能满足我们二分出来的 m i d mid mid.很多人应该会直接想贪心吧,直接贪,发现样例二过不去,然后我当时的想法是用一种类似反悔贪心的想法,踢出前缀的某个数,但是想想做法十分麻烦,似乎不太可做.

当时我看了一下数据范围和时限, 1 e 5 , 4 s 1e5,4s 1e5,4s,很显然要么是一种 n n n\sqrt{n} nn 算法,要么是一种双 l o g log log做法,想想了根号算法有哪些,感觉D题不太可能出根号数据结构,根号分治也没法用.所以应该是一种双 l o g log log做法,而且二分答案用了一个 l o g log log,所以 c h e c k check check应该是一个 l o g log log算法.

然后赛时我想了想dp做法.当时在想返回贪心的时候已经有一种感觉了,就是假设当前枚举的位置是 i i i,那么以 i i i往左的区间和是单调递增的,这个单调性感觉有点用.但是当时在想一个位置有两种状态,留下或者删除,那么这两个状态该怎么转移呢.想了想留下的情况实在是难以转移.所以就放弃然后三题下班了.


o k ok ok,现在讲一下正解:
考虑定义 d p [ i ] dp[i] dp[i]表示删除第 i i i位且前 i i i位合法的最小删数和.
诶,此时会发现留下的状态为什么不记录呢,我感觉这一步是这道题的关键步骤(但是感觉这个想法应该很典,可能是我dp写太少了).此时我们会发现那么dp[n+1]就表示删数第n+1位前n位合法的最小删数和,a[n+1]=0.哇,此时我们的 d p [ n + 1 ] dp[n+1] dp[n+1]就是最终的答案,我们根本不需要记录留下的状态!

P S : PS: PS:其实细想一下这个定义的正确性,会发现其实就是枚举最后一段合法划分区间,这种类似的枚举思想在计数题中其实经常出现.

dp定义出来以后,这道题就没什么难度了.转移方法就是使用上述讲过的单调性,考虑当前 i i i位置我们删除,那么我们可以二分/双指针来轻松维护出能转移的左端点.这里存在一个小贪心可能需要稍微提一下,假设区间 [ l , i − 1 ] [l,i-1] [l,i1]的区间和满足我们的 m i d mid mid,那么显然此时的转移应该是直接转,因为没有必要在区间里再删除一个数.
找出左右端点之后只需要找到最小的dp值,然后 d p [ i ] = M i n ( d p [ j ] ) + a [ i ] dp[i]=Min(dp[j])+a[i] dp[i]=Min(dp[j])+a[i]直接转移.维护区间最小值这种RMQ问题可以使用单调队列/优先队列/数据结构等维护,此处不在赘述.作者在代码中使用的是线段树.


下面是具体的代码部分:

#include 
using namespace std;
typedef long long ll;
#define root 1,n,1
#define ls (rt<<1)
#define rs (rt<<1|1)
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
inline ll read() {
	ll x=0,w=1;char ch=getchar();
	for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;
	for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+ch-'0';
	return x*w;
}
inline void print(__int128 x){
	if(x<0) {putchar('-');x=-x;}
	if(x>9) print(x/10);
	putchar(x%10+'0');
}
#define maxn 1000000
#define int long long
const double eps=1e-8;
#define	int_INF 0x3f3f3f3f
#define ll_INF 0x3f3f3f3f3f3f3f3f
struct Segment_tree{
	int l,r,mn;
}tree[maxn<<2];
void build(int l,int r,int rt) {
	tree[rt].l=l;tree[rt].r=r;tree[rt].mn=0;
	if(l==r) {
		return ;
	}
	int mid=(l+r)>>1;
	build(lson);build(rson);
	tree[rt].mn=min(tree[ls].mn,tree[rs].mn);
}
void update(int pos,int rt,int val) {
	if(tree[rt].l==pos&&tree[rt].r==pos) {
		tree[rt].mn=val;
		return ;
	}
	int mid=(tree[rt].l+tree[rt].r)>>1;
	if(pos<=mid) update(pos,ls,val);
	else update(pos,rs,val);
	tree[rt].mn=min(tree[ls].mn,tree[rs].mn);
}
int query(int l,int r,int rt) {
	if(tree[rt].l==l&&tree[rt].r==r) {
		return tree[rt].mn;
	}
	int mid=(tree[rt].l+tree[rt].r)>>1;
	if(r<=mid) return query(l,r,ls);
	else if(l>mid) return query(l,r,rs);
	else return min(query(l,mid,ls),query(mid+1,r,rs));
} 
int a[maxn];int n;int dp[maxn];
//dp[i]表示删除第i位且前i位合法的最小删数和
//那么dp[n+1]就表示删数第n+1位前n位合法的最小删数和,a[n+1]=0;
int sum[maxn];
int check(int mid) {
	build(1,n+2,1);
	for(int i=1;i<=n+1;i++) dp[i]=0;
	for(int i=1;i<=n+1;i++) {
		int l=0,r=i-1;int pos=0;
		while(l<=r) {
			int MID=(l+r)>>1;
			if(sum[i-1]-sum[MID]<=mid) {
				pos=MID;r=MID-1;
			}
			else {
				l=MID+1;
			}
		}
		dp[i]=query(pos+1,i,1)+a[i];
		update(i+1,1,dp[i]);
	}
	return dp[n+1]<=mid;
}
signed main() {
	int T=read();
	while(T--) {
		n=read();
		for(int i=1;i<=n;i++) {
			a[i]=read();
			sum[i]=sum[i-1]+a[i];
		}
		int l=1,r=1e14;int ans=-1;
		while(l<=r) {
			int mid=(l+r)>>1;
			if(check(mid)) {
				ans=mid;
				r=mid-1;
			}
			else {
				l=mid+1;
			}
		}
		cout<<ans<<endl;
		//clear
		for(int i=0;i<=n;i++) {
			a[i]=sum[i]=0;
		}
	}
	return 0;
}

你可能感兴趣的:(c++算法,#,各类比赛,#,dp学习记录,数据结构,算法,动态规划)