ybtAu「图论」第6章 2-SAT问题
Kosaraju
与上一章一样,这一章部分题目也使用 Kosaraju 求解 SCC。而不同于 Tarjan,Kosaraju 求得的 SCC 顺序是正拓扑序而非倒序的,因此在比较 SCC 编号时与 Tarjan 实现有所不同。
A. 【例题1】聚会
如果两个人有矛盾,那么当其中一个人列席时,一定是另一个人的配偶列席。由每对矛盾的一个人向对方的配偶连边即可。
如果一对夫妻在一个强连通分量里,那么无解。
#include <iostream>
#define N 5005
int n,m,hed[N],deh[N],tal[N*N],nxt[N*N],cnte,col[N],cnt;
void de(int u,int v) {tal[++cnte]=v,nxt[cnte]=hed[u],hed[u]=cnte;}
void ed(int u,int v) {tal[++cnte]=v,nxt[cnte]=deh[u],deh[u]=cnte;}
namespace SCC
{
int vis[N],li[N],idx;
void dfs1(int x)
{
vis[x]=1;
for(int i=hed[x];i;i=nxt[i]) if(!vis[tal[i]]) dfs1(tal[i]);
li[++idx]=x;
}
void dfs2(int x)
{
col[x]=cnt;
for(int i=deh[x];i;i=nxt[i]) if(!col[tal[i]]) dfs2(tal[i]);
}
void kosaraju()
{
cnt=idx=0;
for(int i=0;i<2*n;i++) vis[i]=col[i]=0;
for(int i=0;i<2*n;i++) if(!vis[i]) dfs1(i);
for(int i=idx;i>=1;i--) if(!col[li[i]]) cnt++,dfs2(li[i]);
}
};
void chk()
{
for(int i=0;i<n;i++) if(col[i*2]==col[i*2+1])
{
std::cout<<"NO\n";
return;
}
std::cout<<"YES\n";
}
signed main()
{
std::ios::sync_with_stdio(0);
std::cin.tie(0),std::cout.tie(0);
while(std::cin>>n>>m)
{
for(int i=0;i<n*2;i++) hed[i]=deh[i]=0;
for(;cnte;cnte--) tal[cnte]=nxt[cnte]=0;
for(int i=1,u,v,x,y;i<=m;i++)
{
std::cin>>u>>v>>x>>y;
u=u*2+x,v=v*2+y;
de(u,v^1),ed(v^1,u),de(v,u^1),ed(u^1,v);
}
SCC::kosaraju();
chk();
}
}
B. 【例题2】婚礼
如果只判断有无解那么同上题,但是此题要求输出方案。
如果丈夫的 SCC 编号大于妻子的,那么如果丈夫与新郎同侧,则妻子可以与新娘同侧,而反过来可能不行,所以丈夫与新郎同侧,即妻子与新娘同侧。否则丈夫与新娘同侧。
#include <iostream>
#define N 200005
int n,m,hed[N],deh[N],tal[N],nxt[N],cnte,col[N],cnt;
void de(int u,int v) {tal[++cnte]=v,nxt[cnte]=hed[u],hed[u]=cnte;}
void ed(int u,int v) {tal[++cnte]=v,nxt[cnte]=deh[u],deh[u]=cnte;}
void adde(int u,int v) {de(u,v),ed(v,u);}
int ch(std::string s)
{
int num=0;
for(int i=0;i<s.size()-1;i++) num=num*10+s[i]-'0';
if(s[s.size()-1]=='h') num+=n;
return num;
}
int op(int x) {return x>n?x-n:x+n;}
namespace SCC
{
int vis[N],li[N],idx;
void dfs1(int x)
{
vis[x]=1;
for(int i=hed[x];i;i=nxt[i]) if(!vis[tal[i]]) dfs1(tal[i]);
li[++idx]=x;
}
void dfs2(int x)
{
col[x]=cnt;
for(int i=deh[x];i;i=nxt[i]) if(!col[tal[i]]) dfs2(tal[i]);
}
void kosaraju()
{
cnt=idx=0;
for(int i=1;i<=2*n;i++) vis[i]=col[i]=0;
for(int i=1;i<=2*n;i++) if(!vis[i]) dfs1(i);
for(int i=idx;i>=1;i--) if(!col[li[i]]) cnt++,dfs2(li[i]);
}
}
signed main()
{
std::ios::sync_with_stdio(0);
std::cin.tie(0),std::cout.tie(0);
for(;;)
{
std::cin>>n>>m;
if(!n&&!m) return 0;
for(int i=1;i<=2*n;i++) hed[i]=deh[i]=0;
for(;cnte;cnte--) tal[cnte]=nxt[cnte]=0;
for(int i=1;i<=m;i++)
{
std::string s,t;
std::cin>>s>>t;
int u=ch(s)+1,v=ch(t)+1;
adde(u,op(v)),adde(v,op(u));
}
adde(1,1+n);
SCC::kosaraju();
bool fg=0;
for(int i=1;i<=n;i++) if(col[i]==col[i+n]) {fg=1;break;}
if(fg) std::cout<<"bad luck\n";
else
{
for(int i=2;i<=n;i++) std::cout<<(i-1)<<(col[i]<col[i+n]?'w':'h')<<' ';
std::cout<<'\n';
}
}
}
C. 奶牛议会
对于每个奶牛投票的两个议案,如果其中一个议案的最终结果与她的投票相反,那么另一个一定与她的投票相同。
如果没有 ? 那么同上题,但是现在有 ?,不能通过直接判断 SCC 编号解决。
考虑对每个节点,从它开始搜索,如果搜到的节点里有同一个议案的两个节点,那么这个节点不能被选。
对于一个议案,如果它的两个节点都能被选,那么输出 ?,否则哪个能选输出哪个。
#include <iostream>
#define N 100005
int n,m,hed[N],deh[N],tal[N],nxt[N],cnte,ex[N],col[N],cnt;
void de(int u,int v) {tal[++cnte]=v,nxt[cnte]=hed[u],hed[u]=cnte;}
void ed(int u,int v) {tal[++cnte]=v,nxt[cnte]=deh[u],deh[u]=cnte;}
void adde(int u,int v) {de(u,v),ed(v,u);}
int op(int x) {return x>n?x-n:x+n;}
namespace SCC
{
int vis[N],li[N],idx;
void dfs1(int x)
{
vis[x]=1;
for(int i=hed[x];i;i=nxt[i]) if(!vis[tal[i]]) dfs1(tal[i]);
li[++idx]=x;
}
void dfs2(int x)
{
col[x]=cnt;
for(int i=deh[x];i;i=nxt[i]) if(!col[tal[i]]) dfs2(tal[i]);
}
void kosaraju()
{
cnt=idx=0;
for(int i=1;i<=2*n;i++) if(!vis[i]) dfs1(i);
for(int i=idx;i>=1;i--) if(!col[li[i]]) cnt++,dfs2(li[i]);
}
};
void dfs3(int x)
{
ex[x]=1;
for(int i=hed[x];i;i=nxt[i]) if(!ex[tal[i]]) dfs3(tal[i]);
}
bool chk(int x)
{
for(int i=1;i<=n*2;i++) ex[i]=0;
dfs3(x);
for(int i=1;i<=n;i++) if(ex[i]&&ex[i+n]) return 0;
return 1;
}
signed main()
{
std::ios::sync_with_stdio(0);
std::cin.tie(0),std::cout.tie(0);
std::cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v;
char x,y;
std::cin>>u>>x>>v>>y;
if(x=='Y') u+=n;
if(y=='Y') v+=n;
adde(op(u),v),adde(op(v),u);
}
SCC::kosaraju();
bool fg=0;
for(int i=1;i<=n;i++) if(col[i]==col[i+n]) {fg=1;break;}
if(fg) return std::cout<<"IMPOSSIBLE\n",0;
for(int i=1;i<=n;i++)
{
int c1=chk(i),c2=chk(i+n);
if(c1&&!c2) std::cout<<"N";
else if(!c1&&c2) std::cout<<"Y";
else std::cout<<"?";
}
}
D. 平面图
把边分成两类:哈密顿回路上的边和其他边。
把 \(n\) 个点等距地排布在一个圆上,这样哈密顿回路上的边就和其他边没有交点了。
对于其他边,两条边有交当且仅当它们所对应的弧相交且不包含,并且它们同时在圆内或圆外。
前者相当于若干条两两间限制,后者是二选一,这样就与 A 题一样了。
现在还有一个问题:枚举限制是 \(O(m^2)\) 级别的,无法通过。
然而 通过画图观察可以发现 由平面图定理可知,当 \(m>n+(n-3)+(n-3)=3n-6\) 时,一定无解,特判即可。
时间复杂度 \(O(n^2)\),可以通过。
#include <iostream>
#include <cassert>
#define N 100005
int n,m,col[N],id[N],len;
int gf(int x) {return x==col[x]?x:col[x]=gf(col[x]);}
void de(int u,int v) {col[gf(u)]=gf(v);}
int e[N][2],g[N][2];
signed main()
{
std::ios::sync_with_stdio(0);
std::cin.tie(0),std::cout.tie(0);
int T;
std::cin>>T;
while(T--)
{
std::cin>>n>>m;
for(int i=1,u,v;i<=m;i++) std::cin>>u>>v,e[i][0]=u,e[i][1]=v;
for(int i=1,x;i<=n;i++) std::cin>>x,id[x]=i;
if(m>3*n-6) {std::cout<<"NO\n";continue;}
len=0;
for(int i=1;i<=m;i++)
{
int u=id[e[i][0]],v=id[e[i][1]];
if(u==v-1||v==u-1) continue;
if(u>v) std::swap(u,v);
len++,g[len][0]=u,g[len][1]=v;
}
for(int i=1;i<=2*len;i++) col[i]=i;
for(int i=1;i<=len;i++) for(int j=i+1;j<=len;j++)
{
int u1=g[i][0],v1=g[i][1],u2=g[j][0],v2=g[j][1];
if((u1<u2&&u2<v1&&v1<v2)||(u2<u1&&u1<v2&&v2<v1))
de(i,j+len),de(j,i+len);
}
bool fg=1;
for(int i=1;i<=len;i++) if(gf(i)==gf(i+len)) {fg=0;break;}
if(fg) std::cout<<"YES\n";
else std::cout<<"NO\n";
}
}
多测要清二倍 hed。
题外话
注意到本题限制是双向的,最终得到的图可以看作无向图,缩点只需要并查集就行了。
实际上,如果从二分图的角度考虑,本题就是判断它是否为二分图。这种基于并查集的方法被称为扩展域并查集。
E. 建农场
最大值最小,令我们想到二分。设当前二分到 \(mid\)。
对于相互讨厌和朋友的限制建边。
枚举每一对牛舍 \((x,y)\),有四种距离方案:
- \(x\rightarrow S_1\rightarrow y\)
- \(x\rightarrow S_2\rightarrow y\)
- \(x\rightarrow S_1\rightarrow S_2\rightarrow y\)
- \(x\rightarrow S_2\rightarrow S_1\rightarrow y\)
如果四种方案距离都 \(>mid\),那么不合法。否则对每种方案,如果不合法,那么建边。
判断一个牛舍的两个节点是否在同一个 SCC 即可。
#include <iostream>
#define N 2005
#define M 2000005
int n,m,A,B,hed[N],deh[N],tal[M],nxt[M],cnte,col[N],cnt;
void de(int u,int v) {tal[++cnte]=v,nxt[cnte]=hed[u],hed[u]=cnte;}
void ed(int u,int v) {tal[++cnte]=v,nxt[cnte]=deh[u],deh[u]=cnte;}
void adde(int u,int v) {de(u,v),ed(v,u);}
std::pair<int,int> p[N],a[N],b[N];
int dis(int x,int y) {return abs(p[x].first-p[y].first)+abs(p[x].second-p[y].second);}
int x1y(int x,int y) {return dis(x,n+1)+dis(n+1,y);}
int x2y(int x,int y) {return dis(x,n+2)+dis(n+2,y);}
int x12y(int x,int y) {return dis(x,n+1)+dis(n+1,n+2)+dis(n+2,y);}
int x21y(int x,int y) {return dis(x,n+2)+dis(n+2,n+1)+dis(n+1,y);}
namespace SCC
{
int vis[N],li[N],idx;
void dfs1(int x)
{
vis[x]=1;
for(int i=hed[x];i;i=nxt[i]) if(!vis[tal[i]]) dfs1(tal[i]);
li[++idx]=x;
}
void dfs2(int x)
{
col[x]=cnt;
for(int i=deh[x];i;i=nxt[i]) if(!col[tal[i]]) dfs2(tal[i]);
}
void kosaraju()
{
cnt=idx=0;
for(int i=1;i<=2*n;i++) vis[i]=col[i]=0;
for(int i=1;i<=2*n;i++) if(!vis[i]) dfs1(i);
for(int i=idx;i>=1;i--) if(!col[li[i]]) cnt++,dfs2(li[i]);
}
};
bool check(int d)
{
for(int i=1;i<=n*2;i++) hed[i]=deh[i]=0;
for(;cnte;cnte--) tal[cnte]=nxt[cnte]=0;
for(int i=1,u,v;i<=A;i++) u=a[i].first,v=a[i].second,adde(u,v+n),adde(u+n,v),adde(v,u+n),adde(v+n,u);
for(int i=1,u,v;i<=B;i++) u=b[i].first,v=b[i].second,adde(u,v),adde(u+n,v+n),adde(v,u),adde(v+n,u+n);
for(int i=1;i<=n;i++) for(int j=i+1;j<=n;j++)
{
if(x1y(i,j)>d&&x2y(i,j)>d&&x12y(i,j)>d&&x21y(i,j)>d) return 0;
// if(x1y(i,j)>d&&x12y(i,j)>d) adde(i,i+n);
// if(x2y(i,j)>d&&x21y(i,j)>d) adde(i+n,i);
// if(x1y(i,j)>d&&x21y(i,j)>d) adde(j,j+n);
// if(x2y(i,j)>d&&x12y(i,j)>d) adde(j+n,j);
if(x1y(i,j)>d) adde(i,j+n),adde(j,i+n);
if(x2y(i,j)>d) adde(i+n,j),adde(j+n,i);
if(x12y(i,j)>d) adde(i,j),adde(j+n,i+n);
if(x21y(i,j)>d) adde(i+n,j+n),adde(j,i);
}
SCC::kosaraju();
for(int i=1;i<=n;i++) if(col[i]==col[i+n]) return 0;
return 1;
}
signed main()
{
std::ios::sync_with_stdio(0);
std::cin.tie(0),std::cout.tie(0);
while(std::cin>>n>>A>>B)
{
std::cin>>p[n+1].first>>p[n+1].second>>p[n+2].first>>p[n+2].second;
for(int i=1,x,y;i<=n;i++) std::cin>>x>>y,p[i]={x,y};
for(int i=1,x,y;i<=A;i++) std::cin>>x>>y,a[i]={x,y};
for(int i=1,x,y;i<=B;i++) std::cin>>x>>y,b[i]={x,y};
int l=1,r=1e9,ans=-1;
while(l<=r)
{
int mid=l+r>>1;
if(check(mid)) r=mid-1,ans=mid;
else l=mid+1;
}
std::cout<<ans<<'\n';
}
}
F. 游戏
发现有三种赛车,是三元限制,而我们并不会一种叫 3-SAT 的东西。考虑如何把它转化成 2-SAT 问题。
由于“适合所有赛车参加的地图并不多见,最多只会有 \(d\) 张”,而 \(d\le8\),因此可以暴力枚举 \(x\) 地图不适合使用哪种赛车,之后就和 B 题一样了。
如果枚举的所有方案都无解,那么无解,否则输出任意一个解即可。
#include <iostream>
#define N 200005
int n,m,d,hed[N],tal[N],nxt[N],cnte,col[N],cnt,p[10],a[N],b[N];
void de(int u,int v) {tal[++cnte]=v,nxt[cnte]=hed[u],hed[u]=cnte;}
std::string S;
std::pair<std::pair<int,char>,std::pair<int,char> > e[N];
namespace SCC
{
int dfn[N],low[N],idx,st[N],tp;
void dfs(int x)
{
dfn[x]=low[x]=++idx,st[++tp]=x;
for(int i=hed[x];i;i=nxt[i])
{
if(!dfn[tal[i]]) dfs(tal[i]),low[x]=std::min(low[x],low[tal[i]]);
else if(!col[tal[i]]) low[x]=std::min(low[x],low[tal[i]]);
}
if(low[x]==dfn[x]) for(col[x]=++cnt;st[tp--]!=x;) col[st[tp+1]]=cnt;
}
void solve()
{
idx=tp=0;
for(int i=1;i<=2*n+1;i++) low[i]=dfn[i]=col[i]=0;
for(int i=1;i<=2*n+1;i++) if(!dfn[i]) dfs(i);
}
};
bool check()
{
// printf("check ");
// for(int i=1;i<=n;i++) printf("%c",a[i]+'a');
// printf("\n");
for(int i=1;i<=n*2+1;i++) hed[i]=0;
for(;cnte;cnte--) tal[cnte]=nxt[cnte]=0;
for(int i=1;i<=m;i++)
{
int u=e[i].first.first,v=e[i].second.first;
char x=e[i].first.second-'A',y=e[i].second.second-'A';
if(x==a[u]) continue;
bool fg=0;
if(y==a[v]) fg=1;
if(a[u]==2) u=u*2+(x==1);
else if(a[u]==1) u=u*2+(x==2);
else if(a[u]==0) u=u*2+(x==2);
if(a[v]==2) v=v*2+(y==1);
else if(a[v]==1) v=v*2+(y==2);
else if(a[v]==0) v=v*2+(y==2);
//printf("%d: ",i);
if(fg) de(u,u^1);//,printf("kill %d\n",u);
else de(u,v),de(v^1,u^1);
}
SCC::solve();
//for(int i=1;i<=n;i++) printf("%d,%d\n",col[i*2],col[i*2+1]);
for(int i=1;i<=2*n;i++) if(col[i]==col[i^1]) return 0;
for(int i=1;i<=n;i++)
{
if(col[i*2]<col[i*2+1])
{
if(a[i]==2) b[i]=0;
if(a[i]==1) b[i]=0;
if(a[i]==0) b[i]=1;
}
else
{
if(a[i]==2) b[i]=1;
if(a[i]==1) b[i]=2;
if(a[i]==0) b[i]=2;
}
}
return 1;
}
bool solve(int x)
{
if(x>d) return check();
a[p[x]]=0;
if(solve(x+1)) return 1;
a[p[x]]=1;
if(solve(x+1)) return 1;
a[p[x]]=2;
if(solve(x+1)) return 1;
return 0;
}
signed main()
{
std::ios::sync_with_stdio(0);
std::cin.tie(0),std::cout.tie(0);
std::cin>>n>>d>>S>>m;
int tcc=0;
for(int i=1;i<=n;i++) a[i]=S[i-1]-'a';
for(int i=0;i<n;i++) if(S[i]=='x') p[++tcc]=i+1;
for(int i=1;i<=m;i++)
{
int u,v;
char x,y;
std::cin>>u>>x>>v>>y;
e[i]={{u,x},{v,y}};
}
if(!solve(1)) return std::cout<<"-1",0;
for(int i=1;i<=n;i++) std::cout<<(char)(b[i]+'A');
}
不知道为什么 Kosaraju 会 T 掉最后一个点,所以写的 Tarjan。
实际上每一层可以少枚举一次,因为不能用 \(A\) 和不能用 \(B\) 已经覆盖了不能用 \(C\) 的情况。

浙公网安备 33010602011771号