4

动态规划中的单调性优化

 3 years ago
source link: https://rapiz.me/2017/mono-dp/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

动态规划中的单调性优化

本来对于动态规划我是弃疗的。不过发现单调队列没那么吓人,其实就是个滑动窗口最值问题。
还有就是chenyao的ppt写错了……坑
贴的这个没错

通常做dp的时候f[i]的值依赖从前面K个状态转移过来
由于K的限制,所以并不能全部点均从一个点转移过来
需要维护最优点,次优点,….
同时如果对于f[i] 和 f[j],i < j 且从f[j]转移更优,此时f[i]一定没有用
所以我们维护的点中,位置必须为单调增,贡献的值为单调减
by chenyao

anyway,单调队列能优化的方程一般都长这样:

$f$ 是状态,$c$ 是一个常数数组。
如果你会滑动窗口肯定懂 $b=1$ 时怎么做,然后发现 $b \ne 1$ 时只需在每次循环开始时尝试入队往前数的第 b 个状态即可。
不懂滑动窗口就先去学那个。lrj 的书上不知道哪有来着。
然后需要说明的是题目的方程一般都不会长这么简洁,所以你一般需要把 $j$ 相关的量放到一起,再把 $i$ 相关的量放到一起。每次转移时仅与 $i$ 有关的量可以拿到 $\max$ 外面,于是就可以得到上面这种形式。

还是举个例子更好。
为了抵制bzoj进行垄断,我选择链接cogs上相同的题!
USACO Open11 修剪草坪 from COGS

简要题意:n 个数中选一些数,并且不能选连续的 k 个数,要求选的数和最大。n,k <= 1e5, 数在 long long

设 $f[i]$ 是前 i 个数的答案,那么可以从 $f[j]$ 转移过来,决策是选择 $[j + 2, i]$ 这些数。
方程:

注意这个方程考虑了不选 $i$ 的情况,此时 $j - 1$。
一看见这种东西都是顺手前缀和的……
暴力转移 $O(nk)$,接下来我们把它优化到$O(n)$
我们使用高级数学方法——小学的加法交换律和加法结合律!

看!是不是和最开始的方程形式一样了!
如果说和最原始的滑动窗口哪里不一样的话,就是滑动窗口在一个数组上滑,也在这个数组上赋值。而单调队列优化dp在 $f$ 上赋值,然后把 $f + c$ 放到被滑的那个数组里。

//修剪草坪
#include <cstdio>
#include <algorithm>
#define file(x) "mowlawn." #x
typedef long long ll;
using std::max;
const int N = 1e5 + 10;
int n, k, head, tail;
ll f[N], a[N], ans;
struct D{int i;ll v;}q[N];
int main() {
	freopen(file(in), "r", stdin);
	freopen(file(out), "w", stdout);
	scanf("%d%d", &n, &k);
	for (int i = 1; i <= n; i++) scanf("%lld", a + i), a[i] += a[i - 1];
	q[tail++] = (D){-1, 0};//考虑清楚怎么样才不会漏决策!这个决策代表选一个前缀
	for (int i = 1; i <= n; i++) {
		while (head != tail && i - q[head].i - 1 > k) head++;
		while (head != tail && q[tail - 1].v < f[i - 1] - a[i]) tail--;
		q[tail++] = (D){i - 1, f[i - 1] - a[i]};
		f[i] = q[head].v + a[i];
		ans = max(ans, f[i]);
	}
	printf("%lld", ans);
}

SCOI2010 股票交易 from 洛谷
这些题bzoj都有,貌似都是权限

题意自己看吧。
然后方程我懒得写了,又不难。
注意到这次变成二维了。股票交易
对于每一天都可以做两(买卖)次单调队列,第 i 天的窗口在 i - w - 1天上。

//SCOI2010 股票交易
#include <cstdio>
#include <algorithm>
#include <cstring>
using std::max;
const int N = 2010;
int n, w, p, as[N], bs[N], ap[N], bp[N], f[N][N], head, tail, q[N];
//yesterday(反正是以前), have, today, now have
inline int buy(int yes, int hv, int tod, int now) {
	return f[yes][hv] - (now - hv)*ap[tod];
}
inline int sell(int yes, int hv, int tod, int now) {
	return f[yes][hv] + (hv - now)*bp[tod];
}
int main() {
//	freopen("input", "r", stdin);
	scanf("%d%d%d", &n, &p, &w);
	for (int i = 1; i <= n; i++) scanf("%d%d%d%d", ap + i, bp + i, as + i, bs + i);
	memset(f, 0xc0, sizeof(f));//不能达到的状态收益负无穷
	f[0][0] = 0;
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j <= p; j++) f[i][j] = f[i - 1][j];//不买不卖
		head = tail = 0;
		for (int j = 1; j <= p; j++) {
			while (head != tail && q[head] < j - as[i]) head++;
			while (head != tail && buy(max(i - w - 1, 0), q[tail - 1], i, j) < buy(max(i - w - 1, 0), j - 1, i, j)) tail--;
			q[tail++] = j - 1;
			f[i][j] = max(f[i][j], buy(max(i - w - 1, 0), q[head], i, j));
		}
		head = tail = 0;
		for (int j = p - 1; j >= 0; j--) {
			while (head != tail && q[head] > j + bs[i]) head++;
			while (head != tail && sell(max(i - w - 1, 0), q[tail - 1], i, j) < sell(max(i - w - 1, 0), j + 1, i, j)) tail--;
			q[tail++] = j + 1;
			f[i][j] = max(f[i][j], sell(max(i - w - 1, 0), q[head], i, j));
		}
	}
	printf("%d\n", f[n][0]);
}

