在这片梦想之地,不堪回首的过去像泡沫一样散去|

PassName

园龄:3年1个月粉丝:32关注:16

单调队列优化 dp

单调队列优化 dp

适用条件

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

f[i]=minL(i)jR(i){f[j]+cost(i,j)}

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

AcWing298. 围栏

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

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

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

因此有状态转移方程:

f[i,j]=maxjLikSi1,jSi{f[i1,k]+Pi(jk)}

将常量提出有:

f[i,j]=Pij+maxjLikSi1,jSi{f[i1,k]Pik)}

j 增大时,k 的取值范围上界 s[i]1 不变,下界 jl[i] 变大。

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

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

    1. j 变大时,检查队头元素,把小于 jl[i] 的决策出队
    1. 需要查询最优策略时,队头即为所求
    1. 有一个新的决策要加入候选集合时,在队尾检查 f[i1][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]=min0j<i,k=j+1i{f[j]+maxj+1ki{Ak}}

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

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

    1. A[j]=maxjki{Ak}
  • 2.k=jiAk>M (即 j 是满足 k=j+1iAkM 的最小的 j

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

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

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

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

复杂度 o(nlogn)

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[i1,j],A,B,C}

A=ajjb

B=f[it,k]aj+ak,(0kj1,jkd)

C=f[it,k]+ckcj(j+1km,kjd)

34个方程可以单调队列优化。踢队头保证对头小于 jb 就行,踢队尾可以观察发现有共性 : f[it,k]+ak

submission

P4852 yyf hates choukapai

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

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

f[i][j]=maxicdkicf[k][j1]+a[k+1]+s[i]s[k+c]

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

submission

P5665 [CSP-S2019] 划分

f[i,j] 表示当前已经划分好了前 i 个数,下一次直接划分出 [i+1,j] 的最小运行时间。设 si=j=1iaj

f[i,j]=min(0k<i)(sisksjsi){f[k,i]+(sjsi)2}

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

然后考虑贪心,不难想到如果分的段越少,将求和多项式展开相对于 ai2 项就会越多,所以要尽可能多的分段。对于每一个 i,转移的时候,只需要找到第一个可行的 k 就可以。用 g[i] 记录这个第一个 kg[i]是最大的 k 使得sisksksg[k]

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

然后需要一个高精。

submission

CF940E

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

f[i]=maxi[c,n]{f[i1],f[ic]+minj(ic,i]{aj}} 

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

submission

本文作者:PassName

本文链接:https://www.cnblogs.com/spaceswalker/p/18249366

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   PassName  阅读(33)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起