Skip to content

图论

图的存储

邻接矩阵

\(G[i][j]=1\) 表示有一条边从 \(i\)\(j\)

邻接表

vector<int> es[MAXN];
/*
带权边
struct edge{ int to, w; };
vector<edge> es[MAXN];
*/

链式前向星

struct edge {
    int to, w, next;
}es[MAX_E];

int cnt = 0;

void init() {
    memset(head, -1, sizeof head);
}

void add_edge(int u, int v, int w) {   //加边
    es[cnt].to = v;
    es[cnt].w = w;
    es[cnt].next = head[u];
    head[u] = cnt++;
}
/*遍历
    for(int i = 1; i <= n; i++)
        for(int j = head[i]; i != -1; j = es[j].next)
*/

最短路

Bellman-Ford

单源最短路问题

\(O(VE)\) 支持负权

struct edge{ int from, to, w; };
edge es[MAX_E];

int d[MAX_V];
int V, E;

void BF(int s){
    memset(d, INF, sizeof d);
    d[s] = 0;
    for(int k = 0; k < V; k++){
        for(int i = 0; i < E; i++){
            edge e = es[i];
            if(d[e.from] != INF && d[e.to] > d[e.from]+e.w){
                d[e.to] = d[e.from]+w; //松弛操作
            }
        }
    }
}
//循环至多执行 V-1 次,一次松弛操作至少让确定的最短路+1,所以O(VE),如果第 n 次任然更新了d,表示有负环
//把所有d[i]初始化为0,就可以找到所有的负圈
bool find_negative_loop(){
    memset(d, 0, sizeof d);
    for(int i = 0; i < V; i++){
        for(int j = 0; j < E; j++){
            edge e = es[j];
            if(d[e.to] > d[e.from]+e.w){
                d[e.to] = d[e.from]+e.w;
                if(i == V-1) return true;
            }
        }
    }
    return false;
}

Dijkstra

单源最短路问题

不支持负权边

检查存在 \(d[i][i]\) 为负数来判断是否有负环

\(O(ElogV)\) 用优先队列

typedef pair<int, int> P; 

vector<P> es[MAX_V];  //邻接表中first表示端点,second表示权值
int d[MAX_V];

void dijkstra(int s) {
    priority_queue< P, vector<P>, greater<P> > que; //队列中first表示最短路, second表示端点
    memset(d, INF, sizeof d);
    d[s] = 0;
    que.push(P(0, s));
    while(!que.empty()) {
        P p = que.top(); que.pop();
        int u = p.second;
        if(d[u] < p.first) continue;
        for(int i = 0; i < es[u].size(); i++) {
            int v = es[u][i].first, w = es[u][i].second;
            if(d[v] > d[u]+w) { //松弛操作
                d[v] = d[u]+w;
                que.push(P(d[v], v));
            }
        }
    }
}

Floyd-Warshall

\(O(V^3)\)

dp:状态转移方方程 \(d[k][i][j]=min(d[k-1][i][j],d[k-1][i][k]+d[k-1][k][i])\), 即 \(d[i][j] = min(d[i][j], d[i][k]+d[k][j])\)

