[暑假前集训]图论


2022年暑假前集训——图论

[题A]蜘蛛的网

解题思路

引入定义

图的边连通度:对于给定的无向图,如果至少删除\(K\)条边,可以使这个图不连通,则\(K\)称为图的边连通度;

无向图的割:有无向图\(G = (V, E)\),设\(C\)为图\(G\)中一些弧的集合,若从\(G\)中删去\(C\)中的所有弧能使图\(G\)不是连通图,称\(C\)\(G\)的一个割;

全局最小割:包含的弧的权和最小的割,称为全局最小割。

显然,对于弧权均为1的图,求出全局最小割的大小,即可求出图的边连通度

而对于全局最小割的大小,有Stoer-Wagner算法可以求解

PS. Stoer-Wagner算法学习结合OI Wiki一篇博客

代码
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

const int INF = 1e9;
int sumw[310], n, m, edg[310][310];
bool vis[310], dlt[310];
int stcut(int &s, int &t){
    memset(sumw, 0, sizeof(sumw));
    memset(vis, 0, sizeof(vis));
    int maxw, k, mincut;
    for(int i = 1; i <= n; i++){
        maxw = -1, k = -1;
        for(int j = 1; j <= n; j++){
            if(!dlt[j] && !vis[j] && sumw[j] > maxw) 
                k = j, maxw = sumw[j];
        }
        if(k == -1) return mincut;
        s = t, t = k, mincut = maxw, vis[k] = 1;
        for(int j = 1; j <= n; j++)
            if(!dlt[j] && !vis[j]) sumw[j] += edg[j][k];
    }
    return mincut;
}
int Stoer_Wager(){
    int mincut = INF, s, t;
    for(int i = 1; i < n; i++){
        mincut = min(mincut, stcut(s, t));
        dlt[t] = 1;
        for(int j = 1; j <= n; j++)
            if(!dlt[j]) edg[s][j] = (edg[j][s] += edg[t][j]);
    }
    return mincut;
}

int main(){
    ios::sync_with_stdio(false);
    cin >> n >> m;
    int u, v;
    for(int i = 1; i <= m; i++){
        cin >> u >> v;
        edg[u][v] = edg[v][u] = 1;
    }
    cout << Stoer_Wager() << endl;
    return 0;
}

[K题]居民都居住在房屋里

解题思路

给定图中任意两点间有且只有一条路径可达,显然给定的图是一颗

则问题即\(m\)次查询树上两点间距离,即两点到其公共祖先的距离之和

使用常用的倍增法求LCA

#include <iostream>
#include <cstdio>
using namespace std;

struct Edges{ int v, nxt; } edg[1000010]; int fir[500010], ct = 0;
void add(int u, int v){
    edg[++ct] = (Edges){v, fir[u]}, fir[u] = ct;
}

int dep[500010], fa[500010][22], lg[500010];
void dfs(int u, int frm){
    fa[u][0] = frm, dep[u] = dep[frm] + 1;
    for(int i = 1; i <= lg[dep[u]]; i++) fa[u][i] = fa[fa[u][i - 1]][i - 1];
    for(int i = fir[u]; i; i = edg[i].nxt) 
        if(edg[i].v != frm) dfs(edg[i].v, u);
}
int lca(int x, int y){
    if(dep[x] < dep[y]) swap(x, y);
    while(dep[x] > dep[y]) x = fa[x][lg[dep[x] - dep[y]] - 1];
    if(x == y) return x;
    for(int k = lg[dep[x]] - 1; k >= 0; k--)
        if(fa[x][k] != fa[y][k]) x = fa[x][k], y = fa[y][k];
    return fa[x][0];
}

int main(){
    int n, m, u, v, x, y, cofa;
    scanf("%d%d", &n, &m);
    for(int i = 1; i < n; i++){
        scanf("%d%d", &u, &v);
        add(u, v), add(v, u);
    }
    for(int i = 1; i <= n; i++) lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
    dfs(1, 0);
    for(int i = 1; i <= m; i++){
        scanf("%d%d", &x, &y), cofa = lca(x, y);
        printf("%d\n", dep[x] + dep[y] - 2 * dep[cofa]);
    }
    return 0;
}


[O题]纯白色的少年郎,如今只身在何方

解题思路

单源最短路模板
使用堆优化Dijkstra解决

代码
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;

typedef long long ll;
struct Edges{int v, w, nxt;}edg[200010]; int ct = 0, fir[100010];
struct Nodes{
    ll d; int id;
    bool operator < (const Nodes &a) const {return d > a.d;}
};
void add(int u, int v, int w){
    edg[++ct] = (Edges){v, w, fir[u]}, fir[u] = ct;
}
const ll INF = 1e18; ll dis[100010]; bool vis[100010];
priority_queue <Nodes> que;
void dijkstra(int s){
    dis[s] = 0, que.push((Nodes){0, s});
    while(!que.empty()){
        int u = que.top().id; que.pop(); if(vis[u]) continue; vis[u] = 1;
        for(int i = fir[u]; i; i = edg[i].nxt){
            int v = edg[i].v;
            if(dis[u] + (ll)edg[i].w < dis[v])
                dis[v] = dis[u] + (ll)edg[i].w, que.push((Nodes){dis[v], v});
        }
    }
}

