这是一个图论总结。
图的概念
·图:由一些点和一些边组成的东西
·有向图:边有方向(单向连通)的图
·无向图:边无方向(双向连通)的图
·入度:有多少边连入这个点
·出度:有多少边连出这个点
·DAG:有向无环图
建图
邻接矩阵
用一个二维数组表示两个点之间有没有连边
s[x][y] = dis; //x和y之间有一条长度为dis的边
邻接表
用一个结构体记录所有的边,同时保存以这条边为起点的上一条边的编号,然后保存所有起点的最后一条边即可。
struct Edge{
int next, to, dis;
}e[MAXN * MAXN];
int head[MAXN], num;
inline void Add(int from, int to, int dis){
e[++num].to = to;
e[num].dis = dis;
e[num].next = head[from];
head[from] = num;
}
图的遍历
·DFS遍历
·BFS遍历
拓扑排序
·定义:拓扑排序(Topological Sorting)是一个有向无环图(DAG, Directed Acyclic Graph)的所有顶点的线性序列。且该序列必须满足下面两个条件:
1、每个顶点出现且只出现一次。
2、若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
·求法:先把所有入度为(0)的点加入队列。当队列不为空时,取出队头并输出,删掉所有以该点为起点的边,并把所有新的入度为(0)的点加入队列。重复执行。
queue <int> q;
for(int i = 1; i <= n; ++i)
if(!in[i]) //in记录入度
q.push(i);
while(!q.empty()){
int now = q.front();
q.pop();
printf("%d ", now);
for(int i=head[now];i;i=e[i].next)
if(!(--in[e[i].to]))
q.push(e[i].to);
}
最短路
单源最短路
· Dijkstra
创建一个队列,保存“已经确定最短路”的点的集合。初始时队列里只有源点,除源点外所有点的距离赋值正无穷。
当集合不为空时,取出集合内距离最短的一个点,移除集合,枚举这个点连的所有边,如果这个点的距离加上这条边的距离比到的那个点小,那么更新这条边到的那个点的距离,并把它加入集合。重复执行。使用堆优化后时间复杂度(O(nlog n))。适用于稠密图。
//洛谷 P4779 【模板】单源最短路径(标准版)
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;
inline int read(){
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-')w = -1;ch = getchar();}
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0',ch = getchar();
return s * w;
}
const int MAXN = 100010;
const int MAXM = 200010;
struct Node{
int next, to, dis;
}e[MAXM];
int head[MAXN], num;
ll dis[MAXN];
bool v[MAXN];
void Add(int from, int to, int dis){
e[++num].to = to;
e[num].next = head[from];
e[num].dis = dis;
head[from] = num;
}
int n, m, s;
int a, b, c;
struct cmp{
bool operator () (int a, int b){
return dis[a] > dis[b];
}
};
priority_queue <int, vector<int>, cmp> q;
int main(){
n = read(); m = read(); s = read();
for(i = 1; i <= m; ++i) a = read(), b = read(), c = read(), Add(a, b, c);
for(i = 1; i <= n; ++i) dis[i] = INF;
dis[s] = 0;
q.push(s);
while(!q.empty()){
int now = q.top();
q.pop();
if(v[now]) continue;
v[now] = true;
for(int i = head[now];i;i = e[i].next){
if(!v[e[i].to] && dis[e[i].to] > dis[now] + e[i].dis)
dis[e[i].to] = dis[now] + e[i].dis, q.push(e[i].to);
}
}
for(i = 1; i <= n; ++i) printf("%lld ", dis[i]);
return 0;
}
· SPFA
创建一个队列,初始时队列里只有源点。当队列不为空时,取出队头,和(Dijkstra)一样,枚举所有点,如果能更新距离,更新距离,如果更新的点不在队列里,则加入队列。重复执行。
时间复杂度(O(ne)),其中(e)为平均每个点进队列的次数,一般图很小,适用于稀疏图,但遇到稠密图时间复杂度会退化为(O(nm))。还有就是不能跑有负环的图,因此也可以用来判断负环。
//SPFA
queue <int> q;
q.push(s); //s为源点
for(int i = 1; i <= n; ++i) dis[i] = INF;
dis[s]=0;
while(!q.empty()){
int now = q.front();
flag[now] = 0;
q.pop();
for(int i = head[now]; i; i = e[i].next){
if(dis[e[i].to] > dis[now] + e[i].dis){
dis[e[i].to] = dis[now] + e[i].dis;
if(!flag[e[i].to])
q.push(e[i].to), flag[e[i].to] = 1;
}
}
}
多源最短路
·定义:求任意两点之间的最短路
·Floyd
初始时任意两点之间的距离为正无穷,任意点到自己的距离都为0,枚举所有“中介点”,再枚举所有点对,如果这两个点之间的距离大于起点到中介点再到终点的距离,则更新这两点之间的距离。时间复杂度(O(n^3))。此算法还可推广到求任意两点之间的连通性。
//Floyd求多源最短路
memset(dis, 127, sizeof dis);
for(int i = 1; i <= n; ++i)
dis[i][i] = 0;
for(int k = 1; k <= n; ++k)
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= n; ++j)
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
Tarjan算法
割点
·定义:若将该点及其所有的边删去后,图中剩余点的连通性发生变化,则该点为割点,一张图可能存在多个割点。
·求法(Tarjan):
定义两个数组:(dfn)(时间戳),(low)(追溯值)
先看一下代码:
void Tarjan(int u,int f){ //vis[x]=1表示x为割点
dfn[u]=low[u]=++id;
int Max=0,son=0;
for(int i=head[u];i;i=e[i].next){
if(!dfn[e[i].to]){
Tarjan(e[i].to,f);
low[u]=min(low[u],low[e[i].to]);
if(low[e[i].to]>=dfn[u]&&u!=f) vis[u]=1;
if(u==f) ++son;
}
low[u]=min(low[u],dfn[e[i].to]);
}
if(u==f&&son>=2) vis[u]=1;
}
由代码可知,没访问到一个点就给其分配一个时间戳,(low)可理解为“该点能回到的时间戳最小的点的时间戳”。
一个点(u)是割点,当且仅当该点不是根且存在一个有连边的点(v),满足
或者该点是根且存在两个有连边的点满足上述条件。
根据定义,(dfn[u]<=low[v])时,说明(v)无法到达比(u)更早访问的点,于是把(x)删除后,(v)和(u)的父亲之间无法连通。但如果(x)本来就是根,则无法影响到(v),所以需要存在两个点满足此条件。
割点(桥)
·定义:若把该边删去后,图的连通性发生变化,则该边为桥。
·求法(Tarjan):
设一条边连接(u,v)两个点,当(dfn[u]<low[v])时,该边为桥。
和割点一样,(dfn[u]<low[v])说明(v)无法到达比(u)或比(u)更早的节点,删除该边后,(u)与(v)“失联”,该边为桥。
强联通分量
·定义:一个图中任意两点能互相到达的极大的子图。“极大”就是指若该子图满足条件,且不存在一个包含这个子图的图满足条件,则该子图就是”极大”。
·求法(Tarjan):
Tarjan算法时把每个访问的点存到一个栈里,当对当前点的操作完成后判断,如果(dfn[u]==low[u]),则说明他的所有子节点无法到达比他更早的节点,那么这个点就是这个强联通分量的根,栈中所有在该点之上的点和该点共同构成一个强联通分量。
·缩点:把每个强联通分量看成一个点,重新连边即可。
void Tarjan(int u){
dfn[u] = low[u] = ++ID;
s.push(u);
vis[u] = 1;
for(int i = head[u]; i; i = e[i].next){
if(!dfn[e[i].to])
Tarjan(e[i].to), low[u] = min(low[u], low[e[i].to]);
else if(vis[e[i].to]) low[u] = min(low[u], dfn[e[i].to]);
}
if(dfn[u] == low[u]){
++sum;
int now;
do{
now = s.top();
s.pop();
vis[now] = 0;
belong[now] = sum; //属于哪个强联通分量
}while(now != u);
}
}
·缩点的作用:把有环图变为无环图(树)
树
·定义:无向无环图
·链:树上两点之间的唯一路径
树的直径
·定义:树上最长的链
·求法:从任意一个点(DFS),找到离这个点最远的点,再从那个最远的点(DFS),找到离那个点最远的点,这两个最远的点之间的路径就是树的直径。
树的重心
见这里
最小生成树
·定义:一个(n)个点图中选(n-1)条边使得这张图变成一棵树,且使这(n-1)条边的权值和最小。
·求法:
Kruskal
把所有边按权值升序排序,依次枚举所有边,如果这条边连接的两个点没有连通,则把这条边连上,计数器加上这条边的权值,当连了(n-1)条边后,算法结束。判断连通性用并茶集。算法时间复杂度(O(nlog n)),其实就是排序的复杂度。
//洛谷 P3366 【模板】最小生成树
#include <cstdio>
#include <algorithm>
using std::sort;
const int MAXN = 5010;
const int MAXM = 200010;
int f[MAXN];
int find(int x){
return f[x] == x ? x : f[x] = find(f[x]);
}
void merge(int x, int y){
f[find(y)] = find(x);
}
struct Edge{
int x, y, z;
bool operator < (const Edge A) const{
return z < A.z;
}
}e[MAXM];
int n, m, ans;
int main(){
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; ++i) f[i] = i; --n;
for(int i = 1; i <= m; ++i) scanf("%d%d%d", &e[i].x, &e[i].y, &e[i].z);
sort(e + 1, e + m + 1);
for(int i = 1; i <= m && n; ++i)
if(find(e[i].x) != find(e[i].y))
merge(e[i].x, e[i].y), --n, ans += e[i].z;
printf("%d
", ans);
return 0;
}
次小生成树
见这里
最近公共祖先(LCA)
·定义:距离树上两个节点最近的同时是两个节点的祖先的点。
·求法:
倍增
用(f[u][i])表示节点(u)的第(2^i)个父亲, 用倍增转移状态。
查询(LCA)的时候先跳到同一高度,然后跳到同一高度就行。
具体看代码:
int LCA(int u, int v){
if(dep[u] > dep[v]) swap(u, v);
int cha = dep[v] - dep[u];
for(int i = 0; i <= LOGMAXN; ++i)
if(cha & (1 << i))
v = f[v][i];
if(u == v) return u;
for(int i = LOGMAXN; ~i; --i)
if(f[u][i] != f[v][i])
u = f[u][i], v = f[v][i];
return f[u][0];
}
树链剖分(略)
Tarjan离线求LCA
点分治
·作用:快速枚举树上所有链
·方法:
1、找树的重心 -> 2、从重心开始(DFS) -> 3、找所有经过当前点的链 -> 4、对所有子树进行1、2、3号操作,
//洛谷 P2634 聪聪可可
#include <cstdio>
#include <algorithm>
#define re register
#define il inline
using std::max;
using std::sort;
inline int read(){
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9') { if(ch == '-') w = -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') { s = s * 10 + ch - '0'; ch = getchar(); }
return s * w;
}
const int MAXN = 20010;
struct Edge{
int next, to, dis;
}e[MAXN << 1];
int head[MAXN], num, n, size[MAXN], vis[MAXN], maxson[MAXN], Max, root, ans, s[5], p[5];
inline void Add(int from, int to, int dis){
e[++num].to = to;
e[num].next = head[from];
e[num].dis = dis;
head[from] = num;
}
void getRoot(int u, int fa, int tot){
maxson[u] = 0; size[u] = 1;
for(re int i = head[u]; i; i = e[i].next){
if(e[i].to != fa && !vis[e[i].to]){
getRoot(e[i].to, u, tot);
size[u] += size[e[i].to];
maxson[u] = max(maxson[u], size[e[i].to]);
}
}
maxson[u] = max(maxson[u], tot - size[u]);
if(maxson[u] < Max) Max = maxson[u], root = u;
}
void getADB(int u, int fa, int dep){
if(dep % 3) ans += s[3 - dep % 3];
else ans += s[0] + 1;
++p[dep % 3];
for(re int i = head[u]; i; i = e[i].next)
if(e[i].to != fa && !vis[e[i].to])
getADB(e[i].to, u, dep + e[i].dis);
}
void dfs(int u, int fa){
vis[u] = 1;
s[0] = s[1] = s[2] = p[0] = p[1] = p[2] = 0;
for(re int i = head[u]; i; i = e[i].next){
if(e[i].to != fa && !vis[e[i].to])
getADB(e[i].to, u, e[i].dis), s[0] += p[0], s[1] += p[1], s[2] += p[2], p[0] = p[1] = p[2] = 0;
}
for(re int i = head[u]; i; i = e[i].next){
if(!vis[e[i].to]){
Max = n;
getRoot(e[i].to, u, size[e[i].to]);
dfs(root, u);
}
}
}
int A, B, C;
int gcd(int a, int b){
return !b ? a : gcd(b, a%b);
}
int main(){
Max = n = read();
for(re int i = 1; i < n; ++i){
A = read(); B = read(); C = read();
Add(A, B, C);
Add(B, A, C);
}
getRoot(1, 0, n);
dfs(root, 0);
ans = (ans << 1) + n;
int b = n * n;
int GCD = gcd(ans, b);
ans /= GCD;
b /= GCD;
printf("%d/%d
", ans, b);
return 0;
}
二分图
二分图的判定
·黑白染色法
二分图匹配
·匈牙利算法