网络流学习

网络流

以下笔记大多数都是从董晓算法抄的。大家可以看看董晓算法,然后刷题就是经典的 网络流 24 题

网络:一种特殊的有向图 G = ( V , E ) G = (V,E) G=(V,E),不同之处在于容量和源汇点。

c(u,v) 表示边(u,v)的容量, ( u , v ) ∉ E , c ( u , v ) = 0 (u,v)\notin E,c(u,v) = 0 (u,v)/E,c(u,v)=0

V 有两种特殊的点:源点 s 和汇点 t ( s ≠ t ) (s\not= t) (s=t)

f(x,y) 表示(x,y) 的流量。

可行流满足性质:

  1. 容量限制:对于每条边,流经该边的流量不得超过该边的容量,即 0 ≤ f ( u , v ) ≤ c ( u , v ) 0 \le f(u,v) \le c(u,v) 0f(u,v)c(u,v)

  2. 流守恒性:除源汇点外,任意结点 u 的净流量为 0。其中,我们定义 u 的净流量为 f ( u ) = ∑ x ∈ V f ( u , x ) − ∑ x ∈ V f ( x , u ) f(u) = \sum_{x\in V}f(u,x) - \sum _{x\in V} f(x,u) f(u)=xVf(u,x)xVf(x,u)

    $\sum_{(s,v)\in E} f(s,v) $ 称为整个网络的流量

最大流: 从源点流向汇点的最大流量

增广路:一条从源点到汇点的所有边的剩余容量≥0的路径

残留网:由网络中所有结点和剩余容量大于0的边构成的子图,这里的边包括有向边和其反向边。

luoguP3376

最大流 EK 算法

复杂度:O(nm 2 ^2 2) n=1000,m=10000 也不会卡掉

struct edge{LL v,c,ne;}e[M];
int h[N],idx = 1; // 从2,3 开始配对
LL mf[N],pre[N];
void  add(int a,int b,int c)
{
	e[++idx] = {b,c,h[a]};
    h[a] = idx;
}
add(a,b,c);add(b,a,0);
bool bfs() // 找增广路
{
    memset(mf,0,sizeof mf);
    queue<int> q;
    q.push(S);mf[S] = 1e9;
    while(q.size())
    {
        int u = q.front();q.pop();
        for(int i=h[u];i;i=e[i].ne)
        {
            LL v = e[i].v;
            if(mf[v]=0&&e[i].c)
            {
                mf[v] = min(mf[u],e[i].c);
                pre[v] = i; // 存前驱边
                q.push(v);
                if(v==T)
                    return true;
			}
		}
	}
    return false ;
    
}
LL EK() // 累加可行流
{
    LL flow = 0;
    while(bfs())
    {
        int v= T;
        while(v!=S) // 更新残留网
        {
            int i= pre[v];
            e[i].c -= mf[T];
            e[i^1].c +=mf[T];
			v =e[i^1].v;
        }
        flow +=mf[T];
	}
    return flow;
    
}

最大流 Dinic 算法 宽搜和深搜

dinic 一次可以累加多条增广路的流量

深度 d[u] : 存 u 点所在的图层

当前弧 cur[u] : 存 u 点的当前出边

时间复杂度 : O ( n 2 m ) O(n^2m) O(n2m) 宽搜 O ( n ) O(n) O(n) 最多更新 O ( n ) O(n) O(n) 个点,dfs O ( m ) O(m) O(m) n = 1e5,m=1e6

struct edge {LL v,c,ne;}e[M];
int h[N],idx = 1;
int d[N],cur[N]; 
// d[i]:i节点的深度,cur[u]:u节点的出边,弧优化使得每次不是从h[u] 开始。

