介绍

最短路径问题是图论研究中的一个经典算法问题, 旨在寻找图(由结点和路径组成的)中两结点之间的最短路径。

算法具体的形式包括:

  • 确定起点的最短路径问题 - 即已知起始结点,求最短路径的问题。
  • 确定终点的最短路径问题 - 与确定起点的问题相反,该问题是已知终结结点,求最短路径的问题。
    在无向图中该问题与确定起点的问题完全等同,在有向图中该问题等同于把所有路径方向反转的确定起点的问题。
  • 确定起点终点的最短路径问题 - 即已知起点和终点,求两结点之间的最短路径。
  • 全局最短路径问题 - 求图中所有的最短路径。

用于解决最短路径问题的算法被称做“最短路径算法”。

单源最短路径

SPFA

算法大致流程 是用一个队列来进行维护。
初始时将源点加入队列,每次从队列中取出一个元素,并对所有与他相邻的点进行松弛,若某个相邻的点松弛成功,则将其入队,直到队列为空时算法结束。

SPFA算法(Shortest Path Faster Algorithm),也是求解单源最短路径问题的一种算法,用来解决的问题如下:
给定一个加权有向图G和源点s,对于图G中的任意一点v,求从s到v的最短路径。

SPFA算法是 Bellman-Ford 算法的一种队列实现,减少了冗余计算,他的基本算法和 Bellman-Ford 一样,并用如下方法改进:

  1. 第一步,不是枚举所有节点,而是通过队列来进行优化,设置一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值 对 离开u点所指向的结点v 进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。

  2. 同时,除了通过判断队列是否为空来结束循环,还可以通过下面的方法:即判断有无负环,如果某个点进入队列的次数超过V次,则存在负环(SPFA算法无法处理带负环的图)。

SPFA 在形式上和 BFS(广度优先搜索)非常类似,不同的是:在广度优先搜索中,一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是一个点改进过其它的点之后,过了一段时间可能本身被改进,于是再次用来改进其它的点,这样反复迭代下去。设一个点用来作为迭代点对其它点进行改进的平均次数为k,有办法证明对于通常的情况,k在2左右。

然而在实际的应用中SPFA算法的时间效率不是很稳定,为了避免最坏情况的出现,通常使用时间效率更加稳定的Dijkstra算法。

SPFA模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//gh[][]为有向图(gh[i][j]为结点i到结点j的权值,如果gh[i][j]不存在,则为-1)
//dis[] 中所有元素应初始化为INFINITY(0xfffffff最大值)
//vis[] 中所有元素应初始化为false。
//num[] 记录各结点入队列次数

bool spfa(int s){
//若 s 为终点,直接返回
//path[] 用于记录路径

queue<int> Q;

Q.push(s);
dis[s] = 0;

while(!Q.empty()) {
int u = Q.front();
Q.pop();
vis[u] = true;

for (int v = 1; v <= n; v++) {
if (dis[v] > dis[u] + gh[u][v]){
dis[v] = dis[u] + gh[u][v];
//path[v] = u;
//num[v]++;
//if (num[v] > n) return false; //判断负环
if (!vis[v]) {
vis[v] = true;
Q.push(v);
}
}
}
vis[u] = false; //将出队列的的点置为false
}
return true;
}

任意两点最短路径

使用条件&范围:通常可以在任何图中使用,包括有向图、带负权边的图。

Floyd-Warshall 算法用来找出每对点之间的最短距离。它需要用邻接矩阵来储存边,这个算法通过考虑最佳子路径来得到最佳路径。

注意单独一条边的路径也不一定是最佳路径。

  • 从任意一条单边路径开始。所有两点之间的距离是边的权,或者无穷大,如果两点之间没有边相连。
  • 对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短。如果是更新它。
  • 不可思议的是,只要按排适当,就能得到结果。
1
2
3
4
5
6
7
8
9
10
11
12
// dist(i,j) 为从节点i到节点j的最短距离
for i←1 to n do
for j←1 to n do
dist(i,j) = weight(i,j)

for k1 to n do // k为“媒介节点”
for i1 to n do
if (i<>k) then
for j←1 to n do
if (i<>j) && (k<>j) then
if (dist(i,k) + dist(k,j) < dist(i,j)) then
dist(i,j) = dist(i,k) + dist(k,j)

Reference

