合集 斜率优化

昨天晚上肝了几道斜率优化的题,这里一起把题解写了吧。


BZOJ 1096

题目大意

有$n\le 10^6$个工厂,每个工厂的位置在$x_i$,有$p_i$个存货。
可以在工厂处建造仓库,在第$i$个工厂建造仓库的花费是$c_i$。
每个工厂的存货不能往编号小的仓库运东西。
一个单位货物运送一个单位长度需要一个花费。
求最小的总费用。

解析

考虑dp。
设$s1_i=\sum_{k\in[1,i]} p_k$,$s2_i=\sum_{k\in[1,i]} p_k\times x_k$,那么我们可以推出这个方程:
$$dp_i=min(dp_j + c_i + \sum_{k\in[j+1,i]}(x_i-x_j)\times p_j)=min(dp_j+c_i-(s2_i-s2_j)+x_i\times(s1_i-s1_j))$$
套路一波,假设$k$比$j$优秀($k>j$),那么:
$$dp_k+c_i-(s2_i-s2_k)+x_i\times(s1_i-s1_k)<dp_j+c_i-(s2_i-s2_j)+x_i\times(s1_i-s1_j)$$
随便推一下式子,应该就可以发现:
$$dp_k-dp_j+s2_k-s2_j<x_i\times(s1_k-s1_j)$$
$$\frac{dp_k-dp_j+s2_k-s2_j}{s1_k-s1_j}<x_i$$
那么斜率就出来了,直接上套路板子就可以了。时间复杂度$O(n)$。

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#define DREP(i, s, e) for(int i = s; i >= e ;i--)
#define REP(i, s, e) for(int i = s; i <= e ;i++)

#define DEBUG fprintf(stderr, "Passing [%s] in Line %d\n", __FUNCTION__, __LINE__)
#define chkmax(a, b) a = max(a, b)
#define chkmin(a, b) a = min(a, b)

#include <iostream>
#include <cstdio>

using namespace std;
#define int long long
const int maxn = 1e6 + 10;

template <typename T> inline T read()
{
T ans(0), p(1);
char c = getchar();
while (!isdigit(c))
{
if (c == '-') p = -1;
c = getchar();
}
while (isdigit(c))
{
ans = ans * 10 + c - 48;
c = getchar();
}
return ans * p;
}

int n, x[maxn], p[maxn], c[maxn], dp[maxn], s1[maxn], s2[maxn];

int q[maxn], head, tail;
double K(int k, int j) {return (s2[j] - s2[k] + dp[j] - dp[k]) * 1. / (s1[j] - s1[k]);}

signed main()
{
#ifdef CraZYali
freopen("1096.in", "r", stdin);
freopen("1096.out", "w", stdout);
#endif
cin >> n;
REP(i, 1, n) x[i] = read<int>(), p[i] = read<int>(), c[i] = read<int>();
REP(i, 1, n) s1[i] = s1[i-1] + p[i], s2[i] = s2[i-1] + x[i] * p[i];
REP(i, 1, n)
{
while (head < tail && K(q[head + 1], q[head]) < x[i]) head++;
dp[i] = dp[q[head]] + c[i] + x[i] * (s1[i-1] - s1[q[head]]) - (s2[i-1] - s2[q[head]]);
while (head < tail && K(q[tail - 1], i) < K(q[tail], q[tail - 1])) tail--;
q[++tail] = i;
}
cout << dp[n] << endl;
return 0;
}

BZOJ 1911

题目大意

一个部队中有$n\le 10^6$个人,每个人的战斗力为$x_i$。
假设一坨人的战斗力总和为$x$,那么他们最终的战斗力为$ax^2+bx+c$。
一坨人的编号要连续,最大化最终的战斗力。

解析

设$s_i=\sum_{k\in[1,i]} x_k$,则:
$$dp_i=max(dp_j+a\times(s_i-s_j)^2+b\times(s_i-s_j)+c)$$
套路:
$$dp_k-dp_j<a\times(s_k-s_j)\times(2s_i-s_k-s_j)+b(s_k-s_j)$$
$$\frac{\frac{dp_k-dp_j}{s_k-s_j}-b}{a}+s_k+s_j<2s_i$$
然后上板子

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#define DREP(i, s, e) for(int i = s; i >= e ;i--)
#define REP(i, s, e) for(int i = s; i <= e ;i++)

#define DEBUG fprintf(stderr, "Passing [%s] in Line %d\n", __FUNCTION__, __LINE__)
#define chkmax(a, b) a = max(a, b)
#define chkmin(a, b) a = min(a, b)

