图的搜索
一、概念
1. 数据结构
一般情况下,我们会将数据结构分为逻辑结构和物理结构,其中逻辑结构是我们的逻辑下存储的结构,而物理结构是计算机的逻辑下存储的结构。数据结构的分类可以参见下表:
2. 图
图(graph)是一种点和点之间多对多关系所组成的数据结构。图也是由顶点的非空集合 V V V 和边的集合 E E E 组成的,表示为 G = ( V , E ) G=(V,E) G=(V,E)。一般地,我们用 V ( G ) V(G) V(G) 表示图 G G G 的顶点集,用 E ( G ) E(G) E(G) 表示图 G G G 的边集。
3. 图的术语
图中的点称为 顶点。
图中的点与点所具有的练习称为 边。
点和点的关系称为 邻接。
点与边的关系称为 关联。
有些图点和点之间的关系是相互的,这种图被称为 无向图,无向图的边一般用 ( x , y ) (x, y) (x,y) 来表示,也可以写作 ( y , x ) (y, x) (y,x)。
有些图点和点之间的关系是单向的,这种图被称为 有向图,无向图中由顶点出发的边称为 出边,指向顶点的边称为 入边。有向图的边一般用 < x , y > <x, y> <x,y> 来表示,不可以写作 < y , x > <y, x> <y,x>,因为方向不一样。
图中的边带有某种与之相关的数值,我们称之为 权值,边带有权值的图通常称为 网(加权图)。
如果图是有向的,称为 加权有向图。
如果图是无向的,称为 加权无向图。
没有自环和重边的图就属于 简单图。
对于有很少条边的图(
e
<
n
log
2
n
e<n \log_2n
e<nlog2n)称为 稀疏图,反之称为 稠密图。
一个图从任意一个顶点可以到另外任意一个顶点(不一定直接联通),则称为 连通图。
一个图从任意两个顶点可以互相到达(不一定直接联通),则称为 强连通图。
从图中提取出的图(可以是空图、一个顶点、图本身)称作 子图。
没有重复顶点的一条路径称为 简单路径。
起点和终点相同的路径称为 回路(环)。
4. 图的公式
如果在无向图中,任意两个顶点都有一条边直接相连,这时就称该图为 无向完全图。具有 n n n 个顶点的无向完全图具有 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1) 条边。
如果在有向图中,任意两个顶点都有两条边(双向)直接相连,这时就称该图为 有向完全图。具有 n n n 个顶点的无向完全图具有 n ( n − 1 ) n(n-1) n(n−1) 条边。
无向图中,顶点
v
v
v 的 度 指的是与该顶点相关联的边的数目。
有向图中,顶点
v
v
v 的 度 分别是 入度 和 出度。
一个具有
n
n
n 个顶点,
e
e
e 条边的图,其所有顶点的度数之和等于边数的
2
2
2 倍。
一个简单无向图中有
x
x
x 条边,每个顶点的度数都是
y
y
y,则这个图有
2
x
y
\frac{2x}{y}
y2x 个顶点。
二、图的操作
1. 图的存储
1.1 加权
如果有加权,我们可以使用二维动态(结构体)数组来存储,类似链表的形式来进行作答:
#include <iostream>
#include <vector>
using namespace std;
struct Node
{
int v, w; // v: 点 w: 权值
};
int n, m;
int a, b, c;
vector <Node> adj[105];
int main()
{
cin >> n >> m;
while (m--)
{
cin >> a >> b >> c;
adj[a].push_back({b, c});
}
for (int i = 1; i <= n; i++)
{
cout << i << ":";
for (int j = 0; j < adj[i].size(); j++)
cout << adj[i][j].v << " ";
cout << endl;
}
return 0;
}
1.2 非加权
否则(没有加权)就可以这么写:
#include <iostream>
using namespace std;
int n, m;
int a, b;
int adj[105][105];
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
cin >> a >> b;
adj[a][b] = 1;
adj[b][a] = 1;
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
cout << adj[i][j] << " \n"[j==n];
return 0;
}
2. 图的搜索
图的搜索就是非常典型的旅行商问题(Travelling Salesman Problem,TSP)。问题描述为:给定一个城市地图和一个旅行商,要求旅行商从某个城市出发,遍历所有城市恰好一次,最后返回起点城市,使得旅行路径的总长度最短。
2.1 DFS
DFS 的执行逻辑就是:不撞南墙不回头。深度优先遍历的遍历写法是不唯一的。
void dfs(int u) // u: 起点
{
for (int v : adj[u]) // 遍历可以到达的下一个点
{
if (vis[v] == 0) // 未被访问
{
vis[v] = 1; // 标记
dfs(v); // 递归
vis[v] = 0; // 回溯
}
}
}
2.2 BFS
BFS 的执行逻辑就是:层层递进。
void bfs(int u)
{
cout << u << " ";
q.push(u);
vis[u] = 1;
while (!q.empty())
{
int f = q.front();
q.pop();
for (int v : adj[f])
{
if (vis[v] == 0)
{
vis[v] = 1;
cout << v << " ";
q.push(v);
}
}
}
}
2.3 例题
下面简单图,说法正确的是(_____)。
B
B
B 的错误:
F
F
F 不能到
I
I
I。
C
C
C 的错误:
D
D
D 后面要写
I
I
I。
D
D
D 的错误:
A
A
A 不能到
D
D
D。
故选 A A A。
三、例题
1. 查找文献
题目描述
小 K K K 喜欢翻看洛谷博客获取知识。每篇文章可能会有若干个(也有可能没有)参考文献的链接指向别的博客文章。小 K K K 求知欲旺盛,如果他看了某篇文章,那么他一定会去看这篇文章的参考文献(如果他之前已经看过这篇参考文献的话就不用再看它了)。假设洛谷博客里面一共有 n n n( n ≤ 105 n≤105 n≤105)篇文章(编号为 1 1 1 到 n n n)以及 m m m( m ≤ 106 m≤106 m≤106)条参考文献引用关系。目前小 K K K 已经打开了编号为 1 1 1 的一篇文章,请帮助小 K K K 设计一种方法,使小 K K K 可以不重复、不遗漏的看完所有他能看到的文章。
这边是已经整理好的参考文献关系图,其中,文献 X → Y X → Y X→Y 表示文章 X X X 有参考文献 Y Y Y。不保证编号为 1 1 1 的文章没有被其他文章引用。
请对这个图分别进行DFS
和BFS
,并输出遍历结果。如果有很多篇文章可以参阅,请先看编号较小的那篇(因此你必须要先排序)。
输入格式
第一行两个正整数,表示节点数和边数。
接下来 m m m 行,每行两个整数 u , v u,v u,v,表示点 u u u 到点 v v v 之间有道路相连。
输出格式
输出两行,每行 n n n 个空格分隔的数,表示图的
DFS
和BFS
顺序。
样例1
输入
8 9 1 2 1 3 1 4 2 5 2 6 3 7 4 7 4 8 7 8
输出
1 2 5 6 3 7 8 4 1 2 3 4 5 6 7 8
#include <queue>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
int n, m;
int u, v;
queue <int> q;
bool vis[100005];
vector <int> adj[100005];
void dfs(int u)
{
for (int v : adj[u])
{
if (vis[v] == 0)
{
vis[v] = 1;
cout << v << " ";
dfs(v);
}
}
}
void bfs(int u)
{
cout << u << " ";
q.push(u);
vis[u] = 1;
while (!q.empty())
{
int f = q.front();
q.pop();
for (int v : adj[f])
{
if (vis[v] == 0)
{
vis[v] = 1;
cout << v << " ";
q.push(v);
}
}
}
}
int main()
{
cin >> n >> m;
while (m--)
{
cin >> u >> v;
adj[u].push_back(v);
}
for (int i = 1; i <= n; i++)
sort(adj[i].begin(), adj[i].end());
vis[1] = 1;
cout << "1 ";
dfs(1);
cout << endl;
memset(vis, 0, sizeof(vis));
bfs(1);
return 0;
}
2. SPFA 算法
这是一个典型的最短路径问题(BFS:呵呵),因为又融合了图,所以就是一个标准的旅行商问题了(BFS:呵呵呵呵)。所以,你要宜用 BFS 忌用 DFS。
恐怖片开始。胆小者误入。
题目描述
妈妈下班回家,街坊邻居说小明被一群陌生人强行押上了警车!妈妈丰富的经验告诉她小明被带到了 t t t 区,而自己在 s s s 区。该市有 m m m 条大道连接 n n n 个区,一条大道将两个区相连接,每个大道有一个拥挤度。小明的妈妈虽然很着急,但是不愿意拥挤的人潮冲乱了她优雅的步伐。所以请你帮她规划一条从 s s s 至 t t t 的路线,使得经过道路的拥挤度最大值最小。
输入格式
第一行有四个用空格隔开的 n , m , s , t n,m,s,t n,m,s,t。接下来 m m m 行,每行三个整数 u , v , w u,v,w u,v,w,表示有一条大道连接区 u u u 和区 v v v,且拥挤度为 w w w。注意!!两个区之间可能存在多条大道。
输出格式
输出一行一个整数,代表最大的拥挤度。
样例1
输入
3 3 1 3 1 2 2 2 3 1 1 3 3
输出
2
提示
1 ≤ s ≤ t ≤ n ≤ 1 0 4 1\le s\le t \le n\le10^4 1≤s≤t≤n≤104, 1 ≤ m ≤ 2 × 1 0 4 1\le m\le 2\times10^4 1≤m≤2×104, w ≤ 1 0 4 w\le10^4 w≤104。
#include <queue>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
struct Node
{
int v, w;
};
int n, m;
int s, t;
int u, v, w;
bool vis[10005];
vector <Node> adj[10005];
bool check(int mid)
{
queue <int> q;
memset(vis, 0, sizeof(vis));
q.push(s);
vis[s] = 1;
while (!q.empty())
{
int f = q.front();
q.pop();
if (f == t) return true;
for (auto node : adj[f])
{
int fv = node.v;
int fw = node.w;
if (!vis[fv] && fw<=mid)
{
vis[fv] = 1;
q.push(fv);
}
}
}
return false;
}
int main()
{
cin >> n >> m >> s >> t;
for (int i = 1; i <= m; i++)
{
cin >> u >> v >> w;
adj[u].push_back({v, w});
adj[v].push_back({u, w});
}
int ans;
int l = 0, r = 10000;
while (l <= r)
{
int mid = (l+r)/2;
memset(vis, 0, sizeof(vis));
if (check(mid))
{
ans = mid;
r = mid-1;
}
else
{
l = mid+1;
}
}
cout << ans;
return 0;
}
3. [USACO19OPEN] Milk Factory B
题目描述
牛奶生意正红红火火!农夫 John 的牛奶加工厂内有 N N N 个加工站,编号为 1 , 2 , ⋯ , N − 1 , N 1,2,\cdots,N-1,N 1,2,⋯,N−1,N( 1 ≤ N ≤ 100 1≤N≤100 1≤N≤100),以及 N − 1 N-1 N−1 条通道,每条连接某两个加工站。(通道建设很昂贵,所以农夫 John 选择使用了最小数量的通道,使得从每个加工站出发都可以到达所有其他加工站)。
为了创新和提升效率,农夫 John 在每条通道上安装了了传送带。不幸的是,当他意识到传送带是单向的已经太晚了,现在每条通道只能沿着一个方向通行了!所以现在的情况不再是从每个加工站出发都能够到达其他加工站了。
然而,农夫 John 认为事情可能还不算完全失败,只要至少还存在一个加工站 i i i 满足从其他每个加工站出发都可以到达加工站 i i i。注意从其他任意一个加工站 j j j 前往加工站 i i i 可能会经过 i i i 和 j j j 之间的一些中间站点。请帮助农夫 John 求出是否存在这样的加工站 i i i。
输入描述
输入的第一行包含一个整数 N N N,为加工站的数量。以下 N − 1 N-1 N−1 行每行包含两个空格分隔的整数 a i a_i ai 和 b i b_i bi,满足 1 ≤ a i 1≤ai 1≤ai, b i ≤ N bi≤N bi≤N 以及 a i ≠ b i ai≠bi ai=bi。这表示有一条从加工站 a i a_i ai 向加工站 b i b_i bi 移动的传送带,仅允许沿从 a i a_i ai 到 b i b_i bi 的方向移动。
输出描述
如果存在加工站i满足可以从任意其他加工站出发都可以到达加工站 i i i,输出最小的满足条件的 i i i。否则,输出
-1
。
样例1
输入
3 1 2 3 2
输出
2
这里有一个非常离谱的思路:把要求的点当成起点而不是终点。这样 while n--
的时候要 adj[b].push_back a;
了。
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
int cnt;
int n, m;
int a, b;
bool vis[105];
vector <int> adj[105];
void dfs(int u)
{
for (int v : adj[u])
{
if (vis[v] == 0)
{
vis[v] = 1;
cnt++;
dfs(v);
}
}
}
int main()
{
cin >> n;
for (int i = 1; i < n; i++)
{
cin >> a >> b;
adj[b].push_back(a);
}
for (int i = 1; i <= n; i++)
{
memset(vis, 0, sizeof(vis));
cnt = 0;
vis[i] = 1;
dfs(i);
if (cnt == n-1)
{
cout << i;
return 0;
}
}
cout << -1;
return 0;
}