洛谷阅读更佳
https://www.luogu.com.cn/article/32sdhywg
题意
给定 n n n 个家庭,其中每个家庭距离起点有 d i d_i di 的距离,每个家庭有 t i t_i ti 个人。
题目需要我们在长度为 L L L 的数轴上放置四个点 p 1 , p 2 , p 3 , p 4 ( p 1 ≤ p 2 ≤ p 3 ≤ p 4 ) p_1,p_2,p_3,p_4(p_1 \le p_2 \le p_3 \le p_4) p1,p2,p3,p4(p1≤p2≤p3≤p4) 并且最后一个点必须落在 L L L 上,然后这 n n n 个家庭会往右一直走直到碰到任意一个点,设第 i i i 个家庭到达一个点的距离是 l e n i len_i leni 那么这个家庭的开销是 l e n i ∗ t i len_i * t_i leni∗ti 。
问如何安排这些点使得所有家庭的开销总和最小?
思路
首先贪心地想,这些点肯定是落在某个家庭的位置代价最小。
然后来考虑动态规划,设 d p [ i ] [ j ] dp[i][j] dp[i][j] 为以第 i i i 个家庭结尾,并且放置了 j j j 个点的最小开销,那么转移方程就是从前面的数中找到一个 k k k 作为 j − 1 j - 1 j−1 的结尾,然后 d p [ i ] [ j ] = d p [ k ] [ j − 1 ] + [ k + 1 , i ] 的开销 dp[i][j] = dp[k][j - 1] + [k + 1, i] 的开销 dp[i][j]=dp[k][j−1]+[k+1,i]的开销 。
那么问题来了,如何计算一个区间的开销呢?
设
p
r
e
s
i
pres_i
presi 为只放置一个点在
i
i
i 并且以
i
i
i 结尾时所有家庭的开销。假设我已经知道
p
r
e
s
i
−
1
pres_{i - 1}
presi−1 了,那么我如何计算出
p
r
e
s
i
pres_i
presi 呢?经过观察,对于
p
r
e
s
i
−
1
pres_{i - 1}
presi−1 来说,每多走一个单位距离,就会使得前面所有的家庭又多走了一个单位距离也就是增加了一倍的家庭人数前缀和。所以设
T
i
=
∑
j
=
1
i
t
j
T_i=\sum_{j=1}^{i}t_j
Ti=∑j=1itj ,得:
p
r
e
s
i
=
p
r
e
s
i
−
1
+
T
i
−
1
∗
(
d
i
−
d
i
−
1
)
pres_i = pres_{i - 1} + T_{i - 1} * (d_i - d_{i - 1})
presi=presi−1+Ti−1∗(di−di−1)
得到所有的 p r e s i pres_i presi 后,获取一个区间的开销就变轻松了:
p r e s [ l , r ] = p r e s r − p r e s l − 1 − ( d r − d l − 1 ) ∗ T l − 1 pres_{[l, r]} = pres_r - pres_{l - 1} - (d_r - d_{l - 1}) * T_{l - 1} pres[l,r]=presr−presl−1−(dr−dl−1)∗Tl−1
也就是把 l l l 之前的贡献减掉就好了。
回到我们的状态转移方程,有:
d p [ i ] [ j ] = min k = 0 i − 1 ( d p [ k ] [ j − 1 ] + p r e s i − p r e s k − ( d i − d k ) ∗ T k ) dp[i][j] = \min_{k = 0}^{i - 1}(dp[k][j - 1] + pres_i - pres_k - (d_i - d_k) * T_k) dp[i][j]=k=0mini−1(dp[k][j−1]+presi−presk−(di−dk)∗Tk)
这样的复杂度是
n
2
n^2
n2 显然是会T的,不要着急,我们思考一下点开标签,考虑动态规划优化。整理转移方程的式子得(过程读者可自行推导):
d p [ k ] [ j − 1 ] − p r e s k + T k ∗ d k = T k ∗ d i − p r e s i + d p [ i ] [ j ] dp[k][j - 1] - pres_k + T_k * d_k = T_k * d_i - pres_i + dp[i][j] dp[k][j−1]−presk+Tk∗dk=Tk∗di−presi+dp[i][j]
是不是有点眼熟?有点像形如 y = k x + b y = kx + b y=kx+b 的式子?我们令:
y = d p [ k ] [ j − 1 ] − p r e s k + T k ∗ d k , y = dp[k][j - 1] - pres_k + T_k * d_k, y=dp[k][j−1]−presk+Tk∗dk,
x = T k , x = T_k, x=Tk,
k = d i , k = d_i, k=di,
b = d p [ i ] [ j ] − p r e s i b = dp[i][j] - pres_i b=dp[i][j]−presi
那么对于这个式子来说,只要使得 b b b 最小,那么 d p [ i ] [ j ] dp[i][j] dp[i][j] 就最小,画一个二维坐标系发现横坐标单调递增,所以可以使用优先队列维护我们的答案。
注意的点
运算过程中会爆LL所以使用int128防溢出。
代码
#include <iostream>
using namespace std;
#define MAX_N 100005
#define ll long long
#define ull unsigned ll
ll dp[5][MAX_N] = { 0 };
ll dis[MAX_N] = { 0 };
__int128 T[MAX_N] = { 0 };
__int128 pres[MAX_N] = { 0 };
int head = 1, tail = 1;
int q[MAX_N + 10] = { 0 };
__int128 get_y(int i, int j) { // j - 1
return dp[j - 1][i] - pres[i] + T[i] * dis[i];
}
long double slope(int a, int b, int j) {
ll x1 = T[a], x2 = T[b];
ll y1 = get_y(a, j), y2 = get_y(b, j);
if (x1 == x2) return 999999999999999;
return (y2 - y1) * 1.0f / (x2 - x1);
}
int main() {
int n, L;
scanf("%d%d", &n, &L);
for (int x = 1; x <= n; x++) {
ll ti;
scanf("%lld%lld", dis + x, &ti);
T[x] = ti + T[x - 1];
pres[x] = pres[x - 1] + T[x - 1] * (dis[x] - dis[x - 1]);
//cout << pres[x] << endl;
}
n++;
dis[n] = L;
T[n] = T[n - 1];
pres[n] = pres[n - 1] + T[n - 1] * (dis[n] - dis[n - 1]);
for (int i = 1; i <= n; i++) dp[1][i] = pres[i];
for (int j = 2; j <= 4; j++) {
head = tail = 1;
q[tail++] = 0;
for (int i = 1; i <= n; i++) {
ll xielv = dis[i];
while (tail - head >= 2 && slope(q[head], q[head + 1], j) <
xielv)
head++;
//printf("[%d, %d] head = %d\n", i, j, q[head]);
int k = q[head];
dp[j][i] = dp[j - 1][k] + pres[i] - pres[k] - (dis[i] - dis[k]) * T[k];
//printf("%llu %llu %llu\n", dis[i], dis[k], T[k]);
while (tail - head >= 2 && slope(q[tail - 1], q[tail - 2], j) >=
slope(q[tail - 1], i, j))
tail--;
q[tail++] = i;
//printf("[%d, %d] = %lld\n", i, j, dp[j][i]);
}
}
ll ans = 0x7f7f7f7f7f7f7f7f;
for (int x = 1; x <= 4; x++) ans = min(ans, dp[x][n]);
printf("%lld", ans);
return 0;
}