ACM模板 图论

@(ACM模板)[图论]

      • 图论知识点要求
      • General
      • 建图
        • 使用vector
        • 链式前向星
      • 最短路
        • Dijkstra算法
        • Bellman-Ford算法
        • SPFA算法
        • Floyd算法
        • 差分约束
        • 最长路
        • 次短路
        • 最短路次短路路径计数
        • 拓扑排序求最短路
      • 二分图
        • 若干概念
        • 公式君
        • 二分图最大匹配
          • 匈牙利算法
          • 匈牙利算法dfs版读入二分图的左边
          • 匈牙利算法dfs版读入二分图的全图
        • 必须边
      • 生成树
        • 最小生成树 Kruskal算法
      • 其他经典问题
        • 欧拉路

图论知识点要求

必须会:
- 次短路&&路径数
- 生成树(看里面的过程)
- 最大流(主要是模板)
- 割(论文:最小割模型在信息竞赛里的应用)
- 二分图
- 树的分治,(点分治如重心分解,边分治如树链剖分)

了解,会套模板即可:
- K短路
- 度限制MST~end
- ZKW数组模拟
- 一般图匹配(NP,套模板)
- 各种回路
- 了解经典问题

General

  1. 注意下标是0-indexed的还是1-indexed的
  2. 看好是否有重边、自环
  3. 多组数据,vector要clear
  4. 在无向图中,存边的数组的大小要开边数的两倍
  5. unidirectional 和 one-way都是单向边

建图

使用vector

链式前向星

  1. 设图中n个点,m条边。在稀疏图( mn )中表示图,可以用邻接表。每个结点i有一个链表,保存从i出发的所有边。
  2. 方法:用数组模拟链表:
    • 每条边编号
    • first[u]保存结点u的第一条边的编号
    • next[e]表示编号为e的边的下一条边的编号
    • 注意每次插到链表的首部而非尾部,避免遍历
  3. 代码实现
typedef long long LL;
const int maxn = ?;
const int maxm = ?;
int n, m;
int head[maxn], next[maxm];
struct Edge
{
    int to;
    int dis;//LL
    Edge(int to = 0, int dis = 0):to(to),dis(dis) {}
}edges[maxm];//无向图size为2*maxm
void build_graph()
{
    cin >>  n >> m;
    memset(head, 0xff, sizeof head);
    int u, v, w;
    for(int i = 0; i < m; ++i)
    {
        scanf("%d%d%d", &u, &v, &w);
        edges[i] = Edge(u, v, w);
        //无向图要加两条
        next[i] = head[u];
        head[u] = i;
    }
}

遍历u的所有边

for(int e = first[u]; ~e; e = next[e])
{
    //...
}

最短路

1. Dijkstra算法

思想&步骤:
1. 初始化:d[s] = 0, 其他d为INF, 确定点集里只有s
2. 从未被确定的点的集合里找d最小的,加入确定的点集,并更新其他d
3. 重复2直到所有点都确定

注意:
- 不能处理负权。原因:Dijkstra贪心的找未确定点集里d最小的,若有负权可能之后找到d更小的。
- 此为使用队列优化的版本,复杂度 O(mlog(n)) ,其中 n 为点数, m 为边数
- 基础版执行n次,每次遍历所有点,复杂度 O(n2)
基础版即使在 n2<mlog(n) 时也往往比队列版本慢,因为队列版本执行push操作的前提是能进行松弛操作,若这个式子不常常成立,则push操作会很少。

代码
注意:
- 若最短路是long long 的,下面的代码需要在相应注释的地方更改
- 若需记录路径,需要启用pa[]数组
- 注意点的下标是否从0开始

#include
using namespace std;
typedef long long LL;
const int maxn = 1e5+5;//顶点数
const int maxm = 1e5+5;//边数

struct Dijstra//d[t] < inf代表有解
{
    const int inf = 0x3f3f3f3f;
    //const LL inf = 0x3f3f3f3f3f3f3f3f;

    int n, m;
    bitset done;
    int d[maxn];//LL
    int head[maxn];
    //int par[maxn];//记录路径

    struct Edge
    {
        int to, nxt;
        int dis;//LL
    }e[maxm];

    struct Node
    {
        int u;//结点编号
        int dis;//LL
        bool operator<(const Node &rhs) const
        {
            return dis > rhs.dis;
        }
    };

