首页 > 其他分享 >单调队列优化 dp

单调队列优化 dp

时间:2024-06-15 15:58:35浏览次数:17  
标签:队列 max 决策 int k1 dp 单调

单调队列优化 dp

适用条件

只关注“状态变量”“决策变量”及其所在的维度,如果转移方程形如:

\[f[i]=\min_{L(i)≤j≤R(i)}^{}{\{f[j]+cost(i,j)\}} \]

则可以使用单调队列优化。具体的,把 \(cost(i,j)\) 分成两部分,第一部分仅与 \(i\) 有关,第二部分仅与 \(j\) 有关。对于每个 \(i\),无论采取哪个 \(j\) 作为最优决策,第一部分的值都是相等的,可以在选出最优决策更新 \(f[i]\) 时再进行计算、累加。而当 \(i\) 发生变化时,第二部分的值不会发生变化,从而保证原来较优的决策,在 \(i\) 改变后仍然较优,不会产生乱序现象。于是,可以在队列中维护第二部分的单调性,及时排除不必要的决策,让 DP 更高效。\(cost(i,j)\) 的每一项仅与 \(i\) 和 \(j\) 中的一个有关,是使用单调队列进行优化的前提。

AcWing298. 围栏

先把所有木匠按照 \(s[i]\) 排序,这样一来,每个木匠粉刷的木板一定在上一个木匠粉刷的木板之后,使得能按顺序 dp

设 \(f[i][j]\) 表示安排前 \(i\) 个木匠粉刷前 \(j\) 块木板,能获得的最多报酬。

    1. 第 \(i\) 个木匠可以什么也不刷,此时 \(f[i][j] = f[i - 1][j]\)
    1. 第 \(j\) 块木板可以空着不刷,此时 \(f[i][j] = f[i][j - 1]\)
    1. 第 \(i\) 个木匠粉刷第 \(k + 1\) 块到第 \(j\) 块木板,根据题意,该木匠粉刷总数不能超过 \(l[i]\),且必须粉刷 \(s[i]\),所以需满足:\(k + 1 ≤ s[i] ≤ j , j - k ≤ l[i]\)。

因此有状态转移方程:

\[f[i,j]=\max_{j-L_i≤k≤S_i-1,j≥S_i}\{f[i-1,k]+P_i * (j -k)\} \]

将常量提出有:

\[f[i,j]=P_i*j+\max_{j-L_i≤k≤S_i-1,j≥S_i}\{f[i-1,k]-P_i * k)\} \]

当 \(j\) 增大时,\(k\) 的取值范围上界 \(s[i] - 1\) 不变,下界 \(j - l[i]\) 变大。

我们比较一下任意两个决策 \(k1\) 和 \(k2\),设 \(k1 < k2 ≤ s[i] - 1\)。因为 \(k2\) 比 \(k1\) 更靠后,所以随着 j 的增加,
\(k1\) 会比 \(k2\) 更早从范围 \([j - l[i], s[i] - 1]\) 中被排除,如果还满足 \(f[i - 1][k1] - p[i] * k1 ≤ f[i - 1][k2] - p[i] * k2\),
那么就意味着 \(k2\) 不但比 \(k1\) 更优,还比 \(k1\) 的存活时间更长,在这种情况下,\(k1\) 就是一个无用的决策,应该被直接排除出候选决策集合。

综上所述,我们可以维护一个决策点 $ k$ 单调递增,数值 \(f[i - 1][k] - p[i] * k\) 单调递减的队列,只有这个队列中的决策才有可能在某一时刻称为最优策略,这个单调队列支持如下操作:

    1. 当 \(j\) 变大时,检查队头元素,把小于 \(j - l[i]\) 的决策出队
    1. 需要查询最优策略时,队头即为所求
    1. 有一个新的决策要加入候选集合时,在队尾检查 \(f[i - 1][k] - p[i] * k\) 的单调性,把无用策略从队尾直接出队,
      最后把新决策加入队列

在本题中具体来说,当内层循环开始时 \(j = s[i]\),建立一个空的单调队列,把 \([max(s[i] - l[i], 0), s[i] - 1]\)

中的决策依次加入候选集合(执行操作 \(3\)),对于每个 $j = s[i] $~ \(n\),先在队头检查决策合法性(执行操作 \(1\)),然后取队头为最优决策(执行操作 \(2\))进行状态转移。

由于单调队列的优化,枚举决策的时间复杂度是线性 \(O(1)\) 的,总的时间复杂度为 $ O(nm)$

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. 裁剪序列

设 \(f[i]\) 表示前 \(i\) 个数分成若干段,在满足每段中所有数的和不超过 \(m\) 的前提下,各段的最大值之和最小是多少

枚举最后一段的情况来进行转移,得出状态转移方程:

\[f[i]=\min_{0≤j<i, \sum_{k=j+1}^{i}}\{f[j]+\max_{j+1≤k≤i}\{A_k\}\} \]

