10086

my first title

my first paragraph

图论建模(2-SAT,最大流)

前言

此博客中的分析只作为反思总结所用,不适合当作题解看。
觉得哪里不足的多请大佬们指正~

2-SAT相关

板子

基本 2-SAT

代码
 
inline int calc(int x,int y){ return y ? x + n : x; }
int main(){
	scanf("%d%d",&n,&m);
	for(int i = 1,x,a,y,b;i<=m;++i){
		scanf("%d%d%d%d",&x,&a,&y,&b);
		G.add(calc(x,a^1),calc(y,b));
		G.add(calc(y,b^1),calc(x,a));
	}
	for(int i = 1;i<=n*2;++i)if(!dfn[i])tarjan(i);
	for(int i = 1;i<=n;++i)
		if(bt[i] == bt[i+n])return puts("IMPOSSIBLE"),0;
	puts("POSSIBLE");
	for(int i = 1;i<=n;++i)
		printf(bt[i] < bt[i+n] ? "0 ":"1 ");
	return 0;
}
  

2-SAT 前缀优化建图

代码
 
inline int id(int x,bool yes){ return yes ? x : x + n; }
inline int calc(int x,bool yes){ return yes ? x + 2*n : x + 3*n; }
int main(){
	scanf("%d%d%d",&n,&m,&k);
	for(int i = 1,u,v;i<=m;++i){
		scanf("%d%d",&u,&v);
		G.add(id(u,0),id(v,1)),G.add(id(v,0),id(u,1));
	}
	for(int i = 1,w;i<=k;++i){
		scanf("%d",&w);
		for(int j = 1;j<=w;++j){
			scanf("%d",&a[j]);
			G.add(id(a[j],1),calc(a[j],1)),G.add(calc(a[j],0),id(a[j],0));
			if(j ^ 1){
				G.add(calc(a[j-1],1),calc(a[j],1)),G.add(calc(a[j],0),calc(a[j-1],0));
				G.add(calc(a[j-1],1),id(a[j],0)),G.add(id(a[j],1),calc(a[j-1],0));
			}
		}
	}
	for(int i = 1;i<=4*n;++i)if(!dfn[i])tarjan(i);
	for(int i = 1;i<=n;++i)
		if(bt[id(i,0)] == bt[id(i,1)] || bt[calc(i,0)] == bt[calc(i,1)])return puts("NIE"),0;
	puts("TAK");
	return 0;
}
/*
pre_i 表示该部分中前 i 个点是否有点被选为关键点
a_i -> pre_i, !pre_i -> !a_i
pre_{i-1} -> pre_{i}  !pre_{i} -> !pre_{i-1}
pre_{i-1} -> !a_i    a_i -> !pre_{i-1}
*/
  

2-SAT 线段树优化建图

代码
 
struct Graph{
	int head[N<<3],etot;
	struct node{
		int nxt,v;
	}edge[M];
	void init(){ memset(head,0,sizeof head); etot = 0; }
	void add(int x,int y){
		edge[++etot] = {head[x],y};
		head[x] = etot;
	}
	node & operator [](const int i){ return edge[i]; }
}G;
inline int trans(int x){ return x > n ? x - n : x + n; }
struct Seg{
	struct node{
		int l,r;
	}tr[N<<2];
	#define ls (p<<1)
	#define rs (p<<1|1)
	#define mid ((tr[p].l+tr[p].r)>>1)
	void build(int p,int l,int r){
		tr[p] = {l,r};
		id[p] = ++tot;
		if(l == r)return G.add(id[p],trans(s[l].id)),void();
		build(ls,l,mid),build(rs,mid+1,r);
		G.add(id[p],id[ls]),G.add(id[p],id[rs]);
	}
	void modify(int p,int l,int r,int v){
		if(l <= tr[p].l && tr[p].r <= r)return G.add(v,id[p]),void();
		if(l <= mid)modify(ls,l,r,v);
		if(mid < r)modify(rs,l,r,v);
	}
	#undef mid
}seg;
inline bool check(int lim){
	tim = scc_cnt = top = 0;
	G.init();
	memset(dfn,0,sizeof dfn);
	seg.build(1,1,tot = n<<1);
	for(int i = 1;i<=n*2;++i){
		int l = upper_bound(s+1,s+n*2+1,P{s[i].pos-lim,0})-s;
		int r = upper_bound(s+1,s+n*2+1,P{s[i].pos+lim-1,0})-s-1;
		seg.modify(1,l,i-1,s[i].id),seg.modify(1,i+1,r,s[i].id);
	}
	for(int i = 1;i<=n*2;++i)if(!dfn[i])tarjan(i);
	for(int i = 1;i<=n;++i)
		if(bt[i] == bt[i+n])return false;
	return true;
}
  

