NOIP复习——图论模板

最大流

\(\texttt{dinic}\) 算法,时间复杂度 \(O(n^2m)\),处理二分图匹配问题的复杂度是 \(O(m\sqrt{n})\)

dinic算法
// 最大流,dinic算法
// 最大流,dinic算法
namespace MF {
int n, m, S, T, dep[N];
int head[N], cur[N], nex[M << 1], e[M << 1], tot = 1;
i64 w[M << 1];
void add(int x, int y, i64 z) {
    nex[++tot] = head[x];
    head[x] = tot;
    e[tot] = y;
    w[tot] = z;
}
void link(int x, int y, i64 z) {
    add(x, y, z);
    add(y, x, 0);
}
bool bfs() {
    fill(dep, dep + n + 1, 0); // 注意此处的范围!!!
    queue<int> q;
    q.push(S);
    dep[S] = 1;
    while (!q.empty()) {
        int x = q.front();
        q.pop();
        for (int i = head[x]; i; i = nex[i]) {
            int y = e[i];
            if (w[i] && !dep[y]) { // 忽略 w == 0 的边
                dep[y] = dep[x] + 1;
                q.push(y);
            }
        }
    }
    return dep[T] != 0;
}
i64 dfs(int x, i64 flow) {
    if (x == T) {
        return flow;
    }
    i64 res = 0;
    for (int &i = cur[x]; i; i = nex[i]) {
        int y = e[i];
        if (dep[y] == dep[x] + 1) {
            i64 tem = dfs(y, min(flow, w[i]));
            w[i] -= tem;
            w[i ^ 1] += tem;
            flow -= tem;
            res += tem;
            if (!flow) {
                break;
            }
        }
    }
    return res;
}
i64 Flow() {
    i64 maxf = 0;
    while (bfs()) {
        for(int i = 0; i <= n; ++i) {
            cur[i] = head[i]; // 注意此处的范围!!!
        }
        maxf += dfs(S, INF);
    }
    return maxf;
}
}

最小费用最大流

与最大流一样,使用 \(\texttt{dinic}\) 算法,将 BFS 找增广路替换为使用 SPFA,最小化代价。

dinic算法+spfa
// 最小费用最大流,dinic算法,其中bfs找增广路改为spfa
// 最小费用最大流,dinic算法,其中bfs找增广路改为spfa
namespace MCMF {
int n, m, S, T, vis[N];
i64 dis[N];
int head[N], cur[N], nex[M << 1], e[M << 1], tot = 1;
i64 w[M << 1], c[M << 1];
void add(int x, int y, i64 val, i64 cos) {
    nex[++tot] = head[x];
    head[x] = tot;
    e[tot] = y;
    w[tot] = val;
    c[tot] = cos;
}
void link(int x, int y, i64 val, i64 cos) {
    add(x, y, val, cos);
    add(y, x, 0, -cos);
}
bool spfa() {
    for(int i = 0; i <= n; ++i) { // 注意此处范围
        dis[i] = INF;
        vis[i] = 0;
    }
    dis[S] = 0;
    vis[S] = 1;
    queue<int> q;
    q.push(S);
    while(!q.empty()) {
        int x = q.front();
        q.pop();
        vis[x] = 0;
        for(int i = head[x]; i; i = nex[i]) {
            int y = e[i];
            if(w[i] && dis[y] > dis[x] + c[i]) {
                dis[y] = dis[x] + c[i];
                if(!vis[y]) {
                    vis[y] = 1;
                    q.push(y);
                }
            }
        }
    }
    return dis[T] != INF;
}
i64 dfs(int x, i64 flow) {
    if(x == T) {
        return flow;
    }
    vis[x] = 1; // 对于y,dis[y] = dis[x] + c[i]的点x可能有很多,而y只需要被遍历到1次
    i64 res = 0;
    for(int &i = cur[x]; i; i = nex[i]) {
        int y = e[i];
        if(!vis[y] && w[i] && dis[y] == dis[x] + c[i]) { // 注意!vis[y]
            i64 tem = dfs(y, min(flow, w[i]));
            w[i] -= tem;
            w[i ^ 1] += tem;
            flow -= tem;
            res += tem;
            if(!flow) {
                break;
            }
        }
    }
    vis[x] = 0;
    return res;
}
pair<i64, i64> Flow() {
    i64 maxf = 0, minc = 0;
    while(spfa()) {
        for(int i = 0; i <= n; ++i) { // 注意此处范围
            cur[i] = head[i];
        }
        i64 f = dfs(S, INF);
        maxf += f;
        minc += f * dis[T];
    }
    return {maxf, minc};
}
}