http://www.nocow.cn/index.php/SPFA%E7%AE%97%E6%B3%95s

http://www.nocow.cn/index.php/Floyd-Warshall%E7%AE%97%E6%B3%95

##概念
二分图: 又称作二部图,是图论中的一种特殊模型。 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。

无向图G为二分图的充分必要条件是,G至少有两个顶点,且其所有回路的长度均为偶数。

最大匹配:图中包含边数最多的匹配称为图的最大匹配

##介绍
求最大匹配,匈牙利算法

算法的思路是不停的找增广路径, 并增加匹配的个数,增广路径顾名思义是指一条可以使匹配数变多的路径,在匹配问题中,增广路径的表现形式是一条”交错路径”,也就是说这条由图的边组成的路径, 它的第一条边是目前还没有参与匹配的,第二条边参与了匹配,第三条边没有..最后一条边没有参与匹配,并且始点和终点还没有被选择过。这样交错进行,显然他有奇数条边。那么对于这样一条路径,我们可以将第一条边改为已匹配,第二条边改为未匹配…以此类推。也就是将所有的边进行”反色”,容易发现这样修改以后,匹配仍然是合法的,但是匹配数增加了一对。另外,单独的一条连接两个未匹配点的边显然也是交错路径。可以证明。当不能再找到增广路径时,就得到了一个最大匹配,这也就是匈牙利算法的思路。

参考介绍

##应用

OJ参考题目:poj1469 poj2446 poj2819

##模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//注意:结点标号从1开始!

//u1 -- v1
//u2 -- v2

bool gh[][] //graph that if two points is connected

bool b[] //if b[v] is used
int link[] //link[v] = u

bool Find(int u){
for (int v = 1; v <= (v); v++){
if (gh[u][v] && !b[v]){
b[v] = true;

//如果结点标号从0开始,可以使用 link[v] == -1
if (link[v] == 0 || Find(link[v])){
link[v] = u;
return true;
}
}
}
return false;
}

int main(){
int ans = 0;
for (int i = 1; i <= (u); i++){
memset(b, 0, sizeof(b));
if (Find(i)) ans++;
}
//ans为边的匹配数
}

##介绍

并查集:(union-find sets)是一种简单的用途广泛的集合。并查集是若干个不相交集合,能够实现较快的合并和判断元素所在集合的操作,应用很多。

一般采取树形结构来存储并查集,并利用一个rank数组来存储集合的深度下界,在查找操作时进行路径压缩使后续的查找操作加速。这样优化实现的并查集,空间复杂度为O(N),建立一个集合的时间复杂度为O(1)。并查集的操作可以看作是线性的。

它支持以下几种操作:

  • void Init(int n)
  • void Union(int p, int q)
  • bool Connected(int p, int q)
  • int GetFather(int n)
  • int count()

##解决问题
并查集主要应用于涉及到几个不相交子集的查找和合并运算。

模板题:POJ1611 POJ2524 POJ1182 POJ2492 POJ1988

##模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class UnionFind{
private:
vector<int> father;
vector<int> sum;//the number of child as root
public:
void Init(int n){
father.clear();
father.resize(n+2);//start from 1
sum.clear();
sum.resize(n+2);
for (int i=1; i<= n; i++){
father[i] = i;
sum[i] = 0;
}
}

bool Is_same(int x, int y){
return GetFather(x) == GetFather(y);
}

void Union(int x, int y){
father[GetFather(x)] = GetFather(y);
}

int GetFather(int x){
if (father[x] == x) return x;
else return father[x] = GetFather(father[x]);
}

int FindGroups(){
int ans = 0;
for (int i=1; i<=father.size()-2; i++){
sum[GetFather(i)]++;
}
for (int i=1; i<=father.size()-2; i++){
if (sum[i]) ans++;
}
return ans;
}
};

参考:
http://blog.csdn.net/dm_vincent/article/details/7769159
http://blog.csdn.net/dm_vincent/article/details/7655764

C++中STL标准库中队列以及最大优先队列的使用

##0x00介绍
queue类是提供了一个队列功能的容器适配器。具体而言,它是一个FIFO(先入先出)的数据结构 在头文件<queue>中定义。

##0x01队列
常用方法:

bool empty() const;
size_type size() const;

void pop();
void push (const value_type& val);

value_type& front();/const value_type& front() const;
value_type& back();/const value_type& back() const;

##0x02最大优先队列
常用方法:

