图论选讲1


图的概念

  • 一个图是由点集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 xi ⋁ \bigvee x j x_j xj) ⋀ \bigwedge ( x p x_p xp ⋁ \bigvee x q x_q xq) ⋀ \bigwedge … \dots
    • 现在我们想知道这个问题是否有解;
    • 有时候需要我们构造出一组解;
  • 解决方法:
    • 我们把每个变量拆成两个点,X(True) 和X(False).
    • 比如说现在有一个要求X|Y=True:
      • 我们连一条从X(False)到Y(True)的边,表示如果X是False,Y必须是True;
      • 同样的,我们连一条从Y(False)到X(True) 的边;
    • 连出的图是对称的。
    • 我们跑一边Tarjan,看看每个变量的两个点是不是在同一个强连通
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值