图的概念
一个图是由点集V和边集E组成的,一般我们会记作图G=<V,E>,一条边连接两个顶点。 点集V表示所有顶点,边集E表示所有边,当E为空时称为空图。 全部由无向边构成的图成为无向图,由有向边构成的图称为有向图。 自环:边连接的两个点是同一个点。 重边:无向图中指在两点之间有大于等于2条边连接,有向图中指在两点之间有大于等于2条同方向的边。 简单图:没有自环和重边的图。
完全图
完全图(无向图):设G为一个有n个顶点的无向图,若G中每个顶点都与其余n-1个顶点之间存在边相连,则称G为n阶无向完全图,简称n阶完全图。 完全图(有向图):设G为一个有n个顶点的有向图,若G中每个顶点都有与其余n-1个顶点相连的边,且都有这些顶点连向它的边,则称G为n阶有向完全图。
度数
无向图的度数:对于无向图中的顶点v,v作为边的端点的次数成为v的度数,记作d(v)。 有向图的度数:对于有向图中的顶点v,v作为边的起点的次数成为v的出度,记作d+(v)v作为边的终点的次数称为v的入度,记作d-(v)。 一张图G的所有点的度数和为边数的两倍,有向图中所有点的出度和等于入度和
子图,生成子图
子图:设
G
=
<
V
,
E
>
G=<V,E>
G =< V , E > ,
G
∗
G^*
G ∗ =<
V
∗
V^*
V ∗ ,
E
∗
E^*
E ∗ >,如果
V
∗
V^*
V ∗
∈
\in
∈
V
V
V ,且
E
∗
E^*
E ∗
∈
\in
∈
E
E
E ,则称图
G
∗
G^*
G ∗ 为图
G
G
G 的子图,
G
G
G 称为图
G
∗
G^*
G ∗ 的母图。 如果
V
∗
V^*
V ∗ =
V
V
V ,则称
G
∗
G^*
G ∗ 为图
G
G
G 的生成子图。 如果
V
∗
V^*
V ∗
⊂
\subset
⊂ V或
E
∗
E^*
E ∗
⊂
\subset
⊂ E,则称
G
∗
G^*
G ∗ 为图
G
G
G 的真子图。
图的存储
邻接矩阵
假如图中有n个顶点,邻接矩阵是一个n行n列的矩阵 无向图的邻接矩A:再简单无向图中,如果顶点u和v存在一条边,则A[u][v]=A[v][u]=1,如果没有A[u][v]=A[v][u]=0,对于重边的情况,如果顶点u和v存在k条边,则A[u][v]=A[v][u]=k。 对于有向图:A[u][v]表示从顶点u指向v的边有多少条。
# include <iostream>
# include <algorithm>
using namespace std;
const int N= 1010 ;
int n, m;
int a[ N] [ N] ;
int main ( )
{
cin>> n>> m;
for ( int i= 0 ; i< m; i++ )
{
int x, y;
cin>> x>> y;
a[ x] [ y] = a[ y] [ x] = 1 ;
a[ x] [ y] ++ , a[ y] [ x] ++ ;
a[ x] [ y] ++ ;
}
return 0 ;
}
邻接表
邻接表:当图中顶点比较多而边又比较少的时候,用邻接矩阵存储会浪费很多空间,于是我们采用邻接表的方式存储。对于每个顶点,我们采用一个vector或者结构体存储所有从这个顶点连出去的边。
# include <iostream>
# include <algorithm>
# include <vector>
# include <string>
using namespace std;
const int N= 1010 ;
int n, m;
vector< int > edges[ N] ;
int main ( )
{
cin>> n>> m;
for ( int i= 0 ; i< m; i++ )
{
int x, y;
cin>> x>> y;
edges[ x] . push_back ( y) ;
edges[ y] . push_back ( x) ;
}
return 0 ;
}
路径和距离
从图上一个点到另一个点经过不重合和点和边的集合称为路径,路径中经过边的数量称为路径长度 两点之间的路径可以是多余的。 有些时候,每条边会对应一个边权,这时候两点的路径长度就是这些边的边权之和。 连接这两个点的最短的路径长度称为这两点个点的距离
Dijkstra算法介绍
概念
对于无向图上的一条边(u
↔
\leftrightarrow
↔ v),可以看作有向图中的两条边(u
→
\rightarrow
→ v)和(u
→
\rightarrow
→ v)的结合,我们可以用这种方式将无向图转化成有向图,因此我们接下来只介绍有向图的最短路算法。 用Dijkstra解决最短路问题的前提条件是图中不能存在边权为负的边。 在进行具体介绍之前,我们先定义记号:
G=<V,E>代表我们要处理的简单有向图; n=|V|,m=|E|代表顶点数和边数; l(u,v)代表u到v的边的长度(边权); S表示起点,T表示终点; dist(u)代表我们当前求出从S到u的最短路径的长度,后面简称为u的距离; 我们要维护一个顶点集合C,满足对于所有的集合C中的顶点x,我们都已经找到了起点S到x的最短路,此时dist(x)记录的就是最终的最短路的长度。 Dijkstra算法流程如下:
将C设置为空,将S的距离设置为0,其余的距离全部为正无穷: 在每一轮中将距离起点S最近的并且不在集合C中的点加入到集合C中,并且利用这点连出去的边通过松弛操作尝试更新其他店的dist; 当T在集合中或者没有新的点加入到集合中时算法结束: 由于没有负权边的存在,所以可以证明每次加入到C的点都已经找到从起点到他的最短路。
代码实现:
struct Node
{
int y, v;
Node ( int _y, int _v) { y= _y; v= _v; } ;
} ;
const int N= 1010 ;
vector< Node> edges[ N] ;
int dist[ N] ;
bool b[ N] ;
int n, m;
int dijkstra ( int s, int t)
{
memset ( dist, 127 , sizeof dist) ;
memset ( b, false , sizeof b) ;
dist[ s] = 0 ;
while ( 1 )
{
int x= - 1 ;
for ( int i= 1 ; i<= n; i++ )
if ( ! b[ i] && dist[ i] < 1 << 30 )
if ( x== - 1 || dist[ i] < dist[ x] ) x== - 1 ;
if ( x== - 1 || x== t) break ;
b[ x] = true ;
for ( auto t: edges[ x] ) dist[ t. y] = min ( dist[ t. y] , dist[ x] + t. v) ;
}
return dist[ t] ;
}
代码优化
观察前面的代码,发现我们花费了大量时间在找出dist的最小值上。 我们可以可以采用一个set或者堆(priority_queue)来维护dist数组,算法时间复杂度可以提升至O((n+m)log n)。
struct Node
{
int y, v;
Node ( int _y, int _v) { _y= y; _v= v; } ;
} ;
const int N= 1010 ;
set< pair< int , int >> q;
vector< Node> edges[ N] ;
int dist[ N] ;
int n, m;
int dijkstra ( int s, int t)
{
memset ( dist, 127 , sizeof dist) ;
dist[ s] = 0 ;
for ( int i= 1 ; i<= n; i++ ) q. insert ( make_pair ( dist[ i] , i) ) ;
while ( ! q. empty ( ) )
{
int x= q. begin ( ) -> second;
q. erase ( q. begin ( ) ) ;
if ( x== t|| dist[ x] > 1 << 30 ) break ;
for ( auto t: edges[ x] )
{
if ( dist[ t. y] > dist[ x] + t. v)
{
q. erase ( make_pair ( dist[ t. y] , t. y) ) ;
dist[ t. y] = dist[ x] + t. v;
q. insert ( make_pair ( dist[ t. y] , t. y) ) ;
}
}
}
return dist[ t] ;
}
SCC
连通图
无向图:
连通性:如果顶点u和v之间存在路径,就称u和v之间是连通的。特别的v和v自己是连通的。 连通图:对于无向图G,若G中任意两个顶点都是连通的,则称无向图G是连通图。 有向图
连通性:如果存在顶点u到顶点v的路径,就称u可达v,如果u,v互相可达,则称u,v连通。 强连通图:如果有向图G中任意两个顶点可达,则称图G为强连通图。 弱连通图:如果把有向图G中所有有向边全部替换成无向边,得到无向图
G
∗
G^*
G ∗ 是连通图,则称图G是弱连通图。
连通分量(连通块)
在无向图中,连通分量就是极大连通子图。 我们可以使用DFS/BFS求出图中的每一个连通块。
强连通分量
我们在图中进行DFS,会形成一个森林结构。 图中的边分为4类:
Tree Edge :DFS时的树边。 Back Edge:连向祖先的边。 Forwward Edge:连向子孙的边。 Cross Edge:其他边 每个强连通分量在树中都是连续的一块;
代码实现
# include <iostream>
# include <algorithm>
# include <cstring>
# include <vector>
# include <set>
# include <stack>
using namespace std;
const int N= 10010 ;
stack< int > s;
vector< int > edges[ N] ;
int n, m;
int c[ N] ;
bool b[ N] ;
int siz[ N] ;
int tot= 0 , cnt= 0 ;
int low[ N] , dfn[ N] ;
void tarjan ( int x)
{
low[ x] = dfn[ x] = ++ cnt;
s. push ( x) ;
b[ x] = true ;
for ( int y: edges[ x] )
{
if ( ! dfn[ y] )
{
tarjan ( y) ;
low[ x] = min ( low[ x] , low[ y] ) ;
}
else
if ( b[ y] ) low[ x] = min ( low[ x] , dfn[ y] ) ;
}
if ( low[ x] == dfn[ x] )
{
tot++ ;
while ( 1 )
{
int y= s. top ( ) ;
s. pop ( ) ;
b[ y] = false ;
c[ y] = tot;
siz[ tot] ++ ;
if ( x== y) break ;
}
}
}
int main ( )
{
cin>> n>> m;
for ( int i= 1 ; i<= n; i++ )
if ( ! dfn[ i] ) tarjan ( i) ;
return 0 ;
}
dfn[x]表示x在dfs序中的位置(及x是第几个被搜到的)。 low[x]表示以x为根的子树中的点通过一条边往上最远能回溯到哪里。 s记录哪些点还没有找到其对应的强连通分量。 b[x]表示是否在s中。 c[x]表示点x属于哪个强连通分量。 size[i] 表示第i个强连通分量中有多少点。
2 sat
简单来说,2-sat指的是这样一类问题:
有若干个变量,每个变量只可以是True或者是False; 然后有若干个要求:(
x
i
x_i
x i
⋁
\bigvee
⋁
x
j
x_j
x j )
⋀
\bigwedge
⋀ (
x
p
x_p
x p
⋁
\bigvee
⋁
x
q
x_q
x q )
⋀
\bigwedge
⋀
…
\dots
… 现在我们想知道这个问题是否有解; 有时候需要我们构造出一组解; 解决方法:
我们把每个变量拆成两个点,X(True) 和X(False). 比如说现在有一个要求X|Y=True:
我们连一条从X(False)到Y(True)的边,表示如果X是False,Y必须是True; 同样的,我们连一条从Y(False)到X(True) 的边; 连出的图是对称的。 我们跑一边Tarjan,看看每个变量的两个点是不是在同一个强连通