洛谷 P2389 电脑班的裁员 解题报告

题意:

给定一段长为N的序列,选取其中的至多M段使这些子段和最大。


当N=1000时,我们可以采用动态规划解法

\(dp[i][j][k]\)代表当前选至位置\(i\)处于第\(j\)段当前是否选取(1选0不选)

则转移为
\(dp[i][j][0]=max(dp[i-1][j][1],dp[i-1][j][0])\)
\(dp[i][j][1]=max(dp[i-1][j-1][0],dp[i-1][j][1])+score[i]\)

其中\(i\)的一维可以滚动掉

Code:

#include <cstdio>
#include <cstring>
int max(int x,int y){return x>y?x:y;}
const int N=503;
const int inf=0x3f3f3f3f;
int dp[N][N][2],n,k,score[N];
void init()
{
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++)
        scanf("%d",score+i);
    memset(dp,-0x3f,sizeof(dp));
    dp[1][0][0]=0,dp[1][1][1]=score[1];
}
void work()
{
    for(int i=2;i<=n;i++)
        for(int j=0;j<=k;j++)
        {
            dp[i][j][0]=max(dp[i-1][j][1],dp[i-1][j][0]);
            dp[i][j][1]=max(dp[i-1][j-1][0],dp[i-1][j][1])+score[i];
        }
    int ans=-inf;
    for(int i=0;i<=k;i++)
        ans=max(ans,max(dp[n][i][0],dp[n][i][1]));
    printf("%d\n",ans);
}
int main()
{
    init();
    work();
    return 0;
}


当N=100,000时,我们可以采用贪心解法
注意到每次选取时,一段连续的正数或者连续的负数,要么我们不取选它,要么全部选走。

我们先缩个点,使序列变成正负数交错的,其中,第一项和最后一项的负数我们一定不去选择,故舍去。

设当前有\(k\)个正数。

则当\(M>=k\)时,直接选取\(k\)个正数即可。

\(M<=k\)时,对每个正数,我们有两种选择,放弃它,或者跨过负数选择它。

则当选中一个负数\(a\)时,我们损失了\(-a\),当放弃一个正数\(b\)时,我们损失了\(b\)

则不管哪种情况,我们只需要找到绝对值最小的一个数,选择它或者放弃它

采用二叉堆维护这个最小值

当这个最小值被操作时,其实等价于新产生了一个权值为 它和它旁边的两个元素的权值和 的新元素,这时候产生的新数列一定会少一个正数

值得一提的是,当第一项和最后一项是负数的时候,不一定一定减少一个正数,所以我们根据贪心不去选择这些点即可

合并元素可以采用链表进行维护

Code:(实在是不好看QAQ)

#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
#define ll long long
#define P pair <ll,ll>
using namespace std;
const int N=1000010;
ll a[N],b[N],n0,k,n,pre[N],suc[N],cnt,used[N];
ll labs(ll x){return x>0?x:-x;}
void init()
{
    scanf("%lld%lld",&n0,&k);
    for(int i=1;i<=n0;i++)
        scanf("%lld",a+i);
    int k=0;a[0]=-1;
    while(a[k]<0) k++;
    for(int i=k;i<=n0;i++)
    {
        if(a[i]*a[i-1]>0)
            b[n]+=a[i];
        else
            b[++n]=a[i];
    }
    while(n&&b[n]<0) n--;
    for(int i=1;i<=n;i++)
    {
        pre[i]=i-1;
        suc[i]=i+1;
    }
    suc[0]=1;
    suc[n]=0;
}
priority_queue <P,vector <P >,greater <P > > q;
P p;
void work()
{
    for(int i=1;i<=n;i++)
    {
        if(b[i]>0) cnt++;
        p.first=labs(b[i]);
        p.second=i;
        q.push(p);
    }
    while(cnt>k)
    {
        int pos=q.top().second;
        q.pop();
        if(used[pos]) continue;
        if(!suc[pos]&&b[pos]<0)
        {
            suc[pre[pos]]=suc[pos];
            pre[suc[pos]]=pre[pos];
            continue;
        }
        if(!pre[pos]&&b[pos]<0)
        {
            pre[suc[pos]]=pre[pos];
            suc[pre[pos]]=suc[pos];
            continue;
        }
        used[pre[pos]]=used[suc[pos]]=1;
        b[pos]+=b[pre[pos]]+b[suc[pos]];
        cnt--;
        if(pre[pos])
        {
            pre[pos]=pre[pre[pos]];
            suc[pre[pos]]=pos;
        }
        if(suc[pos])
        {
            suc[pos]=suc[suc[pos]];
            pre[suc[pos]]=pos;
        }
        p.first=labs(b[pos]),p.second=pos;
        q.push(p);
    }
    int now=suc[0];
    ll ans=0;
    while(now)
    {
        if(b[now]>0) ans+=b[now];
        now=suc[now];
    }
    printf("%lld\n",ans);
}
int main()
{
    init();
    work();
    return 0;
}

当N=1,000,000时,我们可以将算法近似优化到\(O(N)\)

具体大家可以参考出题人的题解

我在这里解释一下这个近似,原题解没有说道找到第\(k\)值的问题

主要就是利用基于快速排序思想的STL函数\(nth\_element\),可以在近似\(O(n)\)的复杂度找到第\(k\)

关于第k值

代码我没写,感觉不太好写

posted @ 2018-07-08 15:58  露迭月  阅读(154)  评论(0编辑  收藏  举报