tarjan算法

关于 \(\texttt{tarjan}\) 算法,一定要分有向图和无向图来讨论。

有向图的写法:更新 \(low[x]\) 的时候需要保证 \(y\) 在栈中。

有向图
void tarjan(int x) {
    dfn[x] = low[x] = ++dfntot;
    stk[++top] = x;
    instk[x] = 1;
    for(int i = head[x]; i; i = nex[i]) {
        int y = e[i];
        if(!dfn[y]) {
            tarjan(y);
            low[x] = min(low[x], low[y]);
        } else if(instk[y]) { // y 在栈中
            low[x] = min(low[x], dfn[y]);
        }
    }
}

无向图的写法:需要注意重边,比如求边双的时候,是不能进行去重的,需要处理一下。另外更新 \(low[x]\) 的时候不需要判断 \(y\) 是否在栈中。

无向图
void tarjan(int x, int from) { // from 表示 fa[x]->x 这条边的编号,边从 2 开始编号
    dfn[x] = low[x] = ++dfntot;
    stk[++top] = x;
    for(int i = head[x]; i; i = nex[i]) {
        int y = e[i];
        if(!dfn[y]) {
            tarjan(y, i);
            low[x] = min(low[x], low[y]);
        } else if(i != (from ^ 1)) { // 处理重边
            low[x] = min(low[x], dfn[y]);
        }
    }
}

为什么更新 \(low[x]\) 的时候不需要判断 \(y\) 是否在栈中?

结论:在 \(\texttt{tarjan}\) 算法更新 \(low[x]\) 的时候,对于 \(dfn[y]\not=0\)\(y\)\(y\) 一定在栈中。
证明:考虑反证,假设 \(y\) 不在栈中,由于 \(dfn[y]\not=0\)\(y,x\) 联通,\(y\) 应该会扩展到 \(x\),这说明 \(y\)\(x\) 在树上的祖先。而此时 \(x\) 还没有处理完,则 \(y\) 一定在栈中,与假设矛盾。

我们也可以验证一下这个结论,具体见 这个提交记录,注意第 \(27\) 行的 assert,发现结论正确。

割点和割边(无向图)

【模板】割点
【模板】割边

割点:如果把一个点删除后,图中连通块的个数增加了,那么这个点就是割点。
割边:如果把一条边删除后,图中连通块的个数增加了,那么这条边就是割边。

割点的判断:对整个图跑 \(\texttt{tarjan}\) 算法,假设当前搜索树的根是 \(rt\)。对于 \(rt\) 本身,它是割点等价于它有 \(\ge 2\) 个儿子;而对于 \(x(x\not=rt)\),它是割点等价于它存在一个儿子 \(y\) 使得 \(low[y]\ge dfn[x]\)

割点——部分代码
void tarjan(int x, int fa) { // fa == 0 说明 x 是当前搜索树的根
    dfn[x] = low[x] = ++cnt;
    int son = 0;
    for(int i = head[x]; i; i = nex[i]) {
        int y = e[i];
        if(!dfn[y]) {
            tarjan(y, x);
            low[x] = min(low[x], low[y]);
            ++son;
            if((!fa && son >= 2) || (fa && low[y] >= dfn[x])) {
                tag[x] = 1;
            }
        } else if(y != fa) {
            low[x] = min(low[x], dfn[y]);
        }
    }
}

割边的判断:原理与判断割点类似,只需将 low[y] >= num[x] 改为 low[y]>num[x] 即可。并且不需要特判根节点的情况。