bool empty() const;
size_type size() const;

const value_type& top() const;
void pop();
void push (const value_type& val);

最大优先队列的声明:
priority_queue pq;
标准库默认使用元素类型的<操作符来确定它们之间的优先级关系
优先队列默认的是数据大的优先级高,即降序,队列头部元素最大

若需要自定义优先级,可以重载小于号:

1
2
3
4
5
6
7
8
9
struct node {
int priority;
int value;

friend bool operator < (node n1, node n2){
return n1.priority < n2.priority; 较大值优先
//return n1.priority > n2.priority; 较小值优先
}
};

或者

1
2
//对于基本类型,要使队列头部元素最小,可以使用一下声明
priority_queue<int, vector<int>, greater<int> > q;

或者

1
2
3
4
5
6
7
8
struct cmp{
bool operator() (Node a, Node b){
if(a.x == b.x)

return a.y> b.y;
return a.x> b.x;
}

};
priority_queue<Node, vector<Node>, cmp> pq;

##0x03应用
首先注意,priority_queue放置元素时,不会判断元素是否重复。(因为在模板的第二个参数时顺序容器,不能保证元素的唯一性)

  • The kth great number(求依次加入队列的数中第k大的数)
  • Fence Repair

题目在 –>这里<–

思想就是广度优先搜索,但是代码实现的能力还是有待加强。

记录下这道题遇到的一些问题。

  1. 先是输入问题,直接把顶点存入图中,然后将不能访问的点(obstacle)设为1,然后将四个方向转化为数字(0北1西2南3东)。

  2. 然后就是BFS的实现了,直接套模板不是很方便,很多细节都重写了,但思路还是一致的。队列里存一个结构体表示状态。一个状态有坐标,方向,时间四个变量。然后要GO和TURN,TURN的话巧妙地模4的方法巧妙转化,而GO的话,要在找到下一个状态后,判断是否边界,点是否可行,是否访问过,如果都没问题,则加入队列,然后标记访问。这里同时判断是否到达终点,如果是,直接跳出函数。

  3. 记录两次WA的原因:第一,未读清题目,题目中track是两个方块的重合边,是不包括边界的。而我误将边界点认为可达; 第二,未考虑周全,测试数据中有起始点和终止点相同的case,而我没特殊处理WA了。

总结:分析题目要认真,要考虑情况尽可能全,并记录。

最后,贴代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
//From UVA 314
#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>
using namespace std;

int dx[4]={-1,0,1,0};
int dy[4]={0,-1,0,1};
// 0
// 1 3
// 2

class Problem{
private:
int n, m;
int startx, starty, endx, endy;
int direction;

int gh[52][52];
bool vis[52][52][4];

struct state{
int x, y, d, step;
};
public:
void Solve(){
while(Read()){
if (startx == endx && starty == endy){
printf("%d\n",0);
}else{
printf("%d\n", BFS(startx,starty,direction,0));
}
}
}

int BFS(int x, int y, int d, int s){
struct state u, v;
queue<struct state> q;
u.x = x;
u.y = y;
u.d = d;
u.step = s;
q.push(u);

while(!q.empty()){
u = q.front();
q.pop();

for (int i=1; i<4; i=i+2){
v = u;
v.d = (v.d + i)%4;
v.step++;
if (!vis[v.x][v.y][v.d]) q.push(v);
vis[v.x][v.y][v.d] = true;
}

for (int i=1; i<4; i++){
v = u;
v.x += i * dx[u.d];
v.y += i * dy[u.d];
v.step++;

if (v.x<=0 || v.y<=0 || v.x>=m || v.y>=n)
continue;//border!!!
if (gh[v.x][v.y] == 1)
break;//careful!!!(not continue)
if (v.x == endx && v.y == endy)
return v.step;

if (!vis[v.x][v.y][v.d]) q.push(v);
vis[v.x][v.y][v.d] = true;
}
}
return -1;
}

bool Read(){
int tmp;
string dirt;
scanf("%d%d", &m, &n);
if (n==0 && m==0) return false;
Init();
for (int i=0; i<m; i++){
for (int j=0; j<n; j++){
cin >> tmp;
if (tmp){
gh[i][j] = gh[i+1][j] = 1;
gh[i][j+1] = gh[i+1][j+1] = 1;
}
}
}
scanf("%d%d%d%d", &startx, &starty, &endx, &endy);
cin >> dirt;
if (dirt == "north"){
direction = 0;
}else if (dirt == "west"){
direction = 1;
}else if (dirt == "south"){
direction = 2;
}else if (dirt == "east"){
direction = 3;
}
return true;
}

void Init(){
memset(gh, 0, sizeof(gh));
memset(vis, 0, sizeof(vis));
}
};