int main(){
    int n, m, s, u, v, w;
    scanf("%d%d%d", &n, &m, &s);
    for(int i = 1; i <= m; i++) scanf("%d%d%d", &u, &v, &w), add(u, v, w);
    for(int i = 1; i <= n; i++) dis[i] = INF;
    dijkstra(s);
    for(int i = 1; i <= n; i++) 
        printf("%lld\n", dis[i] != INF ? dis[i] : -1);
    return 0;
}

[C题]魔法少女

解题思路

看作有一个\((N+1)*(M+1)\)的方阵图,每个小方形的顶点为图中的点,对角线为图中的边。
左上角点序号为\(1\),右下角点序号为\((N+1)*(M+1)\)
对于读入的符号,看作权值为0的边,而未读入的对角线看作权值为1的边,
在上述图中求起点到终点的最短路即为所求答案。

代码
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;

struct Edges{int v, w, nxt;}edg[1000010]; int ct = 0, fir[300010];
struct Nodes{
    int d, id;
    bool operator < (const Nodes &a) const {return d > a.d;}
};
void add(int u, int v, int w){
    edg[++ct] = (Edges){v, w, fir[u]}, fir[u] = ct;
    edg[++ct] = (Edges){u, w, fir[v]}, fir[v] = ct;
}
int dis[300010]; const int INF = 1e9; bool vis[300010];
priority_queue <Nodes> que;
void dijkstra(){
    dis[1] = 0; que.push((Nodes){0, 1});
    while(!que.empty()){
        int u = que.top().id; que.pop(); if(vis[u]) continue; vis[u] = 1;
        for(int i = fir[u]; i; i = edg[i].nxt){
            int v = edg[i].v;
            if(dis[u] + edg[i].w < dis[v])
                dis[v] = dis[u] + edg[i].w, que.push((Nodes){dis[v], v});
        }
    }
}

int main(){
    int n, m; char c;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            scanf(" %c", &c);
            add((i - 1) * (m + 1) + j, i * (m + 1) + j + 1, (int)c == 92 ? 0 : 1);
            add(i * (m + 1) + j, (i - 1) * (m + 1) + j + 1, c == '/' ? 0 : 1);
        }
    }
    int tag = (n + 1) * (m + 1);
    for(int i = 1; i <= tag; i++) dis[i] = INF;
    dijkstra();
    if(dis[tag] != INF) printf("%d\n", dis[tag]);
    else printf("NO SOLUTION\n");
    return 0;
}

[L题]最小生成树

解题思路

实际上是最小生成树计数问题
首先考虑生成树计数问题,需要引入矩阵树定理

\(Matrix-Tree \ Theorem:\)设图\(G=(V,E),\)构造\(G\)的拉普拉斯矩阵\(L,\)\(G\)的生成树的个数等于\(detL_0,\)其中\(L_0\)是去掉\(L\)\(i\)行第\(i\)列的子矩阵(\(i\)任意)。
其中\(L(G)\)是一个\(n\times n\)矩阵(\(n\)为节点数),且

\[L_{i,j}= \begin{cases} -m_{i,j}, \ i\ne j\ (m_{i,j}是v_i与v_j之间的边数)\\ deg(v_i),\ i=j \end{cases}\]

详细证明见知乎文章,其中引理\(Binet-Cauchy\ 定理\)的证明见百度百科

然后考虑最小生成树的性质,若按照边的权值大小将边分为若干组,在\(Kruskal\)求最小生成树的过程中,从每一组边中选择的边数必相等,且每处理完一组边,将这组中被选择的边加入图中后,图的连通性是固定的

那么考虑\(Kruska\)l的过程,从最小权值的边集开始,将这些边全部加入图中,形成若干连通块,对这若干连通块分别利用矩阵树定理求最小生成树的数量(记为\(t_i\)),然后将每一个连通块缩点,再处理权值第二小的边集,重复上述过程,直至\(Kruskal\)结束。则$ \prod t_i$即为全图最小生成树个数。

\(Ps.\)求矩阵的行列式要用到高斯消元法,时间复杂度为\(O(n^3)\),由于题目要求取模,且计算过程涉及到除法,正常情况下要用到乘法逆元。本题中模数为合数,部分数无逆元,所以用辗转相除法消元

  • 缩点过程用并查集实现,遍历连通块过程中将所有点指向同一父节点,后续加边过程中将连通块内点用这一父节点代替
  • 对于高斯消元的相关学习见某博客
代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;