割边——部分代码
void tarjan(int x, int fa) {
    dfn[x] = low[x] = ++cnt;
    for(int i = head[x]; i; i = nex[i]) {
        int y = e[i];
        if(!dfn[y]) {
            tarjan(y, x);
            low[x] = min(low[x], low[y]);
            if(low[y] > dfn[x]) {
                tag[i] = tag[i ^ 1] = 1;
            }
        } else if(y != fa) {
            low[x] = min(low[x], dfn[y]);
        }
    }
}

点双和边双(无向图)

【模板】点双连通分量
【模板】边双连通分量

点双连通分量:点双连通子图指一个没有割点的子图,点双连通分量是一个无向图中的极大点双连通子图。
边双连通分量:边双连通子图指一个没有割边的子图,边双连通分量是一个无向图中的极大边双连通子图。

求点双:注意到点双之间至多有一个公共点,并且这个公共点一定是割点。在求割点的过程中,用栈保存搜索树上的点,如果 \(x\) 的儿子 \(y\) 满足 \(low[y]\ge dfn[x]\),那么 \(x\)\(y\) 子树内的点属于同一个点双,不断退栈将 \(y\) 子树内的点计入同一个点双即可。注意只需要将 \(y\) 的子树退栈,并且不需要退 \(x\),因为 \(x\) 可以属于多个点双。

点双——部分代码
void tarjan(int x) {
    dfn[x] = low[x] = ++dfntot;
    if(!deg[x]) { // 特判单独的点 
        vec[++cnt].push_back(x);
    }
    stk[++top] = x;
    instk[x] = 1;
    for(int i = head[x]; i; i = nex[i]) {
        int y = e[i];
        if(!dfn[y]) {
            tarjan(y);
            low[x] = min(low[x], low[y]);
            if(low[y] >= dfn[x]) {
                ++cnt; // 点双个数 
                int tem;
                do {
                    tem = stk[top--];
                    instk[tem] = 0;
                    vec[cnt].push_back(tem); // 统计第cnt个点双中的点 
                } while(tem != y); // 将y的子树退栈
                vec[cnt].push_back(x); // x 不退栈
            }
        } else if(instk[y]) {
            low[x] = min(low[x], dfn[y]);
        }
    }
}

求边双:如果 \(dfn[x]=low[x]\),那么 \(x-fa[x]\) 是割边,不断退栈到 \(x\),这些点在同一个边双中。
另一种方法:既然边双中没有割边,那么删掉所有割边,剩下的每个连通块都是边双。

边双——部分代码
void dfs(int x, int from) {
    dfn[x] = low[x] = ++dfntot;
    stk[++top] = x;
    for(int i = head[x]; i; i = nex[i]) {
        int y = e[i];
        if(!dfn[y]) {
            dfs(y, i);
            low[x] = min(low[x], low[y]);
        } else if(i != (from ^ 1)) {
            low[x] = min(low[x], dfn[y]);
        }
    }
    if(dfn[x] == low[x]) {
        ++cnt;
        int y;
        do {
            y = stk[top--];
            vec[cnt].push_back(y);
        } while(y != x);
    }
}

强连通分量(有向图)

【模板】缩点

部分代码
void tarjan(int x) {
    dfn[x] = low[x] = ++dfntot;
    stk[++top] = x, instk[x] = 1;
    for(int i = head[x]; i; i = nex[i]) {
        int y = e[i];
        if(!dfn[y]) {
            tarjan(y);
            low[x] = min(low[x], low[y]);
        } else if(instk[y]) {
            low[x] = min(low[x], dfn[y]);
        }
    }
    if(dfn[x] == low[x]) {
        ++cnt;
        int y;
        do {
            y = stk[top--];
            instk[y] = 0;
            bel[y] = cnt; // y 属于第 cnt 个强连通分量
        } while(y != x);
    }
}

点分治

一般用于树上路径统计。

每次选择当前子树的重心作为根,统计经过根的贡献,然后递归分治根的所有儿子。

