POJ1821 Fence

传送门

这道题是一道很好的单调队列优化DP的例子。

题目大意是有n个工人,每个人可以粉刷一段长度不超过l[i]的墙,如果一个人粉刷了那么他必须要粉刷第s[i]块墙,一个人粉刷一块墙能得到p[i]的钱,求所有工人得到的钱的最大值。

我们首先把所有工人按s[i]排序,这样方便我们线性DP。

考虑DP,用dp[i][j]表示前i个工人刷j块的得到的钱的最大值,那么我们分类讨论,首先是一个人不刷和一块墙不刷的情况,那么就有dp[i][j] = max(dp[i-1][j],dp[i][j-1]);

之后,我们考虑这个工人刷墙的情况。因为这个人必须要刷s[i],那这个人的转移必然是从s[i] - l[i] ~ s[i]这一段转移过来的。否则的话就成了不合法情况。那么就有dp[i][j] = max{dp[i-1][k] + (j - k) * p[i]} (s[i] - l[i] <= k <= s[i])

这个式子我们朴素的做法是先枚举i,对于每一个工人i,枚举它粉刷的块数j,之后再枚举转移的范围。不过这样会超时,我们考虑优化。我们发现如果把j,k分离出来,那么对于每一个j(状态),它对应的j * p[i]是一个定值,只有内部的式子随着k(决策)的变化而改变,而且其实对于每一个j,有很大一部分的可选取的决策区间是重复的。

说到这里,我们就想到用单调队列维护啦!!那么我们的想法就是,对于每一个工人,我们首先把s[i] - l[i] ~ s[i]这一段区间之内的所有决策压入单调队列,之后随着j的增加,所能选取的k的区间左端点随之变大。也就是说实际上,我们只要维护一段左端点变大,右端点不动的区间最大值就可以了。

所以一般来说,单调队列优化DP的套路就是,如果一个DP方程能被拆成每一个状态都是从一个决策区间中选取最优的转移,然后决策区间随着状态变化左右端点变化,而且有大量重复,我们就可以先拆分式子,拆成内层只与决策有关,这样直接用单调队列维护决策的极值,就省去了一层循环,优化了时间。

看一下代码。

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cmath>
#include<set>
#include<queue>
#define rep(i,a,n) for(int i = a;i <= n;i++)
#define per(i,n,a) for(int i = n;i >= a;i--)
#define enter putchar('
')

using namespace std;
typedef long long ll;
const int M = 10005;
const int INF = 1000000009;

int read()
{
   int ans = 0,op = 1;
   char ch = getchar();
   while(ch < '0' || ch > '9')
   {
      if(ch == '-') op = -1;
      ch = getchar();
   }
   while(ch >= '0' && ch <= '9')
   {
      ans *= 10;
      ans += ch - '0';
      ch = getchar();
   }
   return ans * op;
}

struct worker
{
   int l,p,s;
   bool operator < (const worker &g) const
   {
      return s < g.s;
   }
}a[105];

int n,k,dp[105][20005],q[20005],head,tail;

int calc(int i,int x)
{
   return dp[i-1][x] - x * a[i].p;
}

int main()
{
   n = read(),k = read();
   rep(i,1,k) a[i].l = read(),a[i].p = read(),a[i].s = read();
   sort(a+1,a+1+k);
   rep(i,1,k)
   {
      head = 1,tail = 0;
      rep(j,max(0,a[i].s-a[i].l),a[i].s-1)
      {
     while(head <= tail && calc(i,q[tail]) < calc(i,j)) tail--;
     q[++tail] = j;
      }
      rep(j,1,n)
      {
     dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
     if(j >= a[i].s)
     {
        while(head <= tail && q[head] < j - a[i].l) head++;
        if(head <= tail) dp[i][j] = max(dp[i][j],calc(i,q[head]) + j * a[i].p);
     }
      }
   }
   printf("%d
",dp[k][n]);
   return 0;
}