题意简述
输入货币名称以及货币间的汇率(A到B的汇率可能与B到A的不同),判断是否有可能实现套汇(即从某指定货币开始,通过一系列的货币兑换操作,最终换回某货币并实现盈利)。
问题有多组数据,对于每组数据,第一行代表货币类别数量,第二行起的n行为各货币名称;接下来一行是允许的兑换方式总数,而后每行有三个部分,第一个和第三个分别是原始货币和兑换后货币的名称,第二个是汇率。数据的输入以0结束。
对于每组数据,输出是否可能实现套汇(YES或NO)。
例如:
3 USDollar BritishPound FrenchFranc 3 USDollar 0.5 BritishPound BritishPound 10.0 FrenchFranc FrenchFranc 0.21 USDollar 3 USDollar BritishPound FrenchFranc 6 USDollar 0.5 BritishPound USDollar 4.9 FrenchFranc BritishPound 10.0 FrenchFranc BritishPound 1.99 USDollar FrenchFranc 0.09 BritishPound FrenchFranc 0.19 USDollar 0
输出:
Case 1: Yes Case 2: No
算法分析
这也是一题典型的套汇问题,与POJ-1860有相似之处(《POJ-1860 解题报告》)。
套汇问题的典型解决思路是Bellman Ford算法的逆用。Bellman Ford的一大特点是能够在找最短路过程中发现负权环。在套汇问题中,情况恰好是相反的。我们需要利用Bellman Ford算法寻找正权环。原始Bellman Ford的边权松弛过程,是松弛的更短,因而初始时将原点到任意点的距离设为无穷大。对于套汇问题,边权松弛明显是越松弛越长,所以初值全部置为0。原点到每个点的距离d[i]即从原点代表的货币到该货币能换到的最大值。若经过松弛,能实现原点到自身的距离d[原点]>初始值,则说明实现了盈利。
标准的Bellman Ford算法是松弛V-1次,但对于套汇问题,因为一轮盈利值可能很小,又因为实数比较的不稳定性,所以可能需要松弛若干次才能比较出原点到自身距离的变化。松弛的次数是无法估计的。所以我们将原先的for循环控制改为while,终止条件即判断d[原点]是否大于初始值。为方便,我们将初始值设为1。
但这样一来,一旦不存在正权环(即无法盈利),则会陷入死循环。为避免死循环,我们增加标记位flag,每轮松弛开始前置为0,一旦发生松弛则置1。一轮松弛后若flag仍为0,且d[原点]并没有大于初始值,则可判断图已没法松弛且不存在正权环。直接返回无法盈利。
套汇问题中Bellman Ford的松弛规则,就是题目描述的套汇规则。本题是直接乘以汇率,1860还需减去手续费。
实现时,以第一种货币为原点。最后判断d[1]。
Problem Status: AC。时间766ms,内存784k
细节
关于货币名与标号对应,可以利用C++中的容器,建立字符串到整型的一一对应关系。
#include<map> map<string,int>STL;
使用时,读入阶段给每个字符串标号:STL[str]=i;
调用时,将STL[str1]作为整型数组下标使用即可。
特殊情况
这道题用Bellman Ford在POJ即可AC。但仔细考虑,不难发现Bellman Ford只适用于这张图是连通图的情况下。如果图是不连通的,而正权环恰巧不在第一个货币节点所在的那一部分,则无法发现正权环的存在。
我想到的第一个解决方法,是设立0号节点,建立这个点到每个节点的往返两条边,往返边权值均为1(代表走这条边不会盈利也不会亏损,不影响正权环寻找),这样整张图便成为了连通图。以0号点为原点,最后只需判断d[0]是否大于初始值即可。但调试发现,一旦图中任意一条边的权值大于1,则会返回YES。这个很好理解,假设A到B的权值大于1,则原点出发到A,A到B,B再返回原点,原点即实现了更新。但这并不能证明边AB是正权环的一部分。
为了解决这个情况,我想过把0号点到各节点的边改为单向,只出不入,但这样便无法更新d[0],没有起到连通全图的作用。
最终的解决方案是,从0号点出的边权值设为1,但返回的边权值设为一个极小的值。这样非正权环就无法对其更新,因为返回会消耗增加的部分。但正权环因为可以通过无限次的松弛达到无穷大,所以可以更新它。这种解决方案本机测试通过。
但为了达到足够更新原点的大小,正权环需要被松弛非常多次。这使得算法的时间复杂度一下变的非常高。因而这种算法提交POJ得到Runtime Error提示。
如何解决非连通图呢?
网上有一种思路是用Floyd算法变形。最后扫描全图每点到自身的距离是否被更新。只要有一点被更新则说明存在正权环。
程序样例(Bellman Ford)
#include<iostream> #include<string> #include<map> using namespace std; struct edge { int a; int b; double r; }; int bellman_ford(int m, struct edge e[]) { double d[31] = {0}; int flag, i; d[1] = 1; while (d[1] <= 1) { flag = 0; for (i = 0; i < m; i++) if (d[e[i].b] < d[e[i].a] * e[i].r) { flag = 1; d[e[i].b] = d[e[i].a] * e[i].r; } if (flag == 0) if (d[1] <= 1) return 0; else return 1; } return 1; } int main() { map < string, int > STL; int n, m, i, t, casei = 1; struct edge e[900]; string str1, str2; while ((cin >> n) && (n != 0)) { for (i = 1; i <= n; i++) { cin >> str1; STL[str1] = i; } cin >> m; for (i = 0; i < m; i++) { cin >> str1 >> e[i].r >> str2; e[i].a = STL[str1]; e[i].b = STL[str2]; } if (bellman_ford(m, e)) cout << "Case " << casei++ << ": Yes" << endl; else cout << "Case " << casei++ << ": No" << endl; } return 0; }
程序样例(Bellman Ford修正非连通图)
#include<iostream> #include<string> #include<map> using namespace std; struct edge { int a; int b; double r; }; int bellman_ford(int m, struct edge e[]) { double d[31] = {0}; int flag, i; d[0] = 1; while (d[0] <= 1) { flag = 0; for (i = 0; i < m; i++) if (d[e[i].b] < d[e[i].a] * e[i].r) { flag = 1; d[e[i].b] = d[e[i].a] * e[i].r; } if (flag == 0) if (d[0] <= 1) return 0; else return 1; } return 1; } int main() { map < string, int > STL; int n, m, i, t, casei = 1; struct edge e[900]; string str1, str2; while ((cin >> n) && (n != 0)) { for (i = 1; i <= n; i++) { cin >> str1; STL[str1] = i; } cin >> m; for (i = 0; i < m; i++) { cin >> str1 >> e[i].r >> str2; e[i].a = STL[str1]; e[i].b = STL[str2]; } for (t = 1; t <= n; t++) { e[i].a = 0; e[i].b = t; e[i].r = 1; i++; e[i].b = 0; e[i].a = t; e[i].r = 0.0000001; i++; } m = m + n * 2; if (bellman_ford(m, e)) cout << "Case " << casei++ << ": Yes" << endl; else cout << "Case " << casei++ << ": No" << endl; } return 0; }