由于每次的根都是重心,所以每次递归会让子树大小减半,设计算经过单点的复杂度是 \(O(T)\),则总复杂度是 \(O(T\log{n})\)

【模板】点分治:边带权,判断树上是否有长度为 \(k\) 的路径。
洛谷P4178 Tree:边带权,计算树上长度 \(\le k\) 的路径数量。

【模板】点分治
#include<bits/stdc++.h>

using namespace std;

const int N = 1e5 + 5, M = 1e7 + 5, inf = 1.1e9;

int n, m, q[105], maxK;

vector<pair<int, int>> adj[N];

int siz[N], maxsiz[N], root, tot, vis[N];

void getRoot(int x, int fa) {
    siz[x] = 1;
    maxsiz[x] = 0;
    for(auto [y, w] : adj[x]) {
        if(vis[y] || y == fa) {
            continue;
        }
        getRoot(y, x);
        siz[x] += siz[y];
        maxsiz[x] = max(maxsiz[x], siz[y]);
    }
    maxsiz[x] = max(maxsiz[x], tot - siz[x]);
    if(maxsiz[x] < maxsiz[root]) {
        root = x;
    }
}

int dis[N], path[N], pathtot;

void getDis(int x, int fa) {
    if(dis[x] <= maxK) {
        path[++pathtot] = dis[x];
    }
    for(auto [y, w] : adj[x]) {
        if(vis[y] || y == fa) {
            continue;
        }
        dis[y] = dis[x] + w;
        getDis(y, x);
    }
}

int exist[M], ans[105];

void calc(int x) {
    vector<int> used;
    for(auto [y, w] : adj[x]) {
        if(vis[y]) {
            continue;
        }
        dis[y] = w;
        pathtot = 0;
        getDis(y, x);
        for(int i = 1; i <= pathtot; ++i) {
            for(int j = 1; j <= m; ++j) {
                if(q[j] >= path[i]) {
                    ans[j] |= exist[q[j] - path[i]];
                }
            }
        }
        for(int i = 1; i <= pathtot; ++i) {
            exist[path[i]] = 1;
            used.push_back(path[i]);
        }
    }
    for(auto c : used) {
        exist[c] = 0;
    }
}

