码迷,mamicode.com
首页 > 其他好文 > 详细

HDU6856 Breaking Down News

时间:2020-08-17 16:57:59      阅读:68      评论:0      收藏:0      [点我收藏+]

标签:如何   pos   while   display   需要   sum   维护   总结   lan   

题目大意

题目链接

有一个长度为\(n\)的数组\(a_1,a_2,\dots ,a_n\)\(a_i\in\{-1,0,1\}\)。请你将这个数组,划分为连续的\(m\geq 1\)段(每个位置都恰好位于某一段内),使得每一段的长度,都大于等于\(L\),小于等于\(R\)

我们定义,一段连续子序列\(a_l,a_{l+1},\dots ,a_r\)的价值为:

  • 如果序列里,所有元素之和\(>0\),则价值为\(1\)
  • 如果序列里,所有元素之和\(=0\),则价值为\(0\)
  • 如果序列里,所有元素之和\(<0\),则价值为\(-1\)

也可以用公式表示为:\(v(l,r)=[(\sum_{i=l}^{r}a_i)>0]-[(\sum_{i=l}^{r}a_i)<0]\)

请你通过划分,最大化所有\(m\)段的价值之和(\(m\)是你自己选的,并不是题目规定的)。

数据范围:\(1\leq L\leq R\leq n\leq 10^6\)\(T\)组测试数据,\(1\leq T\leq 1000\)\(\sum n\leq 9\cdot 10^6\)

本题题解

考虑DP。设\(dp[i]\)表示把\(a_1\dots a_i\)划分为若干段,并且\(a_i\)是最后一段的结尾时,的最大价值。

朴素的转移很简单:

\[dp[i]=\max_{j=i-R}^{i-L}\{dp[j]+v(j+1,i)\} \]

其中\(v\)表示一段连续子序列的价值,题面里已给出定义。按这个式子直接DP,时间复杂度\(O(n^3)\)

我们可以对\(a\)序列做前缀和。设\(s_i=\sum_{j=1}^{i}a_i\)。于是转移式改写为:

\[dp[i]=\max_{j=i-R}^{i-L}\{dp[j] + [s_i-s_j>0]-[s_i-s_j<0]\} \]

时间复杂度\(O(n^2)\)

继续优化。首先,因为每个\(i\)只能从一段特定的区间转移过来,容易想到单调队列优化DP:队列从前到后,“价值”单调递减,位置单调递增,也就是永远不保留“又老又不中用”的元素。

然而会遇到一个问题:你如何比较两个元素的“价值”大小呢?我们不能只比较\(dp[j]\)的大小,因为还有后面的\(s_j\)的贡献。但又不能简单地把\(dp[j]-s_j\)作为价值,因为\(s_j\)是要和后面的每个\(i\)共同算出一个价值\(v\in\{-1,0,1\}\)

既然把它们放在一起,怎么比较都不公平,那不如将它们分开来:对每种\(s_j\)的值(从最小的\(-n\)到最大的\(n\),共有\(2n+1\)种可能的值),各开一个单调队列。此时同一个单调队列里,就只需要比\(dp[j]\)的大小了。

对于每个值\(x\) (\(-n\leq x\leq n\)),我们维护出它的队首(最优的),记其DP值为\(f[x]\)。特别地,如果单调队列为空,则\(f[x]=\inf\)

每次转移前,先将\(i-L\)入队,将\(i-R-1\)出队。只需要在对应的\(x=s_{i-L}\)\(x=s_{i-R-1}\)这两个队列里做修改,修改后更新对应的\(f[x]\)的值。这都是单调队列的基本操作。

单调队列是维护好了,怎么转移呢?我们总不能枚举所有\(x\)吧?考虑,使得\(v(j+1,i)=1\)\(j\),一定是\(s_i-s_j>0\),也就是\(s_j<s_i\),所以这样的\(s_j\)值是一段前缀(即\(x\in[-n,s_i-1]\));同理,使得\(v(j+1,i)=-1\)\(j\),一定是一段后缀(\(x\in[s_i+1,n]\));而使得\(v(j+1,i)=0\)\(s_j\),就是\(x=s_i\)。所以,我们只需要在\(f\)数组上做3次区间最大值查询,就能完成转移了!

