斜率优化DP入门(填坑)

斜率优化作为一种优化DP的方式,目的在于将某类具有单调性的DP方程从O(n^2)优化到O(n)

可以进行斜率优化的方程一般有如下几个特征,假设当前位为i,那么有一个和i,j同时有关的元素f(i, j),有一个仅与可继承状态j有关的g(j),可能还有一个常量x

很抽象对不对……所以这里有一道例题(真·例题):hdu3507Print Article

Print Article

Time Limit: 9000/3000 MS (Java/Others)    Memory Limit: 131072/65536 K (Java/Others)
Total Submission(s): 15344    Accepted Submission(s): 4766

Problem Description

Zero has an old printer that doesn't work well sometimes. As it is antique, he still like to use it to print articles. But it is too old to work for a long time and it will certainly wear and tear, so Zero use a cost to evaluate this degree.
One day Zero want to print an article which has N words, and each word i has a cost Ci to be printed. Also, Zero know that print k words in one line will cost


M is a const number.
Now Zero want to know the minimum cost in order to arrange the article perfectly.

Input

There are many test cases. For each test case, There are two numbers N and M in the first line (0 ≤ n ≤ 500000, 0 ≤ M ≤ 1000). Then, there are N numbers in the next 2 to N + 1 lines. Input are terminated by EOF.

Output

A single number, meaning the mininum cost to print the article.

Sample Input

 

5 5 5 9 5 7 5

Sample Output

 

230

Author

Xnozero

Source

2010 ACM-ICPC Multi-University Training Contest(7)——Host by HIT

题目大意:连续k个字符在一行的花费是sum(i , i + k)^2 + m,其中sum是求和,m是一个常量,怎样排版(换行)使得花费最小,输出最小花费

嗯,先想一个n^2的办法,首先i肯定只能从比i小的继承,假设从j换行,那么j + 1到i应该在一行,假设F(x) = x * x + m

for(int i = 1; i <= n; i++) {
		f[i] = 0x3f3f3f3f;
		for(int j = 0; j < i; j++)
			f[i] = min(f[i], f[j] + F(c[i] - c[j]));
	}

这大概是十分明显的(c是前缀和数组)

效率不够也是十分明显的,所以我们要想一个优化方法

假设,从j继承和从k继承相比,j更劣,列式表达如下:

f[j] + (sum[i] - sum[j])^2 + m > f[k] + (sum[i] - sum[k])^2 + m

设F(x) = f[x] + sum[x]^2

化简如下:(1)式:F(j) - 2 * sum[i] * sum[j] > F(k) - 2 * sum[i] * sum[k]

F(x), sum[x]都是只与x有关的量,在x确定时相当于常量,所以我们假设 F(j) = b1,  sum[j] = k1,  F(k) =b2,  sum[k] = k2,sum[i] = x,那么这个不等式就变成了这个样子:

(2)式:-2 * k1 * x + b1 > -2 * k2 * x + b2

所以对于两个决策点j, k而言,满足上述不等式则k更优

现在考虑什么时候一个决策点是无意义的

首先假设j优于k,则式子可以转化为:(3)式(F(j) - F(k))/(2 * (sum[j] - sum[k])) < sum[i]

是否觉得介个很像一个东西?

斜率式!(我们数学老师说是"快k法")

设g(x, y) = (F(x) - F(y))/(2 * (sum[x] - sum[y]))

假设有决策点k < j < i,g(k, j) > g(j, i)当前点为p, 若g(k, j) < sum[p],则k优于j,若g(k, j) > sum[p],则j优于k但是i优于j,所以在这种情况下j无论如何都成不了最优,那就把它扔掉好啦!所以说对于可用决策,我们要求两两之间g(i, i + 1)单调递增,也就是斜率单增,那么我们该用什么来维护这一特性呢?

单调队列!注意一个小细节,sum[x] - sum[y]有可能等于0,所以要特判

献上我改了很久的代码:

