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;
}