Introduction to graphs and their data structures Section III[翻译]
2007-04-24 08:20 老博客哈 阅读(1325) 评论(7) 编辑 收藏 举报 Introduction to graphs and their data structures
Section 3
【原文见: http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=graphsDataStrucs3】
作者: By gladius
Topcoder Member
翻译: 农夫三拳@seu(drizzlecrj@gmail.com)
Finding the best path through a graph
Dijkstra (Heap method)
Floyd-Warshall
Finding the best path through a graph
在TopCoder中一个非常常见的问题是找出从一个位置到另外一个位置的最短路径。解决这个问题的方法各不相同,且各有用途。我们将要讨论的两个不同的算法是:使用堆的Dijkstra和Floyd Warshall方法。
Dijkstra (Heap method)
使用堆的Dijkstra算法是你的TopCoder利器中非常重要的一个。它本质上是一个广度优先搜索,不同之处在于的用优先级队列替代了队列,此外还在结点上定义一个排序函数使得拥有较低耗费的结点在优先级队列的顶端。这个算法允许我们在图中以O(m*logn)的时间复杂度找到最优路径,这里n代表顶点数,m代表图中的边的数目。
首先,按照顺序先介绍一下优先级队列/堆数据结构。堆是一个基础数据结构并且在许多问题中都非常的有用。我们最感兴趣的性质在于它是一个半排序的数据结构,这里用半排序的意思是我们定义了一些排好序的元素并且将它们插入到这种数据结构中,这个结构能够保持最小的(或者最大的)元素处于顶部。堆有一个非常好的性质,就是在插入和移除顶部元素的时间复杂度为O(log n),这里的n指的是堆中的元素的数量,取得顶部值的时间复杂度为O(1),因此堆与我们的需要不谋而合。
堆中的基本操作有:
1. Add - 将一个元素插入到堆中,将元素放到正确的排序位置。
2. Pop - 将顶部元素弹出,顶部元素根据实现,要么是最大或者最小的元素。
3. Top - 返回堆的头部元素。
4. Empty - 测试堆是否为空。
由于它和队列,栈非常相近,我们可以很自然的使用前面用过的搜索技巧,除了将出现队列,栈的地方替换成优先队列。我们基本的搜索(记住这个!)看起来和下面一样:
priorityQueue s;
s.add(start);
while (s.empty() == false) {
top = s.top();
s.pop();
mark top as visited;
}
}
检查终止条件(我们到达目标节点了吗?) 将顶部元素的所有没有访问的邻接元素放到优先队列中。
不幸的是,并不是TopCoder中使用的所有语言都能很容易的使用默认的语言库中的优先队列数据结构。
使用C++的程序员很幸运的有STL中的priority_queue<>,它的使用方法如下:
using namespace std;
priority_queue pq;
1. Add - void pq.push(type)
2. Pop - void pq.pop()
3. Top - type pq.top()
4. Empty - bool pq.empty()
尽管如此,你要当心C++中的priority_queue<>,它首先返回的是*最大的*元素,而不是最小的。【译者注:默认的堆是最大堆,而不是最小堆】这可能会导致许多O(m*log(n))的解决方案复杂度剧增或者根本不能工作。
在类型上定义一个顺序,有许多不同的方法。我所找到的最简单的方法如下:
struct node {
int cost;
int at;
};
并且我们想要按照耗费进行排序,因此我们按照如下为结构定义一个小于的操作符
if (leftNode.cost != rightNode.cost) return leftNode.cost < rightNode.cost;
if (leftNode.at != rightNode.at) return leftNode.at < rightNode.at;
return false;
}
尽管我们不需要堆结构中的'at'元素进行排序,我们仍然要将具有相同耗费的不同的'at'值归为同一个值。末尾的return false就是确保在小于操作符号上进行比较的相同的元素返回false。
使用java的程序员得需要做一些替代工作,由于没有直接的一个堆的实现的数据结构。我们可以用TreeSet结构进行模拟,这里TreeSet堆我们的数据集进行完全排序。它在空间上不是很高效,但是可以很好的满足我们的需要。
TreeSet pq = new TreeSet();
1. Add - boolean add(Object o)
2. Pop - boolean remove(Object o)
在这种情况下,我们可以移除任何我们想要移除的东西,但是弹出的时候需要移除第一个元素,因此我们总是这样调用它:
3. Top - Object first()
4. Empty - int size()
定义排序与C++中的排序类似,如下:
public int cost, at;
public int CompareTo(Object o) {
Node right = (Node)o;
if (cost < right.cost) return -1;
if (cost > right.cost) return 1;
if (at < right.at) return -1;
if (at > right.at) return 1;
return 0;
}
}
使用C#的程序员同样有这个问题,因此他们也需要模拟,不幸的是,我们所能使用的最好的只有SortedList类,它没有我们需要的速度(插入和删除操作都是O(n),而不是O(logn))。很不幸,C#中也没有内建的用来实现堆的算法。HashTable也不是很合适。
现在回到实际的算法中,最漂亮的部分在于它可以应用在具有权重的图中,和广度优先搜索用在无权图中一样。因此现在我们可以解决比使用广度优先搜索来解决的更加难的问题了(在TopCoder中非常常见)。
这里还有一些非常好的性质,由于我们每次都是选取最小耗费的结点进行扩展的,因此我们第一次访问结点就是到达该结点的最佳路径(除非图中有负权重)。因此我们只需要访问每个结点一次,并且最好的地方在于一旦我们到达目标节点,我们就知道任务以及完成了。
这里我们所举的例子是SRM181,Div1 1000的问题 KiloManx。这是应用堆结构的Dijkstra算法的一个绝佳的例子,虽然它看起来像一个动态规划的问题。在这个问题中,节点之间的边权重随着我们选择的武器而改变。因此在我们的结点中我们至少需要保存我们所挑选的武器的路径,和我们当前已经射击的次数(这是我们的耗费)。完美的地方在于我们挑选的武器和我们已经打败的boss相对应,因此我们可以使用它作为我们的访问结构的基础。如果我们用整数中的一个位表示每一个武器,我们需要存储的最大值是32768(2^15,因为最多只有15个武器)。因此我们可以将访问数组简单的设置为拥有32769个二值的数组。在这个问题中定义结点的顺序非常简单,我们想首先扩展具有较少设计次数的结点,因此根据上面的信息我们可以按照如下定义我们的基础数据结构:
class node {
int weapons;
int shots;
// Define a comparator that puts nodes with less shots on top appropriate to your language
};
现在我们使用熟悉的结构来处理这种类型的问题。
priorityQueue pq;
pq.push(node(0, 0));
while (pq.empty() == false) {
node top = pq.top();
pq.pop();
// Make sure we don't visit the same configuration twice
if (visited[top.weapons]) continue;
visited[top.weapons] = true;
// A quick trick to check if we have all the weapons, meaning we defeated all the bosses.
// We use the fact that (2^numWeapons - 1) will have all the numWeapons bits set to 1.
if (top.weapons == (1 << numWeapons) - 1)
return top.shots;
for (int i = 0; i < damageChart.length; i++) {
// Check if we've already visited this boss, then don't bother trying him again
if ((top.weapons >> i) & 1) continue;
// Now figure out what the best amount of time that we can destroy this boss is, given the weapons we have.
// We initialize this value to the boss's health, as that is our default (with our KiloBuster).
int best = bossHealth[i];
for (int j = 0; j < damageChart.length; j++) {
if (i == j) continue;
if (((top.weapons >> j) & 1) && damageChart[j][i] != '0') {
// We have this weapon, so try using it to defeat this boss
int shotsNeeded = bossHealth[i] / (damageChart[j][i] - '0');
if (bossHealth[i] % (damageChart[j][i] - '0') != 0) shotsNeeded++;
best = min(best, shotsNeeded);
}
}
// Add the new node to be searched, showing that we defeated boss i, and we used 'best' shots to defeat him.
pq.add(node(top.weapons | (1 << i), top.shots + best));
}
}
}
在TopCoder中有着很多的这种类型的问题,下面的问题你可以尝试一下:
SRM 150 - Div 1 1000 - RoboCourier
SRM 194 - Div 1 1000 - IslandFerries
SRM 198 - Div 1 500 - DungeonEscape
TCCC '04 Round 4 - 500 - Bombman
Floyd-Warshall
当图用一个邻接矩阵表示时,Floyd-Warshall是一个非常有用的方法。它需要O(n^3)的运行时间,这里n指的是图中顶点的数目。尽管如此,与Dijkstra相比,Dijkstra给出我们单源点的最短路径,而Floyd-Warshall给出我们所有点对的最短路径。Floyd-Warshall还有一些其他的用途;他可以用来查找图的连通性(也就是图的传递闭包)。
首先,尽管如此,我们需要讨论一下Floyd Warshall 所有点对最短路径的算法,它和Dijkstra非常的类似。在邻接矩阵上运行这个算法,元素adj[i][j]表示从结点i到结点j的最短路径的长度。算法的伪代码如下:
for (i = 1 to n)
for (j = 1 to n)
adj[i][j] = min(adj[i][j], adj[i][k] + adj[k][j]);
正如你所看到的,记住并且敲出这些代码相当的容易。如果图比较小的话(少于100个结点),那么这个方法可以让你很快的提交。
一个非常好的用来测试这个算法的问题是SRM 184 Division 2 1000的问题,TeamBuilder