1 条题解

  • 0
    @ 2025-12-9 12:01:58

    你好!我是你的OI教练。看到这道 GESP 七级的题目《商品交易》,不要被题目中复杂的交易规则吓到了。这道题披着“经济交易”的外衣,其实核心是一个非常经典的图论模型。

    我们先像在集训队上课一样,拿出草稿纸,把题目中的“文字游戏”拆解成数学公式,你会发现它意外地简单。


    1. 读题关键词:你的“雷达”响了吗?

    在读题时,请圈出以下决定解题方向的关键词:

    1. “使用第 xjx_j 种商品交换第 yjy_j 种商品”
      • 这表示交易是有方向的:xjyjx_j \to y_j
      • 这是一个有向图
    2. “付给商人 vyjvxjv_{y_j} - v_{x_j}
      • 这是交易的差价成本
    3. “收取 1 元作为手续费”
      • 这是交易的固定成本
    4. “最少的花费”
      • 典型的最短路问题。

    2. 预备知识点

    要解决这道题,你需要掌握:

    • 图的存储:邻接表 (vector<int> adj[])。
    • 广度优先搜索 (BFS):用于求解边权为 1 的最短路问题。
    • 数学推导 (裂项相消):这是本题化繁为简的关键(下面会讲)。

    3. 启发式教学:草稿纸上的推演(核心!)

    题目说每一步的费用是:(vv)+1(v_{新} - v_{旧}) + 1。 乍一看,边权不仅和路径长度有关,还和节点的价值有关,好像很复杂。

    来,我们在草稿纸上画一条路径试试: 假设我们要从商品 AA 换到 BB,中间经过 CC。路径:ACBA \to C \to B

    1. 第一步 ACA \to C 的花费Cost1=(vCvA)+1Cost_1 = (v_C - v_A) + 1
    2. 第二步 CBC \to B 的花费Cost2=(vBvC)+1Cost_2 = (v_B - v_C) + 1

    总花费是多少?

    Total=Cost1+Cost2Total = Cost_1 + Cost_2 Total=(vCvA+1)+(vBvC+1)Total = (v_C - v_A + 1) + (v_B - v_C + 1)

    发现了吗? 中间的 vCv_C 被消掉了!

    Total=vA+1+vB+1Total = -v_A + 1 + v_B + 1 Total=(vBvA)+2Total = (v_B - v_A) + 2

    推广到任意 kk 步的路径: 如果路径长度(交易次数)是 kk,起点是 aa,终点是 bb,那么总花费公式一定是:

    Cost=(vbva)+kCost = (v_b - v_a) + k

    教练的结论:

    • (vbva)(v_b - v_a) 是一个定值(因为起点和终点由输入决定,不可改变)。
    • 要想总花费最小,唯一的办法就是让 kk(交易次数)最小
    • 题目转化:在一个有向图中,寻找从 aabb最短路径(边数最少)

    4. 标准代码 (NOIP C++14)

    /**
     * 题目:P10110 [GESP202312 七级] 商品交易
     * 考点:图论建模、数学推导(裂项相消)、BFS最短路
     * 时间复杂度:O(N + M)
     */
    
    #include <iostream>
    #include <vector>
    #include <queue>
    
    using namespace std;
    
    const int MAXN = 100005; // 根据题目 N <= 10^5
    const int INF = -1;      // 用 -1 表示未访问
    
    // 存储每个商品的价值
    // 注意:虽然题目说 v_i <= 10^9,fit int,但计算结果可能溢出,建议涉及金额用 long long
    long long val[MAXN]; 
    
    // 邻接表存图
    vector<int> adj[MAXN];
    
    // 记录从起点 a 到各点的最短步数
    int dist[MAXN];
    
    int n, m, start_node, end_node;
    
    void solve() {
        // 1. 输入数据
        if (!(cin >> n >> m >> start_node >> end_node)) return;
        
        for (int i = 0; i < n; i++) {
            cin >> val[i];
        }
        
        // 2. 建图
        for (int i = 0; i < m; i++) {
            int u, v;
            cin >> u >> v;
            // 有向边:u -> v
            adj[u].push_back(v);
        }
        
        // 3. BFS求最短步数 (边权为1的最短路)
        // 初始化距离数组
        for (int i = 0; i < n; i++) dist[i] = INF;
        
        queue<int> q;
        q.push(start_node);
        dist[start_node] = 0;
        
        while (!q.empty()) {
            int curr = q.front();
            q.pop();
            
            // 如果已经找到终点,可以提前退出吗?
            // 对于求特定两点最短路,BFS 第一次遇到终点时即为最短,可以 break
            if (curr == end_node) break;
            
            for (int next : adj[curr]) {
                if (dist[next] == INF) { // 如果未访问过
                    dist[next] = dist[curr] + 1;
                    q.push(next);
                }
            }
        }
        
        // 4. 输出结果
        if (dist[end_node] == INF) {
            cout << "No solution" << endl;
        } else {
            // 公式:Total Cost = (v_end - v_start) + steps
            long long ans = (val[end_node] - val[start_node]) + dist[end_node];
            cout << ans << endl;
        }
    }
    
    int main() {
        // IO优化
        ios::sync_with_stdio(false);
        cin.tie(0);
        
        solve();
        
        return 0;
    }
    

    5. 易错点与关键点注释

    1. 公式推导
      • 很多同学会试图用 Dijkstra 跑带权图(边权算上价值差),虽然也能做,但容易写错且慢。一定要看透裂项相消的本质。
    2. 数据类型
      • 价值 viv_i10910^9,花费可能是负数。最终答案建议用 long long,防止 vbvav_b - v_a 溢出(虽然 10910^9int 范围内,但在 OI 中涉及“钱”、“分数”通常无脑 long long 最稳妥)。
    3. 有向图
      • 题目明确说“用 xxyy”,所以只能 adj[x].push_back(y),不能存双向。
    4. No solution
      • 图可能是不连通的,必须判断 dist[b] 是否被更新过。

    6. 数据生成器 (1.in - 10.in)

    这是一个功能完备的数据生成器,覆盖了无解、小数据、大数据、长路径等各种情况。

    /**
     * P10110 [GESP202312 七级] 商品交易 - 数据生成器
     * 功能:生成 1.in/1.out ~ 10.in/10.out
     */
    
    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <queue>
    #include <string>
    #include <cstdlib>
    #include <ctime>
    #include <algorithm>
    #include <set>
    
    using namespace std;
    
    // ==========================================
    // Part 1: 标准题解逻辑 (用于生成 .out)
    // ==========================================
    long long solve_bfs(int n, int start_node, int end_node, const vector<long long>& v_vals, const vector<pair<int,int>>& edges) {
        vector<vector<int>> adj(n);
        for(auto& e : edges) {
            adj[e.first].push_back(e.second);
        }
        
        vector<int> dist(n, -1);
        queue<int> q;
        
        q.push(start_node);
        dist[start_node] = 0;
        
        while(!q.empty()){
            int curr = q.front();
            q.pop();
            if(curr == end_node) return (v_vals[end_node] - v_vals[start_node]) + dist[end_node];
            
            for(int next : adj[curr]){
                if(dist[next] == -1){
                    dist[next] = dist[curr] + 1;
                    q.push(next);
                }
            }
        }
        return -2e18; // 特殊标记:无解
    }
    
    // ==========================================
    // Part 2: 数据构造逻辑
    // ==========================================
    int randInt(int min, int max) {
        return rand() % (max - min + 1) + min;
    }
    
    void makeData(int id) {
        string inName = to_string(id) + ".in";
        string outName = to_string(id) + ".out";
        ofstream fin(inName);
        ofstream fout(outName);
    
        int N, M;
        // 梯度设置规模
        if (id <= 3) { N = 10; M = 20; }
        else if (id <= 7) { N = 1000; M = 5000; }
        else { N = 100000; M = 200000; }
    
        int a = randInt(0, N-1);
        int b = randInt(0, N-1);
        while(a == b) b = randInt(0, N-1); // 保证 a != b
    
        // 生成价值
        vector<long long> vals(N);
        for(int i=0; i<N; i++) {
            vals[i] = randInt(1, 1000000000); // 价值最大 10^9
        }
    
        // 生成边
        vector<pair<int,int>> edges;
        set<pair<int,int>> edgeSet;
    
        // 构造策略
        if (id == 3 || id == 8) {
            // 构造无解情况:不生成任何边,或者只生成不连接 a,b 的边
            // 为了简单,我们生成少量随机边,概率上很难连通大图
            int limit = (id==3)? 5 : 100;
            for(int i=0; i<limit; i++){
                int u = randInt(0, N-1);
                int v = randInt(0, N-1);
                if(u!=v) edges.push_back({u,v});
            }
            // 强制断开 a 的出边
            // (不需要额外操作,随机生成大概率不通,如果不通 solve 会返回无解)
        } else if (id == 4) {
            // 构造一条长链,测试 BFS 深度
            // a -> n1 -> n2 -> ... -> b
            int curr = a;
            vector<int> path;
            path.push_back(a);
            for(int i=0; i<N/2; i++){
                int next = randInt(0, N-1);
                if(next != b && next != curr) path.push_back(next);
            }
            path.push_back(b);
            for(size_t i=0; i<path.size()-1; i++){
                edges.push_back({path[i], path[i+1]});
                edgeSet.insert({path[i], path[i+1]});
            }
            // 补充一些干扰边
            for(int i=0; i<M - (int)path.size(); i++){
                int u = randInt(0, N-1);
                int v = randInt(0, N-1);
                if(u!=v && edgeSet.find({u,v}) == edgeSet.end()){
                    edges.push_back({u,v});
                    edgeSet.insert({u,v});
                }
            }
        } else {
            // 随机图,保证一定连通性(通过生成树)
            // 这里的连通性不一定保证 a->b,但概率较大
            // 如果 solve 返回无解,就让他无解,这本身也是测试点的一种
            for(int i=0; i<M; i++){
                int u = randInt(0, N-1);
                int v = randInt(0, N-1);
                if(u!=v) edges.push_back({u,v});
            }
            // 为了防止运气不好全无解,在 ID 1,2,5,6,9,10 强制加一条直达或短路
            if (id != 3 && id != 8) {
                 edges.push_back({a, b}); // 直达
                 int mid = randInt(0, N-1);
                 edges.push_back({a, mid});
                 edges.push_back({mid, b});
            }
        }
    
        // 写入 Input
        fin << N << " " << (int)edges.size() << " " << a << " " << b << endl;
        for(int i=0; i<N; i++) fin << vals[i] << (i==N-1?"":" ");
        fin << endl;
        for(auto& e : edges) fin << e.first << " " << e.second << endl;
    
        // 计算 Output
        long long ans = solve_bfs(N, a, b, vals, edges);
        if(ans == -2e18) fout << "No solution" << endl;
        else fout << ans << endl;
    
        fin.close();
        fout.close();
        cout << "Generated Case " << id << endl;
    }
    
    int main() {
        srand(time(0));
        for(int i=1; i<=10; i++) makeData(i);
        return 0;
    }
    
    • 1

    信息

    ID
    14431
    时间
    1000ms
    内存
    32MiB
    难度
    5
    标签
    递交数
    1
    已通过
    1
    上传者