CF783D(div2)Optimal Partition(树状数组+离散化)

题目

原题链接


问题描述

给定一个长度为 n n n的数组 a a a,共有 2 n − 1 2^{n-1} 2n1种策略将其划分为连续的段,对于一段 a l . . . a r a_l...a_r al...ar s u m = ∑ i = l r a i sum=\sum_{i=l}^{r}a_i sum=i=lrai,可以用 v a l val val来评定价值:
v a l = { r − l + 1 s u m > 0 0 s u m = 0 − ( r − l + 1 ) s u m < 0 val=\begin{cases} r-l+1 & sum>0 \\ 0 & sum=0 \\ -(r-l+1) & sum<0 \\ \end{cases} val=rl+10(rl+1)sum>0sum=0sum<0
问这个数组划分为若干个连续子段后,价值总和最大为多少?


分析

o ( n 2 ) 暴 力 D P o(n^2)暴力DP o(n2)DP

d p [ i ] dp[i] dp[i]表示前 i i i的元素的最优解, d p [ i ] dp[i] dp[i]的递推公式为: d p [ i ] = m a x (   d p [ j ] + v ( j + 1 , i )   ) j < i dp[i]=max(\ dp[j]+v(j+1,i)\ )_{j<i} dp[i]=max( dp[j]+v(j+1,i) )j<i
v ( j , i ) v(j,i) v(j,i)表示子段 a j a_j aj a i a_i ai v a l val val
但在第二个测试点就会被超时卡住。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
#define fir(i, a, b) for (ll i = (a); i <= (b); i++)
const int N=5e5+5;
ll t,n,ans,a[N],b[N],dp[N];
inline ll f(ll l,ll r){
	ll tmp=b[r]-b[l-1];
	if(tmp>0)return r-l+1;
	else if(!tmp)return 0;
	else return -(r-l+1);
}
int main(){
	cin>>t;
	while(t--){
		cin>>n;
		memset(b,0,sizeof b);
		fir(i,1,n){
			dp[i]=-INT_MAX;
			cin>>a[i];
			b[i]=b[i-1]+a[i];
		}
		ans=dp[1]=f(1,1);
		fir(i,2,n){
			fir(j,1,i){
				dp[i]=max(dp[i],dp[j-1]+f(j,i));
			}
		}	
		cout<<dp[n]<<endl;
	}
	return 0;
}

o ( n l o g n ) o(nlogn) o(nlogn)

之前的思路时间复杂度为 O ( n 2 ) O(n^2) O(n2),因为我们求解 d p [ i ] dp[i] dp[i]依靠的是对之前结果的遍历,所以我们需要对这一部分的时间复杂度进行优化。

讨论优化

先将 d p [ i ] dp[i] dp[i]根据 a i a_{i} ai的值进行初始化

if(a[i]>0)dp[i]=dp[i-1]+a[i];
else if(!a[i])dp[i]=dp[i-1];
else dp[i]=dp[i-1]-1; 

//存疑
在这里插入图片描述

由上可知,我们的目标是从满足 v a l > 0 val>0 val>0的子段中,选出最大的 d p [ j ] − j    ( j < i ) dp[j]-j\ \ (j<i) dp[j]j  (j<i)
我们可以参考逆序对树状数组解法
大思路:
假设维护一个前缀和数组 a n s ans ans,我们是依序处理的,处理完 i i i后,就把 a n s [   p r e [ i ]   ] ans[\ pre[i]\ ] ans[ pre[i] ]以及后面的每个位置加一,这样做我们在处理 i i i时可以直接通过 a n s [ i ] ans[i] ans[i]得知是否存在一个 v a l > 0 val>0 val>0的子段。
但仅得知是否存在不足以用来求解,我们不妨直接用 a n s [   p r e [ i ]   ] ans[\ pre[i]\ ] ans[ pre[i] ]来维护前缀中最大的 d p [ i ] − i dp[i]-i dp[i]i

但此时还有两个问题需要解决:
1. p r e [ i ] pre[i] pre[i]范围过大,无法只通过下标来表示;
2.修改前缀最大值的方法需要优化,否则依然超时。

对于问题1,我们可以采取离散化的方法,对 a a a的前缀和进行离散化;
对于问题2,我们可以采取树状数组或者线段树的方法

需要注意的是,我们采取的离散化中,对于前缀和相同的位置,选择范围更大的前缀和的排序序号更小。
若采取其他的离散化方法,比如说让前缀和值相同的位置的排序序号相同,或者是范围更小的序号更小,这些会导致我们对 v a l = 0 val=0 val=0的情况讨论出错。

不进行讨论,依照公式

如果不对 v a l ≤ 0 val\leq0 val0的子段进行讨论,也可以依照类似的思路,维护三棵线段树。

对于一个始于 a j + 1 a_{j+1} aj+1并以 a i a_i ai为结尾的子段 s s s来说:
v a l < 0 val<0 val<0 d p [ i ] = m a x ( d p [ i ] ,   d p [ j ] + j − i ) dp[i]=max(dp[i],\ dp[j]+j-i) dp[i]=max(dp[i], dp[j]+ji);
v a l = 0 val=0 val=0 d p [ i ] = m a x ( d p [ i ] ,   d p [ j ] ) dp[i]=max(dp[i],\ dp[j]) dp[i]=max(dp[i], dp[j])
v a l > 0 val>0 val>0, d p [ i ] = m a x ( d p [ i ] ,   d p [ j ] + i − j ) dp[i]=max(dp[i],\ dp[j]+i-j) dp[i]=max(dp[i], dp[j]+ij)

线段树做法

代码

//不考虑val<=0,只使用一个树状数组
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
#define fir(i, a, b) for (ll i = (a); i <= (b); i++)
const int N=5e5+5;
ll t,n,a[N],pre[N],ord[N],ans[N],dp[N];
ll lowbit(ll x) {
    return x&(-x);
}
void add(ll pos,ll val) {
    while (pos<=n) {
        ans[pos]=max(ans[pos], val);
        pos+=lowbit(pos);
    }
}
ll ask(ll pos) {
    ll val=-INT_MAX;
    while (pos) {
        val=max(ans[pos], val);
        pos-=lowbit(pos);
    }
    return val;
}
int main(){
	ios_base::sync_with_stdio(false);
    cin.tie(0);
	cin>>t;
	while(t--){
		vector<pair<ll, ll> >v;
   		cin>>n;
   		fir(i,1,n){
   			cin>>a[i];
   			pre[i]=a[i]+pre[i-1];
   			ans[i]=-INT_MAX;
   			v.push_back({pre[i],-i});
		}
		sort(v.begin(), v.end());//对前缀和升序排序,但前缀和相同时,将范围大的排在前方 
        for (int i=0;i<n;i++){
            ord[-v[i].second]=i+1; // 下标表示前缀和范围,此时标出了每段前缀和的排序序号
        }
        fir(i,1,n){
        	dp[i]=(dp[i-1]+(a[i]<0 ? -1 : a[i]>0 ? 1 : 0));
        	if(pre[i]>0)dp[i]=i;
        	dp[i]=max(dp[i],ask(ord[i])+i);
        	add(ord[i],dp[i]-i);//add,更新,对于该位置来说,之前的都是比他小的 ,参考求逆序对数采取树状数组+离散 
		}
		cout<<dp[n]<<endl;
   }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值