费用流

本文章遵守知识共享协议 CC-BY-NC-SA ,转载时须在文章的任一位置附上原文链接和作者署名(rickyxrc)。推荐在我的个人博客阅读。

简介

费用流是网络流的拓展,一般用于求解一些多次取点的问题。

前置知识

  • 网络流
  • 最短路

讲解

定义

在网络流中,在花费费用最小时的最大流。

如果有冲突,则优先满足最大流。

实现

大体逻辑和网络流区别不大,只是将算法中的 bfs 改为spfa或其他最短路算法,以费用为边权搜索最短路。

费用流算法可以基于 EKDinic 改来,下面这一版是基于 Dinic 更改而来的。

bool spfa(int s, int t) {
  memset(dis, 0x3f, sizeof(dis)); // 初始化距离数组
  memcpy(cur, lnk, sizeof(lnk)); // 复制数组状态
  queue<int> q;  // 正常的spfa
  q.push(s), dis[s] = 0, vis[s] = 1;
  while (!q.empty()) {
    int u = q.front();
    q.pop(), vis[u] = 0;
    for (int i = lnk[u]; i; i = nxt[i]) {
      int v = ter[i]; // 队首节点
      if (cap[i] && dis[v] > dis[u] + cost[i]) {
        dis[v] = dis[u] + cost[i]; // 注意这里是以费用为单位更新
        if (!vis[v]) q.push(v), vis[v] = 1;
      }
    }
  }
  return dis[t] != INF; // 代表是否可以到达汇点
}

int dfs(int u, int t, int flow) {
  if (u == t) return flow; // 搜索结束
  vis[u] = 1; // 标记已访问
  int ans = 0;
  for (int &i = cur[u]; i && ans < flow; i = nxt[i]) {
    int v = ter[i]; // 队首节点
    if (!vis[v] && cap[i] && dis[v] == dis[u] + cost[i]) {
      int x = dfs(v, t, min(cap[i], flow - ans)); // 
      if (x)
        ret += x * cost[i],  // 单位费用
        cap[i] -= x, // 正向边减流
        cap[i ^ 1] += x, ans += x; // 反向边增流
    }
  }
  vis[u] = 0; // 撤销标记状态,方便下一次
  return ans;
}

int solve(int s, int t) {
  int ans = 0;
  while (spfa(s, t)) {  // 这里进行了改动,将原先的bfs改成了spfa
    int x;
    while ((x = dfs(s, t, INF))) ans += x; // 一次dfs多次增流
  }
  return ans;
}

 // 最后费用为ret

例题

P1004-[NOIP2000-提高组]-方格取数

P2045 方格取数加强版

这道题的难点在于构建费用流网络,首先分析题目性质:

  • 一个格子只能取一次数字
  • 可以走多次

根据这些可以构建出第一个图:

在每个点的连接处连流量为 \(2\) ,费用为 \(-val_i\) 的边,然后跑最大流。

第一种思路

先别急着翻到下面去,这个构建方法显然是错误的,因为没有考虑到走的次数,并且一个点可能被统计多次。

所以便有了下面这种方法。

首先,我们需要拆S,T点并添加限流才能确保没问题。

还有一点就是像 1->1' 这里要连两条边,第一条容量 \(1\) 费用 \(0\) 确保联通,第二条容量 \(1\) 费用 \(-val_i\) 才是实际能取到的价值

最后构建出的图

最后的图就是这样。

其实不管是网络流,还是费用流,最难的部分都是构建图,需要多理解为什么这么构建才能过这道题,只要理解了,那这一类的题型都不是什么难事。

代码写的有点丑,见谅。

s = 0, t = 2*n*n + 1;
addedge(s, 1, k,0); // 源点连 (1,1)
addedge(2*n*n, t, k,0); // (n,n) 的出点连汇点
for(int i=1;i<=n*n;i++){ // 先输入
  scanf("%d",&val);
  addedge(i,i+n*n,1,-val); // 因为这个点只能取一次
  addedge(i,i+n*n,k-1,0); // 其他时候也需要联通
}
for(int i=1;i<=n*n;i++){ // 然后枚举每个点
  if((i - 1) / n + 1 != n)
    addedge(i+n*n,i+n,k,0); // 出点连下面的入点
  if((i - 1) % n + 1!=n)
    addedge(i+n*n,i+1,k,0); // 出点连左边的入点
}
printf("%d",dinic(s,t));

P1251 餐巾计划问题

这道题目的构建方法很新颖,单独提出来讲一下。

最大的问题是区分干净的餐巾和脏餐巾,这里可以将一天拆成早上和晚上。

首先要保证这里的流量,也就是每天早上的餐巾数量达标。

所以这里可以从每天早上向汇点连边,流量为当天需要的餐巾数量,费用为\(0\)

还要从每天早上到每天晚上连边,容量为当天需要的餐巾数量,费用为\(0\)

再根据题目的规则,很容易想到每天早上到下一天早上,每天晚上到下一天晚上连边,流量\(inf\),费用\(0\)(保留干净的餐巾和脏餐巾)。

然后考虑送洗,中间的变化是 脏餐巾 -> 干净餐巾,所以可以从每天晚上到 当前天数+快洗天数的早上 连一条边,流量\(inf\),费用为快洗费用,慢洗同理。

购买餐巾也很简单,就从源点往每天早上连边,流量\(inf\),费用为购买餐巾费用。

贴上核心代码:

int night(int i){return i+n;} // 每天晚上的编号
for (int i = 1; i <= n; i++)
{
  scanf("%d", &tmp); // 每天需求的餐巾数量
  addedge(i, t, tmp, 0); // 保证餐巾数量足够
  addedge(s, night(i), tmp, 0); // 使用完的餐巾变成脏餐巾
}
scanf("%d%d%d%d%d", &pp, &mm, &f, &nn, &ss);
for (int i = 1; i <= n; i++)
{
  addedge(s, i, INF, pp); // 购买餐巾
  if (i + 1 <= n)
  {
    addedge(i, i + 1, INF, 0); // 保留干净餐巾
    addedge(night(i), night(i + 1), INF, 0); // 保留脏餐巾
  }
  if (i + mm <= n)
    addedge(night(i), i + mm, INF, f); // 快洗
  if (i + nn <= n)
    addedge(night(i), i + nn, INF, ss); // 慢洗
}
posted @ 2022-12-25 16:45  rickyxrc  阅读(69)  评论(0)    收藏  举报