2-SAT是求解一组逻辑变量表达式(avb)^(cvd)...(xvy)=1成立的问题,称为适定性问题(Satisfiability),简称SAT。
对于逻辑表达式(xvy),构造有向图G,也就是说,x和y至少选一个:如果不选x,肯定要选y;不选y,肯定要选x。在图G中添加两条有向边(~x,y)和(~y,x)。
对于逻辑表达式(x^y),x和y都要选:若选x,则必选y(x,y);选y,必选x(y,x);不选x,则无解(~x,x);不选y,则无解(~y,y)。
然后求图G的强连通分量,对于图中的任何一个强连通分量,如果我们选择其中的任何一个点,那么该SCC中所有其它顶点也必须被选择。如果x和~x属于同一个连通分量,那么产生矛盾,该2-SAT无解。如果没有产生矛盾,将图缩点,在其反向图中用拓扑排序和染色操作可以求出一组解。
关于2-SAT的详细介绍,可以参考赵爽和伍昱的论文。
例:HDU3062
题意:有n对夫妻收到一个派对的邀请,由于场地限制,只能让丈夫和妻子其中一位参加(必须参加);并且,有些人之间有仇恨,不能同时出现在派对上。问最终能否有一个方案,使得有n个人能参加这个派对。
解:0~n-1表示妻子参加派对(x),n~2*n-1表示丈夫参加派对(~x)。对于仇恨的一对(x,y),即x和y中至少选一个。
例:POJ2723
题意:有n重门,每把门上有2把锁,打开其中一把这个门就算打开了。现在有2m把钥匙,分为m串,每串2把钥匙,使用其中一把,另一把也自动消失。问最多能打开几重门。
解:能打开n重门,肯定可以打开n-1重门。首先二分答案,对于前k重门的锁(x,y),x和y至少要打开一把,即(~x,y)和(~y,x)。对于这m串钥匙(x,y),x和y不能同时取,即(x,~y)和(y,~x)。
例:POJ3207
题意:有一个圆,圆上有n个点0,1,2,..,n-1。现在想在某两点之间连一条线,这条线可以画在圆内或者圆外。现在有m对点需要连线,问是否有一种方案,使得这些连线之间没有交点。
解:一条线只有两种状态,圆内x或者圆外~x。对任意的两条连线:若他们构成矩形的对角线(即有可能相交),则如果一条在圆内,另一条只能在圆外。即如果两条连线x和y的端点分别为(a,b)和(c,d),如果a<c&&c<b<d或者c<a&&a<d<b(即线段相交),那么有边(x,~y),(~x,y),(y,~x),(~y,x)。最后看是否有x和~x属于同一个scc。
例:POJ3683
题意:9月1号是牧师john最忙的一天,他要帮这天结婚的人举行一个仪式。每对新人的婚礼时间从si到ti,仪式的持续时间是di,并且仪式只能在婚礼开始或者结束的时候做,即仪式在si到si+di或者ti-di到di举行。当然,john在同一时间只能主持一对新人的仪式。问是否有一种方案,使得每对新人都可以顺利的举行仪式,有的话输出任意一种方案。
解:每对新人举行仪式的时间要么在开始(x),要么在结束(~x)。对于每两对新人a和b,分四种情况:a开始b开始,a开始b结束,a结束b开始,a结束b结束。如果其中有时间段重复(重复的条件是线段有公共部分,线段(a,b)和(c,d)有公共部分的条件是a<d&&c<b),例如a开始b开始有重复,则有边(a,~b),(b,~a)。然后求强连通并缩点,将缩点后的图反向,用拓扑排序,求出一组可行的解。
/* 2-sat 构图: 每对新人的仪式可以在开始和结束做。 把每对新人可以做ceremony的两个时间段记为两个顶点, 枚举所有点的冲突情况构图, 这里判断冲突可以这样做: 设顶点A的表示时间段[a1,b1], 顶点B表示时间段[a2,b2], 发生冲突的充要条件是a1<b2&&a2<b1(两条线段有公共部分). 然后就是2-SAT的标准算法, 做强连通, 缩点, 反向拓扑排序, 红蓝标记等等, 赵爽的论文上说得很清楚, 只是这里不需要像论文上说的每次在标记红色后都把不相容的顶点及及其子节点也标记蓝色, 只需要标记不相容顶点即可, 不用DFS取标记子节点. */ #include <iostream> #include <queue> using namespace std; const int MAX = 2005; int n; char s[MAX/2][6],t[MAX/2][6]; int d[MAX/2]; struct Edge { int from; //用于缩图方便 int to; int next; }e[4000000],e2[4000000]; int index[MAX],edgeNum; int index2[MAX],edgeNum2; int dfn[MAX],low[MAX],seq; int stack[MAX],top; bool inStack[MAX]; int belong[MAX],cnt; int color[MAX]; //0未着色,1红色,-1蓝色 int indegree[MAX]; //入度,用于拓扑排序 int opp[MAX]; //连通分量i的矛盾方为cf[i] bool ans[MAX]; int min(int x, int y) { return x<y?x:y; } void addEdge(int from, int to) { e[edgeNum].from = from; e[edgeNum].to = to; e[edgeNum].next = index[from]; index[from] = edgeNum++; } void addEdge2(int from, int to) { e2[edgeNum2].from = from; e2[edgeNum2].to = to; e2[edgeNum2].next = index2[from]; index2[from] = edgeNum2++; } void addTime(char *t, int len, char* tt) { int hour1,minute1; //原来的时间 int hour,minute; //时间增量 hour1 = (t[0]-'0')*10 + t[1]-'0'; minute1 = (t[3]-'0')*10 + t[4]-'0'; hour = len/60; minute = len%60; hour += hour1; minute += minute1; while(minute < 0) { hour--; minute += 60; } while(minute >= 60) { hour++; minute -= 60; } tt[0]=hour/10+'0'; tt[1]=hour%10+'0'; tt[2]=':'; tt[3]=minute/10+'0'; tt[4]=minute%10+'0'; tt[5] = '\0'; } bool check(char* t1, int len1,char *t2, int len2) //判断两个时间段是否有重合 { char *a1,*b1,*a2,*b2; char t11[6],t22[6]; addTime(t1,len1,t11); addTime(t2,len2,t22); if(len1 > 0) { a1 = t1; b1 = t11; } else { a1 = t11; b1 = t1; } if(len2 > 0) { a2 = t2; b2 = t22; } else { a2 = t22; b2 = t2; } if( (strcmp(a1,b2)<0&&strcmp(a2,b1)<0) || (strcmp(a2,b1)<0&&strcmp(a1,b2)<0) ) //两个区间有公共部分的条件 return true; return false; } void tarjan(int u) { dfn[u] = low[u] = seq++; stack[top++] = u; inStack[u] = true; for(int i = index[u]; i != -1; i = e[i].next) { int w = e[i].to; if(dfn[w]<0) { tarjan(w); low[u] = min(low[u],low[w]); } else if(inStack[w]) low[u] = min(low[u],dfn[w]); } if(dfn[u] == low[u]) { int v; cnt++; do { top--; v = stack[top]; inStack[v] = false; belong[v] = cnt; }while(u!=v); } } bool solve() { int i; int t = n<<1; for(i = 0; i < t; i++) { if(dfn[i] < 0) tarjan(i); } for(i = 0; i < n; i++) { if(belong[i] == belong[i+n]) return false; opp[belong[i]] = belong[i+n]; opp[belong[i+n]] = belong[i]; } return true; } void solve1() { int i; queue<int> que; memset(color,0,sizeof(color)); for(i = 0; i < edgeNum; i++) { if(belong[e[i].from] != belong[e[i].to]) { addEdge2(belong[e[i].to], belong[e[i].from]); //反向图 indegree[belong[e[i].from]]++; } } for(i = 1; i <= cnt; i++) if(indegree[i] == 0) que.push(i); while(!que.empty()) { int now = que.front(); if(color[now]==0) { color[now] = 1; //置为红色 color[opp[now]] = -1; //将它的矛盾置为绿色 } que.pop(); for(i = index2[now]; i != -1; i = e2[i].next) { int next = e2[i].to; indegree[next]--; if(indegree[next]==0) que.push(next); } } memset(ans,0,sizeof(ans)); for(i = 0; i < 2*n; i++) //2-sat的一组解就等价于所有缩点后点颜色为红色的点 { if(color[belong[i]]==1) ans[i] = true; } } int main() { int i,j; edgeNum = edgeNum2 = seq = top = cnt = 0; memset(dfn,-1,sizeof(dfn)); memset(index,-1,sizeof(index)); memset(index2,-1,sizeof(index2)); memset(inStack,0,sizeof(inStack)); memset(indegree,0,sizeof(indegree)); memset(opp,0,sizeof(opp)); scanf("%d",&n); for(i = 0; i < n; i++) scanf("%s %s %d",s[i],t[i],&d[i]); for(i = 0; i < n; i++) { for(j = i+1; j < n; j++) { //i前j前 if(check(s[i],d[i],s[j],d[j])) { addEdge(i,j+n); addEdge(j,i+n); } //i前j后 if(check(s[i],d[i],t[j],-d[j])) { addEdge(i,j); addEdge(j+n,i+n); } //i后j前 if(check(t[i],-d[i],s[j],d[j])) { addEdge(i+n,j+n); addEdge(j,i); } //i后j后 if(check(t[i],-d[i],t[j],-d[j])) { addEdge(i+n,j); addEdge(j+n,i); } } } if(solve()) { printf("YES\n"); //缩点得到的图反向 //用拓扑排序得到一个解 solve1(); char result[6]; for(i = 0; i < n; i++) { if(ans[i]) //开始的时候举行 { addTime(s[i],d[i],result); printf("%s %s\n",s[i],result); } else { addTime(t[i],-d[i],result); printf("%s %s\n",result,t[i]); } } } else printf("NO\n"); return 0; }