#include <bits/stdc++.h>
#define LL long long
using namespace std;
inline int read() {
	int ch, f = 1, g = 0; ch = getchar();
	while((ch > '9' || ch < '0') && ch != '-') ch = getchar();
	ch == '-' ? f = -1 : g = ch - '0'; ch = getchar();
	while(ch <= '9' && ch >= '0') g = g * 10 + ch - '0', ch = getchar();
	return g * f;
}
const int MAXN = 500500;
typedef LL Array[MAXN];
Array f, c;
int n, m;
namespace DP {
	int h, t; int Q[MAXN];
	void init() {
		h = t = 0;
		memset(Q, 0, sizeof(Q));
		memset(f, 0, sizeof(f));
	}
	LL F(LL x) {
		return x * x + m;
	} 
	LL G(LL x) {
		return f[x] + c[x] * c[x];
	}
	LL check(int x, int y) {
		LL fy = G(x) - G(y);
		LL fx = 2 * (c[x] - c[y]);
		if(c[x] == c[y]) {
			if(f[x] > f[y]) return -1;
			else return 0x3f3f3f3f;
		}
		return fy / fx;
	}
	void dp() {
		for(int i = 1; i <= n; i++) {
			while(h < t && check(Q[h], Q[h + 1]) < c[i]) h++;//因为函数与x有关,所以check()是必须的 
			int j = Q[h];
			f[i] = f[j] + F(c[i] - c[j]);
			while(h < t && check(Q[t - 1], Q[t]) > check(Q[t], i)) t--;//维护单调性 
			Q[++t] = i;
		}
		printf("%lld\n", f[n]);
	}
}
signed main() {
	while(scanf("%d%d", &n, &m) != EOF) {
		for(int i = 1; i <= n; i++) c[i] = c[i - 1] + read();
		DP::init();
		DP::dp();
	}
	return 0;
}

再看一道题:APIO2010特别行动队

这道题和上一题可以说是大同小异了,先看看40分码(LG):

#include <bits/stdc++.h>
#define LL long long
#define Max(a, b) ((a) > (b) ? (a) : (b)) 
using namespace std;
const int maxn = 1010007;
int a, b, c, n;
LL f[maxn], p[maxn];
template<class T>void read(T &x)
{
    x = 0;int d = 0;int ch = getchar();
    while(ch < '0' || ch > '9')  {d |= (ch == '-'); ch = getchar();}
    while(ch >=  '0' && ch <= '9'){x = x * 10 + (ch^48); ch = getchar();}
    x = d ? -x : x;
    return;
}
LL function (LL x) {
    return (LL) a * x * x + b * x + c;
}
void DP() {
    for(int i = 1; i <= n; i++) 
        for(int j = 0; j < i; j++) 
            f[i] = Max(f[i], f[j] + function(p[i] - p[j]));
}
signed main() {
    read(n), read(a), read(b), read(c);
    for(int i = 1; i <= n; i++) read(p[i]), p[i] += p[i - 1];
    DP();
    printf("%lld\n", f[n]);
    return 0;
}

这是用来测试我的方程写对没有的,然后,同样的方式我们可以推出这道题对应的不等式,然后就是一个大同小异的过程了

就不多解释了,大家试着自己推一推,在这里献上我的代码:

#include <bits/stdc++.h>
#define LL long long
#define Max(a, b) ((a) > (b) ? (a) : (b)) 
using namespace std;
const int MAXN = 1010007;
int a, b, c, n;
LL f[MAXN], p[MAXN];
template<class T>void read(T &x)
{
    x = 0;int d = 0;int ch = getchar();
    while(ch < '0' || ch > '9')  {d |= (ch == '-'); ch = getchar();}
    while(ch >=  '0' && ch <= '9'){x = x * 10 + (ch^48); ch = getchar();}
    x = d ? -x : x;
    return;
}
namespace DP {
    int Q[MAXN];
    int h, t;
    void init() {
        h = t = 0;
        memset(Q, 0, sizeof(Q));
    }
    LL F (LL x) {
        return (LL) a * x * x + b * x + c;
    }
    LL G(int x) {
        return f[x] + a * p[x] * p[x] - b * p[x];
    } 
    double check(int j, int k) {
        return (double)(G(j) - G(k)) / (2.000 * a * (double)(p[j] - p[k]));
    }
    void dp() {
        for(int i = 1; i <= n; i++) {
                while(h < t && check(Q[h], Q[h + 1]) <= (double)p[i] * 1.000) h++;
                int j = Q[h];
                f[i] = f[j] + F(p[i] - p[j]);
                while(h < t && check(Q[t - 1], Q[t]) >= check(Q[t], i)) t--;
                Q[++t] = i;
        }
    }
}
signed main() {
    read(n), read(a), read(b), read(c);
    for(int i = 1; i <= n; i++) read(p[i]), p[i] += p[i - 1];
    DP::init();
    DP::dp();
    printf("%lld\n", f[n]);
    return 0;
}

Thanks for your watching

发布了58 篇原创文章 · 获赞 34 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_37666409/article/details/79312788