题解 P7480 【C】Reboot from Blue

题目简述

数轴上有 \(n\) 个加油站,第 \(i\) 个位于 \(x_i\) ,每升油 \(c_i\) 元,每升油可以走 \(1\) 个单位长度,没油就不能继续行走。

有一辆车,开始时油箱是空的,油箱容量为无限大。求这辆车从 \(s\) 走到 \(t\) 的最小花费,保证 \(s\) 处有加油站。

\(1 \leq n \leq 10^5 ,-10^9 \leq s,t,x_i \leq 10^9 ,s<t ,1 \leq c_i \leq 10^9\)

Solution

对着题目想一会可以发现一个性质:

  • 加油时付的钱一定是单调不上升的。

如果我在现在的加油站加一升油的价钱是 \(c\) 元每升,我在后面又在一个单价以更高的价钱加了油,那还不如直接在这个加油站直接把油全部加完,花的钱肯定更少。

如果用数学的语言表述,假设一共要加 \(a\) 升油,在当前的加油站要花 \(c\) 元加一升油,在后面的路中又有一个加油站,每加一升油要 \(k\) 元( \(k>c\) ),那么有:

\[a \times c < p \times c + (a-p) \times k\ \ |\ \ p \in [0,a] \]

这里的 \(p\) 表示在当前的加油站里加 \(p\) 升油。

如果从图论的角度想一个暴力做法:把每个加油站看成一个点,然后把加油站两两连有向边,其中有向边 \(u \to v\) 的权值是从 \(|x_u-x_v|\times c_u\) ,也就是车在 \(u\) 处加油,使得它刚好能够走到 \(v\) 所需要的花费。

然后建立一个新的点(记作 \(g\) ),这个点的位置是 \(t\) ,从每个加油站 \(u\)\(g\) 连边,权值是 \(|x_u-t|\times c_u\)

然后最小的从 \(s\)\(t\) 的花费转换成在图上找一个从 \(s\)\(t\) 的边权和最小的路径,直接用最短路求就可以。

但是这样会建立 \(\mathcal{O}(n^2)\) 条边,无法接受。我们这个时候就可以用到前面的性质来优化:由于每次付的钱都是单调不上升的,所以对于每个点 \(u\) ,从它向左边第一个单价比它小的加油站连边(如果不存在就不连,权值和上面相同),从它向右边的第一个单价比它小的加油站连边(如果不存在就向 \(g\) 连边,权值和上面相同)。

于是这样就只有 \(2n\) 条边了,可以接受。

最短路最好用 Dijkstra , SPFA 容易被卡成 \(\mathcal{O}(nm)\) 的,虽然好像并没有卡。

至于如何找左边第一个比它小的加油站(右边同理),用单调栈就可以求解。如果不会可以去看一下 【模板】单调栈

一些细节:

  • 题目没说坐标会升序给出,所以要先给坐标从小到大排个序。
  • 记得开 long long ,同时正无穷要设置的比较大。

代码如下(写的比较丑):

#include <cstdio>
#include <cstring>
#include <cctype>
#include <algorithm>
#include <iostream>
#include <queue>
typedef long long LL;
using namespace std;
inline int read() {
	int num = 0 ,f = 1; char c = getchar();
	while (!isdigit(c)) f = c == '-' ? -1 : f ,c = getchar();
	while (isdigit(c)) num = (num << 1) + (num << 3) + (c ^ 48) ,c = getchar();
	return num * f;
}
const int N = 1e5 + 5 ,M = 2e5 + 10;
const LL INF = 0x3f3f3f3f3f3f3f3f; //记得开大
//最短路部分
struct Edge {
	int to; LL w; int next;
	Edge (int to = 0 ,LL w = 0 ,int next = 0) :
		to(to) ,w(w) ,next(next) {}
}G[M]; int head[N] ,cnt;
inline void add(int u ,int v ,LL w) {
	G[++cnt] = Edge(v ,w ,head[u]); head[u] = cnt;
}
struct node {
	int id; LL dis;
	node (int id = 0 ,LL dis = 0) : id(id) ,dis(dis) {}
	friend bool operator < (node a ,node b) {
		return a.dis > b.dis;
	}
};
LL dis[N]; bool done[N];
inline LL dijkstra(int n ,int s ,int t) {
	priority_queue <node> q;
	for (int i = 1; i <= n; i++) dis[i] = INF ,done[i] = false;
	dis[s] = 0; q.push(node(s ,dis[s]));
	while (!q.empty()) {
		int now = q.top().id; q.pop();
		if (done[now]) continue;
		done[now] = true;
		for (int i = head[now]; i ; i = G[i].next) {
			int v = G[i].to;
			if (done[v]) continue;
			if (dis[v] > dis[now] + G[i].w) {
				dis[v] = dis[now] + G[i].w;
				q.push(node(v ,dis[v]));
			}
		}
	}
	return dis[t];
}
//最短路 end
struct station {
	int x ,c;
	station (int x = 0 ,int c = 0) : x(x) ,c(c) {}
	friend bool operator < (station a ,station b) { //双关键字排序
		if (a.x != b.x) return a.x < b.x;
		return a.c < b.c;
	}
}a[N];
int n ,sx ,t ,s[N] ,top ,sid ,idx;
inline LL calc(int x ,int y) { //从 x 直接走到 y 所需要的花费
	return (LL)abs(a[x].x - a[y].x) * a[x].c;
}
signed main() {
	n = read(); sx = read(); t = read();
	for (int i = 1; i <= n; i++) {
		int c = read() ,x = read();
		if (x >= t) continue;
        //小优化:在 t 右边的加油站没有用(因为从 s 走到 t 右边已经经过了 t ,提前停下来会更优)
		a[++idx] = station(x ,c);
	}
	n = idx; idx = 0;
	sort(a + 1 ,a + n + 1); //记得排序
	for (int i = 1; i <= n; i++) {
		if (a[i].x == a[i - 1].x) continue; //这里能直接停下是因为我已经用 c 为第二关键字排序了
		a[++idx] = a[i];
	}
	a[++idx] = station(t ,-0x3f3f3f3f); 
    //这里偷了个懒,直接把 t 设成一个加油站,费用是 -INF
    //这个点左边没有比它小的点,右边没有点,所以这样不会影响结果
	n = idx;
    //避免出现一些奇奇怪怪的错误,这里先去个重,原理就是如果一个点有好几个加油站,那一定只会用到单价最小的那个。
	for (int i = 1; i <= n; i++) if (a[i].x == sx) sid = i;
    //一定要拍完序再去找哪个加油站是起点这个位置上的
    //正着的单调栈
	s[++top] = 1;
	for (int i = 2; i <= n; i++) {
		while (top && a[s[top]].c > a[i].c) top--;
		if (top) add(i ,s[top] ,calc(i ,s[top]));
		s[++top] = i;
	}
    //反着再做一遍
	top = 1; s[top] = n;
	for (int i = n - 1; i >= 1; i--) {
		while (top && a[s[top]].c > a[i].c) top--;
		add(i ,s[top] ,calc(i ,s[top]));
		s[++top] = i;
	}
	printf("%lld\n" ,dijkstra(n ,sid ,n));
	return 0;
}
posted @ 2021-04-06 22:28  recollector  阅读(120)  评论(0编辑  收藏  举报