void add(int a,int b,int c)
{
    e[++idx] = {b,c,h[a]};
    h[a] = idx;
}
bool bfs() // 对点分层,找增广路
{
    memset(d,0,sizeof d);
    queue<int> q;
    q.push(S);d[S] = 1;
    while(q.size())
    {
        int u = q.front();
        q.pop();
        for(int i=h[u];i;i=e[i].ne)
        {
            int v = e[i].v;
            if(d[v] == 0 && e[i].c)
            {
                d[v] = d[u] +1;
                q.push(v);
                if(v==T) 
                    // 为什么可以直接返回? 因为比当前节点小的都搜索过了,直接标记和他相连的也可以转移。
                    return true;
            }
        }
	}
    return false ;
}
LL dfs(int u,LL mf) //mf 走到u 的剩余流量 多路增广
{
    if(u == T) return mf ;
    LL sum = 0;
    
    for(int i=cur[u];i;i = e[i].ne) 
    {
        cur[u] = i;
        // 弧优化,使得每次一搜索都从最后一次没有用完的弧开始
        int v = e[i].v;
        if(d[v] == d[u]+1 && e[i].c)
        {
            LL f = dfs(v,min(mf,e[i].c));
            //去找该点往后的最大流更新回来
            e[i].c -= f;
            e[i^1].c += f;
            sum += f;
            mf -= f;
            //还剩多少流量
            if(mf ==0) break; 
            // 说明从前面一个走到当前的流量都用完了
        }
	}
    if(sum ==0 )d[u] = 0; 
    //残枝优化,可以使得下一次不再访问,踢出图层
    //sum = 0 说明该点往后最大流为0,所以不需要再次走到该点。
    
    return sum;
    
}
LL dinic() // 累加可行流
{
    LL flow = 0;
    while(bfs())
    {
        memcpy(cur,h,sizeof h); // 重新赋值。
        flow += dfs(S,1e9);
    }
    return flow;
}

最小割 dinic P1344

割: 对于一个网络 G = ( V , E ) G = (V,E) G=(V,E) ,我们切一刀,将所有的点划分为 S 和 T 两个集合,称为割 ( S , T ) (S,T) (S,T) 其中源点 s ∈ S s\in S sS ,汇点 t ∈ T t\in T tT,对于任意一个点都会使得网络断流

割的容量: c(s,t) 代表所有从 S 到 T 的出边容量之和

最小割: 求一个割(S,T) 使得 c ( s , t ) c(s,t) c(s,t) 最小

不唯一

最大流最小割定理:

$f(s,t){max} = c(s,t){min} $

struct edge {LL v,c,ne;}e[M];
int h[N],idx = 1;
int d[N],cur[N]; 
// d[i]:i节点的深度,cur[u]:u节点的出边,弧优化使得每次不是从h[u] 开始。
int vis[N];

void add(int a,int b,int c)
{
    e[++idx] = {b,c,h[a]};
    h[a] = idx;
}
bool bfs() // 对点分层,找增广路
{
    memset(d,0,sizeof d);
    queue<int> q;
    q.push(S);d[S] = 1;
    while(q.size())
    {
        int u = q.front();
        q.pop();
        for(int i=h[u];i;i=e[i].ne)
        {
            int v = e[i].v;
            if(d[v] == 0 && e[i].c)
            {
                d[v] = d[u] +1;
                q.push(v);
                if(v==T) 
                    // 为什么可以直接返回? 因为比当前节点小的都搜索过了,直接标记和他相连的也可以转移。
                    return true;
            }
        }
	}
    return false ;
}
LL dfs(int u,LL mf) //mf 走到u 的剩余流量 多路增广
{
    if(u == T) return mf ;
    LL sum = 0;
    
    for(int i=cur[u];i;i = e[i].ne) 
    {
        cur[u] = i;
        // 弧优化,使得每次一搜索都从最后一次没有用完的弧开始
        int v = e[i].v;
        if(d[v] == d[u]+1 && e[i].c)
        {
            LL f = dfs(v,min(mf,e[i].c));
            //去找该点往后的最大流更新回来
            e[i].c -= f;
            e[i^1].c += f;
            sum += f;
            mf -= f;
            //还剩多少流量
            if(mf ==0) break; 
            // 说明从前面一个走到当前的流量都用完了
        }
	}
    if(sum ==0 )d[u] = 0; 
    //残枝优化,可以使得下一次不再访问,踢出图层
    //sum = 0 说明该点往后最大流为0,所以不需要再次走到该点。
    
    return sum;
    
}
LL dinic() // 累加可行流
{
    LL flow = 0;
    while(bfs())
    {
        memcpy(cur,h,sizeof h); // 重新赋值。
        flow += dfs(S,1e9);
    }
}
void mincut(int u) // 标记点,1 代表在 S 集合,0代表 T 集合
{
    vis[u] = 1;
    for(int i=h[u];i;i=e[i].ne)
    {
        int  v = e[i].v;
        if(!vis[v] && e[i].c) mincut(v);
	}
}
int main()
{
    cin>>n>>m;
    S=1,T=n;
    for(int i=1;i<=m;i++)
    {
        cin>>a[i]>>b[i]>>c;
        add(a[i],b[i],c);
        add(b[i],a[i],0);
    }
    cout<<dinic()<<endl;
    // 最小割的划分
    mincut(S);
    for(int i=1;i<=n;i++)
    {
        if(!vis[i]) cout<<i<<' ';
    }
    cout<<endl;
    for(int i =1;i<=n;i++)
    {
        if(vis[i]) cout<<i<<' ';
	}
    cout<<endl;
    // 最小割的边数
    idx = 0;
    memset(h,0,sizeof h);
    for(int i=1;i<=m;i++)
    {
        add(a[i],b[i],1);
        add(b[i],a[i],0);
	}
    cout<<dinic()<<endl;
}

