玩具装箱
题目链接:ybt金牌导航1-3-1 / luogu P3195
题目大意
有一堆物品,要把它分成任意个区间。
对于每个区间,长度
x
x
x 是
j
−
i
+
∑
k
=
i
j
C
k
j-i+\sum\limits_{k=i}^{j}C_k
j−i+k=i∑jCk。
C
i
C_i
Ci 已给出,
j
j
j 是这个区间的右端点,
i
i
i 是这个区间的左端点。
然后这个区间的费用就是
(
x
−
L
)
2
(x-L)^2
(x−L)2。
L
L
L 已给出。
斜率优化的做法
思路1(斜率优化)
首先我们考虑暴力 dp。
设
f
i
f_i
fi 为对前
i
i
i 个物品进行划分的最小费用。
那很容易列出方程。
f
i
=
min
1
≤
j
<
i
{
f
j
+
(
i
−
(
j
+
1
)
+
∑
k
=
j
+
1
i
C
k
−
L
)
2
}
f_i=\min\limits_{1\leq j<i}\{f_j+(i-(j+1)+\sum\limits_{k=j+1}^{i}C_k-L)^2\}
fi=1≤j<imin{fj+(i−(j+1)+k=j+1∑iCk−L)2}
然后我们可以看到里面的
∑
\sum
∑ 可以用前缀和
s
u
m
i
sum_i
sumi 来弄。
f
i
=
min
1
≤
j
<
i
{
f
j
+
(
i
−
(
j
+
1
)
+
s
u
m
i
−
s
u
m
j
−
L
)
2
}
f_i=\min\limits_{1\leq j<i}\{f_j+(i-(j+1)+sum_i-sum_j-L)^2\}
fi=1≤j<imin{fj+(i−(j+1)+sumi−sumj−L)2}
f
i
=
min
1
≤
j
<
i
{
f
j
+
(
i
−
j
−
1
+
s
u
m
i
−
s
u
m
j
−
L
)
2
}
f_i=\min\limits_{1\leq j<i}\{f_j+(i-j-1+sum_i-sum_j-L)^2\}
fi=1≤j<imin{fj+(i−j−1+sumi−sumj−L)2}
f
i
=
min
1
≤
j
<
i
{
f
j
+
(
i
+
s
u
m
i
−
(
j
+
1
+
s
u
m
j
+
L
)
)
2
}
f_i=\min\limits_{1\leq j<i}\{f_j+(i+sum_i-(j+1+sum_j+L))^2\}
fi=1≤j<imin{fj+(i+sumi−(j+1+sumj+L))2}
然后为了计算看的简洁,我们设
a
i
=
i
+
s
u
m
i
,
b
i
=
i
+
1
+
s
u
m
i
+
L
a_i=i+sum_i,b_i=i+1+sum_i+L
ai=i+sumi,bi=i+1+sumi+L。
f
i
=
min
1
≤
j
<
i
{
f
j
+
(
a
i
−
b
j
)
2
}
f_i=\min\limits_{1\leq j<i}\{f_j+(a_i-b_j)^2\}
fi=1≤j<imin{fj+(ai−bj)2}
f
i
=
min
1
≤
j
<
i
{
f
j
+
a
i
2
−
2
×
a
i
×
b
j
+
b
j
2
}
f_i=\min\limits_{1\leq j<i}\{f_j+a_i^2-2\times a_i\times b_j+b_j^2\}
fi=1≤j<imin{fj+ai2−2×ai×bj+bj2}
那有这些求最小值的,那我们考虑能不能斜率优化。
f
i
=
f
j
+
a
i
2
−
2
×
a
i
×
b
j
+
b
j
2
f_i=f_j+a_i^2-2\times a_i\times b_j+b_j^2
fi=fj+ai2−2×ai×bj+bj2
2
×
a
i
×
b
j
+
f
i
−
a
i
2
=
f
j
+
b
j
2
2\times a_i\times b_j + f_i-a_i^2=f_j+b_j^2
2×ai×bj+fi−ai2=fj+bj2
那就可以设
x
=
b
j
,
y
=
f
j
+
b
j
2
x=b_j,y=f_j+b_j^2
x=bj,y=fj+bj2,那斜率就是
2
×
a
i
2\times a_i
2×ai。
然后就是斜率优化的用单调队列来搞就好了。
代码1(斜率优化)
#include<cstdio>
using namespace std;
int n, l, head, tail, st[50001];
double c[50001], sum[50001];
double f[50001];
double a(int i) {
return sum[i] + i;
}
double b(int i) {
return sum[i] + i + l + 1;
}
double X(int i) {
return b(i);
}
double Y(int i) {
return f[i] + b(i) * b(i);
}
double xl(int i, int j) {//计算斜率
return (Y(i) - Y(j)) / (X(i) - X(j));
}
int main() {
scanf("%d %d", &n, &l);
for (int i = 1; i <= n; i++) {
scanf("%lf", &c[i]);
sum[i] = sum[i - 1] + c[i];
}
head = tail = 1;
for (int i = 1; i <= n; i++) {
while (head < tail && xl(st[head], st[head + 1]) < 2 * a(i))
head++;
f[i] = f[st[head]] + (a(i) - b(st[head])) * (a(i) - b(st[head]));
while (head < tail && xl(i, st[tail - 1]) < xl(st[tail - 1], st[tail]))
tail--;
st[++tail] = i;
}
printf("%lld", (long long)f[n]);
return 0;
}
决策单调性的做法
思路2(决策单调性)
这个方法理解起来还好,然后实现……
看着别人的代码我发蒙了一个下午。
终于,在 LYF 老大哥的帮助下,我 A 了。
先讲讲好理解的方法。
首先,你同样列出暴力方程。(这里就不再弄了,忘了的自己到上面看)
然后你会发现它是类似
F
x
=
min
i
=
1
x
−
1
{
F
i
+
f
i
,
x
}
F_x=\min\limits_{i=1}^{x-1}\{F_i+f_{i,x}\}
Fx=i=1minx−1{Fi+fi,x} 的式子,那我们会想到决策单调性。
那你就要看这个东西它是否满足四边形不等式。
如果要用决策单调性来做,就要保证这个式子是四边形不等式。
什么是四边形不等式呢?
就是要满足
f
i
,
j
+
f
i
+
1
,
j
+
1
≤
f
i
+
1
,
j
+
f
i
,
j
+
1
f_{i,j}+f_{i+1,j+1}\leq f_{i+1,j}+f_{i,j+1}
fi,j+fi+1,j+1≤fi+1,j+fi,j+1。
那在这道题中,
f
i
,
j
=
(
i
−
j
−
1
+
s
u
m
i
−
s
u
m
j
−
L
)
2
f_{i,j}=(i-j-1+sum_i-sum_j-L)^2
fi,j=(i−j−1+sumi−sumj−L)2。
你把式子都展开,看一下,就会发现它是满足的。
(当然考试的时候你想不出来你可以猜,猜它就是满足的)
然后我们看,满足这个之后,它就会有这样的情况。
对于
F
i
F_i
Fi,如果
F
i
=
x
F_i=x
Fi=x,那对于大于等于
i
i
i 的
j
j
j,有
F
j
≥
x
F_j\geq x
Fj≥x。
也就是说,它有单调性。
那怎么求呢?
两种方法,分治和二分。这里只介绍二分的方法。
你要想,假设现在已经求得了
F
1
∼
i
−
1
F_{1\sim i-1}
F1∼i−1 的值,然后要求
F
i
F_i
Fi。
假设之前它是这样的:111222333333
那它的出现会使得数列后面一部分改变。
分两种情况:
第一种:111222333444
也就是说它只是拿掉了一部分原来的连续段,那找这个位置我们可以用二分来找。
第二种:111244444444
也就是说它不仅拿走了一个的一部分,还一整个的拿走了一些,那怎么会拿走呢?
就是这个区间的左端点的位置还是新的优。
那你就先看能不能连续的拿走整个区间,然后不能拿了再二分拿一部分。
然后有一点就是如果它吞部分的时候,你二分之后找到之后还要验证一下。要真的可以才能吞。
代码2(决策单调性)
#include<cstdio>
using namespace std;
long long n, L, c[50001], sta[50001], top, ll[50001];
long long f[50001], sum[50001], mid, l, r, re;
long long clac(long long i, long long j) {//算从状态i转移到状态j的花费
return f[i] + (sum[j] - sum[i] + j - i - L - 1ll) * (sum[j] - sum[i] + j - i - L - 1ll);
}
long long find(long long x) {//二分出到它区间是哪个
l = 1, r = top;
while (l <= r) {
mid = (l + r) >> 1;
if (ll[mid] == x) return mid;
if (ll[mid] > x) r = mid - 1;
else l = mid + 1;
}
return r;
}
int main() {
scanf("%lld %lld", &n, &L);
for (long long i = 1; i <= n; i++) {
scanf("%lld", &c[i]);
sum[i] = sum[i - 1] + c[i];//前缀和
}
r = 1;
ll[0] = 1;
sta[1] = 0;
for (long long i = 1; i <= n; i++) {
f[i] = clac(sta[find(i)], i);
while (top && clac(i, ll[top]) < clac(sta[top], ll[top])) top--;
//把能整个吞掉的区间吞掉
l = ll[top], r = n;//二分能吞一部分的区间
while (l <= r) {
mid = (l + r) >> 1;
if (clac(i, mid) < clac(sta[top], mid)) {
re = mid;
r = mid - 1;
}
else l = mid + 1;
}
if (clac(i, re) > clac(sta[top], re)) continue;//吞不了
top++;//吞
sta[top] = i;
ll[top] = re;
}
printf("%lld", f[n]);
return 0;
}