int main(){
Problem ro;
ro.Solve();
}

Depth-First-Search即深度优先搜索

##0X00 算法介绍

深度优先搜索所遵循的搜索策略是尽可能“深”地搜索图。在深度优先搜索中,对于最新发现的顶点,如果它还有以此为起点而未探测到的边,就沿此边继续下去。当结点v的所有边都己被探寻过,搜索将回溯到发现结点v有那条边的始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被发现为止。

##0X01 算法过程

先选定图的类别(有向图、无向图),再选定图的存储结构,根据输入的顶点或者边建立图;

并把相应的邻接表或者邻接矩阵 测试输出

根据已有的邻接矩阵或邻接表用递归方法编写深度优先搜索遍历算法,并输出遍历结果;

假定给定图G的初态是所有顶点均未被访问过,在G中任选一个顶点i作为遍历的初始点,则DFS递归调用包含以下操作:

(1)访问搜索到的未被访问的邻接点;

(2)将此顶点的visited数组元素值置1;

(3)搜索该顶点的未被访问的邻接点,若该邻接点存在,则从此邻接点开始进行同样的访问和搜索。

深度优先搜索DFS可描述为:

(1)访问v0顶点;

(2)置 visited[v0]=1

(3)搜索v0未被访问的邻接点w,若存在邻接点w,则DFS(w)。

##0X02 算法应用

深度优先搜索适用范围:在树深度已知情况下,并且树体系相当庞大时,深度优先搜索往往会比广度优先搜索优秀。

###剪枝策略:

1)最优化剪枝

记录下当前得到的最优值,如果当前所在的节点已经没有可能得到比当前最优值更好的结果,提起回溯。在使用最优化剪枝的过程中,关键是在第一次得到结果或到达叶子节点后记录下得到值作为当前最优值。当后续遍历树的过程中,通过比较如果得到比当前最优值更好的值则更新当前最优值,如果在某个节点中通过比较发现当前所在节点在继续扩展已经无法得到最优解,则剪枝。

2)可行性剪枝

可行性剪枝一般需要根据问题的理解来进行,需要对问题有深刻的理解同时需要敏锐的观察和思考。对于某个具体问题,当运行到某个节点后根据问题信息可以判断是不可行则不需要继续扩展。比较典型的可行性剪枝是奇偶剪枝。

###通常可以解决的问题有:

八皇后问题,迷宫问题,寻找强连通分量个数,最大连通分量的结点数,拓扑排序,Flood fill算法,穷举

用例子说明穷举问题:列出 A ~ F 这六个字母所能列出的所有组合(必须6位,不允许重复)。当然你可以用6重循环给出答案,但是随着字母数量增多,深度优先会是你的不二选择。

##0X03 算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//DFS递归实现
//邻接矩阵存图
gh[][]
vis[][]
dx[], dy[] //可用于DFS时的代码优化
//初始化图
Initial(gh,vis)
//主程序
for u
if not vis[u] DFS(u)
//深度有限搜索
DFS(node u){
judge if u reachable, if not, return
vis[u] = true
...
node v
for v (linked by u)
if not vis[v] DFS(v)
//backtrack
return
}
//注意图的连通性,考虑非连通图的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//DFS非递归实现
//如果栈顶节点的所有直接邻接点都已访问过,则弹出栈顶节点
//否则,将该栈顶节点的未访问的其中一个邻接点压入栈,同时,标记该邻接点为已访问
for u
if not vis[u] DFS(u)

DFS(node u){
stack<node> s
s.push(u)

node v,k
while(!s.empty()){
v = s.top()
vis[v] = true
...
flag = true
for k(linked by v) in nodes{
if(!vis[k]){
s.push(k)
flag = false
break
}
}
if (flag) s.pop()
}
}

loading any more

##binary-search 二分查找

