图论在信息学竞赛中占了很大部分,很多实际问题可以用图论来解决。
定义
-
什么叫图论?
-
研究图的问题一门高深的学科。
-
什么是图?
-
就是由点和线组成的图形
G=<V,E>
G=graph V=vertex E=edge
图的描述:
图的表示:
分类
- 有向图和无向图
有向图就是边有方向的
无向图就是可以两边走的 - 混合图: 既有有向又有无向边
- 简单图:没有重边和自环的图
- 完全图:任意两个点直接都有一条边
比如n个点完全图有C(n,2)条边 - 稀疏图和稠密图
稀疏图: 边的数量相对点来说很少
稠密图 : 边的数量接近于完全图 - 连通图
连通图: 在无向图如果任意两个之间都可以相互到达,就是连通图。
n个点连通图最少需要n-1条边。
强连通图: 在有向图里面,任意两点可以相互到达,就是强连通。
n个点的强连通图最少需要n条。
弱连通图:有向图中,任意两个点,至少有一个点可以到达另外一个点。
顶点的度
顶点的度: 和点相连的边的数量。
有向图的度可以分为入度和出度,A的入度1,A的出度2,A度3。
定理:1 任何一个图里面顶点的度之和一定是边的数量的2倍。
2 有向图中所有顶点的入度之和是等于所有顶点的出度之和。
3 任意一个无向图一定有偶数个奇点.
例题1:
- 一个无向图有16条边(每个点的度至少是2),其中4个度为3,3个度为4,求这个无向图最多有几个点 ?(2003年普及组问题求解)
- 答案是11
例题2:
- 一个无向图有4个结点,其中3个的度数为2,3,3,则第4个结点的度数不可能是___________
A. 0 B. 1 C. 2 D. 4 - 答案是B
例题3:
- 假设我们用d=(a1,a2,….a5)表示无向无自环图G的5个顶点的度数,下面给出的哪组值是可能的?
A.{3,4,4,3,1}
B.{4,2,2,1,1}
C.{3,3,3,2,2}
D.{3,4,3,2,1} - 答案是B
另外,树一个特殊的图,n个点,n-1条边。
图里面边的存储方法
一 相邻矩阵
int a[10][10];
用a[i][j]>0 表示i到j有边
a[i][j]==k 表示i到j的边长
优点:写法简单,能在O(1)得出任意两个点是否有边和边长。
缺点:在稀疏图的时候空间浪费太大,找和i相邻的点需要O(n)的时间。
二 数组模拟邻接表(边表)
int a[10][10];
用a[i][0]表示和i相连的点有几个
a[i][j]表示和i相连的第j个点的编号。
读入:
while(m--) {
int x,y;
cin >> x >> y;
a[x][++a[x][0]]=y;
a[y][++a[y][0]]=x;
}
查找于x相连的点:
for(int i=1; i<=a[x][0]; i++) cout<<a[x][i];
优点: 查询与x相邻的点时间复杂是O(k) 。(k是相邻的点的数量)
缺点: 空间还是需要很大,需要在O(k)时间知道i和j是否有边。
三 利用stl标准模板库里的动态数组vector(前向星)
定义:
vector<int> a; //a一维数组动态数组
vector<vector<int> > a; //a二维数组
vector<int> a[100]; //定义了100个一维
注意:第二种定义方法中>和>间必须加空格,否则会编译错误
使用如果a数组拥有第i个元素,那么直接可以用a[i]表示第i个数,注意a数组从0开始
存放(把x放到a数组的最后):
a.push_back(x); //把x放到a数组的最后
查找所有和x相邻的点:
for(int i=0; i<a[x].size(); i++) cout << a[x][i];
整合:
int n,m;
vector<int> edge[N];
void init() {
cin >> n >> m;
for(int i=0; i<m; i++) {
int x,y;
cin >> x >> y; //边连接的两个顶点
edge[x].push_back(y); //添边x->y
edge[y].push_back(x); //添边y->x
}
}
优点 :节省空间,找x相邻的需要O(k)的复杂度
缺点: 判断i和j是否有边需要O(k),比自己写的邻接表要慢一些。
四 前向星邻接表(链式前向星)
定义:
struct edge{
int to,nt; //to是边的终点,nt(next)是下一条边的序号
} e[边的数量];
int h[N],cnt; // h[i]表示i的第一条在e里面序号,cnt是边的总数
建边:
inline void add(int a,int b){
e[++cnt].to=b;
e[cnt].nt=h[a];
h[a]=cnt;
}
读入:
while(m--) {
int x,y;
scanf(“%d%d”,&x,&y);
add(x,y);
add(y,x);
}
枚举与x的相邻的所有点:
for(int i=h[x]; i; i=e[i].nt) cout<<e[i].to;
整合:
struct edge{
int to,nt;
} e[边的数量];
int h[N],cnt;
void add(int a,int b) {
e[++cnt].to=b;
e[cnt].nt=h[a];
h[a]=cnt;
}
int main() {
while (m--) {
int x,y;
scanf(“%d%d”,&x,&y);
add(x,y);
add(y,x);
}
for(int i=h[x]; i; i=e[i].nt) cout << e[i].to;
}
图的遍历问题
图的遍历问题是搜索图。
图的搜索分为深度优先搜索和宽度优先搜索两种方法。
深度优先搜索
对下图进行深度优先搜索,写出搜索结果。注意:从A出发。
从顶点A出发,进行深度优先搜索的结果为:A,B,C,D,E。
对于一个连通图,深度优先遍历的递归过程如下:
void dfs(int i) { //图用邻接矩阵存储
//访问顶点i;
visited[i]=1;
for(int j=1; j<=n; j++)
if(!visited[j] && a[i][j]) dfs(j);
}
以上dfs(i)的时间复杂度为O(n^2)。
对于一个非连通图,调用一次dfs(i),即按深度优先顺序依次访问了顶点i所在的(强)连通分支,所以只要在主程序中加上:
for(int i=1; i<=n; i++) //深度优先搜索每一个未被访问过的顶点
if(!visited[i]) dfs(i);
广度优先搜索(宽度优先搜索)
对下图从A出发进行宽度优先搜索,写出搜索结果。
从顶点A出发,进行宽度优先遍历的结果为: A,B,C,D,E 。
void bfs(int i) { //宽度优先遍历,图用邻接矩阵表示
queue<int> q;
i=q.pop();
visited[i]=true;
q.push(i);
while(!q.empty()) {
v=q.front();
q.pop();
for(int j=1; j<=n; j++) {
if(!visited[j]) {
visited[j]=1;
q.push(j);
}
}
}
}
时间复杂度是O(n^2).
BFS与DFS的总结:
- DFS:类似回溯,利用堆栈进行搜索
BFS:类似树的层次遍历,利用队列进行搜索 - DFS:尽可能地走“顶点表”
BFS:尽可能地沿着顶点的“边”进行访问 - DFS:容易记录访问过的路径
BFS:不易记录访问过的路径,需要开辟另外的存储空间进行保存路径
图的最短路径算法
分类:
- 多源最短路径算法:求任意两点之间的最短距离。
Floyd算法 - 单源最短路径算法:求一个点到其他所有点的最短路径
Dijkstra算法,Spfa算法,Bellman-ford算法
Floyd算法
时间复杂度O(n^3)
本质上是一个动态规划
f[i][j]表示i到j最短路径长度
开始的时候,如果i到j有边,那么f[i][j]就是直接的边长,如果没边,f[i][j]就是无穷大。
for(int k=1 ;k<=n; k++)
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
为什么k循环要写在最外面?
这个状态数组本来是3维的。
f[i][j][0]表示i到j的最短路径中间经过了0个点。
f[i][j][0]=edge[i][j] edge[i][j]表示i到j直接的边长
如果i到j边不存在,f[i][j][0]=无穷大。
f[i][j][k]表示i到j的最短路径中间最多经过了1到k这些点
答案就是f[i][j][n]
f[i][j][k]=min(一定没有经过k,一定经过k)
=min(f[i][j][k-1],f[i][k][k-1] + f[k][j][k-1])
把一维舍掉:
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
Dijkstra算法
思想 :贪心的思想
步骤:
1 标记所有的点都没有求得最短路径,所有的d[i]=无穷大,除了起点的d值是0。
2 循环n次,每次从没有求得最短路径的点里面找出一个d值最小的点,把他标记,用这个点去更新其他没有求得最短路径的点。
void dijkstra() {
memset(vis,0,sizeof(vis));
for(int i=1;i<=n;i++) d[i]=inf;
d[s]=0;
for(int i=1;i<=n;i++) {
int k=-1;
for(int j=1;j<=n;j++)
if(!vis[j] && (k==-1 || d[k]>d[j])) k=j;
vis[k]=1;
for(int j=1;j<=n;j++)
if(!vis[j] && d[k]+edge[k][j]<d[j])
d[j]=d[k]+edge[k][j];
}
}
Spfa算法
设dist代表s到i点的当前最短距离,fa代表s到i的当前最短路径中i点之前的一个点的编号。开始时dist全部为+∞,只有dist[s]=0,fa全部为0。
维护一个队列,里面存放所有需要进行迭代的点。初始时队列中只有一个点S。用一个布尔数组记录每个点是否处在队列中。
每次迭代,取出队头的点v,依次枚举从v出发的边v->u,设边的长度为len,判断dist[v]+len是否小于dist[u],若小于则改进dist[u],将fa[u]记为v,并且由于s到u的最短距离变小了,有可能u可以改进其它的点,所以若u不在队列中,就将它放入队尾。这样一直迭代下去直到队列变空,也就是S到所有的最短距离都确定下来,结束算法。
int const oo=1e9;
vector<int> a[N],b[N];
queue<int> q;
int s,t;
int v[N],d[N];
int spfa() {
for(int i=1; i<=n; i++) d[i]=oo;
q.push(s);
v[s]=1;
d[s]=0;
while(!q.empty()) {
int x=q.front();
q.pop();
v[x]=0;
for(int i=0; i<a[x].size(); i++) {
int tp=a[x][i];
if(d[tp]>d[x]+b[x][i]) {
d[tp]=d[x]+b[x][i];
if(!v[tp]) {
q.push(tp);
v[tp]=1;
}
}
}
}
if(d[t]==oo) d[t]=-1;
return d[t];
}
未完待续……