BZOJ2006 [NOI2010]超级钢琴(划分树+堆)

【题解】

(划分树做法)

要求出长度为l~r的前k大连续和,可以转化为k次求第i大连续和,这与划分树的作用有关 

先把前缀和预处理出来。一段连续和为 S(j)-S(i),固定i之后为 S(i+len)-S(i-1) (i为起点,len为长度,len∈[l,r])
这里面,若i一定,len就在固定区间内变动,连续和的k大值对应S(i+len)的k大值,问题就转化为"求给定区间的k大值",就可以用划分树了 
但是起点有多个,而题目中"k次求第i大连续和"没办法真的快速做到一次求一个 
可以先求出对于每个起点的区间内最大连续和来预备着,把它们存到堆里,每次询问时取出堆顶并放入这个起点对应的次大和即可 

因此题目转化为:求 S(i+l-1)-s(i-1),S(i+l)-s(i-1),…,S(i+r-1)-s(i-1) (i∈[1,n-l+1])中前k大的值 
Ⅰ首先,建一个大根堆。对于每个i,求出max{ S(i+l-1),S(i+l),…,S(i+r-1) }-s(i-1)加入堆中 
Ⅱ然后,对于k次询问,每次取出堆顶,看这个堆顶是由哪个i得到的,再把这个i对应的{S(i+l-1),S(i+l),…,S(i+r-1)}-s(i-1)的次大值加入堆 

时间复杂度:划分树预处理_O( n*log(n) ) +Ⅰ_O( n*log(n) ) +Ⅱ_O( k*log(n) )


【代码】

#include<stdio.h>
#include<stdlib.h>
typedef long long LL;
struct node
{
    int v[500005],num[500005];
};
node T[20];
int s[500005]={0},p[500005]={0},heap[500005]={0},pos[500005]={0};
int ph=0;
int min(int a,int b)
{
    if(a<b) return a;
    return b;
}
void jh(int* a,int* b)
{
    int t=*a;
    *a=*b;
    *b=t;
}
void jh_heap(int a,int b)
{
    jh(&heap[a],&heap[b]);
    jh(&pos[a],&pos[b]);
}
void kp(int low,int high)
{
    int i=low,j=high,mid=s[(i+j)/2];
    while(i<j)
    {
        while(s[i]<mid) i++;
        while(s[j]>mid) j--;
        if(i<=j)
        {
            jh(&s[i],&s[j]);
            i++;
            j--;
        }
    }
    if(j>low) kp(low,j);
    if(i<high) kp(i,high);
}
void build(int left,int right,int deep)
{
    int mid=(left+right)/2,ln=left,rn=mid+1,isame=mid-left+1,same=0,i;
    if(left==right) return;
    for(i=left;i<=right;i++)
        if(T[deep].v[i]<s[mid]) isame--;
    for(i=left;i<=right;i++)
    {
        if(i==left) T[deep].num[i]=0;
        else T[deep].num[i]=T[deep].num[i-1];
        if(T[deep].v[i]<s[mid])
        {
            T[deep+1].v[ln++]=T[deep].v[i];
            T[deep].num[i]++;
        }
        if(T[deep].v[i]>s[mid]) T[deep+1].v[rn++]=T[deep].v[i];
        if(T[deep].v[i]==s[mid])
        {
            same++;
            if(isame>=same)
            {
                T[deep+1].v[ln++]=T[deep].v[i];
                T[deep].num[i]++;
            }
            else T[deep+1].v[rn++]=T[deep].v[i];
        }
    }
    build(left,mid,deep+1);
    build(mid+1,right,deep+1);
}
int cx(int x,int y,int k,int left,int right,int deep)
{
    int mid=(left+right)/2,before,have,b,h,tx,ty,fk=y-x+1+1-k;
    if(left==right) return T[deep].v[left];
    if(x==left)
    {
        before=0;
        have=T[deep].num[y];
    }
    else
    {
        before=T[deep].num[x-1];
        have=T[deep].num[y]-T[deep].num[x-1];
    }
    if(fk<=have)//左 
    {
        tx=left+before;
        ty=left+before+have-1;
        k=ty-tx+1+1-fk;
        return cx(tx,ty,k,left,mid,deep+1);
    }
    else//右 
    {
        b=x-1-left-before+1;
        h=y-x+1-have;
        tx=mid+1+b;
        ty=mid+1+b+h-1;
        k=ty-tx+1+1-(fk-have);
        return cx(tx,ty,k,mid+1,right,deep+1);
    }
}
void tj(int x,int t)
{
    int i;
    heap[++ph]=t;
    pos[ph]=x;
    for(i=ph;i!=1;i/=2)
    {
        if(heap[i]>heap[i/2]) jh_heap(i,i/2);
        else return;
    }
}
void sc()
{
    int i=1;
    jh_heap(1,ph);
    ph--;
    while(i*2<=ph)
    {
        i*=2;
        if(i+1<=ph&&heap[i+1]>heap[i]) i++;
        if(heap[i]>heap[i/2]) jh_heap(i,i/2);
        else return;
    }
}
int main()
{
    LL ans=0;
    int n,k,l,r,i,x;
    scanf("%d%d%d%d",&n,&k,&l,&r);
    for(i=1;i<=n;i++)
    {
        scanf("%d",&s[i]);
        s[i]+=s[i-1];
        T[0].v[i]=s[i];
    }
    T[0].v[0]=0;
    kp(1,n);
    build(1,n,0);
    for(i=1;i<=n-l+1;i++)
    {
        p[i]=1;
        tj(i,cx(i+l-1,min(n,i+r-1),1,1,n,0)-T[0].v[i-1]);
    }
    for(i=1;i<=k;i++)
    {
        ans+=(LL)heap[1];
        x=pos[1];
        sc();
        p[x]++;
        if((x+l-1)+p[x]-1<=min(n,x+r-1)) tj(x,cx(x+l-1,min(n,x+r-1),p[x],1,n,0)-T[0].v[x-1]);
    }
    printf("%lld",ans);
    return 0;
}


