这是第一次接触单调队列优化DP,心想借这道题熟悉一下单调队列优化DP的写法,并加深一下对此的理解。
理解是有了,但当被这道题卡了一天后,理解都要忘得差不多了。
有一些DP的状态转移是从之前的某些状态中取一个最值,那么它就有可能用单调队列来优化。比如这道题,我们可以轻易的想出状态表示:f[i][j]表示第i个步骤,在第j台机器上完成时,前i个步骤的最小花费。对于状态转移,有两种思路,其中一种是f[i][j] = min{f[i-1][j']+t[i][j]},当然我没有把k写进去,还要多开一个数组记录l,所以这种转移比较麻烦,而且也与单调队列无关。一开始是想到的这个思路,不过本着熟悉单调队列优化DP,我在看了神犇的文档后写的另一种:f[i][j] = min{f[i'][j']+sum[i][j]-sum[i'][j]}。sum[i][j]表示前i个步骤在第j台机器上完成时的总花费。
在这个状态转移中对单调队列优化DP的条件有了一些理解,原始的上面的转移时不可以用单调队列优化的,变形后:f[i][j] = min{f[i'][j']-sum[i'][j]}+sum[i][j],这个原本是不符合单调队列优化的条件的,但是因为j很小,最多只有5个,所以我们对于每个j搞一个单调队列就算是符合条件了。对于每一个单调队列q,用于判断的值是f[i][j]-sum[i][q],出队条件是当前i与队首元素差值大于l。
就我的理解,我们搞m个单调队列的本质是把j变成了常量,状态转移中只有一个变量,而min之外的元素对之内的元素没有影响,或者没有交叉的地方,就能把min之内的作为权值搞单调队列优化,比如说f[i][j] = min{f[i'][j']+sum[i][j]}-sum[i'][j],就不可以了,因为这个决策的最优性会受sum[i'][j]影响,而sum[i][j]是受当前i值影响的,不能放到单调队列中。引用一段比较科学的话:
// → → 代码暂时未AC,不粘。 后5个点能过前5个点齐刷刷wa掉也是醉了。 9.09
表示刚刚AC,还有点迷茫,卡了2天的题终于AC。
在网上搜很多很多的题解,发现他们写的十分令蒟蒻不理解,各种宏,但是单调队列与DP部分基本无异,所以搜题解并没有实质性的改善。终于,在某神犇的博客上看到了一句话:每次转移要先更新本次的f数组,然后再去更新单调队列。否则会挂前5个。
于是就将原来的:
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++){
while(h[j] < t[j] && i-i2 > l) h[j]++;
f[i][j] = f[i2][j2] - sum[i2][j] + sum[i][j] + k;
for(int y = 1; y <= m; y++){
if(y == j) continue;
while(h[y] < t[y] && f[i][j]-sum[i][y] < f[i3][j3]-sum[i3][y]) t[y]--;
q[y[t[y]++] = i*10+j;
}
}
改成了:
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
while(h[j] < t[j] && i-i2 > l) h[j]++;
f[i][j] = f[i2][j2] - sum[i2][j] + sum[i][j] + k;
}
for(int j = 1; j <= m; j++){
for(int y = 1; y <= m; y++){
if(y == j) continue;
while(h[y] < t[y] && f[i][j]-sum[i][y] < f[i3][j3]-sum[i3][y]) t[y]--;
q[t[y]++][y] = i*10+j;
}
}
}
#include <cstdio>
#include <algorithm>
#include <cstring>
#define M 100001
#define i2 q[h[j]][j]/10
#define j2 q[h[j]][j]%10
#define i3 q[t[y]-1][y]/10
#define j3 q[t[y]-1][y]%10
using namespace std;
int n, m, k, l, ans = 1<<30;
int sum[M][6], f[M][6];
int q[M][6], h[6], t[6];
int main()
{
scanf("%d %d %d %d", &n, &m, &k, &l);
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++){
scanf("%d", &sum[j][i]);
sum[j][i] += sum[j-1][i];
}
memset(f, 0x3f, sizeof f);
f[0][0] = 0;
for(int i = 1; i <= m; i++) t[i]++;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
while(h[j] < t[j] && i-i2 > l) h[j]++;
f[i][j] = f[i2][j2] - sum[i2][j] + sum[i][j] + k;
}
for(int j = 1; j <= m; j++){
for(int y = 1; y <= m; y++){
if(y == j) continue;
while(h[y] < t[y] && f[i][j]-sum[i][y] < f[i3][j3]-sum[i3][y]) t[y]--;
q[t[y]++][y] = i*10+j;
}
}
}
for(int i = 1; i <= m; i++)
ans = min(ans, f[n][i]);
printf("%d", ans-k);
return 0;
}
注:我的做法略奇葩,最开始在m条队列里的都只是f[0][0] = 0,在第一次转移的时候加上了K,但是第一个步骤的执行时不需要K的,所以最后减去K;队列中我保存的是i*10+k,这样就少开了一个数组,感觉更清晰一点。