1 条题解
-
0
你好!我是你的OI教练。看到这道 GESP 七级的题目《商品交易》,不要被题目中复杂的交易规则吓到了。这道题披着“经济交易”的外衣,其实核心是一个非常经典的图论模型。
我们先像在集训队上课一样,拿出草稿纸,把题目中的“文字游戏”拆解成数学公式,你会发现它意外地简单。
1. 读题关键词:你的“雷达”响了吗?
在读题时,请圈出以下决定解题方向的关键词:
- “使用第 种商品交换第 种商品”:
- 这表示交易是有方向的:。
- 这是一个有向图。
- “付给商人 ”:
- 这是交易的差价成本。
- “收取 1 元作为手续费”:
- 这是交易的固定成本。
- “最少的花费”:
- 典型的最短路问题。
2. 预备知识点
要解决这道题,你需要掌握:
- 图的存储:邻接表 (
vector<int> adj[])。 - 广度优先搜索 (BFS):用于求解边权为 1 的最短路问题。
- 数学推导 (裂项相消):这是本题化繁为简的关键(下面会讲)。
3. 启发式教学:草稿纸上的推演(核心!)
题目说每一步的费用是:。 乍一看,边权不仅和路径长度有关,还和节点的价值有关,好像很复杂。
来,我们在草稿纸上画一条路径试试: 假设我们要从商品 换到 ,中间经过 。路径:。
- 第一步 的花费:
- 第二步 的花费:
总花费是多少?
发现了吗? 中间的 被消掉了!
推广到任意 步的路径: 如果路径长度(交易次数)是 ,起点是 ,终点是 ,那么总花费公式一定是:
教练的结论:
- 是一个定值(因为起点和终点由输入决定,不可改变)。
- 要想总花费最小,唯一的办法就是让 (交易次数)最小。
- 题目转化:在一个有向图中,寻找从 到 的最短路径(边数最少)。
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. 易错点与关键点注释
- 公式推导:
- 很多同学会试图用 Dijkstra 跑带权图(边权算上价值差),虽然也能做,但容易写错且慢。一定要看透裂项相消的本质。
- 数据类型:
- 价值 是 ,花费可能是负数。最终答案建议用
long long,防止 溢出(虽然 在int范围内,但在 OI 中涉及“钱”、“分数”通常无脑long long最稳妥)。
- 价值 是 ,花费可能是负数。最终答案建议用
- 有向图:
- 题目明确说“用 换 ”,所以只能
adj[x].push_back(y),不能存双向。
- 题目明确说“用 换 ”,所以只能
- 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
- 上传者