题目描述 有一个 $n$ 个点的棋盘,每个点上有一个数字 $a_i$,你需要从 $(1,1)$ 走到 $(n,n)$,每次只能往右或往下走,每个格子只能经过一次,路径上的数字和为 $S$。定义一个点 $(x,y)$ 的权值为 $a_x+a_y$,求所有满足条件的路径中,所有点的权值和的最小值。 输入格式 第一行一个整数 $n$。 接下来 $n$ 行,每行 $n$ 个整数,表示棋盘上每个点的数字。 输出格式 输出一个整数,表示所有满足条件的路径中,所有点的权值和的最小值。 数据范围 $1\leq n\leq 300$ 输入样例 3 1 2 3 4 5 6 7 8 9 输出样例 25 算法1 (形dp) $O(n^3)$ 我们可以先将所有点的权值求出来,然后将其看作是一个有权值的图,问题就转化为了在这个图中求从 $(1,1)$ 到 $(n,n)$ 的所有路径中,所有点的权值和的最小值。 我们可以使用形dp来解决这个问题,具体来说,我们可以将这个图看作是一棵,每个点的父节点是它的前驱或者后继,然后我们从根节点开始,依次向下遍历,对于每个节点,我们可以考虑它的两个儿子,如果它的两个儿子都被遍历过了,那么我们就可以计算出从它的左儿子到它的右儿子的路径中,所有点的权值和的最小值,然后再将这个值加上当前节点的权值,就可以得到从根节点到当前节点的路径中,所有点的权值和的最小值。 时间复杂度 形dp的时间复杂度是 $O(n^3)$。 C++ 代码 算法2 (动态规划) $O(n^3)$ 我们可以使用动态规划来解决这个问题,具体来说,我们可以定义 $f(i,j,s)$ 表示从 $(1,1)$ 到 $(i,j)$ 的所有路径中,所有点的权值和为 $s$ 的最小值,那么我们就可以得到如下的状态转移方程: $$ f(i,j,s)=\min\{f(i-1,j,s-a_{i,j}),f(i,j-1,s-a_{i,j})\} $$ 其中 $a_{i,j}$ 表示点 $(i,j)$ 的权值。 时间复杂度 动态规划的时间复杂度是 $O(n^3)$。 C++ 代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值