    void init(int nn)
    {
        n = nn;
        m = 0;
        memset(head, 0xff, sizeof head);
    }

    void addEdge(int from, int to, int dis)//LL dis
    {
        e[m].to = to;
        e[m].dis = dis;
        e[m].nxt = head[from];
        head[from] = m++;
    }

    void finda(int s)
    {
        priority_queue q;
        for(int i = 0; i < n; ++i) d[i] = inf;//0~n-1,注意下标从哪里开始
        d[s] = 0;
        done.reset();
        q.push((Node){s, 0});

        while(!q.empty())
        {
            int u = q.top().u;
            q.pop();
            if(done[u] == true) continue;
            done[u] = true;
            for(int i = head[u]; ~i; i = e[i].nxt)
            {
                int v = e[i].to;
                int dis = e[i].dis;//LL
                if(d[v] > d[u] + dis)
                {
                    d[v] = d[u] + dis;
                    q.push((Node){v, d[v]});
                }
            }
        }
    }
};

2. Bellman-Ford算法

思想&步骤
1. 初始化 所有顶点 d[i] = INF, 令d[s] = 0
2. 枚举每条边进行松弛,不能松弛时算法结束
3. 重复2,使2进行n-1次

memset(d, 0x3f, sizeof d);
d[s] = 0;
for(int times = 1; times < n; ++times)
{
   for(int i = 0; i < m; ++i)
   {
       int x = u[i], y = v[i];
       if(d[y] < d[x] + w[i]) d[y] = d[x] + w[i];
   }
}

3 .SPFA算法

思想&步骤:
“松弛操作的连锁反应”
用queue存可用来松弛的点,每次用队首点进行松弛,然后将松弛到的&&不在queue中的点加入queue。

注意:
- 此为Bellman-Ford算法的队列优化版本
- 可以判断负环。因为任意一条最短路,所经过的点不会超过n,那么任何一点不可能超过(n-1)次被松弛。
- 最坏情况复杂度 O(nm) ,但实际中往往很快

#include
using namespace std;
typedef long long LL;
const int maxm = 1e5 + 5;
const int maxn = 1e5 + 5;

struct SPFA
{
    const int inf = 0x3f3f3f3f;
    //const LL inf = 0x3f3f3f3f3f3f3f3f;

    bool vis[maxn];
    int c[maxn], head[maxn];//c为入队次数
    int d[maxn];//LL
    int n, m;

    struct Edge
    {
        int to, nxt;
        int dis;//LL
    }e[maxm];

    void init(int nn)
    {
        n = nn;
        m = 0;
        memset(head, 0xff, sizeof head);
    }

    void addEdge(int from, int to, int dis)//LL dis
    {
        e[m].to = to;
        e[m].dis = dis;
        e[m].nxt = head[from];
        head[from] = m++;
    }

    queue<int> q;
    bool finda(int s)//若存在负环返回true
    {
        memset(d, 0x3f, sizeof d);
        d[s] = 0;
        memset(vis, 0, sizeof vis);
        memset(c, 0, sizeof c);

        while(!q.empty()) q.pop();
        q.push(s);
        vis[s] = true;
        c[s] = 1;

        while(!q.empty())
        {
            int x = q.front();
            q.pop();
            vis[x] = false;
            for(int i = head[x]; ~i; i = e[i].nxt)
            {
                int y = e[i].to;
                int dis = e[i].dis;
                if(d[y] > d[x] + dis)
                {
                    d[y] = d[x] + dis;
                    if(!vis[y])
                    {
                        vis[y] = true;
                        ++c[y];
                        q.push(y);
                        if(c[y] > n) return true;
                    }
                }
            }
        }
        return false;
    }
};

4. Floyd算法

for(int k = 0; k < n; ++k)
    for(int i = 0; i < n; ++i)
        for(int j = 0; j < n; ++j)
            if(d[i][j] < INF && d[k][j] < INF)
                d[i][j] [i][k]+d[k][j];

若距离数组为bool,代表是否联通,则得到的结果成为有向图的传递闭包