因为 \(max\{ a[k] \}\) 不容易用一个简单的多项式来表示,不容易找到特性如单调性。

在上述方程中,若 \(j (0 ≤ j < i)\) 可能成为最优决策,则除了 \(\sum_{k=j+1}^{i}A_k≤M\) 外,
一定还满足以下两个条件之一:

    1. \(A[j]=\max_{j≤k≤i}{\{A_k\}}\)
  • 2.$ \sum_{k=j}^{i}A_k>M$ (即 \(j\) 是满足 \(\sum_{k=j+1}^{i}A_k≤M\) 的最小的 \(j\))

第 \(2\) 个条件比较简单,只需预处理出对于每个 \(i\),满足 \(\sum_{k=j+1}^{i}A_k≤M\) 的最小的 \(j\),
记为 \(c[j]\),在计算 \(f[i]\) 时,从 $ c[i]$ 进行一次状态转移即可,下面单独讨论满足定理中第 \(1\) 个条件的决策 \(j\) 的维护方法。

当一个新的决策 \(j_2\) 插入候选集合时,若候选集合中已有的决策 \(j_1\) 满足条件 $j_1 < j_2 $ 并且 \(a[j_1] < a[j_2]\),则 \(j_1\) 就是无用策略,可以直接排除。

综上所述,我们可以维护一个决策点 \(j\) 单调递增、数值 \(a[j]\) 单调递减的队列,只有该队列中的元素才可能成为最优决策。

光这样还不够,该队列只是一个 $a[j] $单调递减的队列,关于转移方程等式右边的 \(f[j] + max\{ a[k] \}\) 并没有单调性。所以不能简单的取队头作为最优决策,而是再加一个额外的数据结构,如二叉堆,二叉堆与单调队列保存相同的候选集合,该插入时一起插入,该删除时一起删除,只不过单调队列以 \(a[j]\) 递减作为比较大小的依据,二叉堆以 \(f[j] + max\{ a[k] \}\) 作为比较大小的依据,保证能快速的在候选集合中查询最值,我们称这种操作为 "二叉堆与单调队列建立了映射关系",但是二叉堆要想跟着队列一起删除元素需要用 "懒惰删除",这里直接采用 \(multiset\) 来实现,队列中某一项的 \(\max_{j+1≤k≤i}\{A_k\}\)
的结果其实就是队列中下一个元素的 \(A\) 值。

复杂度 \(o(n \log n)\)

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] 股票交易

\(f[i][j]\):第 \(i\) 天结束后,持有股票数为 \(j\) 的情况下能获得的最大收益

\[f[i,j] = \max\{f[i-1,j],A,B,C\} \]

\[A=-a*j,j≤b \]

\[B=f[i-t, k]-a*j+a*k,,(0\le k\le j-1,j-k\le d) \]

\[C=f[i-t,k]+c*k-c*j,(j+1\le k\le m, k-j\le d) \]

第\(3\)、\(4\)个方程可以单调队列优化。踢队头保证对头小于 \(j-b\) 就行,踢队尾可以观察发现有共性 : \(f[i-t,k]+a*k\)

submission

P4852 yyf hates choukapai

\(f[i][j]\) 表示前 \(i\) 张牌连抽 \(j\) 次的最大欧气值。

枚举本次连抽的位置 \(k\) 并设 \(k\) 为本次连抽的起始卡牌的前一个位置),转移方程:

\[f[i][j]=\max_{i-c-d≤k≤i-c}f[k][j-1]+a[k+1]+s[i]-s[k+c] \]

当 \(i\) 增大时显然 \(k\) 决策区间的左右边界都单调递增,而队列里维护的为 \(f[k][j-1]+a[k+1]-s[k+c]\)

submission

P5665 [CSP-S2019] 划分

\(f[i,j]\) 表示当前已经划分好了前 \(i\) 个数,下一次直接划分出 \([i+1,j]\) 的最小运行时间。设 \(s_i=\sum_{j=1}^ia_j\)

\[f[i,j]=\min_{(0≤k<i)\land (s_i-s_k\le s_j-s_i)}\{f[k,i]+(s_j-s_i)^2\} \]

优化的用一个 \(val\) 维护 \(f[k,i]\) 最小值即可。

然后考虑贪心,不难想到如果分的段越少,将求和多项式展开相对于 \(a_i^2\) 项就会越多,所以要尽可能多的分段。对于每一个 \(i\),转移的时候,只需要找到第一个可行的 \(k\) 就可以。用 \(g[i]\) 记录这个第一个 \(k\)。 \(g[i]\)是最大的 \(k\) 使得$ s_i-s_k\ge s_k-s_{g[k]}$

同时,\(g[i]\) 是递增的。所以对于 \(i\),如果 \(j<g(i)\),那么 \(j\) 不可能成为 \(g[i+1]\),这就是决策的单调性。考虑往单调队列里面插入一个值。设 \(f[i]=2s_i-s_{g(i)}\),则我们应该在插入的时候弹掉队尾的所有 \(f \ge f[i]\) 的值。假如 \(f\) 对应的位置可以成为一个 \(g\),那么 \(i\) 必然可以,而且更优。

