loj10097 2-sat

前言:

一开始接触2-sat问题的时候我觉得一切都是那么显然。。。然后碰到题目就上2-sat。。。毫无意外地WA了一堆。

然后我以为是有鬼畜的数据,于是没有调。

然后我做到了这道题。。。写着写着突然对自己以前的理解产生了怀疑,感觉每一步都很需要证明啊。。。

没有证明的话,瞎写当然会WA。于是我就证(luan)明(gao)了一发。

题意:裸题不说了。orz。

这里非常重要的一点在于,ab是互相厌恶的。所以建图具有对称性。

对称性有什么用呢??

首先是一种暴力的方法, 可以输出字典序最小的方案。

#include
using namespace std;
#define rep(x,y,z) for (int x=y; x<=z; x++)
#define downrep(x,y,z) for (int x=y; x>=z; x--)
#define ms(x,y,z) memset(x,y,sizeof(z))
#define LL long long
#define repedge(x,y) for (int x=hed[y]; ~x; x=edge[x].nex)
inline int read(){
	int x=0; int w=0; char ch=0;
	while (ch<'0' || ch>'9') w|=ch=='-',ch=getchar();
	while (ch>='0' && ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
	return w? -x:x;
}
const int N=16005;
const int M=N*2;
int col[N],hed[N],tmp[N],cnt,n,m,nedge;
struct Edge{ int to,nex; }edge[M<<1];
int oth(int x){ return (x&1)? (x+1):(x-1); }
void addedge(int a,int b){
	edge[nedge].to=b; edge[nedge].nex=hed[a]; hed[a]=nedge++;
}
int dfs(int k){
	if (col[oth(k)]==1) return 0;
	if (col[k]==1) return 1;
	tmp[++cnt]=k; col[k]=1; 
	repedge(i,k)
	if (!dfs(edge[i].to)) return 0;
	return 1;
}
int work(){
	rep(i,1,n){
		if ((col[i])||(col[oth(i)])) continue;
		cnt=0; if (!dfs(i)){ rep(j,1,cnt) col[tmp[j]]=col[oth(tmp[j])]=0; 
		if (!dfs(oth(i))) return 0; }
	}
	return 1;
}
int main(){
	n=read(); n<<=1; m=read(); nedge=0; ms(hed,-1,hed);
	rep(i,1,m){ int a=read(); int b=read(); addedge(a,oth(b)); addedge(b,oth(a)); }
	if (work()) { rep(i,1,n) if (col[i]) printf("%d\n",i); }
	else puts("NIE");
	return 0;
}

这个做法看起来毫无毛病,其实是要证明的。

对于{i0,i1},

1.只能选i0或只能选i1。那当然直接染就行了啊……

2.看起来两个都可以的样子,染i0。

->Q:有没有可能染了i0以后导致之后存在一个{j0,j1},都不行,而染i1就行?

A:不可能的,如果存在这样的情况则j0j1都和i0互斥,由于对称性,i0到它们两者肯定都有边,i0不是合法点,不可能被染色。

 

以上是O(nm)的暴力染色法,太暴力辣。。。

如果只要求构造任意一种方案的话,其实有更简单的做法。

强连通分量缩点+拓扑排序。

#include
using namespace std;
#define rep(x,y,z) for (int x=y; x<=z; x++)
#define downrep(x,y,z) for (int x=y; x>=z; x--)
#define ms(x,y,z) memset(x,y,sizeof(z))
#define LL long long
#define repedge(x,y) for (int x=hed[y]; ~x; x=edge[x].nex)
inline int read(){
	int x=0; int w=0; char ch=0;
	while (ch<'0' || ch>'9') w|=ch=='-',ch=getchar();
	while (ch>='0' && ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
	return w? -x:x;
}
const int N=16005;
const int M=20005;
int n,m,low[N],dfn[N],hed[N],Nedge,head[N],tot,nedge,Oth[N],bel[N],sz[N],col[N],ind[N],scc;
queue Q;
vector dt[N];
stack s;
map mat[N];
struct Edge{ int to,nex; }edge[M<<1],E[M<<1];
int oth(int x){ return (x&1)? (x+1):(x-1); }
void addedge(int a,int b){
	edge[nedge].to=b; edge[nedge].nex=hed[a]; hed[a]=nedge++;
}
void tarjan(int k){
	dfn[k]=low[k]=++tot; s.push(k);
	repedge(i,k){
		int v=edge[i].to;
		if (!dfn[v]){ tarjan(v); low[k]=min(low[k],low[v]);	}
		else if (!bel[v]) low[k]=min(low[k],dfn[v]);
	}
	if (low[k]==dfn[k]){
		++scc; for(;;){ int x=s.top(); s.pop(); ++sz[scc]; bel[x]=scc;
		dt[scc].push_back(x); if (x==k) break; }
	}
}
#define repE(x,y) for(int x=head[y]; ~x; x=E[x].nex)
void addE(int a,int b){ 
    E[Nedge].to=b; E[Nedge].nex=head[a]; head[a]=Nedge++;
}
void rebuild(int t){
	rep(i,0,sz[t]-1){
		int k=dt[t][i];
		repedge(j,k) if ((bel[edge[j].to]!=t)&&(!mat[bel[edge[j].to]][t]))
		{ mat[bel[edge[j].to]][t]=1; addE(bel[edge[j].to],t); ++ind[t]; }
	}
}
void tuopu(){
	rep(i,1,scc) if (!ind[i]) Q.push(i); ms(col,-1,col);
	while (!Q.empty()){
		int k=Q.front(); Q.pop();
		if (col[k]==-1){ col[k]=1; col[Oth[k]]=0; } 
		repE(i,k){ int v=E[i].to;
		   --ind[v]; if (!ind[v]) Q.push(v);
		}
	}
}
int main(){
	n=read(); n<<=1; m=read(); nedge=0; ms(hed,-1,hed);
	rep(i,1,m){ int a=read(); int b=read(); addedge(a,oth(b)); addedge(b,oth(a)); }
	rep(i,1,n) if (!dfn[i]) tarjan(i);
	rep(i,1,n) if (bel[i]==bel[oth(i)]) { puts("NIE"); return 0; }
	rep(i,1,n) Oth[bel[i]]=bel[oth(i)];
	Nedge=0; ms(head,-1,head); rep(i,1,scc) rebuild(i); 
	tuopu(); rep(i,1,n) if (col[bel[i]]==1) printf("%d\n",i);
	return 0;
}

来研究一下这张图吧。

1.对于任意强连通分量f,它内部选择了一个点则整个f都要选,不选某个点则整个f都不能选。即要么一起选要么一起不选。而所有满足这样性质的点也必然属于同一个强连通分量。

2.对于任意分量f,它内部所有点的反点一定属于同一个强连通分量。这一点由1可以证明。

(如果一起选了这些点,它们的反点一定一起不选;反之亦然,由1得,所有反点属于同一强连通分量。)

于是每个分量有唯一的反分量,证明了分量和点是完全等同的。

3.观察边。边其实是一种依赖关系:

如果选a则一定要选b。这句话其实就是说,选了b可能选a,不选b一定不选a。

即,如果b=0则a=0,如果b=1则a随意。

反着建边,原本a->b,建成b->a,完美地表示了这种依赖关系,可以拓扑排序了。

拓扑时,发现没被染的点就染成合法即可。(不能乱染啊染成不合法要出人命的)

为啥这样是对的呢?

证明:b->c,如果b=0,则c=0;

设f的反分量是f'。

3.1如果f被染成0,一定是因为f'被染成了1。

3.2f和f',一定是先出现的那个被染成1。

3.3根据对称性,如果存在边b->c,一定存在边c'->b'。*重点!!

3.4根据拓扑排序的性质,如果存在边b->c,则time[b]

据3.1,若b=0,则b'=1;

据剩下的性质,得time[c'],所以c'=1,c=0。

其它各种情况的证明非常显然,不说了。

总结:拓扑排序将它们的关系通过建边映射到了时间上,相当于构造了不等式;

2-sat特殊的对称性则保证了不等式中4个元素一定同时出现。

其实步骤不应该是先写出代码再证明,应该是先用有序思想把不等式列出来,然后按照定义01赋值的。拓扑排序遇到

没有染色的点时为什么要染成1原因就在这里。

你可能感兴趣的:(图论)