图的入门
图是点与边的集合。点为节点,图于是与线性表与树有着密切的联系。边为点与点之间的关系,比如:两点相邻,我们可以在这两点间连一条边;当然,两点之间的边也可以代表别的关系。
所以,图G=(V,E),V是一个非空有限集合,代表点的集合,E代表边的集合。在计算机科学中,图是最灵活的数据结构之一 ,很多问题都可以用图模型来建模求解。当然,在这里,我们只介绍图的一些基础知识,帮助大家入门。
在接下来的讨论中我们要用到一些搜索和线性表的基础知识,有不懂的伙伴可以看看下面两篇博客:
搜索
线性表
我们正式开始:
(1)概念
有很多与图相关的概念,这里我们拣其中较重要的说说。
无向图:图的边没有方向,或者都是双向的。
有向图:图的边有方向,表示时可以用箭头表示从起点到终点。
我们用“< >”描述有向边,用“( )”描述无向边。
度:某节点所连的边数。
入度:在有向图中,以这个节点为终点的有向边的数目。
出度:在有向图中,以这个节点为起点的有向边的数目。
在有向图中,入读和=出度和。
简单图:没有自环和重边的图。
路径:一条路径指的是一个节点序列,该节点序列中的相邻节点在图上是邻接的。
简单路径:节点和边在一条路径中不重复出现。
连通图:无向图中,任意两点之间都有路径。反之称为非连通图。
连通分量:非连通图有多个连通分量,每个连通分量是一个极大连通子图。而连通图只有一个连通分量,即本身。
强连通图:在一个有向图中,若对于任意两点节点U和V,都存在一条从U到V的有向路径,同时也存在一条从V到U的有向路径,则称该有向图为强连通图。
强连通分量:该图极大的强连通子图。
(2)图的储存
1)邻接矩阵
存储节点间的相邻关系。由名字可知,由二维数组(矩阵)实现。a[i][j]存储从点i到点j的有向边(也可存无向边)。占用的存储单元数只与节点数有关,而与边数无关。此方法容易造成大量浪费,因为很可能大量点之间没边。
2)邻接表(vector实现)
vector<int>edge[MAXN];//存放邻接表
vector<int>val[MAXN];//存放权值
增加边操作:
void add_edge(int x,int y,int w)//增加一条边<x,y>,加入以x为首的邻接表中
{
edge[x].push_back(y);
val[x].push_back(w);
}
3)邻接表(链式实现/链式前向星)
void add_edge(int x,int y,int z)
{
d[++tot]=y,v[tot]=z;
next[tot]=head[x],head[x]=tot;
}
其中,数组d存放边的终点,数组v存放边的权值,数组next代表以x为起点的边(也即终点)集合的下一条边(也即下一条边的终点),数组head代表以x为首的链。
链式实现邻接表可以动态地增加边,这条链是不断向前增加推进的。
遍历以x为起点的所有边,我们可以用如下代码实现:
for(int i=head[x];i;i=next[i])
(3)图的遍历
要用到DFS和BFS了。
为什么要用visit[i]数组?避免重复和死循环。
接下来的代码中还涉及了连通块。
DFS:
void dfs(int x)
{
visit[x]=cnt;//标号
for(int i=head[x];i;i=next[i] )
{
int t=v[i];
if(visit[t]) continue;
dfs(t);
}
}
int main()
{
...
cnt=0;
for(int i=1;i<=n;i++)//n个点
{
if(!visit[i])
{
cnt++;
dfs(i);
}
}
}
BFS:
void bfs(int x)
{
queue<int>q;
q.push(x);
visit[x]=cnt;
while(!q.empty())
{
int t=q.front();
q.pop();
for(int i=head[t];i;i=next[i])
{
int y=v[i];
if(!visit[y])
{
visit[y]=cnt;
q.push(y);
}
}
}
}
int main()
{
...
cnt=0;
for(int i=1;i<=n;i++)
{
if(!visit[i])
{
cnt++;
bfs(i);
}
}
}
下面看几道题。
[蓝桥杯 2013 国 C] 危险系数
题目背景
抗日战争时期,冀中平原的地道战曾发挥重要作用。
题目描述
地道的多个站点间有通道连接,形成了庞大的网络。但也有隐患,当敌人发现了某个站点后,其它站点间可能因此会失去联系。
我们来定义一个危险系数 D F ( x , y ) DF(x,y) DF(x,y):
对于两个站点 x x x 和 y ( x ≠ y ) , y(x\neq y), y(x=y), 如果能找到一个站点 z z z,当 z z z 被敌人破坏后, x x x 和 y y y 不连通,那么我们称 z z z 为关于 x , y x,y x,y 的关键点。相应的,对于任意一对站点 x x x 和 y y y,危险系数 D F ( x , y ) DF(x,y) DF(x,y) 就表示为这两点之间的关键点个数。
本题的任务是:已知网络结构,求两站点之间的危险系数。
输入格式
输入数据第一行包含 2 2 2 个整数 n ( 2 ≤ n ≤ 1000 ) n(2 \le n \le 1000) n(2≤n≤1000), m ( 0 ≤ m ≤ 2000 ) m(0 \le m \le 2000) m(0≤m≤2000),分别代表站点数,通道数。
接下来 m m m 行,每行两个整数 u , v ( 1 ≤ u , v ≤ n , u ≠ v ) u,v(1 \le u,v \le n,u\neq v) u,v(1≤u,v≤n,u=v) 代表一条通道。
最后 1 1 1 行,两个数 u , v u,v u,v,代表询问两点之间的危险系数 D F ( u , v ) DF(u,v) DF(u,v)。
输出格式
一个整数,如果询问的两点不连通则输出 − 1 -1 −1。
样例 #1
样例输入 #1
7 6
1 3
2 3
3 4
3 5
4 5
5 6
1 6
样例输出 #1
2
提示
时限 1 秒, 64M。蓝桥杯 2013 年第四届国赛
代码
怎样确定是关键点?可以选择DFS并在搜索时对经过的点计数,搜索完后看哪个点的被计次数与路径数相同。
#include <bits/stdc++.h>
using namespace std;
int n,m,u,v;
vector<int>G[1005];
int visit[1005];
int Count[1005];
int tot=0;
void dfs(int now)
{
if(now==v)
{
tot++;
for(int i=1;i<=n;i++)
{
if(visit[i])
Count[i]++;
}
}
else
{
for(int i=0;i<G[now].size();i++)
{
int to=G[now][i];
if(!visit[to])
{
visit[to]=1;
dfs(to);
visit[to]=0;
}
}
}
}
int main()
{
cin>>n>>m;
while(m--)
{
int x,y;
cin>>x>>y;
G[x].push_back(y);
G[y].push_back(x);
}
cin>>u>>v;
visit[u]=1;
dfs(u);
int ans=0;
for(int i=1;i<=n;i++)
{
if(Count[i]==tot)
ans++;
}
cout<<ans-2;
return 0;
}
图的遍历
题目描述
给出 N N N 个点, M M M 条边的有向图,对于每个点 v v v,求 A ( v ) A(v) A(v) 表示从点 v v v 出发,能到达的编号最大的点。
输入格式
第 1 1 1 行 2 2 2 个整数 N , M N,M N,M,表示点数和边数。
接下来 M M M 行,每行 2 2 2 个整数 U i , V i U_i,V_i Ui,Vi,表示边 ( U i , V i ) (U_i,V_i) (Ui,Vi)。点用 1 , 2 , … , N 1,2,\dots,N 1,2,…,N 编号。
输出格式
一行 N N N 个整数 A ( 1 ) , A ( 2 ) , … , A ( N ) A(1),A(2),\dots,A(N) A(1),A(2),…,A(N)。
样例 #1
样例输入 #1
4 3
1 2
2 4
4 3
样例输出 #1
4 4 3 4
提示
- 对于 60 % 60\% 60% 的数据, 1 ≤ N , M ≤ 1 0 3 1 \leq N,M \leq 10^3 1≤N,M≤103。
- 对于 100 % 100\% 100% 的数据, 1 ≤ N , M ≤ 1 0 5 1 \leq N,M \leq 10^5 1≤N,M≤105。
代码
为了不超时,考虑逆方向存入,从编号最大的点开始搜索。
#include <bits/stdc++.h>
#define M 100005
using namespace std;
int n,m;
vector<int>G[M];
int ans[M],visit[M];
void dfs(int now,int a)
{
ans[now]=a;
for(int i=0;i<G[now].size();i++)
{
int to=G[now][i];
if(!visit[to])
{
visit[to]=1;
dfs(to,a);
}
}
}
int main()
{
cin>>n>>m;
while(m--)
{
int x,y;
cin>>x>>y;
G[y].push_back(x);
}
for(int i=n;i>=1;i--)
{
if(!visit[i])
visit[i]=1,dfs(i,i);
}
for(int i=1;i<=n;i++)
cout<<ans[i]<<' ';
return 0;
}
封锁阳光大学
题目描述
曹是一只爱刷街的老曹,暑假期间,他每天都欢快地在阳光大学的校园里刷街。河蟹看到欢快的曹,感到不爽。河蟹决定封锁阳光大学,不让曹刷街。
阳光大学的校园是一张由 n n n 个点构成的无向图, n n n 个点之间由 m m m 条道路连接。每只河蟹可以对一个点进行封锁,当某个点被封锁后,与这个点相连的道路就被封锁了,曹就无法在这些道路上刷街了。非常悲剧的一点是,河蟹是一种不和谐的生物,当两只河蟹封锁了相邻的两个点时,他们会发生冲突。
询问:最少需要多少只河蟹,可以封锁所有道路并且不发生冲突。
输入格式
第一行两个正整数,表示节点数和边数。
接下来
m
m
m 行,每行两个整数
u
,
v
u,v
u,v,表示点
u
u
u 到点
v
v
v 之间有道路相连。
输出格式
仅一行如果河蟹无法封锁所有道路,则输出 Impossible
,否则输出一个整数,表示最少需要多少只河蟹。
样例 #1
样例输入 #1
3 3
1 2
1 3
2 3
样例输出 #1
Impossible
样例 #2
样例输入 #2
3 2
1 2
2 3
样例输出 #2
1
提示
【数据规模】
对于
100
%
100\%
100% 的数据,
1
≤
n
≤
1
0
4
1\le n \le 10^4
1≤n≤104,
1
≤
m
≤
1
0
5
1\le m \le 10^5
1≤m≤105,保证没有重边。
代码
虽然有两种情况,但一共只用搜一遍,因为若可行,两种情况所需河蟹总数等于连通块的点数。写代码时,若用dfs,发现不可行后的返回难办。
#include <bits/stdc++.h>
using namespace std;
int n,m,ans,s=10000000,ss=0,counter=0;
vector<int>G[10005];
int visit[10005];
bool flag;
void dfs(int x)
{
if(visit[x]==0)
{
visit[x]=2;
ans=1;
dfs(x);
if(flag) return;
if(counter-ans<ans) s=counter-ans;
else s=ans;
}
else
{
counter++;
for(int i=0;i<G[x].size();i++)
{
int to=G[x][i];
if(!visit[to])
{
if(visit[x]==1) visit[to]=2,ans++,dfs(to);
else visit[to]=1,dfs(to);
if(flag) return;
}
else
if(visit[x]==visit[to])
{
flag=1;return ;
}
}
}
}
int main()
{
cin>>n>>m;
while(m--)
{
int x,y;
cin>>x>>y;
G[x].push_back(y);
G[y].push_back(x);
}
for(int i=1;i<=n;i++)
{
if(!visit[i])
{
flag=0;
counter=0;
ans=0,s=10000000;
dfs(i);
if(s==10000000)
{
ss=0;
break;
}
ss+=s;
}
}
if(ss==0) cout<<"Impossible";
else cout<<ss;
return 0;
}