例题

P3007 [USACO11JAN] The Continental Cowngress G

这题着实加深了我对 2-SAT 的理解。
在 tarjan 缩点并判断是否有解后,里面的 O(n2)dfs,即从某个状态开始,往下面跑,如果正反两个点都被跑到则说明这个状态不合法。我们 2-SAT 所建成的有向图本质上是阐释不同状态之间的关系,而状态的正确与否。

竞赛图分出三个点集

处理点之间的bool关系,使用2-SAT

ABCA。那么 A,B,C 的先后是无关紧要的,可以钦定 1A。由于是竞赛图,对与那些可能为 B 中的元素 (1,x)E,和可能为 C 的元素 (x,1)E,处理,令其为 B,C

  1. xB,yB,若 (x,y)Ex 属于 By 一定属于 B;若 (x,y)Ex 属于 Ay 一定属于 A
  2. C 的内部同理。
  3. xB,yC,若 (x,y)E,那么 x,y 要么同属于 A,要么同不属于 A;否则 xAyA,yAxA

题面还有一个要求,要求三个集合都不为空,那么就要钦定一些点作为 B,C 中的点。

对于 B 中的点,考虑 B 所在的子图 G。若存在一个点 x0B,x0A,那么若 (y0,x0)E,就有 y0A。就是找到 B 中所有点都可以到达的点 x(竞赛图缩点后是一条链)。C 同样是,就是找到可以从 x0 出发到达 C 中所有点的 x0

#include<bits/stdc++.h>
using namespace std;
constexpr int S1=1<<20;
char buf1[S1],*l1,*r1;
#define getchar() ((l1==r1&&(r1=(l1=buf1)+fread(buf1,1,S1,stdin)),l1!=r1)?*l1++:EOF)
template<typename T=int>inline T read()
{
	T x=0;
	char c=getchar();
	while(c<'0'||c>'9')
		c=getchar();
	while(c>='0'&&c<='9')
	{
		x=c-'0'+x*10;
		c=getchar();
	}
	return x;
}

inline void end(){
	puts("0 0 0");
	exit(0);
}
const int N = 5010;

vector<int> e[N<<1];

int dfn[N << 1],low[N << 1],tim;
int st[N << 1],top,scc_cnt;
bool inst[N << 1];
int bt[N << 1],n;
vector<int> B,C;
bool g[N][N];
void tarjan(int u){
	low[u] = dfn[u] = ++tim;
	st[++top] = u;
	inst[u] = 1;
	for(int v : e[u]){
		if(!dfn[v]){
			tarjan(v);
			low[u] = min(low[u],low[v]);
		}else if(inst[v])low[u] = min(low[u],dfn[v]);
	}
	if(dfn[u] == low[u]){
		++scc_cnt;
		int z;
		do{
			z = st[top--];
			inst[z] = 0;
			bt[z] = scc_cnt;
		}while(z ^ u);
	}
}
int main(){
	freopen("b6e7.in","r",stdin);
	freopen("b6e7.out","w",stdout);
	n = read();
	for(int i = 1;i<=n;++i)for(int j = 1;j<=n;++j)g[i][j] = read();
	for(int i = 2;i<=n;++i)
		if(g[1][i])B.push_back(i);
		else C.push_back(i);
	if(B.empty() || C.empty())end();
	e[n+1].push_back(1);
	for(int x : B)for(int y : B){
		if(x == y)continue;
		if(g[x][y])e[x+n].push_back(y+n);
		else e[x].push_back(y);
	}
	for(int x : C)for(int y : C){
		if(x == y)continue;
		if(g[x][y])e[x].push_back(y);
		else e[x+n].push_back(y+n);
	}
	for(int x : B)for(int y : C)
		if(g[x][y]){
			e[x].push_back(y);
			e[x+n].push_back(y+n);
			e[y].push_back(x),e[y+n].push_back(x+n);
		}else{
			e[x+n].push_back(y);
			e[y+n].push_back(x);
		}
	int x = B[0];
	while(1){
		bool flag = 1;
		inst[x] = 1;
		for(int y : B)if(!inst[y] && g[x][y]){
			x = y, flag = 0;
			break;
		}
		if(flag)break;
	}
	e[x].push_back(x+n);
	x = C[0];
	while(1){
		bool flag = 1;
		inst[x] = 1;
		for(int y : C)if(!inst[y] && g[y][x]){
			x = y, flag = 0;
			break;
		}
		if(flag)break;
	}
	e[x].push_back(x+n);
	memset(inst,0,sizeof inst);
	for(int i = 1;i<=n*2;++i)if(!dfn[i])tarjan(i);
	for(int i = 1;i<=n;++i)if(bt[i] == bt[i+n])end();
	int siza = 0, sizb = 0, sizc = 0;
	for(int i = 1;i<=n;++i)if(bt[i] < bt[i+n])++siza;
	for(int x : B)if(bt[x+n] < bt[x])++sizb;
	for(int x : C)if(bt[x+n] < bt[x])++sizc;
	printf("%d %d %d\n",siza,sizb,sizc);
	for(int i = 1;i<=n;++i)if(bt[i] < bt[i+n])printf("%d ",i); puts("");
	for(int x : B)if(bt[x+n] < bt[x])printf("%d ",x); puts("");
	for(int x : C)if(bt[x+n] < bt[x])printf("%d ",x); puts("");
	return 0;
}

