一、题目:
二、思路分析:
由题目分析可知,本题是一个明显的最短路问题,且所有花费(边权)都是非负的,我们可以使用Dijkstra算法。
但本题的难点在于:
- 走到终点时,需要经过3的倍数步才能逃离,该如何计步?
- 每次瞬移应该如何表示?
Dijkstra算法的核心是每次选择距离最小的节点进行拓展,但是因为步数原因,到达每一个节点时,无法直接比较距离来选择最小节点(不同的步数会有不同的结果,到达终点时不是3的倍数会直接作废)。
因此正确做法是,题中所给3的倍数即我们只用考虑所用边数模3的三个状态,可以把一个点拆分成三个点,分别代表到达这个点时的所用边数模3的状态。也就是说把节点设计为(房间编号,步数模3余数)的二元组。
- 对于每个物理房间i(1-n),我们创建3个状态节点:(i,0),(i,1),(i,2)节点(i,k)表示通过k步(模3意义下)到达房间i的状态。
其次就是瞬移问题,容易想到可以把瞬移中的每两个同类型房间(包含同房间之间)分别建立两条单向普通道路代替瞬移的双向特点,即可跑最短路算法。但是这样的做法问题在于同类型房间,考虑到房间个数n最大为 ,那么双向建边=
,必定超出题目限制。
那么可以给每个房间类型T引入一组辅助节点,可以看做是一个辅助空间,这个空间联通了该类型的所有房间。每次瞬移时,先来到该类型对应的瞬移网络(辅助空间),再由辅助空间传输到任意一个同类型房间,这样就顺利解决了时间以及空间问题。
- 假设总共有C种不同的房间类型。我们为每种类型T也创建3个辅助状态节点:(
,0),(
,1),(
,2)。
- 节点 (
,
) 代表通过
步(模 3 意义下)进入了类型 T 的瞬移网络的状态。
这样我们可以把一次瞬移转化成花费x的代价进⼊瞬移网络和花费0的代价出瞬移网络这两步操作。具体过程可以看下面建边的过程。
现在我们在这些状态节点之间连边:
1. 普通道路: 对于原图中的一条边u→v花费为d。
1)它对应着 3 条状态图中的边:
(u,0)→(v,1),花费d
(u,1)→(v,2),花费d
(u,2)→(v,0),花费d
2)因为走过这条路,步数增加了1,所以模3余数从k变为(k+1)%3。
2. 进入瞬移网络:从一个房间i(类型为T)进入瞬移网络。
1)对于房间i的每个状态k=0,1,2:
添加边:(i,k)→(,(k+1)%3)花费x。
这表示支付x金币进行瞬移,并且计为1步,所以步数模3余数k变为(k + 1)%3。
3. 出瞬移网络:从类型T的瞬移网络出来,到达该类型的任意一个房间j。
1)对于类型T的每个辅助状态=0,1,2:
对于所有属于类型 T 的房间 j:
添加边:(,
) → (j,
),花费0。
这表示从瞬移网络出来到达某个具体房间,这个过程不花费金币,也不额外增加步数。
之后我们就得到了一张有向无负权的图,直接跑最短路算法即可。
三、代码及注释
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
#define M 1000005
#define N 100005
using namespace std;
struct E {
int to, nx, d;
} edge[M * 4];//用于存储道路
//M*4 -> 原有单向边每条边出发点分成3个(m*3<=M*3)
// +
// 进出瞬移网络(最多n*2<m<=M)
int tot, head[6 * N];
void Addedge(int a, int b, int d) {
edge[++tot].to = b;
edge[tot].d = d;
edge[tot].nx = head[a];
head[a] = tot;
}//建立道路邻接表
#define INF 1000000000000000000
int n, m, x;//房间数 单向道路数 瞬移花费
int A[N];//存储房间类型
struct node {
int id;//房间号
long long d;
bool operator<(const node &_) const { return d > _.d; }
//优先队列按照d从小到大排列
};
bool done[N * 6];//该节点是否被走过
long long dis[N * 6];//到房间1的距离
//N*6 -> 物理层面每个房间被分成3个步数状态的点N*3
// +
//每个房间种类对应的空间也有三个步数状态的点 最多N*3
//N*6中顺序是-> 每个房间步数为0(n个) 步数为1(n个) 步数为2(n个)
// 每个类型瞬移空间步数为0(预留n个) 步数为1(预留n个),步数为2(预留n个)
void Solve() {
for (int i = 1; i <= 6 * n; i++) {
dis[i] = INF;
done[i] = false;
}
dis[1] = 0;
for (int i = 1; i <= n; i++) {//对每一个点添加该点和对应瞬移网络之间的路
for (int j = 0; j < 3; j++) {
Addedge(i + j * n, A[i] + 3 * n + ((j + 1) % 3) * n, x);//进入花费x
Addedge(A[i] + 3 * n + ((j + 1) % 3) * n, i + ((j + 1) % 3) * n, 0);//跳出无花费
//+3*n 即跳转到瞬移空间存储位置
}
}
priority_queue<node> Q;
Q.push((node){1, 0});
while (!Q.empty()) {//Dijkstra算法
int now = Q.top().id;
Q.pop();
if (done[now]) continue;
done[now] = true;
for (int i = head[now]; i; i = edge[i].nx) {
int nxt = edge[i].to;
if (dis[nxt] > dis[now] + edge[i].d) {
dis[nxt] = dis[now] + edge[i].d;
Q.push((node){nxt, dis[nxt]});
}
}
}
if (dis[n] < INF)
printf("%lld\n", dis[n]);
else
puts("-1");
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d%d", &n, &m, &x);
for (int i = 1; i <= n; i++) scanf("%d", &A[i]);
tot = 0;
for (int i = 1; i <= 6 * n; i++) head[i] = 0;
while (m--) {
int a, b, d;
scanf("%d%d%d", &a, &b, &d);
for (int i = 0; i < 3; i++) {//添加边 遍历出发点的拆分出的三个点->结束点 更新步数
Addedge(a + i * n, b + ((i + 1) % 3) * n, d);
//该房间的不同步数点,结束点
}
}
Solve();
}
return 0;
}