1. 题目列表
- POJ1860(判断正环,Bellman-Ford、SFPA算法)
- POJ3259(判断负环,任意起点,Floyd算法)
- POJ1062(访问深度限制的Djkstra算法)
- POJ2253(待完成)
- POJ1125(待完成)
- POJ2240(待完成)
2. POJ1860——Currency Exchange
2.1 题目描述
Description
Several currency exchange points are working in our city. Let us suppose that each point specializes in two particular currencies and performs exchange operations only with these currencies. There can be several points specializing in the same pair of currencies. Each point has its own exchange rates, exchange rate of A to B is the quantity of B you get for 1A. Also each exchange point has some commission, the sum you have to pay for your exchange operation. Commission is always collected in source currency.
For example, if you want to exchange 100 US Dollars into Russian Rubles at the exchange point, where the exchange rate is 29.75, and the commission is 0.39 you will get (100 - 0.39) * 29.75 = 2963.3975RUR.
You surely know that there are N different currencies you can deal with in our city. Let us assign unique integer number from 1 to N to each currency. Then each exchange point can be described with 6 numbers: integer A and B - numbers of currencies it exchanges, and real RAB, CAB, RBA and CBA - exchange rates and commissions when exchanging A to B and B to A respectively.
Nick has some money in currency S and wonders if he can somehow, after some exchange operations, increase his capital. Of course, he wants to have his money in currency S in the end. Help him to answer this difficult question. Nick must always have non-negative sum of money while making his operations.
Input
The first line of the input contains four numbers: N - the number of currencies, M - the number of exchange points, S - the number of currency Nick has and V - the quantity of currency units he has. The following M lines contain 6 numbers each - the description of the corresponding exchange point - in specified above order. Numbers are separated by one or more spaces. 1<=S<=N<=100, 1<=M<=100, V is real number, 0<=V<=103.
For each point exchange rates and commissions are real, given with at most two digits after the decimal point, 10-2<=rate<=102, 0<=commission<=102.
Let us call some sequence of the exchange operations simple if no exchange point is used more than once in this sequence. You may assume that ratio of the numeric values of the sums at the end and at the beginning of any simple sequence of the exchange operations will be less than 104.
Output
If Nick can increase his wealth, output YES, in other case output NO to the output file.
Sample Input
3 2 1 20.0
1 2 1.00 1.00 1.00 1.00
2 3 1.10 1.00 1.10 1.00
Sample Output
YES
2.2 解决思路
本题可以抽象为图模型,currency代表的是图节点数,而commision和rate代表的是图的边权,在某一节点的现金总和为图的点权。
该问题是求在图中是否存在一个交换环,使得某源点经过一系列交换回到本身的现金和增加。将问题进一步的抽象,是否在图中存在一个回路,使得回路中每个点的点权不断增加(在本问题中点权即现金和),这就是所谓的判断是否存在正环。
判断负环的一般方法是使用Bellman-Ford算法,以及改进的SPFA算法。其基本思想是:首先求某源点s到所有点的最短路径,若在图中仍存在某点u,以u为中间点,能够使得s到某节点v路径最短,那么图中存在负环。改进的SPFA算法相对于BF算法每一轮需要遍历所有的边增加了限制:只有当某个顶点u的d[u]改变时,从它出发的边的邻接点v的d[v]才有可能被改变。记住模板!!
本问题是判断正环,我们定义d数组,并初始化为0,因为我们的目标是最长路径,期望d是增大的,d[i]表示从源点s到节点i经过一系列的exchange操作可获得的最大资金,因此,松弛条件(路径更新条件):当以某节点u为中介点,若u与其某邻接点v能够通过exchange操作,使得d[v]更大的话,则松弛,即
if (d[v] < (d[u] - g[u][i].c) * g[u][i].r){ // 如果从节点u到v可以增大原来的资金
d[v] = (d[u] - g[u][i].c) * g[u][i].r;
...
}
然后通过SFPA算法,判断是否存在正环,注:SFPA算法仍可以通过priority_queue来优化。
2.3 代码
#include
#include
#include
#include
#include
using namespace std;
struct Edge{
int v;
double c,r;
Edge(int _v, double _c, double _r):v(_v), c(_c), r(_r){
}
};
const int maxn = 110;
const double INF = 1e7;
vector g[maxn];
bool inq[maxn]; // 记录每个节点是否入队
int num[maxn]; // 记录每个节点入队次数
double d[maxn]; // d[i]记录从源点到其他点的最大资金
bool SFPA(int s, double V, int n){ // 判断是否有正权环,与负环思路相反
fill(inq, inq + maxn, false);
fill(d, d + maxn, 0); // 初始化最长路径为负无穷
fill(num, num + maxn, 0);
d[s] = V;
num[s]++;
queue q;
q.push(s);
inq[s] = true;
while(!q.empty()){
// SFPA算法思路,使用队列保存d[i]变化的节点i,
// 只有节点i变化,则从它出发的邻接点才可能变化
int u = q.front();
q.pop();
inq[u] = false; // 注意这里要重新设置u为未入队,因为u可能再后续更新中再次入队
// 访问u的所有邻接点
for(int i = 0; i < g[u].size(); i++){
int v = g[u][i].v;
if (d[v] < (d[u] - g[u][i].c) * g[u][i].r){ // 如果从节点u到v可以增大原来的资金
d[v] = (d[u] - g[u][i].c) * g[u][i].r;
// 如果v不在队列中
if (!inq[v]){
q.push(v);
inq[v] = true;
num[v] ++;
if (num[v] > n - 1)
return true;
}
}
}
}
return false;
}
int main(){
int m, n, start;
double V;
while(~scanf("%d%d%d%lf", &n, &m, &start, &V)){
for(int i = 0; i < maxn; i++)
g[i].clear();
int u,v;
double r1, c1, r2, c2;
for(int i = 0; i < m; i++){
scanf("%d%d%lf%lf%lf%lf", &u, &v, &r1, &c1, &r2, &c2);
g[u].push_back(Edge(v, c1, r1));
g[v].push_back(Edge(u, c2, r2));
}
bool res = SFPA(start, V, n);
if(res){
printf("YES\n");
}else{
printf("NO\n");
}
}
return 0;
}
3. POJ3259——Wormholes
Description
While exploring his many farms, Farmer John has discovered a number of amazing wormholes. A wormhole is very peculiar because it is a one-way path that delivers you to its destination at a time that is BEFORE you entered the wormhole! Each of FJ's farms comprises N (1 ≤ N ≤ 500) fields conveniently numbered 1..N, M (1 ≤ M ≤ 2500) paths, and W (1 ≤ W ≤ 200) wormholes.
As FJ is an avid time-traveling fan, he wants to do the following: start at some field, travel through some paths and wormholes, and return to the starting field a time before his initial departure. Perhaps he will be able to meet himself :) .
To help FJ find out whether this is possible or not, he will supply you with complete maps to F (1 ≤ F ≤ 5) of his farms. No paths will take longer than 10,000 seconds to travel and no wormhole can bring FJ back in time by more than 10,000 seconds.
Input
Line 1: A single integer, F. F farm descriptions follow.
Line 1 of each farm: Three space-separated integers respectively: N, M, and W
Lines 2..M+1 of each farm: Three space-separated numbers (S, E, T) that describe, respectively: a bidirectional path between S and E that requires T seconds to traverse. Two fields might be connected by more than one path.
Lines M+2..M+W+1 of each farm: Three space-separated numbers (S, E, T) that describe, respectively: A one way path from S to E that also moves the traveler back T seconds.
Output
Lines 1..F: For each farm, output "YES" if FJ can achieve his goal, otherwise output "NO" (do not include the quotes).
Sample Input
2
3 3 1
1 2 2
1 3 4
2 3 1
3 1 3
3 2 1
1 2 3
2 3 4
3 1 8
Sample Output
NO
YES
Hint
For farm 1, FJ cannot travel back in time.
For farm 2, FJ could travel back in time by the cycle 1->2->3->1, arriving back at his starting location 1 second before he leaves. He could start from anywhere on the cycle to accomplish this.
3.2 解决思路
该题在图中存在两种边,一种是正权边,另一种是负权边,判断是否存在负环。由于出发点可以选在任意点,因此可以使用Floyd算法判断是否存在负环。
Floyd算法的核心思想是:定义d[i][j]存储i到j的最短路径,对于图中任意一个节点k,当以k为中介点时,是否能够使得i到j的距离更短。
3.3 代码
#include
#include
#include
#include
using namespace std;
const int maxn = 505;
const int INF = 0x3f3f3f3f;
int d[maxn][maxn]; // 全源最短路径,表示顶点i到顶点j的最短路径
bool Floyd(int n){
/*
Floyd算法思想:
如果存在顶点k使得,以k为中介点时,
顶点i与顶点j的当前最短距离缩短
即当d[i][k] + d[k][j] < d[i][j]时,则更新
*/
for (int k = 1; k <= n; k++){
for (int i = 1; i <= n; i++){
for (int j = 1; j<= n; j++){
if (d[i][k] != INF && d[k][j] != INF && d[i][k] + d[k][j] < d[i][j])
d[i][j] = d[i][k] + d[k][j];
}
if (d[i][i] < 0) return true;
}
}
// // 判断是否还有边可以更新
// for (int k = 1; k <= n; k++){
// for (int i = 1; i <= n; i++){
// for (int j = 1; j<= n; j++){
// if (d[i][k] != INF && d[k][j] != INF && d[i][k] + d[k][j] < d[i][j])
// return true;
// }
// }
// }
return false;
}
int main(){
int T;
scanf("%d",&T);
while(T--){
int m, n, w;
scanf("%d%d%d",&n, &m, &w);
int u, v, t;
memset(d, INF, sizeof(d));
for (int i = 1; i <= n; i++){
d[i][i] = 0;
}
for (int i = 0; i < m; i++){
scanf("%d%d%d", &u, &v, &t);
d[u][v] = t; // 无向图
d[v][u] = t;
}
for (int i = 0; i < w; i++){
scanf("%d%d%d", &u, &v, &t);
d[u][v] = -t; // 负权
}
bool res = Floyd(n);
if (res == true){
printf("YES\n");
}else{
printf("NO\n");
}
}
return 0;
}
4. POJ1062——昂贵的聘礼
4.1 题目描述
Description
年轻的探险家来到了一个印第安部落里。在那里他和酋长的女儿相爱了,于是便向酋长去求亲。酋长要他用10000个金币作为聘礼才答应把女儿嫁给他。探险家拿不出这么多金币,便请求酋长降低要求。酋长说:"嗯,如果你能够替我弄到大祭司的皮袄,我可以只要8000金币。如果你能够弄来他的水晶球,那么只要5000金币就行了。"探险家就跑到大祭司那里,向他要求皮袄或水晶球,大祭司要他用金币来换,或者替他弄来其他的东西,他可以降低价格。探险家于是又跑到其他地方,其他人也提出了类似的要求,或者直接用金币换,或者找到其他东西就可以降低价格。不过探险家没必要用多样东西去换一样东西,因为不会得到更低的价格。探险家现在很需要你的帮忙,让他用最少的金币娶到自己的心上人。另外他要告诉你的是,在这个部落里,等级观念十分森严。地位差距超过一定限制的两个人之间不会进行任何形式的直接接触,包括交易。他是一个外来人,所以可以不受这些限制。但是如果他和某个地位较低的人进行了交易,地位较高的的人不会再和他交易,他们认为这样等于是间接接触,反过来也一样。因此你需要在考虑所有的情况以后给他提供一个最好的方案。
为了方便起见,我们把所有的物品从1开始进行编号,酋长的允诺也看作一个物品,并且编号总是1。每个物品都有对应的价格P,主人的地位等级L,以及一系列的替代品Ti和该替代品所对应的"优惠"Vi。如果两人地位等级差距超过了M,就不能"间接交易"。你必须根据这些数据来计算出探险家最少需要多少金币才能娶到酋长的女儿。
Input
输入第一行是两个整数M,N(1 <= N <= 100),依次表示地位等级差距限制和物品的总数。接下来按照编号从小到大依次给出了N个物品的描述。每个物品的描述开头是三个非负整数P、L、X(X < N),依次表示该物品的价格、主人的地位等级和替代品总数。接下来X行每行包括两个整数T和V,分别表示替代品的编号和"优惠价格"。
Output
输出最少需要的金币数。
Sample Input
1 4
10000 3 2
2 8000
3 5000
1000 2 1
4 200
3000 2 1
4 200
50 2 0
Sample Output
5250
4.2 解决思路
该问题可以将所有的优惠关系转换为图的边,将每个物品看作一个节点,从而求从物品1到所有具有优惠关系的其他物品的最短路径,注意:此时求的的最短路径需要加上物品本身的代价,再求最小。
此外,该题还引入了等级限制。我们接下来讨论等级限制:
引入所有物品是否在等级限制内的bool型数组,在求解最短路径时,如果某物品不在等级限制内,则过滤掉。酋长的等级定义为KL,最大等级差距定义为M,所有物品有效的等级区间为[KL - M, KL + M],但是以物品的等级是否在该区间内定义bool型数组,
if (L[i] >= KL - M && L[i] <= KL + M)
L[i] = true;
else L[i] = false;
就会发生错误,因为物品之间的等级不一定满足等级限制M,如M=1,L = {3, 2, 4},KL = L[1] = 3,则物品2和3不能进行交易。
因此,我们穷举所有的可能的等级区间,对于上述例子,正确的物品等级应属于{2, 3} 或 {3, 4},而没有{2, 3, 4}或{2, 4}。所以,我们穷举每一个可能的等级区间,
int minCost = INF, cost;
for (int i = 0; i<= M; i++){
memset(withinLevel, 0, sizeof(withinLevel));
for (int j = 1; j <= n; j++){
if(l[j] >= l[1] - M + i && l[j] <= l[1] + i)
withinLevel[j] = true;
}
cost = Dijkstra(1, n);
if (cost < minCost)
minCost = cost;
}
重复执行Dijsktra算法,获得全局最短路径。
4.3 代码
#include
#include
#include
#include
#include
using namespace std;
struct Node{
int v, len;
Node(int _v, int _len):v(_v), len(_len){
}
};
const int maxn = 110;
const int INF = 1e6;
vector g[maxn];
int w[maxn], l[maxn], d[maxn];
bool visited[maxn], withinLevel[maxn];
int Dijkstra(int s, int n){
fill(d, d + maxn, INF);
fill(visited, visited + maxn, false);
d[s] = 0;
for (int k = 0; k < n; k++){ // 重复n次
// 取出未被访问过的节点的最小值
int MIN = INF, u = -1;
for (int i = 1; i <= n; i++){
if (withinLevel[i] && !visited[i] && d[i] < MIN){
MIN = d[i];
u = i;
}
}
if (u == -1) break; // 如果找不到邻接点
visited[u] = true;
// 访问index所有邻接点
for (int i = 0; i < g[u].size(); i++){
int v = g[u][i].v;
if (!visited[v] && withinLevel[v]){ // 这里根据Djkstra算法的最优性,只需要更新未访问的节点
// 以u为中介点能够更新d[v],路径s->u->v的Level不超过M
// int maxLevelSub = max(l[s], max(l[u], l[v])) - min(l[s], min(l[u], l[v]));
// printf("等级差:%d\n", maxLevelSub);
if (d[u] + g[u][i].len < d[v]){
d[v] = d[u] + g[u][i].len;
// printf("节点%d为中间节点,节点%d被更新\n", u, v);
}
}
}
}
int minimum = INF;
for (int i = 1; i <= n; i++){
d[i] += w[i];
if (d[i] < minimum)
minimum = d[i];
}
return minimum;
}
int main(){
int M, n;
scanf("%d%d",&M, &n);
int x, a, b;
for (int i = 1; i <= n; i++){
scanf("%d%d%d", &w[i], &l[i], &x);
for (int j = 1; j <= x; j++){
scanf("%d%d", &a ,&b);
g[i].push_back(Node(a, b));
}
}
// 枚举所有的可能的等级
int minCost = INF, cost;
for (int i = 0; i<= M; i++){
memset(withinLevel, 0, sizeof(withinLevel));
for (int j = 1; j <= n; j++){
if(l[j] >= l[1] - M + i && l[j] <= l[1] + i)
withinLevel[j] = true;
}
cost = Dijkstra(1, n);
if (cost < minCost)
minCost = cost;
}
printf("%d\n", minCost);
return 0;
}
5. 总结
图的最短路径算法包括Djsktra(用于权值没有负值的图)、Bellman-Ford、SFPA(用于权值有负值的图)、Floyd(用于全源最短路径),问题往往结合额外的边权或点权来考察,典型的问题包括:添加额外限制条件(层次限制、等级限制)求最短路径、判断图中是否存在正环和负环(Floyd、BF、SPFA),在做图最短路径问题时应注意以下问题
- 图的存储结构:使用邻接表,开销较小,代码较复杂。使用邻接矩阵顶点数不能超过200~300个(1000ms)内,邻接矩阵存储的元素是边权。
- 在进行松弛操作时(最短路径更新操作):基本思想都是以某新加入的节点为中介点,判断该节点的所有邻接点的最短路径是否可以被更新。松弛操作的条件最好只在图的边权上进行,不容易出错。
- 合理定义d[]数组(最短路径数组),根据具体问题灵活定义和转换。
- 变量应合理的初始化:正环问题,d数组初始化为最小值;负环问题,d数组初始化为最大值。
- 记住上述4个算法的一般模板,遇到具体问题进行灵活变换。