#include <iostream>
#include <cstdio>
#define int long long

using namespace std;
const int maxn = 1e6 + 10;

template <typename T> inline T read()
{
T ans(0), p(1);
char c = getchar();
while (!isdigit(c))
{
if (c == '-') p = -1;
c = getchar();
}
while (isdigit(c))
{
ans = ans * 10 + c - 48;
c = getchar();
}
return ans * p;
}

int n, a, b, c, s[maxn], dp[maxn];
int q[maxn], head, tail;

double K(int k, int j) {return ((dp[k] - dp[j]) * 1. / (s[k] - s[j]) - b) / a + s[k] + s[j];}

signed main()
{
#ifdef CraZYali
freopen("1911.in", "r", stdin);
freopen("1911.out", "w", stdout);
#endif
cin >> n >> a >> b >> c;
REP(i, 1, n)
{
s[i] = s[i-1] + read<int>();
while (head < tail && K(q[head+1], q[head]) < 2 * s[i]) head++;
int ret = s[i] - s[q[head]];
dp[i] = dp[q[head]] + a * ret * ret + b * ret + c;
while (head < tail && K(q[tail-1], i) < K(q[tail-1], q[tail])) tail--;
q[++tail] = i;
}
cout << dp[n] << endl;
return 0;
}

BZOJ 4518

题目大意

由$n\le 3000$条路$L_i$,分成$m$天走,最小化每天走的路程的方差。
设方差为$v$,输出$v\times m^2$。

解析

我们设总路程为$sum$,第$i$天走的路程为$l_i$,那么我们推一下方差:

要最小化答案,只要最小化$W=\sum_{i\in[1,m]}l_i^2$就可以了。
设$dp_{i,cur}$表示已经$cur$天走了$i$段路的最小$W$,记$s_i=\sum_{k\in[1,m]}L_k$,我们可以写出如下方程:
$$dp_{i,cur}=min(dp_{j,cur-1}+(s_i-s_j)^2)$$
套路一波,设$a(x)=dp_{x,cur-1}$,然后设$k>j$比$j$优秀,那么:
$$a(k)+(s_i-s_k)^2<a(j)+(s_i-s_j)^2$$
$$a(k)-a(j)<(2s_i-s_j-s_k)\times(s_k-s_j)$$
$$\frac{a(k)-a(j)}{s_k-s_j}+s_j+s_k<2s_i$$
然后就可以套路上板子了。时间复杂度$O(n\times m)$。

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#define DREP(i, s, e) for(int i = s; i >= e ;i--)
#define REP(i, s, e) for(int i = s; i <= e ;i++)

#define DEBUG fprintf(stderr, "Passing [%s] in Line %d\n", __FUNCTION__, __LINE__)
#define chkmax(a, b) a = max(a, b)
#define chkmin(a, b) a = min(a, b)

#include <iostream>
#include <cstdio>
#define int long long

using namespace std;
const int maxn = 3000 + 10, inf = 2e9;

template <typename T> inline T read()
{
T ans(0), p(1);
char c = getchar();
while (!isdigit(c))
{
if (c == '-') p = -1;
c = getchar();
}
while (isdigit(c))
{
ans = ans * 10 + c - 48;
c = getchar();
}
return ans * p;
}

int n, m, l[maxn], s[maxn], a[maxn];

int q[maxn], head, tail;
int dp[maxn][2];bool cur;
int A(int x) {return dp[x][cur ^ 1];}
double K(int j, int k) {return (A(j) - A(k)) * 1. / (s[j] - s[k]) + s[j] + s[k];}

signed main()
{
#ifdef CraZYali
freopen("4518.in", "r", stdin);
freopen("4518.out", "w", stdout);
#endif
cin >> n >> m;
REP(i, 1, n) s[i] = s[i-1] + (a[i] = read<int>());
REP(i, 1, n) dp[i][cur ^ 1] = s[i] * s[i];
REP(L, 2, m)
{
q[head = tail = 1] = 0;
REP(i, 1, n)
{
while (head < tail && K(q[head + 1], q[head]) < 2 * s[i]) head++;
dp[i][cur] = dp[q[head]][cur ^ 1] + (s[i] - s[q[head]]) * (s[i] - s[q[head]]);
while (head < tail && K(i, q[tail - 1]) < K(q[tail-1], q[tail])) tail--;
q[++tail] = i;
}
cur ^= 1;
}
cout << m * dp[n][cur ^ 1] - s[n] * s[n];
return 0;
}

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×