cf #787 (div3)

链接

过了好久才补题……当时只有G没做出来。

[G]

题意:

给出\(n\)堆共\(m\)个煎饼\(a_1,a_2,\dots,a_n\),每个煎饼只能移动到它相邻的堆,问把序列\(a\)变成单调不减的序列最少要多少次操作。\(1 \leq n,m \leq 250\)

分析:

想到应该用DP,状态就是\(f[i][j][k]\)表示前\(i\)堆,最后一个数是\(j\),前面总和为\(k\)的最少操作次数。

但是没想出来怎么快速地转移,然后就没做出来。

题解正是这样DP。注意到:

$ f[i][j][k] = min_{0 \leq l \leq m-j} {f[i-1][j+l][k-j] } + add $

其中\(add\)是只与\(j\)\(k\)有关,与\(l\)无关的一个数!

所以可以通过维护一个 \(g[j][k] = min_{0 \leq l \leq m-j}{f[i-1][j+l][k-j]}\) 来实现\(O(1)\)转移。

问题在于\(add\)要怎么算。当前计算\(f[i][j][k]\),考虑第\(i\)堆新产生的代价,发现可以分为两种情况:

1.\(S[i] \geq k\),这里\(S[i]\)\(a[i]\)的前缀和。

那么前\(i-1\)堆已经处理好的情况下,第\(i\)堆新产生的代价就是把第\(i\)堆多余的煎饼放到后面去(因为这时前面\(i-1\)堆已经往第\(i\)堆上多堆了一些煎饼)。也就是\(add = S[i]-k\)

2.\(S[i] < k\)

这时需要考虑第\(i\)堆需要多少煎饼。

由于煎饼从后往前挪是按顺序的,先挪靠前的,再挪靠后的,所以我们新增一个辅助数组\(mv[k]\),表示让第\(1\)堆成为\(k\)个煎饼需要的代价。这样,如果我们要计算从后挪\(k\)个煎饼到第\(i\)堆的代价,就可以借助它(具体怎么借助后面再说)。

然后,我们要算一下当前第\(i\)堆到底还需要多少煎饼。设需要的煎饼数为\(need\),实际上有

\(need = min(k-S[i], j)\)

这是因为,此时前\(i\)个总共需要从后面挪\(k-S[i]\)个煎饼。如果\(k-S[i]>j\),说明在满足前\(i-1\)堆的需求时,已经把第\(i\)堆挪空了。所以此时第\(i\)堆需要\(j\)个煎饼。而如果\(k-S[i]\leq j\),则说明在满足前\(i-1\)堆的需求时,前\(i-1\)堆会挪一些煎饼给第\(i\)堆。这时候第\(i\)堆还需要的煎饼数只是\(k-S[i]\)

得到了\(need\),就可以想前面说的,利用\(mv[k]\)计算\(add\)了:

$add = mv[k] - mv[k-need] - (i-1)*need $

这个式子的意思是:把让第\(1\)堆成为\(k\)个煎饼需要挪的最后\(need\)个煎饼挪到第\(i\)堆。实际上跟第\(1\)堆没关系,我们只是要找出挪到第\(i\)堆的\(need\)个煎饼是哪些,然后算贡献。

之后转移即可。

代码如下
#include<bits/stdc++.h>
using namespace std;
int const N=255,inf=1e9;
int n,m,a[N],mv[N],mcnt,S[N],f[N][N][N],g[N][N];
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]); S[i]=S[i-1]+a[i];
        for(int j=1;j<=a[i];j++)
            mcnt++, mv[mcnt] = mv[mcnt-1] + (i-1); // mv[k]:让第1堆成为k个煎饼的代价
    }
    memset(f,0x3f3f,sizeof(f)); memset(g,0x3f3f,sizeof(g));
    for(int j=0;j<=a[1];j++) f[1][j][j] = a[1]-j;
    for(int j=a[1]+1;j<=m;j++) f[1][j][j] = mv[j];
    for(int j=m;j>=0;j--)
        for(int k=j;k<=m;k++)
            g[j][k] = min(g[j+1][k], f[1][j][k]);
    int add,need;
    for(int i=2;i<=n;i++)
    {
        for(int j=0;j<=m;j++)
            for(int k=j;k<=m;k++)
            {
                if(S[i]>=k) add = S[i] - k;
                else
                {
                    need = min(k-S[i], j); // i位置上需要得到的煎饼数
                    add = mv[k] - mv[k-need] - (i-1)*need; // add:把lend个从后面移动到i的代价(考虑后面已经有一些煎饼移到i之前了)
                }
                f[i][j][k] = g[j][k-j] + add;
            }
        for(int j=m;j>=0;j--)
        {
            for(int k=0;k<=m;k++) g[j][k]=inf; // 必需!
            for(int k=j*i;k<=m;k++)
                g[j][k] = min(g[j+1][k], f[i][j][k]);
        }
    }
    printf("%d\n",g[0][m]);
    return 0;
}

posted @ 2022-05-18 20:34  Zinn  阅读(29)  评论(0编辑  收藏  举报