费用流 EK\dinic算法 P3381

给定一个网络 G = ( V , E ) G = (V,E) G=(V,E) ,每条边有容量限制 c ( u , v ) c(u,v) c(u,v) , 还有单位流量限制 w ( u , v ) w(u,v) w(u,v)

( u , v ) (u,v) (u,v) 的流量为 f ( u , v ) f(u,v) f(u,v) 时,需要花费 f ( u , v ) × w ( u , v ) f(u,v)\times w(u,v) f(u,v)×w(u,v) 的费用

除了容量外多了一个费用属性,一条边上的费用=流量×单位费用,找到流最大的同时,费用最少的一种。只用把EK算法里的BFS换成SPFA;

注意: 如果图上存在单位费用为负的圈(负环),无法求出该网络的最小费用最大流,此时需要使用 消圈算法 消去图上的负圈。

时间复杂度: O ( n m f ) O(nmf) O(nmf) f为流量,上界为 O ( n 3 2 n / 2 ) O(n^32^{n/2}) O(n32n/2)

#define int long long 
const int MAXN =5000+10;
const int MAXM =50000 +10;
#define INF 1e18  
int head[MAXN],cnt = 1;
struct Edge
{
    int to,w,c,next;
}edges[MAXM*2];
inline void add(int from,int to,int w,int c)
    //w:流量,c:费用加边的时候要双向建边,并且另一边为流量为0,费用为-c
{
    edges[++cnt].to = to;
    edges[cnt].w = w;
    edges[cnt].c = c;
    edges[cnt].next = head[from];
    head[from] = cnt;
}
int n,m,s,t,last[MAXN],flow[MAXN],inq[MAXN],dis[MAXN];
queue<int> Q;

bool SPFA()
{
    while(!Q.empty())
        Q.pop();
    memset(last,-1,sizeof(last));
    memset(inq,0,sizeof(inq));
    memset(dis,127,sizeof(dis));
    flow[s] = INF;
    dis[s] = 0;
    Q.push(s);
    while(!Q.empty())
    {
        int p = Q.front();
        Q.pop();
        inq[p] = 0;
        for(int eg = head[p];eg!=0;eg=edges[eg].next)
        {
            int to = edges[eg].to,vol = edges[eg].w;
            if(vol>0&&dis[to]>dis[p]+edges[eg].c)
            {
                last[to] = eg;
                flow[to] = min(flow[p],vol);
                dis[to] = dis[p] + edges[eg].c; 
                //找到走到 T 点的最短路。在更新残留网的时候再去计算权值
                if(!inq[to])
                {
                    Q.push(to);
                    inq[to] = 1;
				}
			}
		}
	}
    return last[t] != - 1;
    
}
// 由于有flow 限制,所以不需要判断负权,而如果判负权的时候,可以用数组记录一个点如果出现n次,那么就会存在负环。
int maxflow,mincost;

void MCMF()
{
    while(SPFA())
    {	
        maxflow += flow[t];
        mincost += dis[t]*flow[t];
        for(int i=t;i!=s;i = edges[last[i]^1].to)
        {
            edges[last[i]].w -= flow[t];
            edges[last[i]^1].w += flow[t];
		}
	}

}

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>

const int N = 5e3 + 5, M = 1e5 + 5;
const int INF = 0x3f3f3f3f;
int n, m, tot = 1, lnk[N], cur[N], ter[M], nxt[M], cap[M], cost[M], dis[N], ret;
bool vis[N];

void add(int u, int v, int w, int c) {
  ter[++tot] = v, nxt[tot] = lnk[u], lnk[u] = tot, cap[tot] = w, cost[tot] = c;
}

void addedge(int u, int v, int w, int c) { add(u, v, w, c), add(v, u, 0, -c); }