学斜率优化……主要是你需要克服公式恐惧症

这方面的变形比较多,如果我写错了,或者你觉得哪里不太清楚,欢迎评论
来几个水题爽爽。


HNOI2008 玩具装箱
dp方程真的好写。

$f$是答案,$s$为前缀和。
然后找到 $i$ 点的两个决策 $j,k$ 使得 $j < k$ 并且 $k$ 优于 $j$。
于是带入转移方程即得

然后我们的工作就是要把它左边化成形如$\frac{y_j - y_k}{x_j - x_k}$的式子。
于是得到:

注意中间由于除数为负变了一次不等式方向
所以左边成为形如$(x + s[x], f[x] + (x + s[x])^2)$这样的两个点的斜率。
注意我们一直进行恒等变形。
也就是说,对于 $k$ 优于 $j$ 且 $j < k$ 的的决策都满足上式。
下证有效决策集在一个下凸包上。

此时考虑上凸的三个点j1, j2, j3,即slope(j1, j2) > slope(j2, j3)
此时若slope(j1, j2) < 2g[i],那么j2比j1优,但此时j3一定比j2优
若slope(j1, j2) >= 2
g[i],则j1比j2优
所以前面的点如过画在平面二位直角坐标系上,一定只有下凸包上的点有意义
by chenyao

然后我们需要在dp时维护这样一个凸包。这道题中插入的点的$x$坐标是单调递增的。所以我们可以用单调队列维护凸包。
每次计算队首两个点的斜率,若满足上式则弹出队首。否则队首即为最优决策。
我们来证明一下队首即为最优决策的正确性:
首先经过一些弹出,队首两个点一定不满足上式了。所以有$slope(q[head], q[head + 1)] >= \dots$,然后我们通过上凸包的图像发现,一个点到它右边若干点的斜率是单减的,所以队首点对队中所有点都不满足上式,于是队首即为最优决策。

//玩具装箱
#include <cstdio>
typedef long long ll;
const int N = 5e4 + 10;
int n, q[N], sz, head, tail;
ll f[N], c[N], l;
inline double y(ll i) {return f[i] + (i + c[i])*1.0*(i + c[i]);}
inline double x(ll i) {return i + c[i];}
inline double slope(int j, int k) {//j < k
	return (y(j) - y(k))/(x(j) - x(k));
}
inline ll sq(ll x) {return x*x;}
int main() {
	freopen("bzoj_1010.in", "r", stdin);
    freopen("bzoj_1010.out", "w", stdout);
	scanf("%d%lld", &n, &l);
	for (int i = 1; i <= n; i++) {
		scanf("%lld", c + i), c[i] += c[i - 1];
		while (head + 1 != tail && slope(q[tail - 2], q[tail - 1]) > slope(q[tail - 1], i - 1)) tail--;
		q[tail++] = i - 1;
		while (head + 1 != tail && slope(q[head], q[head + 1]) < 2*(i + c[i] - l)) head++;
		int j = q[head];
		f[i] = f[j] + sq(i - j - 1 + c[i] - c[j] - l);
	}
	printf("%lld", f[n]);
}

|Prob|Hint|
|——|——|
|1010 HNOI2008 玩具装箱toy||
|4518 SDOI2016 征途|最简单的|
|1911 Apio2010 特别行动队|可见对二次函数型的代价都适用|
|1096 ZJOI2007 仓库建设|中等|
|1597 Usaco2008Mar 土地购买|不会的话就查题解吧……难点不在斜率优化的变形上|
|3156 防御准备|随便写写|

NOI2007 货币兑换cash
首先你要会cdq分治。
然后明白cdq分治本质是计算左半部分对右半部分的影响。

如果你对cdq分治处理dp不太理解可以先做hackrank的一道题,101hack这个比赛的Maximizing Mission Points。
这个网站上有这个题的题解。

然后你可以看cdq的那篇论文。

然后你可以看看hzwer的代码。他的带注释。他的空间还比论文优一个log。
可惜他的状态定义不太优美,是a货币的数目。并且代码有些细节是错的。

所以我贴下我的代码。
我的状态定义就是最大收益。

