HDU 7134 Public Transport System 题解
题目大意
有
T
T
T组数据,对于每组数据:有
n
n
n个点
m
m
m条边的有向图,求单源最短路。
特殊之处是:每条边除了权值
a
a
a以外还有一个优惠属性
b
b
b,只要满足你走过的上一条边的权值严格低于当前边的权值,你就可以获得
b
b
b的优惠,即优惠后的费用为
a
−
b
a-b
a−b.
数据范围: 2 ≤ n ≤ 1 0 5 , 1 ≤ m ≤ 2 × 1 0 5 2≤n≤10^5,1≤m≤2×10^5 2≤n≤105,1≤m≤2×105, 1 ≤ T ≤ 1 0 4 1≤T≤10^4 1≤T≤104, ∑ n ≤ 6 × 1 0 5 , ∑ m ≤ 1.2 × 1 0 6 \sum n \leq 6 \times 10^5, \sum m \leq 1.2 \times 10^6 ∑n≤6×105,∑m≤1.2×106.
思路分析
朴素的想法
注意到这题就是单源最短路而已,但是由于有优惠的存在,要求我们记得自己上次走过的边,以判断是否能获取优惠。问题是普通的单源最短路都无法做到这一点,它们都只能区分点的不同,而无法区分边。
于是我们转念一想,既然能区分点的不同,那就每当有一条边时,就将它的终点复制一份专门为这条边所用,这样每个点的入度为1,于是你总是知道这个点从哪里来,适不适用优惠。
可是问题又来了:复制点就必须复制从这个点出去的边,这个想法的复杂度怎么样呢?
考虑下图:
这个图只有8个点,7条边,但是如果要复制点的话会导致图变为下面这个样子:
mermaid真的丑
但是总之,点的个数增长不多,但是边却有15条了,换句话说,是
(
m
2
)
2
(\frac{m}2)^2
(2m)2,特别恐怖。
因此这个做法的最坏复杂度是
O
(
m
2
l
o
g
n
)
O(m^2log n)
O(m2logn),这显然不能满足要求(我TLE了无穷次才明白的道理啊)
优化
显然我们不希望复制如此多的边,那么做什么优化更好呢?
一种想法是,既然一条边只有享受优惠和不享受优惠两种价格,于是我们就只复制一条边,然后大家共用就行,理想的状况如下:
但是,这显然不正确啊!因为不同的边,优惠的临界值不一致啊!
再次优化
不过能够想到这一步,答案离真实结果就不远了。
实际上,只需要先将所有终点相同的边,按照权值 a a a排序,然后由 a a a小的点向 a a a大的点建一个权值为0的边,然后将优惠边建在能享受优惠的最大的 a a a上,将原价边建在最后一个节点上,即可。
如样例1,建图如下(序号后面没有注释的点就是原图中的点,有注释的点即为单独为每条边加的):
mermaid渲染好难,我确实不会排版,大家将就看看吧(哭)
从此图中,我们可以看到,2,a=3
向3,a=4
的边是一条优惠边,但是这个优惠只有2,a=3
以及可能存在的a
更小的点(如果有,这些点会有0权边连接2,a=3
,而a
更大的点会被2,a=3
连接,由于是有向边,所以无法回头走)可以享受,而从2
直接出发就没有这种优惠(不过从2,a=3
出发也可以通过2
走一条没有优惠的路)
代码
前面我说的一点儿也不清楚,哎,所以还是直接上代码吧:
#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
#define re register
typedef long long LL;
const int maxn = 3e5 + 10;
const int maxm = 6e5 + 10;
const int int_inf = 0x3f3f3f3f;
const LL ll_inf = (LL)int_inf << 32 | int_inf;
int head[maxn], nxt[maxm], to[maxm], val[maxm], cnt;
inline void add_edge(int u, int v, int w)
{
cnt++;
nxt[cnt] = head[u];
head[u] = cnt;
to[cnt] = v;
val[cnt] = w;
}
int n, m;
typedef pair<LL, int> dis_point;
LL dis[maxn];
priority_queue<dis_point> pq;
bool vis[maxn];
inline void dijkstra()
{
memset(vis, 0, sizeof(bool) * (n + m + 1));
dis[1] = 0;
pq.push(make_pair(0, 1));
while(!pq.empty())
{
re int u = pq.top().second;
pq.pop();
if(vis[u]) continue;
vis[u] = 1;
for(re int r = head[u]; r; r = nxt[r])
{
re LL tw = dis[u] + val[r];
re int v = to[r];
if(dis[v] > tw)
{
dis[v] = tw;
pq.push(make_pair(-tw, v)); //默认大根堆,所以负值进入
}
}
}
}
struct Edge
{
int u, v, a, b, no;
bool operator < (const Edge &ot) const
{
return v == ot.v ? a > ot.a : v < ot.v; //以入点为第一关键字,费用为第二关键字排序
}
bool operator > (const Edge &ot) const
{
return u == ot.u ? a > ot.a : u < ot.u; //以出点为第一关键字,费用为第二关键字排序
}
}edge_in[maxn], edge_out[maxn];
int main()
{
int T;
scanf("%d", &T);
while(T--)
{
scanf("%d%d", &n, &m);
memset(head, 0, sizeof(int) * (n + m + 1));
memset(dis, 0x3f, sizeof(LL) * (n + m + 1));
cnt = 0;
//注意:我们构造n+m个点,原来的n个点编号为1~n,新点编号n+1~n+m
for(re int i = 1; i <= m; i++)
{
edge_in[i].no = i + n; //边的编号是n+1~n+m
scanf("%d%d%d%d", &edge_in[i].u, &edge_in[i].v, &edge_in[i].a, &edge_in[i].b);
edge_out[i] = edge_in[i];
}
sort(edge_in + 1, edge_in + m + 1, less<Edge>()); //按照入点排序
for(re int i = 1, p = 0; i <= m; i++)
{
if(edge_in[i].v != p)
{
while(edge_in[i].v != p) p++;
add_edge(edge_in[i].no, p, 0);
}
else
add_edge(edge_in[i].no, edge_in[i-1].no, 0);
}
sort(edge_out + 1, edge_out + m + 1, greater<Edge>()); //按照出点排序
for(re int i = 1, j = 1; i <= m; i++)
{
add_edge(edge_out[i].u, edge_out[i].no, edge_out[i].a);
while(j <= m && edge_in[j].v < edge_out[i].u) j++;
while(j <= m && edge_in[j].v == edge_out[i].u && edge_in[j].a >= edge_out[i].a) j++;
if(j > m || edge_in[j].v > edge_out[i].u) continue;
add_edge(edge_in[j].no, edge_out[i].no, edge_out[i].a - edge_out[i].b);
}
dijkstra();
for(re int i = 1; i <= n; i++)
{
if(i != 1) printf(" ");
if(dis[i] == ll_inf) printf("-1");
else printf("%lld", dis[i]);
}
printf("\n");
}
return 0;
}
完结撒花!