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(n^2)\)dfs,即从某个状态开始,往下面跑,如果正反两个点都被跑到则说明这个状态不合法。我们 2-SAT 所建成的有向图本质上是阐释不同状态之间的关系,而状态的正确与否。

最大流

板子

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\) 就是对于一扇门所拆成的几扇门,\(i\)\(i+1\) 连一条权值为 \(inf\) 的边,这样就可以使得“等待”的过程得到实现。

完整代码
 
#include
#define print(a) cout << #a"=" << a << endl
#define debug() cout << "Line:" << __LINE__ << endl
#define sign() puts("----------")
using namespace std;
typedef pair pii;
const int inf = 0x3f3f3f3f;
int n,m;

const int N = 1e5 + 10;
void build(int mid);
namespace Net{
int S,T;
int head[N],work[N],etot = 1;
struct node{ int nxt,v,cap; }edge[N<<1];
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[N];
bool vis[N];
inline void init(){
memset(head,0,sizeof head);
etot = 1;
}
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 mid){
build(mid);
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;
}
}
using namespace Net;

int tot;

char s[25][25];
bool mark[25][25];
int id[25][25];

struct P{ int x,y,d; };

const int step[4][2] = {{1,0},{0,1},{-1,0},{0,-1}};
void bfs(int sx,int sy,int mid,int st){
memset(mark,0,sizeof mark);
queue

q;
q.push({sx,sy,0});
mark[sx][sy] = 1;
while(!q.empty()){
int x = q.front().x, y = q.front().y,d = q.front().d; q.pop();
if(d > mid)continue;
if(s[x][y] == '.')addedge(id[x][y],st+d,1);
for(int i = 0;i<4;++i){
int xx = x + step[i][0], yy = y + step[i][1];
if(xx < 1 || xx > n || yy < 1 || yy > m)continue;
if(mark[xx][yy] || s[xx][yy] == 'X' || s[xx][yy] == 'D')continue;
q.push({xx,yy,d+1});
mark[xx][yy] = 1;
}
}
}

vector v;
void build(int mid){
init();
int point = v.size();
S = 0, T = tot + point * mid + 1;
/*
1~P ren
P+1 ~ P + point * mid
/
for(int i = 1;i<=tot;++i)addedge(S,i,1);
for(int i = tot+1;i<=tot+point
mid;++i)addedge(i,T,1);
for(int i = 0;i<point;++i){
// P+imid + [1,mid]
for(int j = tot+i
mid+1;j<tot+(i+1)*mid;++j)addedge(j,j+1,inf);
}

for(int i = 0;i<point;++i)
	bfs(v[i].first,v[i].second,mid,tot+i*mid);

}

int sum;
int main(){
scanf("%d%d",&n,&m);
for(int i = 1;i<=n;++i)
scanf("%s",s[i]+1);
for(int i = 1;i<=n;++i)
for(int j = 1;j<=m;++j)
if(s[i][j] == 'D')v.push_back({i,j});
else if(s[i][j] == '.')id[i][j] = ++tot,++sum;
int l = 1, r = 1000, res = -1;
while(l <= r){
int mid = (l + r) >> 1;
if(Dinic(mid) == sum){
res = mid;
r = mid - 1;
}else l = mid + 1;
}
if(res == -1)puts("impossible");
else printf("%d",res);
return 0;
}

总结

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

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

这便是建模的魅力所在。

posted @ 2024-08-28 11:04  Luzexxi  阅读(12)  评论(0编辑  收藏  举报