[BJWC2012] 冻结

GitHub跳转原题关系图返回列表

把状态定义成"当前所在城市 + 已用卡数"。走一条边时要么正常通过,要么额外消耗一张卡把这条边代价减半,在这个状态图上跑 Dijkstra。

OJ: luogu

题目 ID: P4822

难度:普及+/提高

标签:最短路图论

日期: 2026-06-20 04:58

题意

给你一张无向带权图,从 11 走到 NN

你有最多 KK 张卡。
每张卡只能在一条边上使用一次,效果是把这条边的通过时间减半。

要求输出从 11NN 的最小时间。
不要求把卡全部用完。

思路

先看一个更直观的小数据暴力:

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 直接把"用了几张卡"这件事展开成分层图:

  • 00 层:一张卡都还没用
  • 11 层:已经用过 11
  • KK 层:已经用过 KK

如果原图里有一条边 uvu \leftrightarrow v,那么在分层图里就有两种走法:

  1. 不用卡:
    • 还留在当前层
    • 花费原边权
  2. 用卡:
    • 跳到下一层
    • 花费原边权的一半

这个思路完全贴着题意,但直接把整张分层图展开出来再跑 Floyd,只适合很小的数据。

真正的主解不必显式建出整张分层图,只需要把状态写进 Dijkstra 里。

状态定义

设:

  • dist[u][used]dist[u][used] 表示到达城市 uu,并且已经用了 usedused 张卡时的最短时间

那么从 (u,used)(u, used) 走一条边到 vv 时,有两种转移:

  1. 不用卡:
    • (v,used)(v, used)
    • 代价加 ww
  2. 如果 used<Kused < K,还可以用卡:
    • (v,used+1)(v, used + 1)
    • 代价加 w/2w / 2

这个过程可以用下面这张图来理解:

flowchart LR
  A["(u, used)"] -->|"w"| B["(v, used)"]
  A -->|"w/2"| C["(v, used+1)"]

图里每一次"向下一层"就代表多消耗了一张卡。
而"留在本层"则表示这条边正常通过。

因为所有边权都非负,所以在这个状态图上直接跑 Dijkstra 就可以了。

最后答案不是只看 dist[N][K]dist[N][K],而是:

  • dist[N][0K]dist[N][0 \dots K] 的最小值

因为卡片可以不用完。

代码

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;
}

复杂度

状态数是:

  • N×(K+1)N \times (K + 1)

每条原图边在每一层里都会产生常数条转移。
因此总复杂度大致是:

  • O(KMlog(NK))O(KM \log (NK))

在本题 N50,M1000,K50N \leqslant 50, M \leqslant 1000, K \leqslant 50 的范围内完全足够。

空间复杂度:

  • O(NK)O(NK)

总结

这题最重要的不是"边权减半"本身,而是把它翻译成状态。

一旦你把"已经用了多少张卡"加进状态里,问题就重新变回了最熟悉的那种:

  • 非负权状态图最短路

所以它本质上是一道非常标准的分层图 / 状态最短路题。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析