###模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//递归实现
int binary_search(int A[], int key, int imin, int imax){
// test if array is empty
if (imax < imin)
// set is empty, so return value showing not found
return KEY_NOT_FOUND;
else{
// calculate midpoint to cut set in half
int imid = midpoint(imin, imax);
// eg: imid = (imin+imax)>>1

// three-way comparison
if (A[imid] > key)
// key is in lower subset
return binary_search(A, key, imin, imid-1);
else if (A[imid] < key)
// key is in upper subset
return binary_search(A, key, imid+1, imax);
else
// key has been found
return imid;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//非递归实现
int binary_search(int A[], int key, int imin, int imax){
// continue searching while [imin,imax] is not empty
while (imax >= imin){
// calculate the midpoint for roughly equal partition
int imid = midpoint(imin, imax);
// eg: imid = (imin+imax)>>1

if(A[imid] == key)
// key found at index imid
return imid;
// determine which subarray to search
else if (A[imid] < key)
// change min index to search upper subarray
imin = imid + 1;
else
// change max index to search lower subarray
imax = imid - 1;
}
// key was not found
return KEY_NOT_FOUND;
}

##STL函数

1
2
3
4
5
6
7
8
9
10
template <class ForwardIterator, class T, class Compare>
bool binary_search (ForwardIterator first, ForwardIterator last,
const T& val, Compare comp);
```
Returns true if any element in the range [first,last) is equivalent to val, and false otherwise.
The elements are compared using operator< for the first version, and comp for the second. Two elements, a and b are considered equivalent if (!(a<b) && !(b<a)) or if (!comp(a,b) && !comp(b,a)).

The elements in the range shall already be sorted according to this same criterion (operator< or comp), or at least partitioned with respect to val.

The function optimizes the number of comparisons performed by comparing non-consecutive elements of the sorted range, which is specially efficient for random-access iterators.

template
bool binary_search (ForwardIterator first, ForwardIterator last, const T& val)
{
first = std::lower_bound(first,last,val);
return (first!=last && !(val<*first));
}
```

from http://en.wikipedia.org/wiki/Binary_search_algorithm

关于排序问题,通常题目会设定一种“排序”的方法,我们直接通过模拟就可以完成。

直接的排序算法有很多,老师主要教会我们stl中的排序函数sort().

另外,stl中还有关于全排序的函数next_permutation(),以及涉及字符串翻转的函数reverse().

##0X00 sort()
如何使用sort()对数据进行排序

#include <algorithm>

void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);

Sorts the elements in the range [first,last) into ascending order. The elements are compared using operator< for the first version, and comp for the second.

Examples:

1
2
3
vector<int> myvector
sort (myvector, myvector+3);
sort (myvector.begin(), myvector.end(), myfunction);

但是如果你时自己定义的类型或者你需要按照其他方式排序,有两种方法来达到效果:一种是自己写比较函数。另一种是重载类型的’<’操作符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class myclass {
public:
int first, second;
bool operator < (const myclass &m)const {
return first < m.first;
}//ascending order by first
};
vector< myclass > vect;
sort(vect.begin(), vect.end());

----------------------------------------------------

bool cmp(const myclass & m1, const myclass & m2) {
return m1.second < m2.second;
}//ascending order by second
sort(vect.begin(), vect.end(), cmp);

参考:[http://www.cppblog.com/mzty/archive/2005/12/15/1770.html]

##0X01 reverse()
reverse() can reverse the vector/string

void reverse (BidirectionalIterator first, BidirectionalIterator last);

Reverses the order of the elements in the range [first,last).

And we need to include <algorithm>

Example:

1
2
3
std::vector<int> myvector;
for (int i=1; i<10; ++i) myvector.push_back(i); // 1 2 3 4 5 6 7 8 9
std::reverse(myvector.begin(),myvector.end()); // 9 8 7 6 5 4 3 2 1

##0X02 next_permutation()
next_permutation()prev_permutation()
分别是求一个序列的下一个排列和前一个排列。

bool next_permutation (BidirectionalIterator first, BidirectionalIterator last);
The range used is [first,last)
we need include <algorithm>
如果存在下一个序列,则返回True,同时将序列变为其下一个排列。

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int myints[] = {1,2,3};
do {
std::cout << myints[0] << ' ' << myints[1] << ' ' << myints[2] << '\n';
} while (next_permutation(myints,myints+3) );
cout << "After loop: " << myints[0] << ' ' << myints[1] << ' ' << myints[2] << '\n';

Output:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
After loop: 1 2 3