【专题总结】网络流与二分图(持续更新)

本文解析了三个经典的图论问题:POJ2112挤奶机分配问题、HDU3315棋子对战策略问题及HDU3395鱼群攻击行为问题。通过最小费用最大流算法实现解决方案,并详细介绍了建图方法和核心代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

POJ 2112

大意

农夫约翰有 k 台挤奶机和 c 只奶牛。任意两个实体(挤奶机或奶牛)都在不同的地点,因此它们之间有相隔距离。每个挤奶机可以为 m 只奶牛挤奶。问怎样分配挤奶机使得任意两个实体之间的最长距离最小(每个奶牛都要分配到挤奶机,题目保证有解)。

思路

显然这是最小化最大值问题,适合用二分查找解决。于是我们可以二分最小值 x ,看看在最长距离小于等于 x 的情况下是否有满足要求的分配方案。于是问题转化成了分配问题,根据题目给出的数据范围我们可以尝试用网络流来解决。建图方式如下:

  • 因为每个挤奶机都只能供 m 只牛使用,所以想到建立超级源点,从超级源点到代表挤奶机的节点连接容量为 m 的边。
  • 因为每个挤奶机可以供距其距离小于 x 的牛使用,所以想到为距其距离小于 x 的奶牛对应的节点连容量为 1 的边。
    • 因为我们要判定是否有合法的分配方案,所以想到建立超级汇点,从每个奶牛对应的节点连接一条容量为1的边。

    然后对这个图求最大流,若最大流值等于奶牛的数量的话存在合法的分配方案,否则不存在。

    另外题中的两两实体之间的距离用多源最短路的算法就能够求出来了。

    代码

    #include <cstdio>
    #include <cstring>
    #include <queue>
    #include <vector>
    #include <algorithm>
    using namespace std;
    
    const int maxn = 300, INF = 1e8;
    int n, s, t, l, r, K, C, M, mid;
    int G[maxn][maxn], d[maxn][maxn];
    
    // 最大流用的边
    struct edge {
        int from, to, cap;
        edge(int from, int to, int cap): from(from), to(to), cap(cap) {}
    };
    
    // 求最大流的Dinic算法
    struct dinic {
        int level[maxn];
        int iter[maxn];
        vector <edge> edges;
        vector <int> G[maxn];
        void addedge(int u, int v, int w) {
            G[u].push_back(edges.size());
            edges.push_back(edge(u, v, w));
            G[v].push_back(edges.size());
            edges.push_back(edge(v, u, 0));
        }
        void init(int n) {
            edges.clear();
            for(int i = 1; i <= n; i++) {
                G[i].clear();
            }
        }
        bool bfs(int s, int t) {
            queue <int> q;
            q.push(s);
            memset(level, -1, sizeof(level));
            level[s] = 0;
            while(!q.empty()) {
                int u = q.front();
                q.pop();
                for(int i = 0; i < G[u].size(); i++) {
                    edge& e = edges[G[u][i]];
                    if(e.cap > 0 && level[e.to] < 0) {
                        level[e.to] = level[u] + 1;
                        q.push(e.to);
                    }
                }
            }
            return level[t] > 0;
        }
        int dfs(int u, int t, int f) {
            if(u == t) {
                return f;
            }
            for(int& i = iter[u]; i < G[u].size(); i++) {
                edge& e = edges[G[u][i]];
                if(e.cap > 0 && level[e.to] > level[u]) {
                    int d = dfs(e.to, t, min(f, e.cap));
                    if(d > 0) {
                        e.cap -= d;
                        edges[G[u][i]^1].cap += d;
                        return d;
                    }
                }
            }
            level[u] = -1;
            return 0;
        }
        int max_flow(int s, int t) {
            int flow = 0;
            for(; bfs(s, t);) {
                memset(iter, 0, sizeof(iter));
                for(int f; (f = dfs(s, t, INF)) > 0; flow += f);
            }
            return flow;
        }
    }o;
    
    // 求多源最短路的floyd算法
    void floyd() {
        for(int k = 1; k <= n; k++) {
            for(int i = 1; i <= n; i++) {
                for(int j = 1; j <= n; j++) {
                    if(d[i][k] == INF || d[k][j] == INF) {
                        continue;
                    }
                    d[i][j] = min(d[i][j], d[i][k] + d[j][k]);
                }
            }
        }
    }
    
    // 二分查找中用于判断最长距离x是否合法的函数
    bool ok(int x) {
        o.init(n);
        for(int i = 1; i <= K; i++) {
            o.addedge(s, i, M);
        }
        for(int i = 1; i <= K; i++) {
            for(int j = K + 1; j <= K + C; j++) {
                if(d[j][i] > x) {
                    continue;
                }
                o.addedge(i, j, 1);
            }
        }
        for(int i = K + 1; i <= K + C; i++) {
            o.addedge(i, t, 1);
        }
        int f = o.max_flow(s, t);
        if(f < C) {
            return false;
        }
        return true;
    }
    
    int main() {
        scanf("%d%d%d", &K, &C, &M);
        n = K + C;
        for(int i = 1; i <= n; i++) {
            for(int j = 1; j <= n; j++) {
                scanf("%d", &G[i][j]);
                d[i][j] = G[i][j] ? G[i][j] : (i == j ? 0 : INF);
            }
        }
        floyd();
        s = 0;
        t = K + C + 1;
        l = -1;
        r = INF;
        // 二分查找
        while(r - l > 1) {
            mid = (l + r) >> 1;
            if(ok(mid)) {
                r = mid;
            }
            else {
                l = mid;
            }
        }
        printf("%d\n", r);
        return 0;
    }

    HDU 3315

    大意

    在一次游戏中,我方和对方各有 n 枚棋子。对方棋子出战顺序是固定的,而我方棋子的出战顺序是任意的(有一个初始顺序但可以改动)。每个棋子有攻击力和生命值两个属性,与对方棋子战斗的时候,每回合可以让对方棋子的生命值减少其攻击力的值。当我方棋子第 i 次出战时战胜对方棋子,我方可赢得 vi 分数,否则则输掉 vi 分数。问怎样安排我方棋子的出战顺序,使得我方得到的分数最大,还要求出顺序改动的百分比,当分数同样时优先选择顺序改动最小的方案。

    思路

    这是一个匹配问题,由于在匹配的情况下还要求某个权值最大,因此可以选择的模型有最小费用最大流和最大权匹配。这里用最小费用最大流解决。建图方式如下:

    • 因为双方棋子可以任意配对,所以从我方棋子到对方所有棋子连边,以保证双方每两个棋子之间都有边相连。
    • 因为对战结果是可以预测的,所以每条边的费用是此边相连的两个棋子能给我方带来的分数的相反数(因为只能求最小费用)。
    • 因为还要知道顺序改动的情况,所以先将每条边的费用乘 1000 ,如果这条边连接的对手棋子的编号与我方棋子的初始编号相同的话将边权减 1 (本应该加 1 ,但是因为边权要取相反数所以变为减 1 )以示奖励。乘 1000 的原因就是为了不让奖励分数影响到主要分数。
    • 建立超级源点和超级汇点,超级源点到所有我方棋子之间都要连边,对方棋子到所有超级汇点之间也都要连边,这些边的费用为 0
    • 所有边的流量都为 1

    然后对图求最小费用最大流。其中的最小费用为 ans 。那么我方能够获得的最大分数就是 ans/1000 。改变的顺序就是 ans\mathchoicemod1000

    代码

    #include <bits/stdc++.h>
    using namespace std;
    
    const int maxn = 100, maxv = 3 * maxn, maxVal = 1000, INF = 1e7;
    int c, n, s, t, N, ans, score, similar;
    int v[maxn], h[maxn], p[maxn], a[maxn], b[maxn];
    
    // 图中的边
    struct edge {
        int from, to, cap, flow, cost;
        edge(int from, int to, int cap, int flow, int cost):
            from(from), to(to), cap(cap), flow(flow), cost(cost) {}
    };
    
    // 最小费用最大流算法
    struct MCMF {
        int v;
        vector <edge> edges;
        vector <int> G[maxv];
        int inq[maxv];                  // 是否在队列中
        int d[maxv];                    // Bellman-Ford
        int p[maxv];                    // 上一条弧
        int a[maxv];                    // 可改进量
        void init(int v) {
            this->v = v;
            for(int i = 0; i < v; i++) {
                G[i].clear();
            }
            edges.clear();
        }
        void addEdge(int from, int to, int cap, int cost) {
            G[from].push_back(edges.size());
            edges.push_back(edge(from, to, cap, 0, cost));
            G[to].push_back(edges.size());
            edges.push_back(edge(to, from, 0, 0, -cost));
        }
        bool BellmanFord(int s, int t, int& flow, int& cost) {
            for(int i = 0; i < v; i++) {
                d[i] = INF;
            }
            memset(inq, 0, sizeof(inq));
            d[s] = 0;
            inq[s] = 1;
            p[s] = 0;
            a[s] = INF;
            queue <int> q;
            q.push(s);
            while(!q.empty()) {
                int u = q.front();
                q.pop();
                inq[u] = 0;
                for(int i = 0; i < G[u].size(); i++) {
                    edge& e = edges[G[u][i]];
                    if(e.cap > e.flow && d[e.to] > d[u] + e.cost) {
                        d[e.to] = d[u] + e.cost;
                        p[e.to] = G[u][i];
                        a[e.to] = min(a[u], e.cap - e.flow);
                        if(!inq[e.to]) {
                            q.push(e.to);
                            inq[e.to] = 1;
                        }
                    }
                }
            }
            if(d[t] == INF) return false; // s-t不连通,失败退出
            flow += a[t];
            cost += d[t] * a[t];
            for(int u = t; u != s; u = edges[p[u]].from) {
                edges[p[u]].flow += a[t];
                edges[p[u]^1].flow -= a[t];
            }
            return true;
        }
        int Mincost(int s, int t) {
            int flow = 0, cost = 0;
            while(BellmanFord(s, t, flow, cost));
            return cost;
        }
    }o;
    
    // 用于判断胜负的函数
    inline int beat(int x, int y) {
        int tx = h[x] / b[y] + (h[x] % b[y] > 0);
        int ty = p[y] / a[x] + (p[y] % a[x] > 0);
        return tx >= ty;
    }
    
    int main() {
        while(scanf("%d", &n), n) {
            // 输入部分
            for(int i = 1; i <= n; i++) {
                scanf("%d", &v[i]);
            }
            for(int i = 1; i <= n; i++) {
                scanf("%d", &h[i]);
            }
            for(int i = 1; i <= n; i++) {
                scanf("%d", &p[i]);
            }
            for(int i = 1; i <= n; i++) {
                scanf("%d", &a[i]);
            }
            for(int i = 1; i <= n; i++) {
                scanf("%d", &b[i]);
            }
            // 建图部分
            s = 0;
            t = 2 * n + 1;
            N = t + 1;
            o.init(N);
            for(int i = 1; i <= n; i++) {
                o.addEdge(s, i, 1, 0);
            }
            for(int i = 1; i <= n; i++) {
                o.addEdge(n + i, t, 1, 0);
            }
            for(int i = 1; i <= n; i++) {
                for(int j = 1; j <= n; j++) {
                    c = (beat(i, j) ? -1 : 1) * maxVal * v[i] - (i == j);
                    o.addEdge(i, n + j, 1, c);
                }
            }
            // 计算结果部分
            ans = o.Mincost(s, t);
            score = -ans / maxVal;
            similar = -ans % maxVal;
            if(score > 0) {
                printf("%d %.3f%%\n", score, 100.0 * similar / n);
            }
            else {
                puts("Oh, I lose my dear seaco!");
            }
        }
        return 0;
    }

    HDU 3395

    大意

    n 个鱼,它们会攻击认为是异性的鱼,然后被攻击的鱼就会产卵。每只鱼有一个权值,鱼卵的权值为该鱼卵的双亲的权值得异或和。问所有鱼卵的权值之和是多少?

    思路

    该问题也是一个类似于分配的问题。由于题目中涉及“最大权值之和”,因此适合建模为二分图带权匹配或最小费用最大流(将原权值变为相反数)。这里采用最小费用最大流的方法,建图方法如下:

    • 根据题给矩阵,只要一只鱼认为另一只鱼是异性,那么就给这两条鱼连一条容量为 1 ,权值为两只鱼权值异或和的相反数的边。注意,这里将一只鱼拆成两个点,分别代表攻击者和被攻击者。
      • 因为每条鱼只能攻击一次,所以建立超级源点,从超级源点向所有代表攻击者的点连容量为 1 ,边权为 0 的边。
      • 因为每条鱼只能被攻击一次,所以建立超级汇点,从每个被攻击者对应的点向超级汇点连容量为 1 ,边权为 0 的边。
      • 因为最小费用最大流算法先满足最大流然后才考虑最小费用,因此一定要让图满流(还要不影响权值)。方法是让所有攻击者向超级汇点连容量为 1 ,权值为 0 的边。(即使有鱼没有攻击机会,仍然能够满流)

      然后对图求最小费用最大流。假设最小费用是 ans ,那么答案就是 ans

      代码

      #include <cstdio>
      #include <cstring>
      #include <vector>
      #include <queue>
      #include <algorithm>
      using namespace std;
      
      const int maxn = 1010, INF = 1e6;
      
      // 图中的边
      struct edge {
          int from, to, cap, flow, cost;
          edge(int from, int to, int cap, int flow, int cost):
              from(from), to(to), cap(cap), flow(flow), cost(cost) {}
      };
      
      // 最小费用最大流算法
      struct MCMF {
          int v;
          vector <edge> edges;
          vector <int> G[maxn];
          int inq[maxn];             // 是否在队列中
          int d[maxn];               // Bellman-Ford
          int p[maxn];               // 上一条弧
          int a[maxn];               // 可改进量
          void init(int v) {
              this->v = v;
              for(int i = 0; i < v; i++) {
                  G[i].clear();
              }
              edges.clear();
          }
          void addEdge(int from, int to, int cap, int cost) {
              G[from].push_back(edges.size());
              edges.push_back(edge(from, to, cap, 0, cost));
              G[to].push_back(edges.size());
              edges.push_back(edge(to, from, 0, 0, -cost));
          }
          bool BellmanFord(int s, int t, int& flow, int& cost) {
              for(int i = 0; i < v; i++) {
                  d[i] = INF;
              }
              memset(inq, 0, sizeof(inq));
              d[s] = 0;
              inq[s] = 1;
              p[s] = 0;
              a[s] = INF;
              queue <int> q;
              q.push(s);
              while(!q.empty()) {
                  int u = q.front();
                  q.pop();
                  inq[u] = 0;
                  for(int i = 0; i < G[u].size(); i++) {
                      edge& e = edges[G[u][i]];
                      if(e.cap > e.flow && d[e.to] > d[u] + e.cost) {
                          d[e.to] = d[u] + e.cost;
                          p[e.to] = G[u][i];
                          a[e.to] = min(a[u], e.cap - e.flow);
                          if(!inq[e.to]) {
                              q.push(e.to);
                              inq[e.to] = 1;
                          }
                      }
                  }
              }
              if(d[t] == INF) return false; // s-t不连通,失败退出
              flow += a[t];
              cost += d[t] * a[t];
              for(int u = t; u != s; u = edges[p[u]].from) {
                  edges[p[u]].flow += a[t];
                  edges[p[u]^1].flow -= a[t];
              }
              return true;
          }
          int Mincost(int s, int t) {
              int flow = 0, cost = 0;
              while(BellmanFord(s, t, flow, cost));
              return cost;
          }
      }o;
      
      char G[maxn][maxn];
      int n, s, t, ans, v[maxn];
      
      int main() {
          while(scanf("%d", &n), n) {
              for(int i = 1; i <= n; i++) {
                  scanf("%d", &v[i]);
              }
              for(int i = 1; i <= n; i++) {
                  scanf("%s", G[i] + 1);
              }
              // 建图部分
              s = 0;
              t = 2 * n + 1;
              o.init(2 * n + 2);
              for(int i = 1; i <= n; i++) {
                  o.addEdge(s, i, 1, 0);
              }
              // 为了满流连的边
          for(int i = 1; i <= n; i++) {
              o.addEdge(i, t, 1, 0);
          }
              for(int i = 1; i <= n; i++) {
                  for(int j = 1; j <= n; j++) {
                      if(G[i][j] == '0') {
                          continue;
                      }
                      o.addEdge(i, n + j, 1, - (v[i] ^ v[j]));
                  }
              }
              for(int i = 1; i <= n; i++) {
                  o.addEdge(n + i, t, 1, 0);
              }
              ans = o.Mincost(s, t);
              printf("%d\n", -ans);
          }
          return 0;
      }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值