//货币兑换
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
using std::max;
const int N = 1e6 + 10;
const double EPS = 1e-7;
int n, sk[N];
double f[N], s, a[N], b[N], rate[N];
struct P{int i;double x, y, d;
	bool operator<(const P& b)const {return d > b.d;}
	inline void init() {
		x = f[i]/(b[i] + rate[i]*a[i]);
		y = f[i]*rate[i]/(b[i] + rate[i]*a[i]);
	}
}p[N], q[N];
double slope(int i, int j) {
	if (fabs(p[i].x - p[j].x) < EPS) return 1e20;
	return (p[i].y - p[j].y)/(p[i].x - p[j].x);
}
void solve(int l, int r) {
	if (l == r) {
		f[l] = max(f[l], f[l - 1]);
		p[l].init();
		return;
	}
	int mid = (l + r) >> 1;
	for (int k = l, i = 0, j = mid - l + 1; k <= r; k++) q[(p[k].i <= mid ? i : j)++] = p[k];
	memcpy(p + l, q, (r - l + 1)*sizeof(P));
	solve(l, mid);
	int top = 0;
	for (int i = l; i <= mid; i++) {
		while (top > 1 && slope(sk[top], sk[top - 1]) < slope(sk[top], i)) top--;
		sk[++top] = i;
	}
	for (int x = 1, y = mid + 1; y <= r; y++) {
		while (x < top && slope(sk[x], sk[x + 1]) + EPS > p[y].d) x++;
		int i = p[y].i, j = sk[x];
		f[i] = max(f[i], p[j].x*b[i] + p[j].y*a[i]);
	}
	solve(mid + 1, r);
	int tot = 0;
	for (int i = l, j = mid + 1; i <= mid || j <= r; tot++) {
		if (i > mid || (j <= r && p[j].x < p[i].x)) q[tot] = p[j++];
		else q[tot] = p[i++];
	}
	memcpy(p + l, q, tot*sizeof(P));
}
int main() {
	freopen("cash.in", "r", stdin);
	freopen("cash.out", "w", stdout);
	scanf("%d%lf", &n, f);
	for (int i = 1; i <= n; i++) scanf("%lf%lf%lf", a + i, b + i, rate + i), p[i].d = -b[i]/a[i], p[i].i = i;
	std::sort(p + 1, p + 1 + n);
	solve(1, n);
	printf("%.3lf\n", f[n]);
}



Recommend

  • 145

    背景动态规划是一个降低时间的很有效的方法,通常一个可以使用递归从上到下来解决的题目,必定可以转化为动态规划,从下往上计算,并通过设置一个数组存放子问题的解,来降低问题的求解步骤。LeetCode上的第55题可以使用回溯法和动态规划来解决,但回溯法会造成栈...

  • 137
    • 掘金 juejin.im 6 years ago
    • Cache

    漫画:什么是动态规划?

    ​————————————题目:有一座高度是10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。要求用程序来求出一共有多少种走法。比如,每次走1级台阶,一共走10步,这是其中一种走法。我们可以简写成 1,1,1,1,1,1,1,1,1,1。再比如,

  • 62
    • jiajunhuang.com 6 years ago
    • Cache

    动态规划民科教程

    动态规划民科教程 这是我本人近段时间学习和练习动态规划的总结,因为本人不是练过ACM的,所以自称民科。文章末尾是一些有用的引用。 动态规划(Dynamic Programming),一听就是一个高大上的词语,我们先来看看维基百科...

  • 114

    文章开头先上demo,只需键入任意内容的两个字符串,页面上就能自动计算并呈现字符串之间的差分。 demo地址:string-diff-demo.herokuapp.com 源码地址:github.com/lqt0223/str… 动态规划 动态规划(dynam

  • 73
    • www.cocoachina.com 6 years ago
    • Cache

    程序员算法基础——动态规划

  • 92

    比特科技 关注我们,即时收到优秀面经/面试题/技术博文及IT最新资讯  

  • 57
    • beyondlimits.site 6 years ago
    • Cache

    专题测试复习之动态规划

    只考了emmm100... 有点懵。 居然01背包维度写法出了问题。。。。 1.陈老师搬书 题目链接: https://www.luogu.org/problemnew/show/U365...

  • 49
    • 微信 mp.weixin.qq.com 5 years ago
    • Cache

    图论动态规划算法——Floyd最短路径

  • 73

    在学习「数据结构和算法」的过程中,因为人习惯了平铺直叙的思维方式,所以「递归」与「动态规划」这种带循环概念(绕来绕去)的往往是相对比较难以理解的两个抽象知识点。 程序员小吴打算使用动画的形式来帮助理解「递归」,然后通过「递归」的概念延伸至理解「动...

  • 8

    懒人如何判断二元函数的单调性 谢益辉 / 2005-07-28 晚上陈淼同学问我一个关于二元函数单调性的问题,最近我也没有准备复习考研,所以数学分析也没看,脑子里没有一点数学思维了。拿起题目一看,有些傻眼,不知该咋个整。函数是这...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK