本文非原创,向原创致敬。转发自https://blog.csdn.net/qq_24451605/article/details/47126143
给出一个序列,每个数是一个bool值,给出一些限制关系,得到最终的可行解的问题叫做适应性问题,也就是sat问题,2-sat问题就是给出的限制最多是两两元素之间的限制。
这种适应性问题的解决,同样是能够抽象为我们已知的图论模型的。
1.我们利用一条有向边
(1)如果给出A和B的限制关系,A和B必须一起选,(A and B)||(!A and !B )==true 那么选A必须选B,建边和还有和
(2)如果给出A和B的限制关系,选A不能选B,那么(A && !B)||(!A && B )==true,建边和
(3)如果必须选A,那么A==true,建边
(4)如果A一定不能选,那么!A==true.建边
这么建图之后,会出现一个有向图,这个有向图会导致一个连通环,导致某个点一旦选取,那么这条链上的所有点都要被选中。如果我们找到一个强连通分量,那么这个强连通分量当中的点,如果选取必须全部选取,不选取的话一定是全部不选取,所以只要满足这个有向图中连通的点不会导致i和i’同时被选取,如果不存在矛盾,那么当前问题就是有解的。但是往往在求解过程中,我们要求的解会要求一些性质,所以提供以下几种解决方案。
1.求字典序最小的解的方法:暴力dfs求解(复杂度O(N*M))
2.判断当前的2-sa问题t是否有解:tarjan强连通缩点,加判断(复杂度O(N+M))
3.求出当前的2-sat问题的任意一组解:tarjan强连通缩点+拓扑排序+构建一组解(复杂度O(N+M))
算法思想:
1.首先定义我们需要用到的数组,mark数组用来标记某个点是否被选取,对于序列中的一个数我们会拆成两个点i和i’,所以我们在利用mark数组进行标记的时候,采用如下这种标记方法:
mark[i<<1]表示i,而mark[i<<1|1]表示i’
一个用来存本次标记过的点的一个队列s
2.枚举每个点,然后判断当前点拆出的两个点是否已经有其中一个被选取,如果有的话,那么继续枚举下一个点,如果没有被标记,那么转到操作3
3.如果某一点拆出的两个点都没有被标记,那么我们先尝试标记第一个点,因为如果标记第一个点会导致一些点必须被标记,所以要进行dfs,然后判断过程中会不会出现矛盾的情况,如果出现了,那么将本次标记的点全部还原,然后就剩下第二个点一种情况,所以我们查看第二种情况,判断会不会出现,,如果出现矛盾,那么问题无解,结束算法如果当前成功标记,那么继续像2那样枚举,直至枚举过所有的点算法结束。
4.因为每次dfs的过程会把所有当前点可达的点都进行标记,所以之后每次标记的过程中,因为已经标记的点,有一个不选的话,那么代表所有的点均不选,且会导致与它同源的那个点一定被选,所以一旦被选中,不能导致出现有解的情况,那么当前情况一定无解,因为每次做的操作只可能会导致图上的点不变或者整体颜色反转,所以只需要让新染色的点两种选择即可,因为得到的结果只有两种,而且同时做反转操作与没做的效果是一样的。
5.因为是按照深搜序做的,所以得到解一定是字典序最小的。
代码如下:
struct TwoSat
{
int n;
vector<int> e[MAX<<1];
int s[MAX<<1],c;
bool mark[MAX<<1];
//mark[i<<1]数组等于1,表示点i被选择
//mark[i<<1|1]数组等于1,表示点i没有被选择
bool dfs ( int x )
//用来判断当前的强连通分量当中会不会出现矛盾
{
//如果需要被选的不能被选那么矛盾
if ( mark[x^1] ) return false;
//如果需要被选的已经被选,那么当前联通分量一定
//不会出现矛盾
if ( mark[x] ) return true;
//如果当前点需要被选,那么选上它,并且标记
mark[x] = true;
//当前的强连通分量加上这个点
s[c++] = x;
//找到与当前点相连点,判断他们的状态
for ( int i = 0 ; i if ( !dfs( e[x][i] ))
return false;
return true;
}
void init ( int n )
{
this->n = n;
for ( int i = 0 ; i < 2*n ; i++ )
e[i].clear();
memset ( mark , 0 , sizeof ( mark ));
}
void add ( int x , int y )
{
e[x].push_back ( y^1 ); //建边操作考虑实际情况修改
e[y].push_back ( x^1 );
}
bool solve ( )
{
for ( int i = 0 ; i < 2*n ; i += 2 )
if ( !mark[i] && !mark[i+1] )
{
c = 0;
if ( !dfs(i) )
{
//如果矛盾,那么这个强连通分量里的点都不能
//选取
while ( c > 0 ) mark[s[--c]]= false;
if ( !dfs(i+1) ) return false;
}
}
return true;
}
算法思想:
1.利用强连通缩点得到一个DAG(有向无环图);
2.然后对于每个强连通分量当中,所有点都是选就一起选,不选就一起不选的,所以如果i和i’同时存在一个强连通分量里,就一定无解
3.如果强连通分量内部不出现矛盾,那么剩下的就是这个有向无环图,因为有向无环图,可以进行拓扑排序,所以只需要交替的染不同的颜色,就能够得到一个解,所以只要强连通分量内部不出现矛盾,那么久一定有解
for ( int i = 0 ; i < 2*n ; i++ )
if ( !mark[i] ) tarjan ( i );
for ( int i = 0 ; i < n ; i++ )
if ( belong[i<<1] == belong[i<<1|1] )
return false;
return true;
void tarjan ( int u )
{
dfn[u] = low[u] = ++times;
mark[u] = 1;
s.push ( u );
int len = e[u].size();
for ( int i= 0 ; i < len ; i++ )
{
int v = e[u][i];
if ( !mark[v] )
{
tarjan ( v );
low[u] = min ( low[u] , low[v] );
}
if ( mark[v] == 1 )
low[u] = min ( low[u] , dfn[v] );
}
if ( dfn[u] == low[u] )
{
int temp;
do
{
temp = s.top();
belong[temp] = cnt;
mark[temp] = 2;
s.pop();
}while ( temp != u );
cnt++;
}
}
1.首先依旧要进行强连通缩点,我们得到一个DAG
2.然后我们要得到新得到的图中的矛盾关系,也就是i和i’所在的强连通分量是矛盾的。
3.然后我们对DAG进行染色,在拓扑排序的过程中进行染色,如果某个点没有染色,那么染为1,并且将与他矛盾的点染为2,因为矛盾关系是两两之间的,所以不会与其他点出现矛盾。
4.那么在拓扑排序结束之后就对所有点进行完染色了。拓扑只是在有向无环图中的一种很好的遍历方式
代码如下:
void topsort ( )
{
int i,j;
queue<int> q;
for ( int i = 1 ; i < t ; i++ )
{
if (!in[i])
q.push ( i );
}
while (!q.empty())
{
int u = q.front();
q.pop();
if ( !col[u] )
{
col[u] = 1;
col[conflict[u]] = 2;
}
for ( int i = 0 ; i < g[u].size(); i++ )
{
int v = g[u][i];
in[v]--;
if ( !in[v] ) q.push ( v );
}
}
}