图论建模(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 缩点并判断是否有解后,里面的 dfs
,即从某个状态开始,往下面跑,如果正反两个点都被跑到则说明这个状态不合法。我们 2-SAT 所建成的有向图本质上是阐释不同状态之间的关系,而状态的正确与否。
竞赛图分出三个点集
处理点之间的bool关系,使用2-SAT
,若 , 属于 则 一定属于 ;若 , 属于 则 一定属于 。 的内部同理。 ,若 ,那么 要么同属于 ,要么同不属于 ;否则 。
题面还有一个要求,要求三个集合都不为空,那么就要钦定一些点作为
对于
#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
套路地,将人看作“流水”,那么我们可以通过人数的流向具象化桌子与单位之间的关系。具体做法:
每一个单位作为一个点,每一个桌子看作一个点,源点
对于中间的,由于题目条件“同一个单位来的代表不在同一个餐桌就餐”,因此,每一个单位向每一个餐桌连一条权值为
方案就是去看看对于一条 单位->桌子
的边,跑完后这条边的剩余容量是否为
核心代码
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,边权为牛棚容量
- 对于在二分的
时间内可以到达的位置,连出一条权值为 的边,因为“路很宽,无限量的牛可以通过”。
观察到数据范围很小,最短路可以用 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 的思路是错误的。
那么我们应当利用网络流中边的限定功能。
考虑拆点,一只牛被拆成了两个点,两点间连了一条权值为
拆点是个好东西!
建图代码
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]的门
去建边。
一个
完整代码
总结
我们构造网络流的过程就是一个将题意转化为具体的的图,并将其丢给网络流解决的过程。
我们并不需要过多地去关注过程中的决策,而是将那些限制转化到图上即可。
这便是建模
的魅力所在。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效