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)\),有四种距离方案:

  1. \(x\rightarrow S_1\rightarrow y\)
  2. \(x\rightarrow S_2\rightarrow y\)
  3. \(x\rightarrow S_1\rightarrow S_2\rightarrow y\)
  4. \(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\) 的情况。

posted @ 2025-06-25 15:43  整齐的艾萨克  阅读(9)  评论(0)    收藏  举报