struct Edges{ int u, v, w; }e[1010];
struct realEdges{ int v, w, nxt; } edg[2020]; int ct = 0, fir[1010];
int n, m, L[110][110], num, fa[110], pos[110]; const int p = 10000;
bool cmp(Edges a, Edges b){ return a.w < b.w; }
int find(int x){ return (fa[x] ? fa[x] = find(fa[x]) : x); }
void add(int i){
    int u = find(e[i].u), v = find(e[i].v);
    edg[++ct] = (realEdges){v, e[i].w, fir[u]}, fir[u] = ct;
    edg[++ct] = (realEdges){u, e[i].w, fir[v]}, fir[v] = ct;
} 
void dfs(int u, int w, int fu){
    if(u != fu) fa[u] = fu;
    pos[u] = num++;
    for(int i = 0; i <= pos[u]; i++) L[i][pos[u]] = L[pos[u]][i] = 0;
    for(int i = fir[u]; edg[i].w == w; i = edg[i].nxt){
        int v = edg[i].v;
        if(pos[v] == -1) dfs(v, w, fu);
        L[pos[u]][pos[v]]--, L[pos[u]][pos[u]]++;
    }
}
int solve(int L[110][110], int n){
    int det = 1; n--;
    for(int i = 0; i < n; i++){
        for(int j = i + 1; j < n; j++){
            while(L[j][i]){
                int t = L[i][i] / L[j][i];
                for(int k = i; k < n; k++) 
                    L[i][k] = (L[i][k] - 1ll * L[j][k] * t % p + p) % p;
                swap(L[i], L[j]), det *= -1;
            }
            if(!L[i][i]) return 0;
        }
        det = 1ll * det * L[i][i] % p;
    }
    return (det + p) % p;
}
int ans = 1;
void Divide(){
    for(int i = 0; i < m; ){
        int j = i;
        while(j < m && e[j].w == e[i].w) j++;
        for(int k = i; k < j; k++) add(k);
        for(int k = 1; k <= n; k++) pos[k] = -1;
        for(int k = 1; k <= n; k++) if(find(k) == k){
            num = 0, dfs(k, e[i].w, k);
            ans = ans * solve(L, num) % p;
        }
        i = j;
    }
}

int main(){
    ios::sync_with_stdio(false);
    int u, v, w;
    cin >> n >> m;
    for(int i = 0; i < m; i++) cin >> e[i].u >> e[i].v >> e[i].w;
    sort(e, e + m, cmp);
    Divide();
    int t = 0;
    for(int i = 1; i <= n; i++) t += (find(i) == i);
    printf("%d\n", t > 1 ? 0 : ans);
    return 0;
}


[J题]tarjan

解题思路

\(tarjan\)求割点、桥以及点双的问题模板
\(tarjan\)\(dfs\)过程中,当前节点为\(u\),目标子节点为\(v\),所在树根节点为\(root\)

\(low_v \ge dfn_u \land u \ne root\),则\(u\)为割点,所维护栈中从栈顶到\(v\)的点及点\(u\)构成点双
若存在两个及以上子节点满足\(low_v \ge dfn_u\),则\(root\)为割点
\(low_v > dfn_u\)则边\(u \to v\)是桥

代码
#include <iostream>
#include <cstdio>
using namespace std;
struct Edges { int v, nxt; }edg[1000010]; int ct = 0, fir[1010];
void add(int u, int v) { edg[++ct] = (Edges){v, fir[u]}, fir[u] = ct; }
int dfn[1010], low[1010], fa[1010], vn, en, col[1010];
int sta[1000010], til, cirmax, cirn, cir[1010], t2, e[1010][1010];
void countcir(){
    int ret = 0;
    for(int i = 1; i <= t2 - 1; i++){
        for(int j = i + 1; j <= t2; j++){
            if(e[cir[i]][cir[j]]) ret++;
        }
    }
    cirmax = max(cirmax, ret);
}
void tarjan(int u, int rt, int k){
    dfn[u] = low[u] = ++ct; int flg = 0;
    for(int i = fir[u]; i; i = edg[i].nxt){
        int v = edg[i].v; if(v == fa[u]) continue;
        if(!dfn[v]){
            fa[v] = u, sta[++til] = i;
            tarjan(v, rt, i), low[u] = min(low[u], low[v]);
            en += (low[v] > dfn[u]);
            if(low[v] >= dfn[u]){
                t2 = 0, flg++;
                while(sta[til] != i) cir[++t2] = edg[sta[til--]].v;
                til--, cir[++t2] = v, cir[++t2] = u, countcir(), cirn++;
            }
        }else low[u] = min(low[u], dfn[v]);
    }
    vn += (u == rt && flg > 1 || u != rt && flg);
}
        
int main(){
    ios::sync_with_stdio(false);
    int n, m, u, v;
    cin >> n >> m;
    for(int i = 1; i <= m; i++){
        cin >> u >> v;
        add(u, v), add(v, u);
        e[u][v] = e[v][u] = 1;
    }
    ct = 0;
    for(int i = 1; i <= n; i++) if(!dfn[i]) tarjan(i, i, 0);
    cout << vn << ' ' << en << ' ' << cirn << ' ' << cirmax << endl;
    return 0;
}
posted @ 2022-04-18 11:00  skyliyu  阅读(32)  评论(0编辑  收藏  举报