1. 程式人生 > 實用技巧 >【2020杭電多校round8】HDU6856 Breaking Down News

【2020杭電多校round8】HDU6856 Breaking Down News

題目大意

題目連結

有一個長度為\(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;
}