单调队列优化 dp
单调队列优化 dp
适用条件
只关注“状态变量”“决策变量”及其所在的维度,如果转移方程形如:
则可以使用单调队列优化。具体的,把 分成两部分,第一部分仅与 有关,第二部分仅与 有关。对于每个 ,无论采取哪个 作为最优决策,第一部分的值都是相等的,可以在选出最优决策更新 时再进行计算、累加。而当 发生变化时,第二部分的值不会发生变化,从而保证原来较优的决策,在 改变后仍然较优,不会产生乱序现象。于是,可以在队列中维护第二部分的单调性,及时排除不必要的决策,让 DP 更高效。 的每一项仅与 和 中的一个有关,是使用单调队列进行优化的前提。
AcWing298. 围栏
先把所有木匠按照 排序,这样一来,每个木匠粉刷的木板一定在上一个木匠粉刷的木板之后,使得能按顺序 dp
设 表示安排前 个木匠粉刷前 块木板,能获得的最多报酬。
-
- 第 个木匠可以什么也不刷,此时
-
- 第 块木板可以空着不刷,此时
-
- 第 个木匠粉刷第 块到第 块木板,根据题意,该木匠粉刷总数不能超过 ,且必须粉刷 ,所以需满足:。
因此有状态转移方程:
将常量提出有:
当 增大时, 的取值范围上界 不变,下界 变大。
我们比较一下任意两个决策 和 ,设 。因为 比 更靠后,所以随着 j 的增加,
会比 更早从范围 中被排除,如果还满足 ,
那么就意味着 不但比 更优,还比 的存活时间更长,在这种情况下, 就是一个无用的决策,应该被直接排除出候选决策集合。
综上所述,我们可以维护一个决策点 单调递增,数值 单调递减的队列,只有这个队列中的决策才有可能在某一时刻称为最优策略,这个单调队列支持如下操作:
-
- 当 变大时,检查队头元素,把小于 的决策出队
-
- 需要查询最优策略时,队头即为所求
-
- 有一个新的决策要加入候选集合时,在队尾检查 的单调性,把无用策略从队尾直接出队,
最后把新决策加入队列
- 有一个新的决策要加入候选集合时,在队尾检查 的单调性,把无用策略从队尾直接出队,
在本题中具体来说,当内层循环开始时 ,建立一个空的单调队列,把
中的决策依次加入候选集合(执行操作 ),对于每个 ~ ,先在队头检查决策合法性(执行操作 ),然后取队头为最优决策(执行操作 )进行状态转移。
由于单调队列的优化,枚举决策的时间复杂度是线性 的,总的时间复杂度为
int n, m;
int f[N][M], q[M];
struct rec
{
int l, p, s;
friend bool operator < (rec a, rec b)
{
return a.s < b.s;
}
} a[N];
int calc(int i, int k)
{
return f[i - 1][k] - a[i].p * k;
}
signed main()
{
cin >> n >> m;
for (rint i = 1; i <= m; i++)
{
cin >> a[i].l >> a[i].p >> a[i].s;
}
sort(a + 1, a + m + 1);
for (rint i = 1; i <= m; i++)
{
int l = 1, r = 0;
int st = max(0ll, a[i].s - a[i].l);
for (rint k = st; k <= a[i].s - 1; k++)
{
while (l <= r && calc(i, q[r]) <= calc(i, k)) r--;
q[++r] = k;
}
for (rint j = 1; j <= n; j++)
{
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
if (j >= a[i].s)
{
while (l <= r && q[l] < j - a[i].l) l++;
if (l <= r)
f[i][j] = max(f[i][j], calc(i, q[l]) + a[i].p * j);
}
}
}
cout << f[m][n] << endl;
return 0;
}
AcWing299. 裁剪序列
设 表示前 个数分成若干段,在满足每段中所有数的和不超过 的前提下,各段的最大值之和最小是多少
枚举最后一段的情况来进行转移,得出状态转移方程:
因为 不容易用一个简单的多项式来表示,不容易找到特性如单调性。
在上述方程中,若 可能成为最优决策,则除了 外,
一定还满足以下两个条件之一:
-
- 2. (即 是满足 的最小的 )
第 个条件比较简单,只需预处理出对于每个 ,满足 的最小的 ,
记为 ,在计算 时,从 进行一次状态转移即可,下面单独讨论满足定理中第 个条件的决策 的维护方法。
当一个新的决策 插入候选集合时,若候选集合中已有的决策 满足条件 并且 ,则 就是无用策略,可以直接排除。
综上所述,我们可以维护一个决策点 单调递增、数值 单调递减的队列,只有该队列中的元素才可能成为最优决策。
光这样还不够,该队列只是一个 单调递减的队列,关于转移方程等式右边的 并没有单调性。所以不能简单的取队头作为最优决策,而是再加一个额外的数据结构,如二叉堆,二叉堆与单调队列保存相同的候选集合,该插入时一起插入,该删除时一起删除,只不过单调队列以 递减作为比较大小的依据,二叉堆以 作为比较大小的依据,保证能快速的在候选集合中查询最值,我们称这种操作为 "二叉堆与单调队列建立了映射关系",但是二叉堆要想跟着队列一起删除元素需要用 "懒惰删除",这里直接采用 来实现,队列中某一项的
的结果其实就是队列中下一个元素的 值。
复杂度
int n, m;
int a[N], f[N], c[N];
int q[N];
//队首 q[j] 表示 max{a[k]} , j <= k <= i
multiset<int> s;
//存储 f[j] + max{a[k]} , j + 1 <= k <= i
//将队列 q[j] 和 set 存储的 f[q[j]] + a[q[j + 1]] 之间建立映射关系
signed main()
{
cin >> n >> m;
for (rint i = 1; i <= n; i++)
{
cin >> a[i];
if (a[i] > m)
{
puts("-1");
return 0;
}
}
int sum = 0;
for (rint i = 1, j = 0; i <= n; i++)
{
sum += a[i];
while (sum > m)
{
sum -= a[j + 1];
j++;
}
c[i] = j;
}
int l = 1, r = 0;
for (rint i = 1; i <= n; i++)
{
while (l <= r && q[l] < c[i])
{
s.erase(f[q[l]] + a[q[l + 1]]);
l++;
}
while (l <= r && a[q[r]] <= a[i])
{
s.erase(f[q[r - 1]] + a[q[r]]);
r--;
}
if (l <= r) s.insert(f[q[r]] + a[i]);
q[++r] = i;
f[i] = f[c[i]] + a[q[l]];
if (s.size()) f[i] = min(f[i], *s.begin());
}
cout << f[n] << endl;
return 0;
}
P2569 [SCOI2010] 股票交易
:第 天结束后,持有股票数为 的情况下能获得的最大收益
第、个方程可以单调队列优化。踢队头保证对头小于 就行,踢队尾可以观察发现有共性 :
P4852 yyf hates choukapai
表示前 张牌连抽 次的最大欧气值。
枚举本次连抽的位置 并设 为本次连抽的起始卡牌的前一个位置),转移方程:
当 增大时显然 决策区间的左右边界都单调递增,而队列里维护的为
P5665 [CSP-S2019] 划分
表示当前已经划分好了前 个数,下一次直接划分出 的最小运行时间。设
优化的用一个 维护 最小值即可。
然后考虑贪心,不难想到如果分的段越少,将求和多项式展开相对于 项就会越多,所以要尽可能多的分段。对于每一个 ,转移的时候,只需要找到第一个可行的 就可以。用 记录这个第一个 。 是最大的 使得
同时, 是递增的。所以对于 ,如果 ,那么 不可能成为 ,这就是决策的单调性。考虑往单调队列里面插入一个值。设 ,则我们应该在插入的时候弹掉队尾的所有 的值。假如 对应的位置可以成为一个 ,那么 必然可以,而且更优。
然后需要一个高精。
CF940E
每段越短越好,如果把一个段增大的话,段内的最小值可能减小,而删去的数的总数也可能减少。对于长度小于 的区间对答案没有正向贡献,于是每次取长度为 的一段是最优的,设 表示把前 个数分成若干段,每段的最小值之和的最大值。那么方程的两项分别对应断开区间和不断。有
的值是连续的,后面那一堆可以用单调队列优化掉,复杂度
本文作者:PassName
本文链接:https://www.cnblogs.com/spaceswalker/p/18249366
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步