bool spfa(int s, int t) {
  memset(dis, 0x3f, sizeof(dis));
  memcpy(cur, lnk, sizeof(lnk));
  std::queue<int> q;
  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, std::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 mcmf(int s, int t) {
  int ans = 0;
  while (spfa(s, t)) {
    int x;
    while ((x = dfs(s, t, INF))) ans += x;
  }
  return ans;
}

int main() {
  int s, t;
  scanf("%d%d%d%d", &n, &m, &s, &t);
  while (m--) {
    int u, v, w, c;
    scanf("%d%d%d%d", &u, &v, &w, &c);
    addedge(u, v, w, c);
  }
  int ans = mcmf(s, t);
  printf("%d %d\n", ans, ret);
  return 0;
}

网络流24题

P1251 餐巾计划问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

该题刚刚开始感觉没有想法,肯定是知道源点要和每一天连点,然后流量为 +inf ,然后花费为 p,然后每一天的点都要和 汇点连一个流量为 r i r_i ri 且费用为 0 0 0的边,然后我们肯定也知道对于拿去快洗和慢洗要从 i i i 转移到 i + n , i + m i+n,i+m i+n,i+m 但是不能直接从原来的点去连,因为原来的点一定要转移到汇点,这样分流显然很怪。

那应该怎么做呢?

其实可以分点,看题目会发现,其实对于早上我们要处理 干净毛巾,对于晚上我们要处理 肮脏的毛巾,那么其实我们对一天分成两个点,一个点表示干净的。分成两个点后就可以简单的建模了,具体看代码中的注释。

#include<bits/stdc++.h>
using namespace std;
#define int long long  
#define inf 0x3f3f3f3f3f3f3f3f
const int M = 4e5+10;
int n;
struct node 
{
	int v,c,w,nex;
}e[M];
const int MN = 4e3+10;
int N;

int head[MN],idx = 1;
void add(int u,int v,int c,int w)
{
	e[++idx] = {v,c,w,head[u]};
	head[u] = idx;
}
int r[MN];
int S,T;
int last[MN],flow[MN],inq[MN],dis[MN];
queue<int> Q;

bool SPFA()
{
	memset(last,-1,sizeof last);
	memset(dis,0x3f,sizeof dis);
	memset(inq,0,sizeof inq);
	flow[S] = inf;
	dis[S] = 0;
	Q.push(S);
	while(!Q.empty())
	{
		int p = Q.front();
		Q.pop();
		inq[p] = 0;
		for(int i = head[p];i!=0;i=e[i].nex)
		{
			int v = e[i].v;
			int vol = e[i].c;
			if(vol >0 && dis[v] > dis[p] + e[i].w)
			{	
				
				last[v] = i;
				flow[v] = min(flow[p],vol);
				dis[v] = dis[p] + e[i].w;
				if(!inq[v])
				{
				inq[v] = 1;
				Q.push(v);
				}
			}
		}
	}
	return last[T] != -1;
	
}
int maxflow,mincost;

void MCMF()
{	
	while(SPFA())
	{	

		maxflow += flow[T];
		mincost += flow[T]*dis[T];

		for(int i = T;i!=S;i = e[last[i]^1].v)
		{
			e[last[i]].c -= flow[T];
			e[last[i]^1].c += flow[T];
		}
	}

}

/*
一共要六条边,每个点要分成两个点
	连边					流量		费用
1.第i晚上的连汇点 			r_i		0
2.源点与第 i 白天			+inf	p
3.源点与第 i 晚上			r_i		0
4.第	i天晚上 第i+m天白天	+inf	f
5.第	i天晚上 第i+n天白天	+inf	s
6.第	i天晚上 第i+1天晚上	+inf	0
//为什么要第 6条呢,因为任意两天之间也可以留下的
//那为什么我们补存储干净毛巾呢?
因为干净毛巾总是需要多余的花费,而我们连脏毛巾不需要。
*/

signed main (){
	std::ios::sync_with_stdio(false);  
	cin.tie(NULL); 
	cout.tie(NULL);
	
	cin>>N;
	for(int i=1;i<=N;i++)
		cin>>r[i];
	int p,m,f,n,s;
	cin>>p>>m>>f>>n>>s;
	S = 0,T = 2*N+2;
	//1.第i晚上的连汇点 			r_i		0
	for(int i=1;i<=N;i++)
	{
		add(i,T,r[i],0);
		add(T,i,0,0);
	}
	//2.源点与第 i 白天			+inf	p
	for(int i=1;i<=N;i++)
	{
		add(S,i,inf,p);
		add(i,S,0,-p);
	}
	//3.源点与第 i 晚上			r_i		0
	for(int i=1;i<=N;i++)
	{
		add(S,i+N,r[i],0);
		add(i+N,S,0,0);
	}
	//4.第	i天晚上 第i+m天白天	+inf	f
	for(int i=1;i<=N;i++)
	{	if(i+m<=N)
		{
			add(i+N,i+m,inf,f);
			add(i+m,i+N,0,-f);
		}
	}
	//5.第	i天晚上 第i+n天白天	+inf	s
	for(int i=1;i<=N;i++)
	{	if(i+n <= N)
		{
			add(i+N,i+n,inf,s);
			add(i+n,i+N,0,-s);
		}
	}
	//6.第	i天晚上 第i+1天晚上	+inf	0
	for(int i=1;i<=N;i++)
	{	if(i+1<=N)
		{
			add(i+N,i+N+1,inf,0);
			add(i+N+1,i+N,0,0);
		}
	}
	MCMF();
	cout<<mincost<<endl;
	
	
} 

[P2754 CTSC1999] 家园 / 星际转移问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

该题其实很想太空站拿来建立点,然后建源点和地球连接,汇点和月球连接,但是不知道怎么建边,此时我们可以在dinic 的时候边dinic 边建立边,当最大流 ≥ \ge 人数的时候,就停止输出天数即可,但是我们可能不知道,可能没有解呢? 那其实可以先用并查集判断是否连通即可。然后我们时间设置最多多少呢? 因为有 13个空间站,他们间的连边最多 1 3 2 13^2 132 然后可能从一条边到另一条还要 13 的时间,所以最坏的是 1 3 2 ≈ 2000 13^2 \approx 2000 1322000 然后设置时间2000即可。

#include<bits/stdc++.h>
using namespace std;
#define int long long  
/*
该题会发现是按照时间周期性的转移,那么我们就可以while 
判断是否可以到第 月球,可以用并查集判断。

*/
const int inf = 0x3f3f3f3f;
const int N =500000;
int n,m,k;
struct node 
{
	int v,c,nex;
}e[N];
int head[N],idx = 1;
int S,T;

int fa[N];
int h[N],r[N];
vector<int> s[N];
int cur[N];
int d[N];
void add(int u,int v,int c)
{
	e[++idx] = {v,c,head[u]};
	head[u] = idx;
}
int findfa(int x)
{
	if(fa[x] == x)
		return x;
	return fa[x] = findfa(fa[x]);
}
bool bfs()
{
	memset(d,0,sizeof d);
	queue<int> q;
	q.push(S);
	d[S] = 1;
	while(!q.empty())
	{
		auto c = q.front();
		q.pop();
		for(int i=head[c];i;i = e[i].nex)
		{	
			int v = e[i].v;
			if(e[i].c> 0 && !d[v])
			{
				d[v] = d[c] +1;
				q.push(v);
				if(v==T)
					return true;
			}
		}
	}
	return false ;
}
int dfs(int u,int mf)
{
	if(u==T) return mf;
	int sum = 0;
	for(int i=cur[u];i;i = e[i].nex)
	{	cur[u] = i;
		int v = e[i].v;
		if(d[v] == d[u] +1 && e[i].c)
		{
			int f = dfs(v,min(mf,e[i].c));
			e[i].c -= f;
			e[i^1].c +=f;
			sum += f;
			mf -=f;
			if(mf == 0) break;
		}
	}
	if(sum ==0)
		d[u] = 0;
	return sum;
}
int dinic()
{
	int flow = 0;
	while(bfs())
	{
		memcpy(cur,head,sizeof head);
		flow += dfs(S,1e9);
	}
	return flow ;
	
}
signed main (){
	std::ios::sync_with_stdio(false);  
	cin.tie(NULL); 
	cout.tie(NULL);
	
	cin>>n>>m>>k;
	n+=2;
	// 2 地球 1 月球
	for(int i=1;i<=m;i++)
	{
		cin>>h[i]>>r[i];
		for(int j=1;j<=r[i];j++)
		{
			int x;
			cin>>x;
			s[i].push_back(x + 2);
		}
	}
	for(int i=1;i<=n;i++)
	{
		fa[i] = i;
	}
	for(int i=1;i<=m;i++)
	{
		for(int j=1;j<r[i];j++)
		{
			fa[findfa(s[i][j])] = findfa(s[i][0]);
		}
	}
	if(findfa(1) != findfa(2))
	{
		cout<<0<<endl;
		return 0;
		
	}
	
	int flow = 0;
	int day = 0;
	S =0 ,T = 40000;
	
	// 地球与源点分开,地球的点很散,全连到源点就知道从哪开始
	// 
	//同理月球和汇点
	while(1)
	{
		
		//先连接源点和汇点
		add(S,day*n+2,inf);
		add(day*n+2,S,0);
		
		add(day*n+1,T,inf);
		add(T,day*n+1,0);
		
		// 先将当前的每个点连到前面一天的每个点
		if(day !=0)
		{
			for(int i=1;i<=n;i++)
			{
				add((day-1)*n + i,day*n+i,inf);
				add(day*n+i,(day-1)*n+i,0);
			}
			for(int i=1;i<=m;i++)
			{	//当前点在哪
				int now = s[i][day%r[i]];
				int ls = s[i][(day-1+ r[i]) %r[i]];
				
				add((day-1)*n + ls,day*n + now,h[i]);
				add(day*n + now,(day-1)*n + ls,0);
			}
			
		}
		flow += dinic();
		//cout<<flow<<endl;
		
		if(flow >= k)
		{
			cout<<day<<endl;
			return 0;
		}
		day++;
		
		
	}
} 

P2756 飞行员配对方案问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

用dinic 跑二分图匹配即可。

#include<bits/stdc++.h>
using namespace std;
#define ll long long  
const int M = 100000;
struct node 
{
	int from,v,c,nex;
}e[M];
const int N = 200+10;

int idx = 1;
int head[N];
int S,T;
void add(int u,int v,int c)
{
	e[++idx] = {u,v,c,head[u]};
	head[u] = idx;
}
int d[N];
bool bfs()
{
	memset(d,0,sizeof d);
	queue<int> q;
	d[S] = 1;
	q.push(S);
	while(!q.empty())
	{
		auto u = q.front();
		q.pop();
		for(int i=head[u];i;i = e[i].nex)
		{
			int v = e[i].v;
			if(e[i].c && !d[v])
			{
				d[v] = d[u] +1;
				q.push(v);
				if(v==T)
				{	
					return true;
				}
			}
		}
	}
	return false ;
}
int cur[N];
ll dfs(int u,int mf)
{
	if(u == T) return mf;
	int sum = 0;
	for(int i = cur[u];i;i = e[i].nex)
	{	cur[u] = i;
		int v = e[i].v;
		if(d[v] == d[u]+1 && e[i].c)
		{
			ll f = dfs(v,min(mf,e[i].c));
			e[i].c -= f;
			e[i^1].c +=f;
			mf -=f;
			sum += f;
			if(mf == 0)
				break ;
		}
	}
	if(sum == 0)
		d[u] = 0;
	return sum ;
}
ll dinic()
{
	ll flow = 0;
	while(bfs())
	{
		memcpy(cur,head,sizeof head);
		flow += dfs(S,1e9);
	}
	return flow ;
}
signed main (){
	std::ios::sync_with_stdio(false);  
	cin.tie(NULL); 
	cout.tie(NULL);
	int m,n;
	cin>>m>>n;
	S = 0 ,T = n+1;
	int u,v;
	
	while(cin>>u>>v&& (u!=-1&&v!=-1))
	{	
		add(u,v,1);
		add(v,u,0);
	}
	
	for(int i=1;i<=m;i++)
	{
		add(S,i,1);
		add(i,S,0);
	}
	for(int i=m+1;i<=n;i++)
	{
		add(i,T,1);
		add(T,i,0);
	}
	cout<<dinic()<<endl;
	for(int i=2;i<=idx;i+=2)
	{
		if(e[i].from !=S && e[i].v !=T && e[i].c == 0)
		{
			cout<<e[i].from<<' '<<e[i].v<<endl;
		}
	}

} 

P2761 软件补丁问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

和网络流没关系。。。

跑一下最短路就行了。

#include<bits/stdc++.h>
using namespace std;
#define int long long  
const int N = 1<<20;
const int M = 100+10;
array<int,5> e[M];
int dis[N];
int vis[N];

signed main (){
	std::ios::sync_with_stdio(false);  
	cin.tie(NULL); 
	cout.tie(NULL);
	int n,m;
	cin>>n>>m;
	
	for(int i=1;i<=m;i++)
	{	
		int w;
		cin>>w;
		int u1 = 0,u2=0,v1 = 0,v2 = 0;
		string a,b;
		cin>>a>>b;
		for(int j=0;j<n;j++)
		{
			if(a[j] == '-')
				u2|=(1<<j);
			else if(a[j]=='+')
				u1|=(1<<j);
		}
		for(int j=0;j<n;j++)
		{
			if(b[j]=='-')
				v1|=(1<<j);
			else if(b[j] =='+')
				v2|=(1<<j);
		}
		
		//cout<<u1<<' '<<u2<<" "<<v1<<' '<<v2<<endl;
		
		e[i] = {u1,u2,v1,v2,w};
	}
	priority_queue<pair<int,int>,vector<pair<int,int> > ,greater<pair<int,int> > > q;
	memset(dis,0x3f,sizeof dis);
	
	q.push({0,(1<<n) - 1});
	dis[(1<<n) - 1]= 0;
	
	while(!q.empty())
	{
		auto [w,c] = q.top();
		q.pop();

		for(int i=1;i<=m;i++)
		{	
			
			if((c&e[i][0]) == e[i][0] && (c&e[i][1]) == 0 && dis[(c^(c&e[i][2]))|e[i][3]] > dis[c] + e[i][4])
			{
				dis[(c^(c&e[i][2]))|e[i][3]] = dis[c] +e[i][4];
				q.push({dis[(c^(c&e[i][2]))|e[i][3]],(c^(c&e[i][2]))|e[i][3]});
			}
		}
	}
	if(dis[0] == 0x3f3f3f3f3f3f3f3f)
		cout<<0<<endl;
	else
		cout<<dis[0]<<endl;
	
	
		
} 

P2762 太空飞行计划问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

最大权闭合子图

模型:

每个点都有一个权值,有正有负,选一个权值和最大的子图,使得每个点的后继都在子图里面,这样的图叫最大闭合子图。

可以转化为最小割,让源点与正权点连,汇点和负权点连,中间的点都为正无穷的流量,然后最大闭合子图的权值为 正权值之和 - 最小割。

可以理性的理解为:

如果当前流量连接源点的仍在,说明源点选完后继节点后任然有剩余,即净利润为正可选。

如果连接汇点的边流量存在,那么说明对于连接该点的前继节点与源点必然断开。

代码上要注意一些细节。就是不要只看和源点和汇点是不是有流量不为 0 的点。

因为可能为 0 的去贡献了,应该看哪些点和源点连通才对。

可以用处理的 d 数组来解决。

#include<bits/stdc++.h>
using namespace std;
#define int long long  
/*
汇点和负权连边,即配置的仪器
源点和试验连边,即经费

*/
int S,T;
const int M = 10000;
struct node 
{
	int v,c,nex;
}e[M];
const int N  =200;
int head[N];
int idx = 1;
vector<int> res ;

void add(int u,int v,int c)
{
	e[++idx]={v,c,head[u]};
	head[u] = idx;
}
char tools[10000];
#define inf 0x3f3f3f3f3f3f3f3f
int d[N];
bool bfs()
{
	memset(d,0,sizeof d);
	queue<int> q;
	d[S] = 1;
	q.push(S);
	while(!q.empty())
	{
		auto u = q.front();
		q.pop();
		for(int i=head[u];i;i = e[i].nex)
		{
			int v = e[i].v;
			if(e[i].c && !d[v])
			{
				d[v] = d[u] +1;
				q.push(v);
				if(v==T)
				{	
					return true;
				}
			}
		}
	}
	return false ;
}
int cur[N];
int dfs(int u,int mf)
{
	if(u == T) return mf;
	int sum = 0;
	for(int i = cur[u];i;i = e[i].nex)
	{	cur[u] = i;
		int v = e[i].v;
		if(d[v] == d[u]+1 && e[i].c)
		{
			int f = dfs(v,min(mf,e[i].c));
			e[i].c -= f;
			e[i^1].c +=f;
			mf -=f;
			sum += f;
			if(mf == 0)
				break ;
		}
	}
	if(sum == 0)
		d[u] = 0;
	return sum ;
}
int dinic()
{
	int flow = 0;
	while(bfs())
	{
		memcpy(cur,head,sizeof head);
		flow += dfs(S,1e9);
	}
	return flow ;
}
int vis[N];
signed main (){

	int m,n;
	cin>>m>>n;
	S = 0 ,T = n+m+1;
	// 试验 1-m,仪器m+1 - m+n;
	int ans = 0;
	for(int i=1;i<=m;i++)
	{	
		int c;
		cin>>c;
		ans += c;
		add(S,i,c);
		add(i,S,0);
		memset(tools,0,sizeof tools);
		cin.getline(tools,10000);
		int ulen=0,tool;
		while (sscanf(tools+ulen,"%lld",&tool)==1)//之前已经用scanf读完了赞助商同意支付该实验的费用
		{//tool是该实验所需仪器的其中一个      
		    //这一行,你可以将读进来的编号进行储存、处理,如连边。
		    add(i,m+tool,inf);
		    add(tool+m,i,0);
		    if (tool==0) 
		        ulen++;
		    else {
		        while (tool) {
		            tool/=10;
		            ulen++;
		        }
		    }
		    ulen++;
		}
	}
	for(int i=1;i<=n;i++)
	{
		int c;
		cin>>c;
		add(i+m,T,c);
		add(T,i+m,0);
	}
	int x = dinic();
	queue<int> q;
	q.push(S);
	while(!q.empty())
	{
		auto c = q.front();
		q.pop();
		
		if(vis[c])
			continue;
		res.push_back(c);
		vis[c] = 1;
		for(int i = head[c];i;i = e[i].nex)
		{
			int v = e[i].v;
			if(vis[v] || e[i].c == 0)
				continue ;
			q.push(v);
			
		}
	}
	
	for(int i=1;i<=n+m;i++)
	{
		if(d[i] && i<=m)
			cout<<i<<' ';
		if(i==m)
			cout<<endl;
		if(d[i] && i>m)
			cout<<i-m<<' ';
			
			
	}
	cout<<endl;
	
	cout<<ans - x;
	puts("");
	
} 

P2764 最小路径覆盖问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

真想不到建模,只可以想到对于每个点连源点和汇点,但是这样连显然两个可能就直接到汇点了,不会走最长的那条路,如果按照我连的,可能就是最大流就是我们要的。但显然是错的。

题解是把每个点拆分成了两个点。因为他算的是两个点间连接一条边就可以使得答案 -1.那我们算最多能多少两个点之间连边即可。然后 x i x_i xi 为入边, y i y_i yi 为出边,显然这样建图不会使得一个边就是和前一个连入后还能被下一个连出。然后 x i x_i xi 连 S 源点 y i y_i yi 连汇点。

#include<bits/stdc++.h>
using namespace std;
#define int long long  

int S,T;
const int M = 100000;
struct node 
{
	int v,c,nex;
}e[M];
const int N  =2000;
int head[N];
int idx = 1;

void add(int u,int v,int c)
{
	e[++idx]={v,c,head[u]};
	head[u] = idx;
}

#define inf 0x3f3f3f3f3f3f3f3f
int d[N];
bool bfs()
{
	memset(d,0,sizeof d);
	queue<int> q;
	d[S] = 1;
	q.push(S);
	while(!q.empty())
	{
		auto u = q.front();
		q.pop();
		for(int i=head[u];i;i = e[i].nex)
		{
			int v = e[i].v;
			if(e[i].c && !d[v])
			{
				d[v] = d[u] +1;
				q.push(v);
				if(v==T)
				{	
					return true;
				}
			}
		}
	}
	return false ;
}
int cur[N];
int dfs(int u,int mf)
{
	if(u == T) return mf;
	int sum = 0;
	for(int i = cur[u];i;i = e[i].nex)
	{	cur[u] = i;
		int v = e[i].v;
		if(d[v] == d[u]+1 && e[i].c)
		{
			int f = dfs(v,min(mf,e[i].c));
			e[i].c -= f;
			e[i^1].c +=f;
			mf -=f;
			sum += f;
			if(mf == 0)
				break ;
		}
	}
	if(sum == 0)
		d[u] = 0;
	return sum ;
}
int dinic()
{
	int flow = 0;
	while(bfs())
	{
		memcpy(cur,head,sizeof head);
		flow += dfs(S,1e9);
	}
	return flow ;
}
int fa[N];
int findfa(int x)
{
	return fa[x] == x ? x : (fa[x] =  findfa(fa[x]));
}

signed main (){
{
	int n,m;
	cin>>n>>m;
	S = 0,T = 2*n+2;
	for(int i=1;i<=n;i++)
	{
		add(S,i,1);
		add(i,S,0);
	}
	for(int i=1;i<=n;i++)
	{
		add(i+n,T,1);
		add(T,i+n,0);
	}
	for(int i=1;i<=m;i++)
	{
		int u,v;
		cin>>u>>v;
		add(u,v+n,1);
		add(v+n,u,0);
	}
	int x = dinic();
	for(int i=1;i<=n;i++)
		fa[i] = i ;
	for(int i=1;i<=n;i++)
	{
		for(int j = head[i];j;j=e[j].nex)
		{
			if(e[j].v!=S && (e[j].c  == 0 ))
				fa[findfa(i)] = findfa(e[j].v - n);
		}
	}
	for(int i=1;i<=n;i++)
	{
		if(fa[i] == i)
		{	
			for(int j=1;j<=n;j++)
			if(findfa(j) == i)
				cout<<j<<' ';
			cout<<endl;
			
		}
	}
	cout<<n-x<<endl;
	
}

	
	
	
	
	
} 

  • 22
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值