int d[MAX_V[MAX_V];
void floyd() {
    for(int k = 0; k < V; k++)
        for(int i = 0; i < V; i++)
            for(int j = 0; j < V; j++)
                d[i][j] = min(d[i][j], d[i][k]+d[k][j]);
}   

路径还原

在松弛操作时记录每个结点的前趋结点\(path[i]\)即可, 查询时从后往前遍历

次短路

//每次更新最短路时看看被抛弃的值能不能更新次短路
typedef pair<int, int> P;
const int inf = 0x3f3f3f3f;

vector<P> es[max_v];

int n, r, a, b, c;
int d1[max_v], d2[max_v]; //d2 次短路

void dijkstra(int s){
    memset(d1, inf, sizeof d1);
    memset(d2, inf, sizeof d2);
    priority_queue< P, vector<P>, greater<P> > que;

    d1[s] = 0;
    que.push(P(0, s));
    while(!que.empty()){
        P p = que.top(); que.pop();
        int u = p.second;
        if(d2[u] < p.first) continue;

        for(int i = 0; i < es[u].size(); i++){
            int v = es[u][i].first, w = es[u][i].second;
            int tmp = p.first+w;
            if(d1[v] > tmp){
                swap(tmp, d1[v]);
                que.push(P(d1[v], v));
            }
            if(tmp < d2[v] && tmp > d1[v]){
                d2[v] = tmp;
                que.push(P(d2[v], v));
            }
        }
    }
}

最小生成树

前提:图是连通的

Prim

\(O(V^2)\)

vector<edge> es[max_v];
int dis[MAX_V];
bool vis[MAX_V];

int prime(){
    memset(dis, inf, sizeof dis);
    memset(vis, false, vis);
    dis[0] = 0; //s
    int res = 0;
    while(true){
        int v = -1, mn = inf;
        for(int i = 0; i < V; i++)
            if(!visited[i] && dis[i] < mn)
                mn = dis[i], v = i;        
        if(v == -1) break;
        vis[v] = true;
        res += mn;
        for(int i = 0; i < es[v].size(); i++){
            edge e = es[v][i];
            dis[e.to] = min(dis[e.to], e.w);
        }
    }
    return res;
}

Kruskal

按照边的权值从小到达,利用并查集判断是否会产出圈,不会就加入

\(O(ElogE)\) 排序的复杂度

struct edge{ int from, to, w; };
edge es[MAX_E];

bool cmp(edge a, edge b){ return a.w < b.w; }

int kruskal(){
    sort(es, es+E, cmp);
    init(MAX_V);  //并查集的初始化
    int res = 0;
    for(int i = 0; i < E; i++){
        edge e = es[i];
        if(!same(e.from, e.to)){
            unite(e.from, e.to);
            res += e.w;
        }
    }
    return res;
}

网络流

最大流 Dinic

struct edge{ int to, cap, rev; } //rev记录反向边在 es[to] 中的索引

vector<edge> es[max_v];  //邻接表
int level[max_v];        //分层图
int iter[max_v];         //弧优化,记录结点增广过哪些边了,下次就不增广了

void add_edge(int from, int to, int cap)  {//加边

    es[from].push_back(edge{to, cap, es[to].size()});
    es[to].push_back(edge{from, 0, es[from].size()-1});   
}

void bfs(int s) {//分层图
    memset(level, -1, sizeof level);
    queue<int> que;
    level[s] = 0;
    que.push(s);
    while(!que.empty()){
        int v = que.front(), que.pop();
        for(int i = 0; i < es[i].size(); i++){
            edge &e = es[v][i];
            if(e.cap > 0 && level[e.to] < 0){
                level[e.to] = level[v]+1;
                que.push(e.to);
            }
        }
    }
}

int dfs(int cur, int t, int f) { //增广路
    if(cur == t) return f;
    for(int &i = iter[cur]; i < es[cur].size(); i++){
        edge &e = es[cur][i];
        if(e.cap > 0 && level[cur] < level[e.to]){
            int d = dfs(e.to, t, min(f, e.cap));
            if(d > 0) {
                e.cap -= d;
                es[e.to][e.rev].cap += d;
                return d;
            }
        }
    }
    return 0;
}

int max_flow(int s, int t){
    int flow = 0;
    while(1){
        bfs(s);
        if(level[t] < 0) return flow;
        memset(iter, 0, sizeof iter);
        int f;
        while(f = dfs(s, t, inf) > 0) flow += f;
    }
}

最小费用流

负权边,用 BF 算法

const int inf = 0x3f3f3f3f;
struct edge{int to, cap, cost, rev; }

vector<edge> es[max_v];
int dist[max_v];
int pree[max_e], prev[max_v];   //前导顶点的对应的边的索引

void add_edge(int from, int to, int cap, int cost){
    es[from].push_back(edge{to, cap, cost, es[to].size()});
    es[to].push_back(edge{from, 0, -cost, es[from].size()-1});
}

int min_cost_flow(int s, int t, int f) { //起点 终点 流量
    int res = 0;
    while(f > 0){
        memset(dist, inf, sizeof dist);
        dist[s] = 0;
        while(true)  
            bool update = false;
            for(int i = 0; i < V; i++) {
                if(dist[i] == inf) continue;
                for(int j = 0; j < es[i].size(); j++) {
                    edge &e = es[i][j];
                    if(e.cap > 0 && dist[e.to] > dist[e.from]+e.cost)  {
                        dist[to] = dist[from]+e.cost;
                        prev[e.to] = i;
                        pree[e.to] = j;
                        update = true;
                    }
                }
            }
            if(!update) break;
        }

        if(dist[t] == inf) return -1;
        //沿s到t的最短路尽量增广
        int d = f;
        for(int i = t; i != s; i = prev[i])
            d = min(d, es[prev[i]][pree[i]].cap);

        f -= d;
        res += d*dist[t];
        for(int i = t; i != s; i = prev[i]) {
            edge &e = es[prev[i][pree[i]]];
            e.cap -= d;
            es[i][e.rev].cap += d;
        }
    }
    return res;
}