void solve(int x) {
    vis[x] = exist[0] = 1;
    calc(x);
    for(auto [y, w] : adj[x]) {
        if(vis[y]) {
            continue;
        }
        root = 0;
        tot = siz[y];
        getRoot(y, x);
        solve(root);
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> m;
    for(int i = 1, u, v, w; i < n; ++i) {
        cin >> u >> v >> w;
        adj[u].push_back({v, w});
        adj[v].push_back({u, w});
    }
    for(int i = 1; i <= m; ++i) {
        cin >> q[i];
        maxK = max(maxK, q[i]);
    }
    maxsiz[0] = inf;
    tot = n;
    getRoot(1, 1);
    solve(root);
    for(int i = 1; i <= m; ++i) {
        cout << (ans[i] ? "AYE" : "NAY") << "\n";
    }
    return 0;
}

2-SAT 问题

给定若干变量 \(x_1,x_2,\dots,x_n\),每个变量有两个取值 \(true\)\(false\),还有一些限制条件,如 \(\neg x_i\or x_j\)

判断是否有解,有解指存在一种给所有变量赋值的方法,使得所有限制条件都被满足。
如果有解,需要构造一组解。

这就是经典的 \(2-SAT\) 问题,我们将每个变量 \(x_i\) 拆为两个点 \(x_i\)\(\neg x_i\),分别表示 \(x_i=true/false\) 的状态。然后再根据限制条件建立有向边,如 \(\neg x_i\or x_j\) 可以转化为 \(x_i\rightarrow x_j\)\(\neg x_j\rightarrow \neg x_i\) 两条边。

然后用 \(\texttt{tarjan}\) 进行缩点,根据 强连通分量的定义,可以发现当 \(x_i\)\(\neg x_i\) 在同一个强连通分量中时,如果取 \(x_i=true\) 则会推出矛盾,而取 \(x_i=false\) 仍然会推出矛盾,因此无解。

否则所有的强连通分量会构成一个 \(DAG\)此时 \(x_i\)\(\neg x_i\) 的拓扑序谁大就取谁,就可以构造出一组解。这是因为如果取拓扑序小的,则有可能沿有向边走到拓扑序大的,就推出矛盾了;而取拓扑序大的一定不会走到拓扑序小的,一定合法。

技巧 + 大坑:\(\texttt{tarjan}\) 缩点之后,强连通分量的标号就是拓扑序(标号为 \(k\) 的强连通分量会比 \(k+1\) 更早退栈),所以不用再写一个拓扑排序。并且也不要写拓扑排序,在图有多个拓扑起点的时候,直接令所有的起点拓扑序为 \(1\) 在一些题中是错误的!比如 [POI2001] 和平委员会 的样例。

【模板】2-SAT问题

模板题代码
#include<bits/stdc++.h>

using namespace std;

const int N = 2e6 + 5;

int n, m, x[N], nx[N], ans[N];

int head[N], nex[N], e[N], tot = 1;
void add(int x, int y) {
    nex[++tot] = head[x];
    head[x] = tot;
    e[tot] = y;
}

int dfn[N], low[N], dfntot, stk[N], instk[N], top, bel[N], cnt, in[N], topn[N];
vector<int> adj[N];
void tarjan(int x) {
    dfn[x] = low[x] = ++dfntot;
    stk[++top] = x;
    instk[x] = 1;
    for(int i = head[x]; i; i = nex[i]) {
        int y = e[i];
        if(!dfn[y]) {
            tarjan(y);
            low[x] = min(low[x], low[y]);
        } else if(instk[y]) {
            low[x] = min(low[x], dfn[y]);
        }
    }
    if(dfn[x] == low[x]) {
        int y;
        ++cnt;
        do {
            y = stk[top--];
            instk[y] = 0;
            bel[y] = cnt;
        } while(y != x);
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> m;
    for(int i = 1; i <= n; ++i) {
        x[i] = i;
        nx[i] = i + n;
    }
    for(int ii = 1; ii <= m; ++ii) {
        int i, a, j, b;
        cin >> i >> a >> j >> b;
        // x[i] = a or y[j] = b
        if(a && b) {
            add(nx[i], x[j]);
            add(nx[j], x[i]);
        } else if(a && !b) {
            add(nx[i], nx[j]);
            add(x[j], x[i]);
        } else if(!a && b) {
            add(x[i], x[j]);
            add(nx[j], nx[i]);
        } else { // !a && !b
            add(x[i], nx[j]);
            add(x[j], nx[i]);
        }
    }
    // 注意图中有 2n 个点
    for(int i = 1; i <= 2 * n; ++i) {
        if(!dfn[i]) {
            tarjan(i);
        }
    }
    for(int i = 1; i <= n; ++i) {
        if(bel[x[i]] == bel[nx[i]]) {
            cout << "IMPOSSIBLE\n";
            return 0;
        }
    }
    cout << "POSSIBLE\n";
    for(int x = 1; x <= 2 * n; ++x) {
        for(int i = head[x]; i; i = nex[i]) {
            int y = e[i];
            if(bel[x] != bel[y]) {
                adj[bel[x]].push_back(bel[y]);
                ++in[bel[y]];
            }
        }
    }
    queue<int> q;
    for(int i = 1; i <= cnt; ++i) {
        if(!in[i]) {
            q.push(i);
            topn[i] = 1; // 拓扑序
        }
    }
    while(!q.empty()) {
        int x = q.front();
        q.pop();
        for(auto y : adj[x]) {
            if(--in[y] == 0) {
                topn[y] = topn[x] + 1;
                q.push(y);
            }
        }
    }
    for(int i = 1; i <= n; ++i) {
        cout << (topn[bel[x[i]]] > topn[bel[nx[i]]]) << " \n"[i == n];
    }
    return 0;
}
posted @ 2022-10-10 20:27  hzy1  阅读(30)  评论(0编辑  收藏  举报