单调队列优化DP

昨天在HDU上看到的这题 :  HDU 3401 Trade

题意 : 一个人一开始有很多钱,然后买股票,总共天数是T天,然后如果在i天买进(卖出)一部分股票的话,下一次操作至少要在W+1天后了,然后告诉你这只股票每天的买进和卖出价格,每天最多买进和卖出的股票数有一定的限制,一个人最多拥有的股票是MaxP,问你最后一天最多能够赚多少钱。

思路 : 其实转移方程感觉很裸,令DP[i][j]表示第i天拥有j只股票所能够获得的最大价值,然后DP[i][j] = max(dp[i-1][j],dp[i-W-1][k] + F(买进或者卖出一部分)); 其中DP[i-W-1]k]来源有两部分,一部分是k < j然后在i天时买进(j-k)只,即DP[i-W-1][k] - (j - k) * buy[i] ,另一部分是k > j 在i 天卖出了(k - j)只,即DP[i-W-1][k] + (k - j) * sell[i];

思路很清晰,但是因为朴素的DP的话复杂度在O(Maxp * T * T),这道题是肯定爆的。

后来看到有种叫做单调队列的东西,然后学了下,切了一道水题 : poj 2823 Sliding Window

单调队里做法 : 考虑在i 天买进了一些stocks : DP[i][j] = max(dp[i][j],dp[i-W-1][k] - (j - k) * buy[i]);

将这个状态转移方程变下形 : DP[i][j] = max(dp[i][j],{dp[i-W-1][k] + k * buy[i]} - j * buy[i]);

因为在考虑(i,j)这个状态时j * buy[i] 是不会变的,我们需要的就是找到一个k使得它的P(k) = DP[i-W-1][k] + k * buy[i]最大,也就是简化成了 : 在 k 属于 [j-i天最大能买多少stocks , j)这个范围下的最大P(k),所以我们只要开一个单调队里来维护即可,队首元素最大。同理另一种也是如此。


#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int INF = 9999999;
const int MAXN = 2005;


int n,W,MaxP,buy[MAXN],sell[MAXN],MaxB[MAXN],MaxS[MAXN];
int que[MAXN],tot[MAXN],head,tail;
int dp[MAXN][MAXN];

void init()
{
      for (int i = 0;i <= n;i++)
            for (int j = 0;j <= MaxP;j++)
            dp[i][j] = -INF;
      for (int i = 1;i <= W+1;i++)
            for (int j = 0;j <= min(MaxP,MaxB[i]);j++)
            dp[i][j] = - buy[i] * j;
}

int solve()
{
      init();
      for (int i = 1;i <= n;i++)
      {
            for (int j = 0;j <= MaxP;j++)
                  dp[i][j] = max(dp[i-1][j],dp[i][j]);
            if (i <= W + 1)continue;
            int cur = i - W - 1;

            head = tail = 0;
            for (int k = 0;k <= MaxP;k++)
            {
                  while (head != tail && que[tail] < dp[cur][k] + k * buy[i]) tail--;
                  que[++tail] = dp[cur][k] + k * buy[i]; tot[tail] = k;
                  while (tot[head+1] < k - MaxB[i]) head++;
                  dp[i][k] = max(dp[i][k],que[head+1] - k * buy[i]);
            }

            head = tail = 0;
            for (int k = MaxP;k >= 0;k--)
            {
                  while (head != tail && que[tail] < dp[cur][k] + k * sell[i]) tail--;
                  que[++tail] = dp[cur][k] + k * sell[i]; tot[tail] = k;
                  while (tot[head+1] > k + MaxS[i]) head++;
                  dp[i][k] = max(dp[i][k],que[head+1] - k * sell[i]);
            }
      }
      int ans = -INF;
      for (int i = 0;i <= MaxP;i++)ans = max(ans,dp[n][i]);
      return ans;
}

int main()
{
      int T;
      scanf("%d",&T);
      for (int cas = 1;cas <= T;cas++)
      {
            scanf("%d%d%d",&n,&MaxP,&W);
            for (int i = 1;i <= n;i++)
            {
                  scanf("%d%d%d%d",&buy[i],&sell[i],&MaxB[i],&MaxS[i]);
            }
            printf("%d\n",solve());
      }
      return 0;
}


另外单调队列还可以优化多重背包,不过我觉得多重背包问题二进制优化就已经差不多了。


单调队列优化DP是一种常用的优化方法,可以将时间复杂度从 $O(n^2)$ 降低到 $O(n)$ 或者 $O(n \log n)$。以下是一道利用单调队列优化DP的典型题目: 题目描述: 给定一个长度为 $n$ 的序列 $a_i$,定义 $f(i)$ 为 $a_i$ 到 $a_n$ 中的最小值,即 $f(i) = \min\limits_{j=i}^n a_j$。现在定义 $g(i)$ 为满足 $f(j) \ge a_i$ 的最小下标 $j$,即 $g(i) = \min\{j \mid j > i, f(j) \ge a_i\}$。如果不存在这样的下标 $j$,则 $g(i) = n+1$。 现在请你计算出 $1 \le i \le n$ 的所有 $g(i)$ 的值。 输入格式: 第一行包含一个整数 $n$。 第二行包含 $n$ 个整数 $a_1,a_2,\cdots,a_n$。 输出格式: 输出 $n$ 行,第 $i$ 行输出 $g(i)$ 的值。 输入样例: 5 3 1 2 4 5 输出样例: 2 5 5 5 6 解题思路: 设 $dp(i)$ 表示 $g(i)$,那么 $dp(i)$ 与 $dp(i+1)$ 的转移关系可以表示为: $$dp(i)=\begin{cases}i+1, &\text{if}\ f(i+1)\ge a_i \\dp(i+1), &\text{else}\end{cases}$$ 这个转移方程可以使用暴力 DP 解决,时间复杂度为 $O(n^2)$。但是,我们可以使用单调队列优化 DP,将时间复杂度降为 $O(n)$。 我们定义一个单调队列 $q$,存储下标。队列 $q$ 中的元素满足: - 队列中的元素是单调递减的,即 $q_1 < q_2 < \cdots < q_k$; - 对于任意的 $i\in [1,k]$,有 $f(q_i) \ge f(q_{i+1})$。 队列 $q$ 的作用是维护一个长度为 $k$ 的区间 $[i+1,q_k]$,满足这个区间中的所有 $j$ 都满足 $f(j) < f(i+1)$。 根据定义,当我们要求 $dp(i)$ 时,只需要查找队列 $q$ 中第一个满足 $f(q_j) \ge a_i$ 的位置 $q_j$,那么 $g(i) = q_j$,如果队列 $q$ 中不存在这样的位置,则 $g(i) = n+1$。 那么如何维护单调队列 $q$ 呢?我们可以在每次 DP 的过程中,将 $i$ 加入队尾。然后判断队首元素 $q_1$ 是否满足 $f(q_1) \ge a_i$,如果满足则弹出队首元素,直到队首元素不满足条件为止。 由于每个元素最多被加入队列一次,并且最多被弹出一次,因此时间复杂度为 $O(n)$。具体实现细节可以参考下面的代码实现:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值