5. 差分约束

  1. 若全部不等式均为 xixjaij 的形式(注意有等号),求 xsxt 的最值。可以建图求最路。
  2. 对于 xixjaij ,建立 xjxi 的有向边,权值为 aij
  3. 解的存在性:
    • 存在最短路: xsxt 存在最
    • 存在负环: xsxt 无穷小
    • 图中不可达,无最短路: xsxt 无穷大
  4. 若全部不等式均为 xixjaij 的形式,求 xsxt 的最值。可以改为在建好的图中求最路。
  5. 若不等式中既含有 又含有 ,可以通过变号统一处理成 ,处理成 还是 取决于最后是求最大值还是最小值。
  6. 若含有不带等号的不等式 xixj<aij ,且涉及到的都是整数,可以等价变形为 xixjaij+1
  7. 最终的每个 di 值为 xi 的可行解。若 {x0,x1,xn1,xn} 是一组可行解,则 {x0+y,x1+y,xn1+y,xn+y} 也是。

6. 最长路

  1. 方法一:将原图中的每条边权取相反数,再用Bellman-FordSPFA求解最短路。注意不能使用Dijkstra算法,因为其无法处理负权。
  2. 方法二:将再用Bellman-FordSPFA中的松弛条件反向,直接求解最长路。
  3. 再次注意不要用Dijkstra算法
  4. 存在正环时,不再计算d[t],此时要注意图的联通性,即从该正环是否能走到终点。(Floyd)
    例题:poj1932

7. 次短路

对Dijkstra算法进行修改。增加一个用于记录次短路的数组d2。
在进行松弛操作时
- 若可更新最短路,则更新最短路,原最短路变为次短路
- 若可更新次短路,则更新次短路

#include
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 5;//顶点数
const int maxm = 1e5 + 5;//边数
const int inf = 0x3f3f3f3f;
//const LL inf = 0x3f3f3f3f3f3f3f3f;

struct Dijstra//d2[t] < inf代表有解
{

    int n, m;
    int d[maxn], d2[maxn];//LL,d记录最短路,d2记录次短路
    int head[maxn];
    //int par[maxn];//记录路径

    struct Edge
    {
        int to, nxt;
        int dis;//LL
    }e[maxm];

    struct Node
    {
        int u;//结点编号
        int dis;//LL
        bool operator<(const Node &rhs) const
        {
            return dis > rhs.dis;
        }
    };

    void init(int nn)
    {
        n = nn;
        m = 0;
        memset(head, 0xff, sizeof head);
    }

    void addEdge(int from, int to, int dis)//LL dis
    {
        e[m].to = to;
        e[m].dis = dis;
        e[m].nxt = head[from];
        head[from] = m++;
    }

    void finda(int s)
    {
        priority_queue q;
        for(int i = 0; i < n; ++i) d[i] = inf;//0~n-1,注意下标从哪里开始
        for(int i = 0; i < n; ++i) d2[i] = inf;//0~n-1,注意下标从哪里开始
        d[s] = 0;
        q.push((Node){s, 0});

        while(!q.empty())
        {
            Node x = q.top(); q.pop();
            int u = x.u;
            int dis = x.dis;
            if(d2[u] < dis) continue;
            for(int i = head[u]; ~i; i = e[i].nxt)
            {
                int v = e[i].to;
                int dd = dis + e[i].dis;//LL
                if(d[v] > dd)
                {
                    swap(d[v], dd);
                    q.push((Node){v, d[v]});
                }
                if(dd > d[v] && dd < d2[v])
                {
                    d2[v] = dd;
                    q.push((Node){v, d2[v]});
                }
            }
        }
    }
};

8. 最短路、次短路路径计数

在7的基础上加个cnt数组,将松弛处的代码略加改动即可

#include
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 5;//顶点数
const int maxm = 1e5 + 5;//边数
const int inf = 0x3f3f3f3f;
//const LL inf = 0x3f3f3f3f3f3f3f3f;

struct Dijstra//d2[t] < inf代表有解
{

    int n, m;
    int d[2][maxn];//LL,d[0]记录最短路,d[1]记录次短路
    int cnt[2][maxn];//cnt[0]记录最短路条数,cnt[1]记录次短路径条数
    int head[maxn];
    //int par[maxn];//记录路径

    struct Edge
    {
        int to, nxt;
        int dis;//LL
    }e[maxm];

