【2018 校ACM选拔赛】最短路
Tags:最短路问题 SPFA(+SLF+LLL) BFS分层 链式前向星
题目描述
n n n 个点的无向有根树,以 1 1 1 号点为根,走一条边需要花费相应的代价。
任意深度相差为
1
1
1 的点之间可以相互跳跃,花费代价为
p
p
p ,求
s
s
s 走到
t
t
t 的最小代价。
输入
T T T 组。第一行输入组数 T T T
对于每组数据,包含若干行,第一行四个整数 n , p , s , t n,p,s,t n,p,s,t
接下来 n − 1 n−1 n−1 行描述这棵树,每行三个整数 u , v , w u,v,w u,v,w,表示走一条连接 u u u 和 v v v 号点的边的代价为 w w w 。
其中, 1 ≤ T ≤ 20 , 1 ≤ n ≤ 1 0 5 , 1 ≤ s , t , u , v ≤ n , 0 ≤ p , w ≤ 1 0 9 1≤T≤20,1≤n≤10^5,1≤s,t,u,v≤n,0≤p,w≤10^9 1≤T≤20,1≤n≤105,1≤s,t,u,v≤n,0≤p,w≤109
输出
对于每组数据,输出一行,格式为 Case #number: result
,其中
n
u
m
b
e
r
number
number 表示这是第
n
u
m
b
e
r
number
number 组数据,而
r
e
s
u
l
t
result
result 为答案。
输入样例
2
2 1 1 2
1 2 3
7 999999999 2 7
1 2 1000000000
1 3 1000000000
2 4 999999998
3 5 1000000000
5 6 1000000000
6 7 999999998
输出样例
Case #1: 1
Case #2: 2999999995
分析
本题关键在于建图。难度不大,但是细节很容易出错。
首先规规矩矩地建树(边数是结点数-1):
- 没什么好说的。由于是稀疏图,所以应该用链式前向星或者
s
t
d
:
:
v
e
c
t
o
r
std::vector
std::vector 实现的邻接表存图
然后考虑如何实现跳跃,从而建图:
-
为了实现跳跃,需要允许在树的层间进行
中转
。下面是最直接想到的方法,直接加边:
但是仅仅像这样,把层数差 1 1 1 的所有点对
之间直接连边是不行的。这样添加边的数目最坏情况(比如除了根结点就两层,那每层都近似是 V / 2 V/2 V/2 个点,一共有 V 2 / 8 V^2/8 V2/8 条边需要加)将达到 V 2 V^2 V2 数量级,而这道题 V ≤ 1 0 5 V≤10^5 V≤105, V 2 V^2 V2 肯定会爆。 -
所以为了实现跳跃,需要在树的每两层之间加两个额外的结点用于中转,而不是直接加边:
通过这种中转
实现层间的全连接,最多连边不超过 4 ∗ V 4*V 4∗V(最多每个点连不超过四条线),在接受范围内 -
如上图,对于原来的树上任一结点(黑色结点) u u u,如果其有父节点 f f f,那么就有中转结点 L L L(可以形象地把它画在 u u u 层和 f f f 层之间的
左侧
)使得存在一条路径 f → L → u f\rightarrow L\rightarrow u f→L→u,其长度之和为跳跃花费。
- 比如结点 4 4 4,其父节点为 2 2 2,可利用中转结点 7 7 7,实现 2 → 7 → 4 2\rightarrow 7\rightarrow 4 2→7→4。不妨让 2 → 7 2\rightarrow 7 2→7 距离为 q / 2 q/2 q/2, 7 → 4 7\rightarrow 4 7→4距离为 q − q / 2 q-q/2 q−q/2,这样就实现了向下中转的目的。这样, 2 、 3 2、3 2、3 都可以通过 7 7 7中转
到达 4 4 4。 -
同样地,
右边
还有中转结点 R R R,使得存在一条路径 u → R → f u\rightarrow R\rightarrow f u→R→f,其长度也为跳跃花费; -
这样的话就能完美实现层间跳跃,并且边数也不会太大~(实际上边数仍然是 Θ ( n ) \Theta(n) Θ(n) 的)
-
然后通过找规律(这里还需要知道原来树上每个点的
层数
,这个 B F S BFS BFS分层 即可)即可确定给出每个点它左上、右上、左下、右下的中转结点的编号 -
再像上面距离那样规定 f → L f\rightarrow L f→L 长度为 p / 2 p/2 p/2, L → u L\rightarrow u L→u 长度为剩余部分; f → R f\rightarrow R f→R 长度为 p / 2 p/2 p/2, R → u R\rightarrow u R→u 长度为剩余部分,就可以完整地把这些有向的中转边都加进去了。
然后就可以愉快地跑SPFA了(边数是和顶点数同数量级的,SPFA更快,亲测):
- 时间复杂度:建树 Θ ( N ) \Theta(N) Θ(N), B F S BFS BFS 分层 Θ ( N ) \Theta(N) Θ(N),加中转点建图 Θ ( N ) \Theta(N) Θ(N), S P F A SPFA SPFA O ( k N ) O(kN) O(kN),总的 O ( k N ) O(kN) O(kN)
- 空间复杂度:多个
Θ
(
N
)
\Theta(N)
Θ(N),还是
Θ
(
N
)
\Theta(N)
Θ(N)
AC代码
首先是没有 SLF + LLL 的版本:
#include <stdio.h>
#include <string.h>
#define add_edge(e, u, v, d) \
do{ \
edge[e].next = head[u]; \
edge[e].dest = v; \
edge[e].dist = d; \
head[u] = e; \
} while (0)
#define LL long long
#define sc(x) {register char _c=getchar(),_v=1;for(x=0;_c<48||_c>57;_c=getchar())if(_c==45)_v=-1;for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=getchar());x*=_v;}
constexpr int MV(3e5+7);
constexpr LL INF(0x3f3f3f3f3f3f3f3fLL);
struct Edge
{
int dest;
int dist;
int next;
} edge[MV * 4];
int head[MV], tot;
int depth[MV], max_depth;
LL dist_src[MV];
bool inq[MV];
int queue[MV];
void read_edge(const int V)
{
memset(head+1, 0, sizeof(*head) * 3 * V);
tot = 2*(V-1);
for (int e=1; e<=tot; ++e)
{
int u, v, d;
sc(u)sc(v)sc(d)
add_edge(e, u, v, d);
++e;
add_edge(e, v, u, d);
}
}
void bfs(const int V, const int root)
{
memset(inq+1, 0, sizeof(*inq) * V);
inq[root] = true;
depth[root] = 1;
int h = 0, t = 0;
queue[t++] = root;
while (h != t)
{
const int u = queue[h++];
for (int i=head[u]; i; i=edge[i].next)
{
const int v = edge[i].dest;
if (!inq[v])
{
inq[v] = true;
queue[t++] = v;
depth[v] = depth[u] + 1;
if (max_depth < depth[v])
max_depth = depth[v];
}
}
}
}
void add_vertex(int &V, const int k)
{
const int VV = V + 2 * (max_depth - 1);
int k1 = k>>1;
int k2 = k-k1;
for (int v=1; v<=V; ++v)
{
const int LU = V+depth[v]-1, RU = VV-depth[v]+2,
LD = V+depth[v], RD = VV-depth[v]+1;
if (depth[v] != 1)
{
++tot;
add_edge(tot, LU, v, k1);
++tot;
add_edge(tot, v, RU, k2);
}
if (depth[v] != max_depth)
{
++tot;
add_edge(tot, RD, v, k1);
++tot;
add_edge(tot, v, LD, k2);
}
}
V = VV;
}
void spfa(const int V, const int s)
{
memset(inq+1, false, sizeof(*inq) * V);
memset(dist_src+1, 0x3f, sizeof(*dist_src) * V);
inq[s] = true;
dist_src[s] = 0;
int h = 0, t = 0;
queue[t++] = s;
while (h != t)
{
const int u = queue[h++];
inq[u] = false;
for (int i=head[u]; i; i=edge[i].next)
{
const auto v = edge[i].dest;
const auto duv = edge[i].dist;
if (dist_src[v] > dist_src[u] + duv)
{
dist_src[v] = dist_src[u] + duv;
if (!inq[v])
{
inq[v] = true;
queue[t++] = v;
}
}
}
}
}
int main()
{
int T;
sc(T)
for (int _=1; _<=T; ++_)
{
int V, k, s, t;
sc(V)sc(k)sc(s)sc(t)
read_edge(V);
bfs(V, 1);
add_vertex(V, k); // V changed
spfa(V, s);
printf("Case #%d: %lld\n", _, dist_src[t]);
}
}
然后是加了 SLF + LLL 的版本(为了避免数组越界用了一些奇技淫巧):
#include <stdio.h>
#include <string.h>
#define add_edge(e, u, v, d) \
do{ \
edge[e].next = head[u]; \
edge[e].dest = v; \
edge[e].dist = d; \
head[u] = e; \
} while (0)
#define LL long long
#define sc(x) {register char _c=getchar(),_v=1;for(x=0;_c<48||_c>57;_c=getchar())if(_c==45)_v=-1;for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=getchar());x*=_v;}
constexpr int MV(3e5+7);
constexpr LL INF(0x3f3f3f3f3f3f3f3fLL);
struct Edge
{
int dest;
int dist;
int next;
} edge[MV * 4];
int head[MV], tot;
int depth[MV], max_depth;
LL dist_src[MV];
bool inq[MV];
int _queue[MV * 10], *queue = _queue + MV * 5;
void read_edge(const int V)
{
memset(head+1, 0, sizeof(*head) * 3 * V);
tot = 2*(V-1);
for (int e=1; e<=tot; ++e)
{
int u, v, d;
sc(u)sc(v)sc(d)
add_edge(e, u, v, d);
++e;
add_edge(e, v, u, d);
}
}
void bfs(const int V, const int root)
{
memset(inq+1, 0, sizeof(*inq) * V);
inq[root] = true;
depth[root] = 1;
int h = 0, t = 0;
queue[t++] = root;
while (h != t)
{
const int u = queue[h++];
for (int i=head[u]; i; i=edge[i].next)
{
const int v = edge[i].dest;
if (!inq[v])
{
inq[v] = true;
queue[t++] = v;
depth[v] = depth[u] + 1;
if (max_depth < depth[v])
max_depth = depth[v];
}
}
}
}
void add_vertex(int &V, const int k)
{
const int VV = V + 2 * (max_depth - 1);
int k1 = k>>1;
int k2 = k-k1;
for (int v=1; v<=V; ++v)
{
const int LU = V+depth[v]-1, RU = VV-depth[v]+2,
LD = V+depth[v], RD = VV-depth[v]+1;
if (depth[v] != 1)
{
++tot;
add_edge(tot, LU, v, k1);
++tot;
add_edge(tot, v, RU, k2);
}
if (depth[v] != max_depth)
{
++tot;
add_edge(tot, RD, v, k1);
++tot;
add_edge(tot, v, LD, k2);
}
}
V = VV;
}
void spfa(const int V, const int s)
{
memset(inq+1, false, sizeof(*inq) * V);
memset(dist_src+1, 0x3f, sizeof(*dist_src) * V);
inq[s] = true;
dist_src[s] = 0;
int h = 0, t = 0, q_size = 0;
queue[t++] = s;
LL sum;
while (h != t)
{
while (dist_src[queue[h]] * q_size > sum) // LLL
queue[t++] = queue[h], ++h;
const int u = queue[h++];
inq[u] = false;
sum -= dist_src[u], --q_size;
for (int i=head[u]; i; i=edge[i].next)
{
const auto v = edge[i].dest;
const auto duv = edge[i].dist;
if (dist_src[v] > dist_src[u] + duv)
{
dist_src[v] = dist_src[u] + duv;
if (!inq[v])
{
inq[v] = true;
if (h < t && dist_src[queue[h]] > dist_src[v]) // SLF
queue[--h] = v;
else
queue[t++] = v;
sum += dist_src[v], ++q_size;
}
}
}
}
}
int main()
{
int T;
sc(T)
for (int _=1; _<=T; ++_)
{
int V, k, s, t;
sc(V)sc(k)sc(s)sc(t)
read_edge(V);
bfs(V, 1);
add_vertex(V, k); // V changed
spfa(V, s);
printf("Case #%d: %lld\n", _, dist_src[t]);
}
}
div2 只有十分钟啦,踩点写完博客,马上开打~