二分图匹配

最大流

添加原点和汇点,计算最大流

//计算机处理任务
bool can[max_n][max_m]; // can[i][j] : 计算机 i 能处理任务 j

void MaxMatch(){
    //计算机对应的顶点:0 ~ n-1
    //任务对应的顶点:n ~ n+m-1
    int s = n+m, t = n+m+1;
    for(int i = 0; i < n; i++){
        add_edge(s, i, 1);
    }
    for(int i = 0; i < m; i++){
        add_edge(n+i, t, 1);
    }
    for(int i = 0; i < n; i++)
        for(int j = 0; j < m; j++)
            add_edge(i, n+j, 1);
    cout << max_flow(s, t) << endl;
}

匈牙利算法

\(O(VE)\)

bool vis[max_v];
int match[max_v];
vector<int> es[max_v];

void add_edge(int u, int v){
    es[u].push_back(v);
    es[v].push_back(u);
}

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

int MaxMatch(){
    int res = 0;
    for(int i = 1; i <= n; i++){  //寻找增广路径
        memset(vis, 0, sizeof vis);
        if(!match[i] && dfs(i)) res++;     
    }
    return res;
}

Hopcroft-Karp

\(O(E\sqrt{V})\)

HDU 2389

int match[max_v], dep[max_v];
vector<int> es[max_v];

bool bfs() {
    memset(dep, 0, sizeof dep);
    queue<int> q;
    for (int i = 1; i <= n; i++)
        if (!match[i]) q.push(i);
    bool flag = false;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int i = 0; i < es[u].size(); i++) {
            int v = es[u][i];
            if (dep[v]) continue;
            dep[v] = dep[u] + 1;
            if (!match[v]) flag = true;
            else dep[match[v]] = dep[v] + 1, q.push(match[v]);
        }
    }
    return flag;
}

bool dfs(int u) {
    for (int i = 0; i < es[u].size(); i++) {
        int v = es[u][i];
        if (dep[v] != dep[u] + 1) continue;
        dep[v] = 0;
        if (!match[v] || dfs(match[v])) {
            match[v] = u; match[u]  = v;
             return true;
        }
    }
    return false;
}

int MaxMatch() {
    int res = 0;
    while (bfs()) {
    for (int i = 1; i <= n; i++)
        if (!match[i] && dfs(i)) res++;
    }
    return res;
}

连通性相关

强连通分量分解

\(O(V+E)\)

两次DFS,第一遍后序遍历并给顶点标号,第二遍对反向图遍历

vector<int> G[max_v];
vector<int> rG[max_v];    //反向图
vector<int> vs;           //vertex sequence
bool vis[max_v];
int cmp[max_v];           //所属强连通图的拓扑序

void add_edge(int u, int v){
    G[u].push_back(v);
    rG[v].push_back(u);
}

void dfs(int v){
    vis[v] = true;
    for(int i = 0; i < G[v].size(); i++)
        if(!vis[G[v][i]]) dfs(G[v][i]);
    vs.push_back(v);
}

void rdfs(int v, int k){
    vis[v] = true;
    cmp[v] = k;
    for(int i = 0; i < rG[v].size(); i++)
        if(!vis[rG[v][i]]) rdfs(rG[v][i], k);
}

int scc() { //strongly connected component
    memset(vis, false, sizeof vis);
    vs.clear();
    for(int v = 0; v < V; v++)
        if(!vis[v]) dfs(v);

    memset(vis, false, sizeof vis);
    int k = 0;
    for(int i = vs.size()-1; i >= 0; i--)
        if(!vis[vs[i]]) rdfs(vs[i], k++);
    return k;
}

连通分支个数

并查集

树上问题

最近公共祖先(LCA)

\(O(n)\)

vector<int> G[maxn_v];
int root, parent[max_v], depth[max_v];

