图的存储
以下总结了一些算法竞赛中常见的有关图的存储方式
- 邻接矩阵
非常容易理解,但是一般不会使用,因为邻接矩阵非常耗费内存。
- 邻接表
最常见的一种图的存储方式。
typedef struct ArcNode { //表示一条弧
int adjvex;
struct ArcNode *nextarc;
InfoType *info;
} ArcNode;
typedef struct VNode { //表示每一个节点,可以理解为头节点
VertexType data;
ArcNode *firstarc;
} VNode, AdjList[manm];
- 链式前向星
本质为邻接表。在写算法题目时,书写邻接表结构还是稍显麻烦,所以人们发明了链式前向星来保存图。(详细可以参考我的另外一篇博客)
struct E {
int to, w, next;
} Edge[Maxm];
int tot = 1; head[Maxm];
- 节点和边的信息分开存储的方式
用一个Edge结构体vector存储图的所有边信息(从u到v);然后用一个二维矩阵存储以该节点出发的所有边的序号。所以当我们当前在节点 x 时,我们可以通过 s[x] 知道从 x 出发的所有边的序号。用该序号就可以到所有边信息的 e 数组中找到该边所指向的节点 v ,从而可以方便的进行遍历!
代码如下:
// P5318 重写版
const int maxm = 1000005;
// 一种新的保存图的方式,适用于所有的情况
struct edge {
int u, v;
};
vector<edge> e;
vector<int> s[maxm]; // 用于保存节点的边信息
bool cmp(const edge le, const edge re) {
// 按照每条边的终点排序,重点相同再按照出发点排序
if(le.v == re.v) return le.u < re.u;
else return le.v < re.v;
}
int N, M;
int vis[maxm];
void dfs(int i) {
if(vis[i]) return;
cout << i << " "; vis[i] = 1;
for(int j = 0; j < s[i].size(); j++) {
if(vis[e[s[i][j]].v] == 0) dfs(e[s[i][j]].v);
}
}
图的遍历
图的遍历分为 BFS 和 DFS 两种方式。
BFS(广度优先搜索)
引用队列以辅助实现图的广度优先遍历。
queue<int> q;
void BFS() {
for(int i = 1; i <= n; i++) {
if(!visited[i]) {
visited[i] = 1; ans[k++] = i; q.push(i);
while(!q.empty()) {
q.pop();
for(int j = head[i]; j; j = Edge[j].next) {
if(!visited[Edge[j].to]) {
visited[Edge[j].to] = 1; ans[k++] = Edge[j].to; q.push(Edge[j].to);
}
}
}
}
}
}
POJ1753
在BFS中,visit数组的作用是标记某个节点是否被访问过,但是有时候可以通过设置结点间的访问次序,进而省去visit数组,达到同样的将所有节点都遍历完全同时不重复的效果。
#include <iostream>
using namespace std;
bool chess[6][6]; // 棋盘,默认全部为false-白面.
int counts = 99; // 理论上,走的最大的步数为16.只要成功,必然小于17.
//判断是否全部为白面或黑面
bool judge(){
bool flag = false;
for(int i = 1; i < 5; i++){
for(int j = 1; j < 5; j++){
if(!chess[i][j]){
flag = true;
}
}
}
if(!flag)
return true;
for(int i = 1; i < 5; i++){
for(int j = 1; j < 5; j++){
if(chess[i][j]){
flag = false;
}
}
}
if(flag)
return true;
return false;
}
// 按照题目要求变化
void change(int i,int j){
chess[i-1][j] = !chess[i-1][j];
chess[i][j] = !chess[i][j];
chess[i+1][j] = !chess[i+1][j];
chess[i][j-1] = !chess[i][j-1];
chess[i][j+1] = !chess[i][j+1];
}
void bfs(int m,int n,int num){
change(m,n);
if(judge()){
if(counts>num)
counts = num;
}else{
++num;
for(int j = n+1; j < 5; j++)
bfs(m,j,num);
for(int i = m+1; i < 5; i++)
for(int j = 1; j < 5; j++)
bfs(i,j,num);
}
change(m,n);
}
int main()
{
char ch;
//这里依次对各个节点进行搜索,进而可以省略掉设置visit数组
for(int i = 1; i < 5; i++){
for(int j = 1; j < 5; j++){
cin >> ch;
if(ch == 'b')
chess[i][j] = true;
}
}
if(judge())
cout << 0 << endl;
else{
for(int i = 1; i < 5; i++){
for(int j = 1; j < 5; j++){
bfs(i,j,1);
}
}
if(counts<17)
cout << counts << endl;
else
cout << "Impossible" << endl;
}
return 0;
}
DFS(深度优先搜索)
深度优先搜索算法采用递归的方式进行。
void DFS(int i) { // 从i节点开始进行深度优先搜索
visited[i] = 1; ans[k++] = i; // 保存答案
for(int j = head[i]; j; j = Edge[j].next) { // 遍历所有以 j 为起始点的临界边
// 这里容易出错,容易写成 visited[j] :注意区分 Edge[j].to 与 j 的区别
// 前者表示的是一条边指向的终点,而后者只是表示这条边在内存中的位置坐标!!!
if(!visited[Edge[j].to]) DFS(Edge[j].to);
}
}
最短路问题
针对不同种类图的最短路问题进行总结
Dijkstra算法
-
常用于求解单源最短路径,无法处理存在负权边的情况。(因为该算法利用了贪心思想,在正确性证明中不允许出现负权边)
-
算法流程
为每个节点构建一个dis和vis数组,前者保存从源点到每个点的距离,后者记录某个点是否被访问过。
该算法不断从未被访问过的节点中寻找距离最小的点 u ,然后将其标记为访问过。针对 u ,如果对于某个未被访问过的点 v ,通过 u 到达 v 可以缩短 dis[v],那么我们更新 dis[v],然后继续找最短的点。
- 代码
void dijkstra(){
for (int i = 1; i <= n; dist[i++] = inf);
dist[s] = 0; // s为源点, t为目标点。
while(!visited[t]){
int node, lowest = inf; //inf 为预设最大值
for (int j = 1; j <= n; ++j){
if (!visited[j] && dist[j] < lowest){
lowest = dist[j];
node = j;
}
} //找到当前距离最小的节点
visited[node] = 1;
for (int e = head[node]; e != 0; e = edges[e].next){
int v = edges[e].to, w = edges[e].weight;
if (!visited[v] && dist[node] + w < dist[v]){
dist[v] = dist[node] + w;
pre[v] = e; //修改当前距离
}
}
}
}
中间每次寻找最短距离的节点的操作可以用自定义结构体的优先队列进行处理。
具体定义方式可以参考博客,这里只记录两种常用方式
- method 1
struct node {
int x, y;
bool operator<(node a) const {return y < a.y; }
bool operator>(node a) const {return y > a.y; }
};
priority_queue<node> A; // 默认情况下为大顶堆
priority_queue<node, vector<node>, greater<node>> B; // 小顶堆
- method 2
struct node {
int x, y;
};
struct cmp {
bool operator() (const node& a, const node& b) { return a.y < b.y; }
};
priority_queue<node, vector<node>, cmp> A;
return就是希望如何排列为true。如果希望由大到小,就将大到小的情况return;反则亦然。和sort的自定义cmp是一样的。
Floyd算法
-
求解多源最短路时通常使用Floyd算法,可以处理负权边,但是无法处理负权环!
-
算法流程
算法维护一个 dis[v][v] 数组,保存任意两个点之间的距离。然后通过一个三重循环,我们遍历每个点 v ,用该点对图中任意两个节点进行松弛操作。松弛完成之后即可得到任意两个点之间的最小距离。
Floyd算法利用了动态规划的思想
- 代码
int dist[100][100];
//初始化dist数组
for (int k = 0; k < v; ++k){ // 我们利用 k 节点进行松弛
for (int i = 0; i < v; ++i){
for (int j = 0; j < v; ++j){
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
图的其它知识点以及技巧
洛谷P1551亲戚问题——并查集
1)知识点:数据结构中的并查集问题(《啊哈!算法》中出现过)
并查集:分为init()、find()、merge()。三个主要函数
主要思想:用一个集合中的主要元素(帮主)来代表整个集合中的所有元素。其实就是在维护一个森林。
int fa[n];//表示每个元素所在帮派的帮主是谁
inline void init(){
for(int i = 0; i < n; i++) fa[i] = i;//开始是自己为帮主
}
int find(int x){
if(fa[x] == x) return x;
else{
return fa[x] = find(fa[x]);//在后面递归的时候顺便修改fa[x]的值为修改后的祖先(压缩路径算法)
}
}
void merge(int x, int y){
fa[x] = find(fa[y]);//此处为错误示例
fa[find(x)] = find(y);//先找到代表元素,在进行赋值操作!
}
2)并查集的应用:最小生成树的克鲁斯卡尔算法中对树的描述等。凡是涉及到数据的分组管理的问题都可以用考虑使用并查集进行维护。
- kruskal算法原理:
现在我们假设一个图有m个节点,n条边。首先,我们需要把m个节点看成m个独立的生成树,并且把n条边按照从小到大的数据进行排列。在n条边中,我们依次取出其中的每一条边,如果发现边的两个节点分别位于两棵树上(利用并查集处理),那么把两棵树合并成为一棵树;如果树的两个节点位于同一棵树上,那么忽略这条边,继续遍历。等到所有的边都遍历结束之后,如果所有的生成树可以合并成一条生成树,那么它就是我们需要寻找的最小生成树,反之则没有最小生成树。
//并查集实现kruskal算法
//第i条边的两个端点序号和权值分别保存在u[i],v[i]和w[i]中。
int cmp(const int i, const int j) {return w[i] < w[j];}
int find(int x) {return p[x] == x? x: p[x] = find(p[x]);}
int Kruskal(){
int ans = 0;
for(int i = 0; i < n; i++) p[i] = i;
for(int i = 0; i < n; i++) r[i] = i;
sort(r, r+m, cmp);//间接排序操作(学会利用)
for(int i = 0; i < m; i++){
int e = r[i]; int x = find(u[e]); int y = find(v[e]);
if(x != y) { ans += w[e]; p[x] = y;}
}
return ans;
}
洛谷UVA540团体队列
#include<iostream>
#include<queue>
#include<list>
#include<map>
#include<string>
using namespace std;
int main(){
int t, i = 0; cin >> t;
do{
cout << "Scenario #" << i+1 << endl;i++;
list<queue<int>> team;
//此处是利用并查集处理队伍的代码部分
map<int, int> f;
for(int i = 0; i < t; i++){
int num; cin >> num;
int x; cin >> x; f[x] = x;
for(int j = 1; j < num; j++){
int y; cin >> y;
f[y] = x;
}
}
//一个回合
string sign; cin >> sign;
while(sign != "STOP"){
if(sign == "ENQUEUE"){
int pep; cin >> pep;
list<queue<int>>::iterator it;
for(it = team.begin(); it != team.end(); it++){
if(f[pep] == f[(*it).front()]){//使用并查集的方式处理
(*it).push(pep);
break;
}
}
if(it == team.end()){
queue<int> temp; temp.push(pep);
team.push_back(temp);//在最后加入一组数据
}
}
else{//出队伍
cout << (*team.begin()).front() << endl;
(*team.begin()).pop();
if(!(*team.begin()).size()) {team.pop_front();}
}
cin >> sign;
}
cout << endl;
cin >> t;
}while(t);
}
洛谷P1983车站分级——拓扑排序
拓扑排序的前提是有向无环图。排完序后得到的为关于这个图的线性序列。常常用来表示元素之间的依赖关系。
例如一些活动的进行需要其他别的活动的完成作为前提,比如A活动的开展需要B和C活动完成。就可以将B和C指向A,表示这种先后关系。
- 如何进行拓扑排序
一般都是采用队列来辅助进行排序。(栈也可以)
首先将图中入度为零的节点加入队列。然后依次从队列中取出节点x,将x和以x为起点的边在图中全部删除(同时减少该边终点节点的入度)。当队列为空时,再次将图中其它入度为零的节点加入队列。
直到图中所有的节点全部删除后,即完成了拓扑排序!
AC代码:
#include<iostream>
#include<cstring>
using namespace std;
int n, m;
const int N = 1005;
int Arr[N][N], st[N], is[N], de[N];
int s[N], out[N]; // 记录入度,以及该点是否被删除
int ans = 0, top = 0;
int main() {
cin >> n >> m;
for(int i = 1; i <= m; i++) {
memset(is,0,sizeof(is));
int ti; cin >> ti;
for(int j = 1; j <= ti; j++) {
cin >> st[j]; is[st[j]] = 1;//记录是否为需要停靠的站
}
for(int j = st[1]; j <= st[ti]; j++) {
if(!is[j]) {
for(int k = 1; k <= ti; k++) { //枚举要停靠的站,然后进行加边
if(!Arr[j][st[k]]) {Arr[j][st[k]] = 1; de[st[k]]++;}
}
}
}
}
do{
top = 0;
//先将出度为零的节点加入栈中
for(int i = 1; i <= n; i++) {
if(de[i]==0 && !out[i]) {
s[++top] = i; out[i] = 1;
}
}
for(int i = 1; i <= top; i++) {
for(int j = 1; j <= n; j++) {
if(Arr[s[i]][j]) Arr[s[i]][j] = 0, de[j]--;
}
}
ans++;
} while(top);
cout << ans-1;
return 0;
}