2023年12月GESP真题及题解(C++七级): 商品交易

题目描述
市场上共有 N N N 种商品,编号从 0 0 0 至 N − 1 N-1 N−1 ,其中,第 i i i 种商品价值 v i v_i vi 元。
现在共有 M M M 个商人,编号从 0 0 0 至 M − 1 M-1 M−1 。在第 j j j 个商人这,你可以使用你手上的第 x j x_j xj 种商品交换商人手上的第 y j y_j yj 种商品。每个商人都会按照商品价值进行交易,具体来说,如果 v x j > v y j v_{x_j}>v_{y_j} vxj>vyj,他将会付给你 v x j − v y j v_{x_j}-v_{y_j} vxj−vyj元钱;否则,那么你需要付给商人 v y j − v x j v_{y_j}-v_{x_j} vyj−vxj 元钱。除此之外,每次交易商人还会收取 1 1 1 元作为手续费,不论交易商品的价值孰高孰低。
你现在拥有商品 a a a ,并希望通过一些交换来获得商品 b b b 。请问你至少要花费多少钱?(当然,这个最小花费也可能是负数,这表示你可以在完成目标的同时赚取一些钱。)
输入格式
第一行四个整数 N , M , a , b N , M , a , b N,M,a,b,分别表示商品的数量、商人的数量、你持有的商品以及你希望获得的商品。保证 0 ≤ a , b < N 0 \le a,b < N 0≤a,b<N ,保证 a ≠ b a \ne b a=b。
第二行 N N N 个用单个空格隔开的正整数 v 0 , v 1 , ... , v N − 1 v_0,v_1,...,v_{N-1} v0,v1,...,vN−1 ,依次表示每种商品的价值。保证 1 ≤ v i ≤ 10 9 1≤v_i≤10^9 1≤vi≤109。
接下来 M M M 行,每行两个整数 x j , y j x_j,y_j xj,yj ,表示在第 j j j 个商人这,你可以使用第 x j x_j xj 种商品交换第 y j y_j yj 种商品。保证 0 ≤ x j , y j < N 0≤x_j,y_j<N 0≤xj,yj<N,保证 x j ≠ y j x_j≠y_j xj=yj 。
输出格式
输出一行一个整数,表示最少的花费。特别地,如果无法通过交换换取商品 b b b ,请输出 No solution。
输入输出样例 1
输入 1
3 5 0 2
1 2 4
1 0
2 0
0 1
2 1
1 2
输出 1
5
输入输出样例 2
输入 2
3 3 0 2
100 2 4
0 1
1 2
0 2
输出 2
-95
输入输出样例 3
输入 3
4 4 3 0
1 2 3 4
1 0
0 1
3 2
2 3
输出 3
No solution
说明/提示
数据范围
对于30%的测试点,保证 N ≤ 10 N ≤ 10 N≤10 , M ≤ 20 M ≤ 20 M≤20。
对于70%的测试点,保证 N ≤ 10 3 N ≤10^3 N≤103 , M ≤ 10 4 M≤10^4 M≤104。
对于100%的测试点,保证 N ≤ 10 5 N≤10^5 N≤105, M ≤ 2 × 10 5 M≤2×10^5 M≤2×105。
思路分析
这个问题可以转化为图论中的最短路径问题。核心思路是:
1. 问题建模
- 将每种商品看作图中的节点
- 将商人提供的交换关系看作有向图的边
- 边的权重(花费) =
abs(v[x] - v[y]) + 1abs(v[x] - v[y]):商品差价+1:固定的手续费
2. 特殊处理:负权边
由于交易可能赚钱(即花费为负),所以图中存在负权边。因此不能使用普通的Dijkstra算法,需要使用能处理负权的最短路径算法。
3. 算法选择
- SPFA算法:适用于有负权边的图,最坏时间复杂度O(VE),但实际运行较快
- Bellman-Ford算法:更稳定,但时间复杂度O(VE),在本题数据范围下可能超时
- 本题选择SPFA ,并加入负环检测
4. 负环处理
如果图中存在负环,且起点a能到达负环,且从负环能到达终点b,那么理论上可以通过无限次交易赚取无限多的钱,最小花费是负无穷。但题目未明确说明这种情况,我们按常规处理:如果检测到负环且能到达b,则输出"No solution"(因为这种情况在实际中不可能实现无限次交易)。
代码实现
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll; // 使用long long防止数据溢出
const ll INF = 1e18; // 定义无穷大值
const int MAXN = 1e5 + 10; // 最大商品数量
int n, m, a, b; // n:商品数, m:商人数, a:起点商品, b:目标商品
ll v[MAXN]; // v[i]:第i种商品的价值
vector<pair<int, ll>> g[MAXN]; // 邻接表存储图,g[u] = {(v, w)}表示从u到v的边,权重为w
/**
* SPFA算法求最短路径
* ans 输出参数,存储计算得到的最小花费
* 返回值:true:找到解,false:无解(不可达或存在负环)
*/
bool spfa(ll &ans) {
// d[i]:从起点a到商品i的最小花费
vector<ll> d(n, INF);
// cnt[i]:节点i入队次数,用于检测负环
vector<int> cnt(n, 0);
// inq[i]:节点i是否在队列中
vector<bool> inq(n, false);
// 队列用于存储待处理的节点
queue<int> q;
// 初始化起点
d[a] = 0;
q.push(a);
inq[a] = true;
cnt[a] = 1;
// SPFA主循环
while (!q.empty()) {
int u = q.front(); // 取出队首节点
q.pop();
inq[u] = false; // 标记为不在队列中
// 遍历节点u的所有出边
for (auto &e : g[u]) {
int to = e.first; // 目标节点
ll w = e.second; // 边权(交易花费)
// 松弛操作:如果通过u到达to的距离更短,则更新
if (d[u] + w < d[to]) {
d[to] = d[u] + w; // 更新最短距离
// 如果to不在队列中,加入队列
if (!inq[to]) {
q.push(to);
inq[to] = true;
cnt[to]++; // 入队次数+1
// 负环检测:如果一个节点入队超过n次,说明存在负环
// 在负环中可以无限减少花费,这种情况视为无解
if (cnt[to] > n) {
return false; // 存在负环,返回无解
}
}
}
}
}
// 检查是否可达目标商品b
if (d[b] == INF) {
return false; // 不可达,返回无解
}
// 将结果赋值给输出参数
ans = d[b];
return true; // 成功找到最短路径
}
int main() {
// 加速输入输出
ios::sync_with_stdio(false);
cin.tie(0);
// 读入基本参数
cin >> n >> m >> a >> b;
// 读入商品价值
for (int i = 0; i < n; i++) {
cin >> v[i];
}
// 建立交易关系图
for (int i = 0; i < m; i++) {
int x, y;
cin >> x >> y;
// 计算交易花费:v[y] - v[x] + 1
// 公式推导:
// 1. 如果v[x] > v[y]:我们得到(v[x]-v[y])元差价,支付1元手续费
// 总花费 = -(v[x]-v[y]) + 1 = v[y] - v[x] + 1(负花费表示赚钱)
// 2. 如果v[x] < v[y]:我们支付(v[y]-v[x])元差价,支付1元手续费
// 总花费 = v[y] - v[x] + 1
// 两种情况统一为:花费 = v[y] - v[x] + 1
ll cost = v[y] - v[x] + 1;
// 添加有向边:从商品x到商品y,权重为cost
// 注意:交易是单向的,只能使用x交换y
g[x].push_back({y, cost});
}
// 调用SPFA算法计算最小花费
ll ans;
if (spfa(ans)) {
cout << ans << "\n"; // 输出最小花费
} else {
cout << "No solution\n"; // 无解输出
}
return 0;
}
功能分析
1. 算法设计思路
图建模
- 将每种商品视为图中的节点
- 将商人提供的交易关系视为有向边
- 边的权重计算公式:
v[y] - v[x] + 1v[y] - v[x]: 商品价值差+1: 固定手续费
算法选择
- 由于存在负权边(当v[y] < v[x]时),不能使用Dijkstra算法
- 选择SPFA (Shortest Path Faster Algorithm) 算法
- 本质是Bellman-Ford算法的队列优化版本
- 能处理负权边
- 平均时间复杂度优于Bellman-Ford
2. 核心功能模块
数据结构
-
邻接表 (
g[MAXN]):- 存储图结构
- 每个元素为
pair<int, ll>,表示(目标节点, 边权重) - 空间复杂度: O(N+M)
-
距离数组 (
d[n]):- 存储从起点a到每个节点的最短距离
- 初始值为INF(无穷大)
-
辅助数组:
cnt[n]: 记录节点入队次数,用于负环检测inq[n]: 标记节点是否在队列中,避免重复入队
SPFA算法流程
- 初始化: 起点距离为0,加入队列
- 循环处理队列 :
- 取出队首节点u
- 遍历u的所有出边
- 尝试松弛操作:如果通过u到达邻居的距离更短,则更新
- 如果邻居被更新且不在队列中,则加入队列
- 负环检测: 如果一个节点入队超过n次,说明存在负环
- 结果判断: 如果目标节点b的距离仍为INF,说明不可达
3. 关键点说明
负权边处理
- 边权公式
v[y] - v[x] + 1可能为负值 - 负权边表示交易可以赚钱(获得差价大于手续费)
- SPFA算法能正确处理负权边
负环检测
- 负环: 环上所有边的权重之和为负数
- 影响: 在负环上可以无限循环交易,无限减少花费
- 处理: 检测到负环则视为无解,返回"No solution"
不可达判断
- 初始化所有节点距离为INF
- 如果经过算法后b的距离仍为INF,说明从a无法到达b
- 返回"No solution"
4. 时间复杂度分析
理论复杂度
- 最坏情况: O(VE) = O(2×10^10)
- 平均情况: 远好于最坏情况
- 实际表现: 在本题数据范围内通常可以接受
优化特点
- 队列优化: 只处理距离发生变化的节点
- 避免冗余 : 使用
inq数组防止重复入队 - 提前终止: 检测到负环时提前返回
5. 空间复杂度分析
- 邻接表: O(N+M) ≈ 3×10^5
- 辅助数组: O(N) ≈ 10^5
- 总空间: 约1.6MB,远小于内存限制
6. 边界情况处理
数据类型
- 使用
long long存储距离,防止溢出 - 商品价值最大10 9 ^9 9,M最大2×10 5 ^5 5
- 最坏情况下总花费可能超过int范围
特殊输入
-
无解情况:
- 不可达(无交易路径)
- 存在负环且能到达目标
-
零花费/赚钱情况:
- 最小花费为负数表示交易赚钱
- 算法能正确处理负距离
完整GESP C++考级真题题解专栏:

GESP(C++ 一级+二级+三级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12858102.html 点击跳转

GESP(C++ 四级+五级+六级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12869848.html 点击跳转

GESP(C++ 七级+八级)真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13117178.html
更多csp信奥赛C++学习资料汇总:
1、csp/信奥赛C++,完整信奥赛系列课程(永久学习):
https://edu.csdn.net/lecturer/7901 点击跳转



2、CSP信奥赛C++竞赛拿奖视频课:
https://edu.csdn.net/course/detail/40437 点击跳转

3、csp信奥赛高频考点知识详解及案例实践:
CSP信奥赛C++动态规划:
https://blog.csdn.net/weixin_66461496/category_13096895.html点击跳转
CSP信奥赛C++标准模板库STL:
https://blog.csdn.net/weixin_66461496/category_13108077.html 点击跳转
信奥赛C++提高组csp-s知识详解及案例实践:
https://blog.csdn.net/weixin_66461496/category_13113932.html
4、csp信奥赛冲刺一等奖有效刷题题解:
CSP信奥赛C++初赛及复赛高频考点真题解析(持续更新):https://blog.csdn.net/weixin_66461496/category_12808781.html 点击跳转
CSP信奥赛C++一等奖通关刷题题单及题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12673810.html 点击跳转
· 文末祝福 ·
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"跟着王老师一起学习信奥赛C++";
cout<<" 成就更好的自己! ";
cout<<" csp信奥赛一等奖属于你! ";
return 0;
}