单调队列优化 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

posted @ 2024-06-15 15:41  PassName  阅读(27)  评论(0编辑  收藏  举报