最大流

板子

Dinic:

代码

    namespace Net{
	int S,T;
	int head[510],work[510],etot = 1;
	struct node{ int nxt,v,cap; }edge[160010];
	inline void add(int x,int y,int w){
		edge[++etot] = {head[x],y,w};
		head[x] = etot;
	}
	inline void addedge(int u,int v,int w){ add(u,v,w),add(v,u,0); }
	int dis[510];
	bool vis[510];
	bool bfs(){
		queue q;
		for(int i = S;i<=T;++i)dis[i] = -1;
		dis[S] = 0;
		q.push(S);
		while(!q.empty()){
			int x = q.front(); q.pop();
			for(int i = head[x];i;i = edge[i].nxt){
				int v = edge[i].v;
				if(edge[i].cap > 0 && dis[v] == -1){
					dis[v] = dis[x] + 1;
					q.push(v);
				}
			}
		}
		return (dis[T] >= 0);
	}	
	int dfs(int u,int flow){
		if(u == T)return flow;
		for(int &i = work[u];i;i = edge[i].nxt){
			int v = edge[i].v;
			if(edge[i].cap > 0 && dis[v] == dis[u] + 1){
				int tmp = dfs(v,min(flow,edge[i].cap));
				if(tmp > 0){
					edge[i].cap -= tmp;
					edge[i^1].cap += tmp;
					return tmp;
				}
			}
		}
		return 0;
	}
	int Dinic(){
		int ans = 0;
		while(bfs()){
			for(int i = S;i<=T;++i)work[i] = head[i];
			while(1){
				int flow = dfs(S,inf);
				if(flow == 0)break;
				ans += flow;
			}
		}
		return ans;
	}
}
  

Dinic 邻接矩阵(没有当前弧优化)

代码
 
namespace Net{
	int S,T;
	ll cap[N][N];
	inline void addedge(int x,int y,ll w){
		cap[x][y] = w;
		cap[y][x] = 0;
	}
	int dist[N];
	bool vis[N];	
	ll dfs(int u,ll flow){
		if(u == T)return flow;
		for(int i = S;i<=T;++i)if(cap[u][i] != -1){
			if(cap[u][i] > 0 && dist[i] == dist[u] + 1){
				int tmp = dfs(i,min(cap[u][i],flow));
				if(tmp > 0){
					cap[u][i] -= tmp;
					cap[i][u] += tmp;
					return tmp;
				}
			}
		}
		return 0;
	}
	bool bfs(){
		queue q;
		for(int i = S;i<=T;++i)dist[i] = -1;
		q.push(S); dist[S] = 0;
		while(!q.empty()){
			int x = q.front(); q.pop();
			for(int i = S;i<=T;++i)if(cap[x][i] > 0 && dist[i] == -1){
				dist[i] = dist[x] + 1;
				q.push(i);
			}
		}
		return (dist[T] >= 0);
	}
	ll Dinic(ll lim){
		S = 0, T = F * 2 + 1;
		for(int i = S;i<=T;++i)
			for(int j = S;j<=T;++j)cap[i][j] = -1;	
		for(int i = 1;i<=F;++i){
			addedge(S,i,cow[i]);
			addedge(i+F,T,pen[i]);
			for(int j = 1;j<=F;++j)
				if(dis[i][j] <= lim)addedge(i,j+F,inf);
		}
		ll ans = 0;
		while(bfs()){
			while(1){
				ll flow = dfs(S,inf);
				if(flow == 0)break;
				ans += flow;
			}
		}
		return ans;
	}
}
using namespace Net;
  

例题:

P3254圆桌问题

basic

套路地,将人看作“流水”,那么我们可以通过人数的流向具象化桌子与单位之间的关系。具体做法:

