Codeforces #783 div2 D. Optimal Partition(dp+离散化+树状数组维护)

题目链接

https://codeforces.com/contest/1668/problem/D

题意

给出一个长度为n的数组a,现在需要将该数组分成任意段
设s为 [ a l , a r ] [a_l,a_r] [al,ar]中各元素之和,即 a l + . . . + a r a_l+...+a_r al+...+ar
对于每一段 [ a l , a r ] [a_l,a_r] [al,ar],其val为:

v a l = r − l + 1 ( s > 0 ) val=r-l+1(s>0) val=rl+1(s>0)
v a l = 0 ( s = = 0 ) val=0(s==0) val=0(s==0)
v a l = r − l + 1 ( s > 0 ) val=r-l+1 (s>0) val=rl+1(s>0)

现在需要求出分成任意段后,各段val值的和最大是多少

分析

这题需要进行状态转移, d p [ i ] dp[i] dp[i] 表示前 i i i个元素分段后得到的最大val
不过,对于怎么转移方程,则需要分情况分析了

子段和小于0

结论: 子段和小于0的段,其长度最大只会为1
分析:
若该子段长度大于1
如果其中全是负数,则将其拆成全为1的小段,得到的总val值不变
如果其中存在0或者正数,拆成长度为1的小段肯定能得到更优的总val值

子段和为0

结论: 子段和为0的段,其长度最长也只能会是1
分析:
若该子段长度大于1,我们可以将子段分成长度相等的两段
a a . . . a b b . . . b aa...abb...b aa...abb...b 或者 a a . . . a k b b . . . b aa...akbb...b aa...akbb...b(a、b数量相等)
如果a段b段的和都为0,则我们可以将这段继续拆分下去

如果在前一半中存在前缀和小于0的位置,例如: a a . ∣ . . a b b . . . b aa. | ..abb...b aa...abb...b
|的前段前缀和小于0,那么我们可以得到|的后端的和就是大于0的

同理,如果在后一半中存在后缀和小于0的位置,例如: a a . . . a b b . ∣ . . b aa...abb. | ..b aa...abb...b
|的后面的和小于0,那么我们可以得到|的前端的和就是大于0的

那么我们将该段从 | 的位置分开,得到的val值就肯定大于0,比直接将整段看作0得到的val值更优

将每个段分下去就会发现,最后等于0的段只会是长度为1的 a [ i ] a[i] a[i] 为0的段

子段和大于0

对于子段和大于0的段,我们则可以直接进行dp转移
例如对于 . . . j [ . . . i ] ...j[...i] ...j[...i],对于 [ ] [ ] []内的段的状态转移为: d p [ i ] = d p [ j ] + ( i − j ) dp[i]=dp[j]+(i-j) dp[i]=dp[j]+(ij)
稍微移一下项: d p [ i ] = ( d p [ j ] − j ) + i dp[i]=(dp[j]-j)+i dp[i]=(dp[j]j)+i
可以发现,当前情况只需在确保子段和大于0的条件下,从 d p [ j ] − j dp[j]-j dp[j]j 最大转移过来即可

对此,我们可以对前缀和进行离散化,使用树状数组对 d p [ j ] − j dp[j]-j dp[j]j进行维护,每次求小于当前前缀和(保证差值大于0)的位置中, d p [ j ] − j dp[j]-j dp[j]j 的最大值对 d p [ i ] dp[i] dp[i] 进行更新

代码

#include<bits/stdc++.h>
#define ll long long
#define endl '\n'
#define PII pair<int,int>
using namespace std;
const int inf=0x3f3f3f3f;
const int mod=1e9+7;
const double eps=1e-8;
const int N=5e5+5;
int n,cnt,dp[N],c[N];
ll a[N],pre[N];
set<ll> st;
map<ll,int> id;
void add(int x,int k)
{
    while(x<=n)
        c[x]=max(c[x],k),x+=(x&-x);
}
int query(int x)
{
    int cnt=-inf;
    while(x>0)
        cnt=max(cnt,c[x]),x-=(x&-x);
    return cnt;
}
void solve()
{
    cin>>n;
    cnt=0;   //初始化
    st.clear(); id.clear();
    for(int i=1;i<=n;i++)
        dp[i]=-inf,c[i]=-inf;
    st.insert(0);
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        pre[i]=pre[i-1]+a[i];
        st.insert(pre[i]);
    }
    for(ll p:st)  //对前缀和进行离散化
        id[p]=++cnt;
    add(id[0],0);   //将起始无元素状态时的0状态加入树状数组中
    for(int i=1;i<=n;i++)
    {
        if(a[i]<0)
            dp[i]=dp[i-1]-1;
        else if(a[i]==0)
            dp[i]=dp[i-1];
        dp[i]=max(dp[i],query(id[pre[i]]-1)+i);
        add(id[pre[i]],dp[i]-i);
    }
    cout<<dp[n]<<endl;
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);
    int __=1;
    cin>>__;
    while(__--)
        solve();
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值