图论总结

这是一个图论总结。

图的概念

·图:由一些点和一些边组成的东西
·有向图:边有方向(单向连通)的图
·无向图:边无方向(双向连通)的图
·入度:有多少边连入这个点
·出度:有多少边连出这个点
·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(n\log 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] \]

或者该点是根且存在两个有连边的点满足上述条件。
根据定义,\(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(n\log 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\n", 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\n", ans, b);
    return 0;
}

二分图

二分图的判定

·黑白染色法

二分图匹配

·匈牙利算法

网络流

posted @ 2018-09-09 16:56  Qihoo360  阅读(264)  评论(0编辑  收藏  举报
You're powerful!