每一个单位作为一个点,每一个桌子看作一个点,源点 S 向每一个单位 i 连一条权值为 r[i] 的边,每一个桌子 i 向汇点连一条权值为 c[i] 的边。这样我们看所有人是否可以都入座就是看最大流是不是等于总人数。

对于中间的,由于题目条件“同一个单位来的代表不在同一个餐桌就餐”,因此,每一个单位向每一个餐桌连一条权值为 1 的有向边。

方案就是去看看对于一条 单位->桌子 的边,跑完后这条边的剩余容量是否为 0,若是,则表明这一条边有人选择。

核心代码

scanf("%d%d",&m,&n);
S = 0, T = m + n + 1;
for(int i = 1;i<=m;++i){
	scanf("%d",&r[i]);
	addedge(S,i,r[i]);
	cnt += r[i];
}
for(int i = 1;i<=n;++i){
	scanf("%d",&c[i]);
	addedge(i+m,T,c[i]);
}
for(int i = 1;i<=m;++i)
	for(int j = m+1;j<=m+n;++j)
		addedge(i,j,1);
int res = Dinic();
if(res != cnt)return puts("0"),0;
puts("1");
for(int i = 1;i<=m;++i){
	for(int j = head[i];j;j = edge[j].nxt){
		int v = edge[j].v - m;
		if(edge[j].cap == 0)printf("%d ",v);
	}
	puts("");
} 
  

P6768 [USACO05MAR] Ombrophobic Bovines 发抖的牛

二分

要求最小时间,明显符合单调性,故二分答案。

与上题类似,将牛作为“水流”。

建图:

  1. S->每一个田地(代表这个田地的牛),边权为该位置牛的数量
  2. 每一个田地(代表这个田地的牛棚)->T,边权为牛棚容量
  3. 对于在二分的 mid 时间内可以到达的位置,连出一条权值为 inf 的边,因为“路很宽,无限量的牛可以通过”。

观察到数据范围很小,最短路可以用 Floyed 处理。

核心代码
 
ll Dinic(ll lim){
	S = 0, T = F * 2 + 1;
	for(int i = S;i<=T;++i)
		for(int j = S;j<=T;++j)cap[i][j] = -1;
	for(int i = 1;i<=F;++i){
		addedge(S,i,cow[i]);
		addedge(i+F,T,pen[i]);
		for(int j = 1;j<=F;++j)
			if(dis[i][j] <= lim)addedge(i,j+F,inf);
	}
	ll ans = 0;
	while(bfs()){
		while(1){
			ll flow = dfs(S,inf);
			if(flow == 0)break;
			ans += flow;
		}
	}
	return ans;
}
  

P2891 [USACO07OPEN] Dining G

拆点

由于“每头牛只享用一种食物和一种饮料”,我们直接 S->食物->牛->饮料->T 的思路是错误的。

那么我们应当利用网络流中边的限定功能。

考虑拆点,一只牛被拆成了两个点,两点间连了一条权值为 1 的边,这样就可以保证一只牛只吃一对了。

拆点是个好东西!

建图代码
 
S = 0, T = 2 * n + F + D + 1;
for(int i = 1;i<=F;++i)addedge(S,i,1);
for(int i = 2*n+F+1;i<=2*n+F+D;++i)addedge(i,T,1);
for(int i = 1;i<=n;++i)addedge(F+i,F+i+n,1);
for(int i = 1,a,b;i<=n;++i){
	scanf("%d%d",&a,&b);
	// cow_i F+i->F+i+n
	for(int j = 1,x;j<=a;++j){
		scanf("%d",&x);
		addedge(x,F+i,1);
	}
	for(int j = 1,x;j<=b;++j){
		scanf("%d",&x);
		addedge(F+i+n,2*n+F+x,1);
	}
}
printf("%d",Dinic());
  

P3191 [HNOI2007] 紧急疏散EVACUATE

二分 + 拆点

二分时间。

拆点,将每一扇门按照时间都拆成 mid 扇。

如果按照最为朴素的建图方式,边会存不下,因为我们要按照一个空位->[到达该门的时间,mid]的门去建边。

一个 trick 就是对于一扇门所拆成的几扇门,ii+1 连一条权值为 inf 的边,这样就可以使得“等待”的过程得到实现。

完整代码



总结

我们构造网络流的过程就是一个将题意转化为具体的的图,并将其丢给网络流解决的过程。

我们并不需要过多地去关注过程中的决策,而是将那些限制转化到图上即可。

这便是建模的魅力所在。

posted @   Luzexxi  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示