题解:P8632 [蓝桥杯 2015 国 B] 居民集会

洛谷阅读更佳

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(p1p2p3p4) 并且最后一个点必须落在 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 leniti

问如何安排这些点使得所有家庭的开销总和最小?

思路

首先贪心地想,这些点肯定是落在某个家庭的位置代价最小。

然后来考虑动态规划,设 d p [ i ] [ j ] dp[i][j] dp[i][j] 为以第 i i i 个家庭结尾,并且放置了 j j j 个点的最小开销,那么转移方程就是从前面的数中找到一个 k k k 作为 j − 1 j - 1 j1 的结尾,然后 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][j1]+[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} presi1 了,那么我如何计算出 p r e s i pres_i presi 呢?经过观察,对于 p r e s i − 1 pres_{i - 1} presi1 来说,每多走一个单位距离,就会使得前面所有的家庭又多走了一个单位距离也就是增加了一倍的家庭人数前缀和。所以设 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=presi1+Ti1(didi1)

得到所有的 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]=presrpresl1(drdl1)Tl1

也就是把 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=0mini1(dp[k][j1]+presipresk(didk)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][j1]presk+Tkdk=Tkdipresi+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][j1]presk+Tkdk,

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;
}
  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值