25钉耙春季赛10-1006小塔的梦境迷宫-巧妙转化至Dijkstra算法解决(在官方题解上的扩展)

一、题目: 

 二、思路分析:

    由题目分析可知,本题是一个明显的最短路问题,且所有花费(边权)都是非负的,我们可以使用Dijkstra算法。

    但本题的难点在于:

  1. 走到终点时,需要经过3的倍数步才能逃离,该如何计步?
  2. 每次瞬移应该如何表示?

    Dijkstra算法的核心是每次选择距离最小的节点进行拓展,但是因为步数原因,到达每一个节点时,无法直接比较距离来选择最小节点(不同的步数会有不同的结果,到达终点时不是3的倍数会直接作废)。

    因此正确做法是,题中所给3的倍数即我们只用考虑所用边数模3的三个状态,可以把一个点拆分成三个点,分别代表到达这个点时的所用边数模3的状态。也就是说把节点设计为(房间编号,步数模3余数)的二元组。

  1. 对于每个物理房间i(1-n),我们创建3个状态节点:(i,0),(i,1),(i,2)节点(i,k)表示通过k步(模3意义下)到达房间i的状态。

    其次就是瞬移问题,容易想到可以把瞬移中的每两个同类型房间(包含同房间之间)分别建立两条单向普通道路代替瞬移的双向特点,即可跑最短路算法。但是这样的做法问题在于同类型房间,考虑到房间个数n最大为10^{5} ,那么双向建边=O(n^{2}),必定超出题目限制。

    那么可以给每个房间类型T引入一组辅助节点,可以看做是一个辅助空间,这个空间联通了该类型的所有房间。每次瞬移时,先来到该类型对应的瞬移网络(辅助空间),再由辅助空间传输到任意一个同类型房间,这样就顺利解决了时间以及空间问题。

  1. 假设总共有C种不同的房间类型。我们为每种类型T也创建3个辅助状态节点:(Aux_{T},0),(Aux_{T},1),(Aux_{T},2)。
  2. 节点 (Aux_{T}, {k}') 代表通过{k}'步(模 3 意义下)进入了类型 T 的瞬移网络的状态。

这样我们可以把一次瞬移转化成花费x的代价进⼊瞬移网络和花费0的代价出瞬移网络这两步操作。具体过程可以看下面建边的过程。

现在我们在这些状态节点之间连边:

1. 普通道路: 对于原图中的一条边u→v花费为d

    1)它对应着 3 条状态图中的边:

          (u0)→(v1),花费d

          (u1)→(v2),花费d

          (u2)→(v0),花费d

    2)因为走过这条路,步数增加了1,所以模3余数从k变为(k+1)%3

2. 进入瞬移网络:从一个房间i(类型为T)进入瞬移网络。

    1)对于房间i的每个状态k=0,1,2

           添加边:(i,k)→(Aux_{T},(k+1)%3)花费x

           这表示支付x金币进行瞬移,并且计为1步,所以步数模3余数k变为(k + 1)%3

3. 出瞬移网络:从类型T的瞬移网络出来,到达该类型的任意一个房间j

    1)对于类型T的每个辅助状态{k}'=0,1,2:

           对于所有属于类型 T 的房间 j

           添加边:(Aux_{T}{k}') → (j,{k}'),花费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;
}

 

### 关于钉耙编程春季题解 针对2025年“钉耙编程”中国大学生算法设计春季热身中的题目解析,维护特定变量如 `preN`、`preY`、`preYE`、`sufO`、`sufS` 和 `sufES` 是解决问题的关键部分之一[^1]。对于动态规划(DP)问题,在处理三个序列的匹配时,如果满足条件 `s1[i]==s2[j]==s3[k]` 即 `a[i]==a[j]==a[k]` 的情况下,新的状态可以通过之前的状态转移而来,具体来说是前 `i-1`, `j-1`, `k-1` 位置的答案总和的两倍加一,这表示新增加了一个符合条件的位置组合[^2]。 #### 动态规划解决方案示例 当面对涉及多个数组或字符串比较的问题时,可以采用三维动态规划的方法来解决。下面是一个简化版的例子: ```cpp #include <bits/stdc++.h> using namespace std; int dp[100][100][100]; // 假设最大长度不超过99 // 初始化边界条件... for (int i = 1; i <= n; ++i){ for(int j = 1; j <= m; ++j){ for(int k = 1; k <= l; ++k){ if(s1[i-1] == s2[j-1] && s2[j-1] == s3[k-1]){ dp[i][j][k] = dp[i-1][j-1][k-1]*2 + 1; }else{ dp[i][j][k] = max({dp[i-1][j][k], dp[i][j-1][k], dp[i][j][k-1]}); } } } } ``` 此代码片段展示了如何通过检查当前字符是否相同来进行状态转换,并基于之前的最优解构建新解。需要注意的是实际比中可能会有更多的细节需要考虑,比如输入范围限制等。 另外,在处理位运算相关的问题时,通常会先分析哪些操作能够使得最终结果变为1,再统计每一种可能的发生次数并求其乘积作为总的方案数量[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值