把状态定义成"当前所在城市 + 已用卡数"。走一条边时要么正常通过,要么额外消耗一张卡把这条边代价减半,在这个状态图上跑 Dijkstra。
OJ: luogu
题目 ID: P4822
难度:普及+/提高
标签:最短路图论堆
日期: 2026-06-20 04:58
题意
给你一张无向带权图,从
你有最多
每张卡只能在一条边上使用一次,效果是把这条边的通过时间减半。
要求输出从
不要求把卡全部用完。
思路
先看一个更直观的小数据暴力:
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10;
const long long INF = (1LL << 60);
int n, m, k;
long long dist_arr[105][105];
int state_id(int city, int used) {
return used * n + city;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k;
int tot = (k + 1) * n;
for (int i = 1; i <= tot; i++) {
for (int j = 1; j <= tot; j++) {
if (i == j) {
dist_arr[i][j] = 0;
}
else {
dist_arr[i][j] = INF;
}
}
}
// 直接把分层图完整建出来:
// 第 used 层表示已经用了 used 张卡。
for (int i = 1; i <= m; i++) {
int u, v, len;
cin >> u >> v >> len;
for (int used = 0; used <= k; used++) {
int a = state_id(u, used);
int b = state_id(v, used);
if (len < dist_arr[a][b]) {
dist_arr[a][b] = len;
dist_arr[b][a] = len;
}
if (used < k) {
int c = state_id(u, used);
int d = state_id(v, used + 1);
if (len / 2 < dist_arr[c][d]) {
dist_arr[c][d] = len / 2;
}
c = state_id(v, used);
d = state_id(u, used + 1);
if (len / 2 < dist_arr[c][d]) {
dist_arr[c][d] = len / 2;
}
}
}
}
for (int mid = 1; mid <= tot; mid++) {
for (int i = 1; i <= tot; i++) {
if (dist_arr[i][mid] >= INF / 2) {
continue;
}
for (int j = 1; j <= tot; j++) {
if (dist_arr[mid][j] >= INF / 2) {
continue;
}
long long nd = dist_arr[i][mid] + dist_arr[mid][j];
if (nd < dist_arr[i][j]) {
dist_arr[i][j] = nd;
}
}
}
}
long long answer = INF;
for (int used = 0; used <= k; used++) {
if (dist_arr[state_id(1, 0)][state_id(n, used)] < answer) {
answer = dist_arr[state_id(1, 0)][state_id(n, used)];
}
}
cout << answer << '\n';
return 0;
}brute.cpp 直接把"用了几张卡"这件事展开成分层图:
- 第
层:一张卡都还没用 - 第
层:已经用过 张 - …
- 第
层:已经用过 张
如果原图里有一条边
- 不用卡:
- 还留在当前层
- 花费原边权
- 用卡:
- 跳到下一层
- 花费原边权的一半
这个思路完全贴着题意,但直接把整张分层图展开出来再跑 Floyd,只适合很小的数据。
真正的主解不必显式建出整张分层图,只需要把状态写进 Dijkstra 里。
状态定义
设:
表示到达城市 ,并且已经用了 张卡时的最短时间
那么从
- 不用卡:
- 到
- 代价加
- 到
- 如果
,还可以用卡: - 到
- 代价加
- 到
这个过程可以用下面这张图来理解:
flowchart LR A["(u, used)"] -->|"w"| B["(v, used)"] A -->|"w/2"| C["(v, used+1)"]
图里每一次"向下一层"就代表多消耗了一张卡。
而"留在本层"则表示这条边正常通过。
因为所有边权都非负,所以在这个状态图上直接跑 Dijkstra 就可以了。
最后答案不是只看
的最小值
因为卡片可以不用完。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 50 + 5;
const int MAXM = 1000 * 2 + 5;
const long long INF = (1LL << 60);
struct HeapNode {
int u;
int used;
long long dist;
bool operator < (const HeapNode &other) const {
return dist > other.dist;
}
};
int n, m, k;
int head[MAXN], to[MAXM], nxt[MAXM], w[MAXM], edge_cnt;
long long dist_arr[MAXN][MAXN];
bool vis[MAXN][MAXN];
void init_graph() {
edge_cnt = 0;
for (int i = 1; i <= n; i++) {
head[i] = 0;
}
}
void add_edge(int u, int v, int len) {
edge_cnt++;
to[edge_cnt] = v;
w[edge_cnt] = len;
nxt[edge_cnt] = head[u];
head[u] = edge_cnt;
}
void dijkstra() {
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= k; j++) {
dist_arr[i][j] = INF;
vis[i][j] = false;
}
}
priority_queue<HeapNode> pq;
dist_arr[1][0] = 0;
pq.push({1, 0, 0});
while (!pq.empty()) {
HeapNode cur = pq.top();
pq.pop();
int u = cur.u;
int used = cur.used;
if (vis[u][used]) {
continue;
}
vis[u][used] = true;
for (int i = head[u]; i != 0; i = nxt[i]) {
int v = to[i];
long long nd = dist_arr[u][used] + w[i];
if (nd < dist_arr[v][used]) {
dist_arr[v][used] = nd;
pq.push({v, used, nd});
}
// 在这条边上再额外用一张卡,时间减半。
if (used < k) {
long long freeze_dist = dist_arr[u][used] + w[i] / 2;
if (freeze_dist < dist_arr[v][used + 1]) {
dist_arr[v][used + 1] = freeze_dist;
pq.push({v, used + 1, freeze_dist});
}
}
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k;
init_graph();
for (int i = 1; i <= m; i++) {
int u, v, len;
cin >> u >> v >> len;
add_edge(u, v, len);
add_edge(v, u, len);
}
dijkstra();
long long answer = INF;
for (int used = 0; used <= k; used++) {
if (dist_arr[n][used] < answer) {
answer = dist_arr[n][used];
}
}
cout << answer << '\n';
return 0;
}复杂度
状态数是:
每条原图边在每一层里都会产生常数条转移。
因此总复杂度大致是:
在本题
空间复杂度:
总结
这题最重要的不是"边权减半"本身,而是把它翻译成状态。
一旦你把"已经用了多少张卡"加进状态里,问题就重新变回了最熟悉的那种:
- 非负权状态图最短路
所以它本质上是一道非常标准的分层图 / 状态最短路题。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
