航线(2025“钉耙编程”中国大学生算法设计春季联赛(1))
题目大意:
染染船长在一次航行中,来到了一个复杂的海洋区域。这个海洋区域可以被视为一个二维网格,每个网格点代表一个海域,船需要从左上角(1, 1)出发,最后到达右下角(n, m)。在网格中,每个格点有两个花费:
t[i][j]
:表示直接通过该海域所需要的时间。d[i][j]
:表示如果船在通过该海域时需要转向,那么需要的额外时间。
船一开始朝右驶出,目标是从起始位置(1, 1)驶到目标位置(n, m)。沿途的航行可能需要转向,每次转向都会增加额外的时间开销。转向的方向包括向左、向右、向上和向下,转向都会增加d[i][j]
的花费。我们需要计算出从(1, 1)到(n, m)的最短时间。
输入:
- 一个整数
T
表示测试数据组数。 - 对于每组数据:
- 两个整数
n
和m
,分别表示海洋的行数和列数。 - 接下来有
n
行,每行有m
个整数,表示直接通过每个海域所需要的时间t[i][j]
。 - 接下来有
n
行,每行有m
个整数,表示如果需要转向所需的额外时间d[i][j]
。
- 两个整数
输出:
对于每组数据,输出一个整数,表示从(1, 1)到(n, m)的最短时间。
样例输入:
2
1 1
1
1
3 3
1 1 1
1 2 1
1 1 1
0 999 999
0 0 999
999 0 0
样例输出:
2
6
题解:
本题是一个典型的最短路径问题,结合了网格的结构和转向的额外费用。可以通过广度优先搜索(BFS)或者Dijkstra算法来求解。
我们选择使用Dijkstra算法,因为网格的每个格点存在多条路径,每条路径的花费可能会因为转向而增加。Dijkstra算法能够有效地处理图中带有不同边权的最短路径问题。
解题步骤:
-
建模和状态表示:
- 我们可以把海洋看作一个图,其中每个海域
(i, j)
是一个节点。每个节点之间的边代表着船在不同方向上移动的时间。通过考虑船是否需要转向,边的权重(即时间消耗)可以增加。 - 每个节点的状态由
(i, j, direction)
来表示,其中direction
为船当前的朝向。我们可以定义4个方向:右(0)、下(1)、左(2)、上(3)。
- 我们可以把海洋看作一个图,其中每个海域
-
初始化:
- 我们需要一个距离数组
dist[i][j][direction]
来表示从起始位置到达节点(i, j)
并且朝向direction
的最短时间。 - 初始化起始位置(1, 1)的状态:船开始时朝右(方向为0),因此其距离为
dist[0][0][0] = 0
。
- 我们需要一个距离数组
-
Dijkstra算法:
- 使用优先队列(最小堆)来保存当前的状态。每次取出堆顶的元素,更新该状态的邻居节点的最短时间。
- 对于每个方向,计算从当前位置到达其邻居节点的时间。考虑转向的额外费用。
- 如果通过某个方向转移到新的位置的时间比当前记录的时间要小,则更新该状态,并将新的状态加入队列。
-
处理终点:
- 终点是右下角的海域( n-1, m-1 )。在到达该海域时,我们需要考虑其相邻位置的不同方向。如果是从右下角向下移出海洋,则结束计算。
-
复杂度:
- 每个位置的状态有4种可能的方向,因此总共有
n * m * 4
个状态,使用Dijkstra算法进行优化时,时间复杂度为O((n * m * 4) log(n * m * 4))
。
- 每个位置的状态有4种可能的方向,因此总共有
代码实现:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = LLONG_MAX;
const vector<pair<int, int>> dirs = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int T;
cin >> T;
while (T--)
{
int n, m;
cin >> n >> m;
vector<vector<ll>> t(n, vector<ll>(m));
for (int i = 0; i < n; ++i)
{
for (int j = 0; j < m; ++j)
{
cin >> t[i][j];
}
}
vector<vector<ll>> d(n, vector<ll>(m));
for (int i = 0; i < n; ++i)
{
for (int j = 0; j < m; ++j)
{
cin >> d[i][j];
}
}
vector<vector<vector<ll>>> dist(n, vector<vector<ll>>(m, vector<ll>(4, INF)));
priority_queue<tuple<ll, int, int, int>, vector<tuple<ll, int, int, int>>, greater<>> pq;
dist[0][0][0] = 0;
pq.emplace(0, 0, 0, 0);
ll ans = INF;
while (!pq.empty())
{
auto [time_so_far, i, j, dir_in] = pq.top();
pq.pop();
if (time_so_far > dist[i][j][dir_in])
{
continue;
}
if (i == n - 1 && j == m - 1)
{
int k = 1;
ll cost = t[i][j];
if (dir_in != k)
{
cost += d[i][j];
}
ans = min(ans, time_so_far + cost);
}
for (int k = 0; k < 4; ++k)
{
auto [dx, dy] = dirs[k];
int ni = i + dx;
int nj = j + dy;
ll cost = t[i][j];
if (dir_in != k)
{
cost += d[i][j];
}
ll new_time = time_so_far + cost;
bool is_inside = (ni >= 0 && ni < n && nj >= 0 && nj < m);
if (is_inside)
{
if (new_time < dist[ni][nj][k])
{
dist[ni][nj][k] = new_time;
pq.emplace(new_time, ni, nj, k);
}
}
else
{
if (i == n - 1 && j == m - 1 && k == 1)
{
ans = min(ans, new_time);
}
}
}
}
cout << ans << '\n';
}
return 0;
}
代码解释:
- 输入解析:先输入T,表示数据组数。每组数据包括
n
和m
,接着输入两组矩阵分别表示直线通过时间t
和转向时间d
。 - 优先队列:使用
priority_queue
来执行Dijkstra算法,每次从队列中取出最小时间并进行状态更新。 - 状态更新:每次从当前格点开始,尝试移动到相邻的4个方向,并计算移动的时间。如果移动后的时间更短,就更新状态并将新的状态放入队列中。
时间复杂度:
- 每次更新都要进行
O(log(n * m * 4))
的操作,因此总体复杂度为O((n * m * 4) log(n * m * 4))
,对于每组数据,这个复杂度是可以接受的。