    struct Node
    {
        int f;//0代表最短路,1代表次短路
        int u;//结点编号
        int dis;//LL
        bool operator<(const Node &rhs) const
        {
            return dis > rhs.dis;
        }
    };

    void init(int nn)
    {
        n = nn;
        m = 0;
        memset(head, 0xff, sizeof head);
    }

    void addEdge(int from, int to, int dis)//LL dis
    {
        e[m].to = to;
        e[m].dis = dis;
        e[m].nxt = head[from];
        head[from] = m++;
    }

    void finda(int s)
    {
        priority_queue q;
        for(int i = 0; i < n; ++i) d[0][i]  = d[1][i] = inf;//0~n-1,注意下标从哪里开始
        d[0][s] = 0;
        for(int i = 0; i < n; ++i) cnt[0][i] = cnt[1][i] = 0;
        cnt[0][s] = 1;
        q.push((Node){0, s, 0});

        while(!q.empty())
        {
            Node x = q.top(); q.pop();
            int u = x.u;
            int dis = x.dis;
            int f = x.f;
            if(d[f][u] < dis) continue;
            for(int i = head[u]; ~i; i = e[i].nxt)
            {
                int v = e[i].to;
                int dd = dis + e[i].dis;//LL
                if(d[0][v] > dd)//能更新最短路就先更新最短路
                {
                    if(d[0][v] < inf)//之前的最短路变为次短路
                    {
                        d[1][v] = d[0][v];
                        cnt[1][v] = cnt[0][v];
                        q.push((Node){1, v, d[1][v]});
                    }

                    //更新最短路
                    d[0][v] = dd;
                    cnt[0][v] = cnt[f][u];
                    q.push((Node){0, v, d[0][v]});
                }
                else if(d[0][v] == dd)//更新最短路条数
                {
                    cnt[0][v] += cnt[f][u];
                }
                else if(d[1][v] > dd)//不能更新最短路,只能更新次短路
                {
                    d[1][v] = dd;
                    cnt[1][v] = cnt[f][u];
                    q.push((Node){1, v, d[1][v]});
                }
                else if(d[1][v] == dd)//更新次短路条数
                {
                    cnt[1][v] += cnt[f][u];
                }
            }
        }
    }
};

9. 拓扑排序求最短路

在DAG中,可以按照点的拓扑序进行松弛操作,求得最短路。
复杂度 O(V+E)

二分图

二分图的一个等价定义是:不含有「含奇数条边的环」的图

1.若干概念:

  1. 匹配:图中两两没有公共端点的边集M
    最大匹配:边最多的M
    完美匹配:所有点都为匹配点,满足2|M| = |V|的最大匹配
  2. 边覆盖:图中任意顶点都是F中某边的端点的边集合F
  3. 独立集:图中两两不相连的顶点集合S
  4. 顶点覆盖:图中的任意一条边都至少有一个端点属于S的点集S
  5. 路径覆盖:在DAG中覆盖所有点的路径数(路径不相交)

2.公式君

  1. 对于不存在孤立点的图中,|最小边覆盖| = |V| - |最大匹配|
  2. |最大独立集| = |V| - |最小顶点覆盖|
  3. 二分图中,|最大匹配| = |最小顶点覆盖|
  4. |最小路径覆盖| = |V| - |对于二分图中的最大匹配|

3.二分图最大匹配

注意:
- 二分图最大匹配可以dfs,但也可bfs,dfs好写
- 为了加快速度,一般用某种贪心先求出某个匹配,然后在此基础上继续求增广路

1.匈牙利算法

匹配点:匹配边集的端点
未盖点:不是匹配点的点
交替路:由“未盖点-非匹配边-匹配边–非匹配边-匹配边…”形成的路
增广路:终点是未盖点的交替路
增广路定理:任意图(不限于二分图)中,一个匹配为最大匹配的充要条件为:不存在增广路
增广路算法:
通过反复求增广路来求最大匹配。

  1. 选一个未盖点 u,uX
  2. 选非匹配边 (u,v)vY
  3. v 为未盖点,则找到了增广路
  4. v 为匹配点,从 v 开始走一条匹配边 (v,matchv),matchvX
  5. 以4中的 matchv 作为1中的 u 继续寻找,直到不能再扩展

最终会扩展出一个匈牙利树

2.匈牙利算法dfs版(读入二分图的左边)

