Educational Codeforces Round 81 E. Permutation Separation(dp+线段树优化)

传送门

题意:
给定一个排列p,每个位置有一个花费a[i],先可以在任意位置把p切开成头尾两部分,非空,然后从两个部分中选几个数放到另一个部分,使得左边集合的每个数都小于右边集合的最小值(某个集合为空也符合要求)。 移动第i个数花费为a[i],求最小花费。
(1<=n<=2e5)

  • 我们观察发现,最终两个集合的答案只能是这样 1 2...m ; m+1,m+2..n

  • 于是,我们可以枚举分割点t(t在左边),再枚举左边集合的最大值m,[1,t]中找到大于m的元素给他们移到右边去,[t+1,n]中找到小于等于m的元素移到左边去。
    总共O(n³)计算答案。

  • 考虑优化,我们发现,如果我们记f[i]为左边最大值为i时的答案,当我们枚举了分割点t求出f[]数组,我们是可以转移得到分割点为t+1时的f[]数组答案的:

  • 对于i>=p[t+1],f[i]=f[i]-a[t+1] 因为之前需要花费a[t+1]把p[t+1]移动到左边,而现在分割完后p[t+1]已经在左边了,所以不需要花费这个代价。
    对于i<p[t+1],f[i]=f[i]+a[t+1] 对于这些状态,p[t+1]应该在右边集合才对,在t处分割后已经在右边了,而在t+1处分割后位于左边,所以需要多话费a[t+1]。

每枚举一个分割点,(从1到n-1),就对f[]数组求一遍最小值,再用最小值去更新答案,也是取min。

我们需要一个可以维护最小值,维护区间加法的数据结构,可以用线段树。

具体做法:
先暴力求出分割点为1的时候的答案(枚举左边最大值),用线段树维护f[i]。

然后枚举分割点,转移,每次都是区间更新。

#include<bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(2)
#define ull unsigned long long
#define ll long long
#define pii pair<int, int>
const int maxn = 2e5 + 10;
const ll mod = 998244353;
const ll inf = (ll)4e16+5;
const int INF = 1e9 + 7;
const double pi = acos(-1.0);
ll inv(ll b){if(b==1)return 1;return(mod-mod/b)*inv(mod%b)%mod;}
inline ll read()
{
    ll x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
struct node 
{
    #define lc rt<<1
    #define rc rt<<1|1
    int l,r;
    ll val;
}tree[maxn<<2];
ll tag[maxn<<2];
inline void pushup(int rt) 
{
    tree[rt].val=min(tree[lc].val,tree[rc].val);
}
inline void build(int rt,int l,int r)
{
    tree[rt].l=l,tree[rt].r=r;
    if(l==r) return ;
    int mid=l+r>>1;
    build(lc,l,mid);build(rc,mid+1,r);
}
inline void change(int rt,ll v) 
{
    tag[rt]+=v;
    tree[rt].val+=v;
}
inline void pushdown(int rt)
{
    if(tag[rt]) 
    {
        change(lc,tag[rt]);
        change(rc,tag[rt]);
        tag[rt]=0;
    }
}
inline void upd(int rt,int vl,int vr,ll v) 
{
    int l=tree[rt].l,r=tree[rt].r;
    if(r<vl || l>vr) return ;
    if(vl<=l && r<=vr) 
    {
        change(rt,v);
        return ;
    }
    int mid=l+r>>1;
    pushdown(rt);
    upd(lc,vl,vr,v);upd(rc,vl,vr,v);
    pushup(rt);
}
int p[maxn];
ll a[maxn];
int n;
ll pre[maxn];//1 2 3..i这些数移动的代价之和 不是a的前缀和
int main()
{
    scanf("%d",&n);
    build(1,0,n);
    for(int i=1;i<=n;i++) 
    {
        scanf("%d",p+i);
    }
    for(int i=1;i<=n;i++) 
    {
        scanf("%lld",a+i);
        pre[p[i]]=a[i];
    }
    for(int i=1;i<=n;i++) pre[i]+=pre[i-1];
    //当前分割点是1  枚举左边最大值求出答案 nlogn
    //假设分割点是p[1] 排列被分成了 p[1] / p[2]p[3]..p[n]
    for(int i=0;i<=n;i++)//左边最大值为i 
    {
		if(i<p[1]) //左边应该有1 2 ...i
        	upd(1,i,i,pre[i]+a[1]);
		else upd(1,i,i,pre[i]-a[1]);
    }
    //分割点不能为0  所以此时不能去更新ans
    ll ans=tree[1].val;
    for(int t=2;t<n;t++)//枚举分割点
    {
        int d=p[t];
        upd(1,0,d-1,a[t]);
        upd(1,d,n,-a[t]);
        ans=min(ans,tree[1].val);
    }
    //空集的情况 就是线段树中0 n的位置
    printf("%lld\n",ans);
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值