图论建模(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 发抖的牛
二分
要求最小时间,明显符合单调性,故二分答案。
与上题类似,将牛作为“水流”。
建图:
- S->每一个田地(代表这个田地的牛),边权为该位置牛的数量
- 每一个田地(代表这个田地的牛棚)->T,边权为牛棚容量
- 对于在二分的 \(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;
}
拆点
由于“每头牛只享用一种食物和一种饮料”,我们直接 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(){
queueq;
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);
queueq;
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+pointmid;++i)addedge(i,T,1);
for(int i = 0;i<point;++i){
// P+imid + [1,mid]
for(int j = tot+imid+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;
}
总结
我们构造网络流的过程就是一个将题意转化为具体的的图,并将其丢给网络流解决的过程。
我们并不需要过多地去关注过程中的决策,而是将那些限制转化到图上即可。
这便是建模
的魅力所在。