总结一下,我们在\(\text{push},\text{pop}\)一个元素,也就是对单调队列做改动时,要支持对\(f\)数组单点修改。因为每个\(j\)只会入队、出队一次,所以总修改次数是\(O(n)\)的。在转移时,需要做3次区间最大值查询,总查询次数也是\(O(n)\)的。用线段树来维护这个\(f\)数组即可。时间复杂度\(O(n\log n)\)

单调队列,可以用\(\texttt{vector}\)维护(\(\texttt{deque}\)时间空间常数都太大了)。\(\texttt{pop_front}\)操作,可以用变量记录每个\(\texttt{vector}\)的实际队首元素在哪里,要\(\texttt{pop_front}\)时令这个队首位置\(\texttt{++}\)即可。因为每个元素只会入队一次,所以空间复杂度是\(O(n)\)的。

总时间复杂度\(O(n\log n)\),空间复杂度\(O(n)\)

参考代码(本代码仅供参考,实际提交时建议使用读入优化,详见本博客公告栏):

//problem:1002
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

template<typename T>inline void ckmax(T& x,T y){x=(y>x?y:x);}
template<typename T>inline void ckmin(T& x,T y){x=(y<x?y:x);}

const int MAXN=1e6,INF=1e9;
int n,L,R,a[MAXN+5],s[MAXN+5],dp[MAXN+5];
int lpt[MAXN*2+5],bas;
vector<int>vec[MAXN*2+5];
struct SegmentTree{
	int mx[MAXN*8+100];
	void build(int p,int l,int r){
		mx[p]=-INF;
		if(l==r) return;
		int mid=(l+r)>>1;
		build(p<<1,l,mid);
		build(p<<1|1,mid+1,r);
	}
	void modify(int p,int l,int r,int pos,int v){
		if(l==r){
			mx[p]=v;
			return;
		}
		int mid=(l+r)>>1;
		if(pos<=mid)
			modify(p<<1,l,mid,pos,v);
		else
			modify(p<<1|1,mid+1,r,pos,v);
		mx[p]=max(mx[p<<1],mx[p<<1|1]);
	}
	int query(int p,int l,int r,int ql,int qr){
		if(ql<=l && qr>=r)
			return mx[p];
		int mid=(l+r)>>1;
		int res=-INF;
		if(ql<=mid)
			res=query(p<<1,l,mid,ql,qr);
		if(qr>mid)
			ckmax(res,query(p<<1|1,mid+1,r,ql,qr));
		return res;
	}
	SegmentTree(){}
}T;
void ins(int p){
	int vv=s[p]+bas;
	while(SZ(vec[vv])>lpt[vv] && dp[vec[vv].back()]<=dp[p])
		vec[vv].pop_back();
	vec[vv].pb(p);
	T.modify(1,1,n+bas,vv,(SZ(vec[vv])>lpt[vv] ? dp[vec[vv][lpt[vv]]] : -INF));
}
void del(int p){
	int vv=s[p]+bas;
	if(SZ(vec[vv])>lpt[vv] && vec[vv][lpt[vv]]==p)
		++lpt[vv];
	T.modify(1,1,n+bas,vv,(SZ(vec[vv])>lpt[vv] ? dp[vec[vv][lpt[vv]]] : -INF));
}
void solve_case(){
	cin>>n>>L>>R;
	for(int i=1;i<=n;++i){
		cin>>a[i];
		s[i]=s[i-1]+a[i];
	}
	bas=n+1;
	for(int i=-n;i<=n;++i){
		vector<int>().swap(vec[i+bas]);
		lpt[i+bas]=0;
	}
	T.build(1,1,n+bas);
	for(int i=L;i<=n;++i){
		int mid=s[i]+bas;
		if(i-R-1==0 || i-R-1>=L){
			del(i-R-1);
		}
		if(i-L==0 || i-L>=L){
			ins(i-L);
		}
		dp[i]=-INF;
		if(mid>1){
			ckmax(dp[i],T.query(1,1,n+bas,1,mid-1)+1);
		}
		ckmax(dp[i],T.query(1,1,n+bas,mid,mid));
		if(mid<n+bas){
			ckmax(dp[i],T.query(1,1,n+bas,mid+1,n+bas)-1);
		}
	}
	cout<<dp[n]<<endl;
}
int main() {
	int T;cin>>T;while(T--){
		solve_case();
	}
	return 0;
}

HDU6856 Breaking Down News

标签:如何   pos   while   display   需要   sum   维护   总结   lan   

原文地址:https://www.cnblogs.com/dysyn1314/p/13501291.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!