然后需要一个高精。

submission

CF940E

每段越短越好,如果把一个段增大的话,段内的最小值可能减小,而删去的数的总数也可能减少。对于长度小于 \(c\) 的区间对答案没有正向贡献,于是每次取长度为 \(c\) 的一段是最优的,设 \(f[i]\) 表示把前 \(i\) 个数分成若干段,每段的最小值之和的最大值。那么方程的两项分别对应断开区间和不断。有

\[f[i]=\max_{i\in[c,n]}\{f[i-1],f[i-c]+\min_{j\in(i-c,i]}\{a_j \}\}\ \]

\(i\) 的值是连续的,后面那一堆可以用单调队列优化掉,复杂度 \(O(n)\)

submission

标签:队列,max,决策,int,k1,dp,单调
From: https://www.cnblogs.com/spaceswalker/p/18249366

相关文章

  • dp板子
    01背包f[x]表示装x重量时最大价值,f初值0;n物品数量,m最大重量。w表示容量,v时价值for(inti=1;i<=n;i++)//物品{for(intj=m;j>=w[i];j--){//容量f[j]=max(f[j],f[j-w[i]]+v[i]);}}完全背包for(inti=0;i<=m;i++){//背包容量for(intj=1;j<=n;j++){//物品数量if(i......
  • MQTT消息队列版本对比
    MQTT3.1.1和MQTT5.0在多个方面存在显著的区别。以下是这两个版本之间区别的详细比较:连接过程:MQTT3.1.1的连接过程包括四个明确的步骤:连接请求、连接确认、订阅请求和订阅确认。MQTT5.0则将连接过程简化为三个步骤:连接请求、连接确认和属性交换。会话状态:MQTT3.1.1依......
  • 195K数字音频接收器CS8416替代型号DP7416无需修改软硬件PIN对PIN兼容
    DP7416替代CS8416无需修改软硬件192K数字音频接收器产品特性支持EIAJCP1201、IEC-60958、AES3、S/PDIF8:2多路输入选择器32K到192KHz的采样频率支持差分和单端输入自动检测被压缩的输入音频数据流支持SPI和I2C通讯接口协议解码CD和Q-SubCodeDP7416是一款192K数......
  • 代码随想录算法训练营第第37天 | 56. 合并区间 、738.单调递增的数字、968.监控二叉
    合并区间本题也是重叠区间问题,如果昨天三道都吸收的话,本题就容易理解了。https://programmercarl.com/0056.合并区间.html能做出来/***@param{number[][]}intervals*@return{number[][]}*/varmerge=function(intervals){intervals.sort((a,b)=>{......
  • 代码随想录算法训练营第三十七天 | 56.合并区间 738.单调递增的数字
    56.合并区间题目链接文章讲解视频讲解思路:  按左区间排序;  遍历所有区间,如果当前区间的左边界小于等于上一个区间的右边界,则合并区间(新区间的左边界为上一个区间的左边界,新区间的右边界为上一个区间的有边界和当前区间有边界中较大的一个)classSolution{public:......
  • 6.13-栈与队列
    基础知识首先大家要知道栈和队列是STL(C++标准库)里面的两个数据结构。C++标准库是有多个版本的,要知道我们使用的STL是哪个版本,才能知道对应的栈和队列的实现原理。那么来介绍一下,三个最为普遍的STL版本:HPSTL其他版本的C++STL,一般是以HPSTL为蓝本实现出来的,HPSTL是C++S......
  • JAVAEE值之网络原理(1)_用户数据报协议(UDP)、概念、特点、结构、代码实例
    前言 在前两节中我们介绍了UDP数据报套接字编程,但是并没有对UDP进行详细介绍,本节中我们将会详细介绍传输层中的UDP协议。一、什么是UDP? UDP工作在传输层,用于程序之间传输数据的。数据一般包含:文件类型,视频类型,jpg图片等。1.1基本概念: UDP的全称:用户数据报协议(U......
  • kubernetes-外部数据库服务映射至集群内-Service与Endpoints的关系
    创建yaml文件配置数据库信息kind:ServiceapiVersion:v1metadata:name:mysql-svcnamespace:ops-systemspec:type:ClusterIP #Kubernetes将为此服务随机分配一个集群内部的IP地址ClusterIP类型的服务只能在集群内部访问,提供了一个内部访问的固定IP地址,不对......
  • CPN Tools学习——时间和队列【重要】
    -TimedColorSets时间颜色集-TokenStamps令牌时间戳-EventClock全局/事件/模拟时钟-TimeDelaysonTransitions过渡的时间延迟-ListColorSet列表颜色集-Queue排队1.时间颜色集在定时CPN模型令牌中有:(1)象征性的颜色(2)时间戳:时间戳是一个非负整数.句法:1`e@+表......
  • 【C++/STL】stack和queue(容器适配器、优先队列、双端队列)
      ......