void dfs(int v, int p, int d){
    parent[v] = p, depth[v] = d;
    for(int i = 0; i < G[v].size(); i++)
        if(G[v][i] != p) dfs(G[v][i], v, d+1);
}

void init(){
    dfs(root, -1, 0);
}

int lca(int u, int v){
    //先把u,v走到同一深度
    while(depth[u] > depth[v]) u = parent[u]; 
    while(depth[v] > depth[u]) v = parent[v];
    //一起向上走
    while(u != v){
        u = parent[u], v = parent[v];
    }
    return u;
}

基于二分搜索的算法

对于任意结点v,可以通过

parent2[v] = parent[parent[v]]

parent4[v] = parent2[parent2[v]]

...

得到其向上走 \(2^k\)​ 步到达的顶点

每次搜索的复杂度: \(O(logn)\),预处理的复杂度:\(O(nlogn)\)

vector<int> G[max_v];
int root, parent[max_k][max_v], depth[max_v];

void dfs(int v, int p, int d){
    depth[v] = d, parent[v] = p;
    for(int i = 0; i < G[v].size(); i++)
        if(G[v][i] != p) dfs(G[v][i], v, d+1);
}

void init(int V){
    //预处理parent[0]和depth
    dfs(root, -1, 0);
    //预处理parent
    for(int k = 0; k+1 < max_k; k++)
        for(int i = 0; i < V; i++) { //parent[k][v] 表示从 v 结点向上走 2^k 次的结点, 超过根时记作-1
            if(parent[k][i] < 0) parent[k+1][i] = -1;
            else parent[k+1][i] = parent[k][parent[k][i]];
        }
}

int lca(int u, int v){
    //让u和v走到同一深度
    if(depth[u] > depth[v]) swap(u, v); //让v的深度深一些
    for(int k = 0; k < max_k; k++) {
        if ((depth[v] - depth[u]) >> k & 1) v = parent[k][v]; 
    }
    if (u == v) return u;
    //二分搜索计算LCA
    for (int k = max_k-1; k >= 0; k--) {
        if (parent[k][v] != parent[k][u]) {  //如果超过了他们的LCA也一定是一样的,不一样一定还没到LCA
            v = parent[k][v], u = parent[k][u];
        }
    }
    return parnet[0][u]; //?这里好像不太对?//
}

点分治

洛谷 P3806 POJ 1741

其他

二分图相关结论

定义:

匹配:在G中两两没有公共点的边集合M

边覆盖:G中任意顶点都至少是F中某条边的端点的边集合F

独立集:在G中两两互不相连的顶点集合S

顶点覆盖:G中的任意边都至少有一个端点属于S的顶点集合S

结论:

  • 对于无孤立点的图,|最大匹配|+|最小边覆盖| = |V|

  • |最大独立集|+|最小顶点覆盖| = |V|

  • 对于二分图,|最大匹配| = |最小顶点覆盖|

简单证明:最大匹配时是每一对匹配中,不可能2个点都连接着未匹配的点(不然的话就会有增广路径,最大匹配还可以更大),

所以2个点中最多一个点连接着未匹配的点,选择那个点作为最小定点覆盖即可

2-SAT

布尔方程的可满足性问题

合取范式: \((a\vee b\vee\dots)\wedge(c\vee d\vee\dots)\)

2-SAT问题:合取范式的每个子句的文字不超过2的布尔方程的可满足性问题

将每个 \(a\vee b\) 改写成 \((\urcorner a \Rightarrow b \wedge \urcorner b \Rightarrow a)\),以 \(\Rightarrow\)​ 关系为边建有向图,利用强连通分量分解

  • 如果存在 \(x\)\(\urcorner x\) ​​存在同一个强连通分量,无解

  • 否则,对于每个布尔变量 \(x\),如果 \(x\) 所在的强连通分量的拓扑序在 \(\urcorner x\) 之后,\(x\)​​ 为true

因为拓扑序在前的强连通分量可能存在通路到达在后的,所以在后的一定为 true

DFS序

当前结点的 in 和 out 包含了子树的所有结点

int in[N], out[N], tot = 0;
void dfs(int x, int dep) {
    in[x] = ++tot;
    for (int i = 0; i < es[x].size(); i++) dfs(es[x][i], dep + 1);
    out[x] = tot;
}