此版本中,若二分图左边有x∈X,右边有y∈Y,且(x,y)为一条边,则所存的图中x有一条边y,不存储Y中点的边
当题目输入数据中未分好二分图可用此模板需除以2
多组数据vector要clear

const int maxn1 = 2000+5;
const int maxn2 = 2000+5;

vector<int> G[maxn1];
typedef vector<int>::iterator it;
int n, nx, ny;

int match[maxn2];
bool vis[maxn2];

bool dfs(int cur)
{
    for(int i = 0; i < (int)G[cur].size(); i++)
    {
        int v = G[cur][i];
        if(!vis[v])
        {
            vis[v] = true;
            if(match[v] == -1 || dfs(match[v]))
            {
                match[v] = cur;
                return true;
            }
        }
    }
    return false;
}

int hungary()
{
    int res = 0;
    memset(match, -1, sizeof match);
    for(int i = 0; i < nx; i++)
    {
        memset(vis, 0, sizeof vis);
        res += dfs(i);
    }
    return res;
}
3.匈牙利算法dfs版(读入二分图的全图)

此版本中,若二分图左边有x∈X,右边有y∈Y,且(x,y)为一条边,则所存的图中x有一条边y,y有一条边x
当题目输入数据中未分好二分图可用此模板
多组数据vector要clear

const int maxn = 2000+5;

vector<int> G[maxn];
typedef vector<int>::iterator it;
int n;

int match[maxn];
bool vis[maxn];

bool dfs(int cur)
{
    for(int i = 0; i < (int)G[cur].size(); i++)
    {
        int v = G[cur][i];
        if(!vis[v])
        {
            vis[v] = true;
            if(match[v] == -1 || dfs(match[v]))
            {
                match[cur] = v;
                match[v] = cur;
                return true;
            }
        }
    }
    return false;
}

int hungary()
{
    int res = 0;
    memset(match, -1, sizeof match);
    for(int i = 0; i < n; i++)
    {
        if(match[i] == -1)
        {
            memset(vis, 0, sizeof vis);
            res += dfs(i);
        }
    }
    return res;
}

4.必须边

若去掉某边,图的最大匹配变小,则该边为必须边
去掉某边重复计算匈牙利即可

        int res = 0;
        int mx = hungarian();
        for(int u = 0; u < nl; u++)
        {
            int left = G[u].size();
            while(left--)
            {
                it be = G[u].begin();
                int v = *be;
                G[u].erase(be);
                if(hungarian() != mx) res++;
                G[u].push_back(v);
            }
        }

生成树

1.最小生成树 Kruskal算法

需要用到并查集,如下:

const int maxn = 105;
const int maxe = maxn*maxn/2;
int n;
int par[maxn], rk[maxn];//par记录祖先,rk记录其在所在树中的深度
void init()
{
    for(int i = 0; i < n; i++)
        par[i] = i, rk[i] = 0;
}

int finda(int x)
{
    if(par[x] == x)  return x;
    return par[x] = finda(par[x]);
}

bool unite(int x, int y)
{
    x = finda(x);
    y = finda(y);
    if(x == y) return false;
    if(rk[x] < rk[y]) par[x] = y;
    else
    {
        par[y] = x;
        if(rk[x] == rk[y]) rk[x]++;
    }
    return true;
}
struct Edge
{
    int x,y,v;
    friend bool operator< (const Edge& x, const Edge& y)
    {
        return x.v < y.v;
    }
}e[maxe];
int m;

int kruskal()
{
    int res = 0;
    sort(e, e+m);
    int left = n-1;//还差几条边
    for(int i = 0; i < m; i++)
    {
        int x = finda(e[i].x);
        int y = finda(e[i].y);
        if(!unite(x, y)) continue;
        res += e[i].v;
        left--;
        if(left == 0) break;
    }
    return res;
}

其他经典问题

欧拉路

  1. 问题定义:从一个节点出发,每个边恰好经过一次。“一笔画问题”
  2. 无向图存在欧拉路的充要条件:
    • 连通
    • 最多有两个奇点
  3. 有向图存在欧拉路的重要条件:
    • 改成无向边后连通
    • 最多有两个入度不等于出的的点(一个入度比出度大一,另一个相反)
  4. 欧拉回路:起点终点相同

你可能感兴趣的:(acm,acm模板,图论)