题目
问题描述
给定一个长度为
n
n
n的数组
a
a
a,共有
2
n
−
1
2^{n-1}
2n−1种策略将其划分为连续的段,对于一段
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=⎩⎪⎨⎪⎧r−l+10−(r−l+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 val≤0的子段进行讨论,也可以依照类似的思路,维护三棵线段树。
对于一个始于
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]+j−i);
若
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]+i−j)
代码
//不考虑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;
}