说在前面
学了一天2-SAT
从不会到寻找罪犯,感觉成就感满满
顺便还嘴巴AC了 NOI2017游戏 (虽然这就是一道水题,真的是水题)
题目
UOJ210传送门
题面me就不贴了
要看的话可以戳传送门
解法
Emmmm网上的做法为什么都那么简单啊woc?感觉全世界就me建边最多
这道题的限制条件还是很清晰了:
1. 一句话,不是真的就是假的
2. 一个人,不是真人(好人)就是假人(坏人)
3. 一个人如果是假人,那么至多有一句话是假的
然而发现这第3个条件十分坑,数据规模太大,直接建边是会爆炸的…
于是对于每个人,再定义一个前缀真话,表示这个人的 这句话以及这句话之前的所有话中,是否全是真话
建边很好想的,画个图梳理逻辑结构(图me会附在后面),然后建好了跑就是。
最后输出方案的时候,选择所在强连通分量编号较小的那个即可。关于正确性:如果A和!A在不同的图里面,那么选谁都是一样的。如果A和!A在同一张图里,就需要选拓扑序靠后的,而先dfs到的强连通分量,深度更深,拓扑序也相对靠后。
下面是me自带大常数的代码
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;
int N , M , tp , id_c , head[600007] ;
int personT[100005] , personF[100005] , provT[100005] , provF[100005] ;
int preT[100005] , preF[100005] , las[100005] , oppo[600005] ;
struct Path{
int pre , to ;
}p[100000*10 + 100000*2 + 5];
void GG(){
puts( "Impossible" ) ;
exit( 0 ) ;
}
void In( int t1 , int t2 ){
p[++tp].pre = head[t1] ;
p[ head[t1] = tp ].to = t2 ;
}
int sta[600005] , topp ;
int dfn[600005] , dfs_c , scc[600005] , scc_cnt ;
int dfs( int u ){
sta[++topp] = u ;
int lowu = dfn[u] = ++dfs_c ;
for( int i = head[u] ; i ; i = p[i].pre ){
int v = p[i].to ;
if( !dfn[v] ) lowu = min( lowu , dfs( v ) ) ;
else if( !scc[v] ) lowu = min( lowu , dfn[v] ) ;
}
if( lowu == dfn[u] ){
++scc_cnt ;
while( 1 ){
int x = sta[topp--] ;
scc[x] = scc_cnt ;
if( x == u ) break ;
}
} return lowu ;
}
bool choose[600005] ;
void solve(){
for( int i = 1 ; i <= id_c ; i ++ )
if( !dfn[i] ) dfs( i ) ;
for( int i = 1 ; i <= id_c ; i ++ ){
if( scc[i] == scc[ oppo[i] ] ) GG() ;
else if( scc[i] < scc[ oppo[i] ] ) choose[ scc[i] ] = true ;
else choose[ scc[oppo[i]] ] = true ;
}
int cnt = 0 ;
for( int i = 1 ; i <= N ; i ++ )
if( choose[ scc[personF[i]] ] ) cnt ++ ;
printf( "%d\n" , cnt ) ;
for( int i = 1 ; i <= N ; i ++ )
if( choose[ scc[personF[i]] ] ) printf( "%d " , i ) ;
}
void init(){
for( int i = 1 ; i <= N ; i ++ ){
personT[i] = ++id_c , personF[i] = ++id_c ;
oppo[ personT[i] ] = personF[i] ;
oppo[ personF[i] ] = personT[i] ;
} for( int i = 1 ; i <= M ; i ++ ){
preT[i] = ++id_c , preF[i] = ++id_c ;
provT[i] = ++id_c , provF[i] = ++id_c ;
oppo[ preT[i] ] = preF[i] ;
oppo[ preF[i] ] = preT[i] ;
oppo[ provT[i]] = provF[i] ;
oppo[ provF[i]] = provT[i] ;
}
}
int main(){
scanf( "%d%d" , &N , &M ) ;
init() ;
for( int i = 1 , x , y , isgood ; i <= M ; i ++ ){
scanf( "%d%d%d" , &x , &y , &isgood ) ;
if( isgood ){//证词和实际的人可互推
In( provT[i] , personT[y] ) ;
In( personT[y] , provT[i] ) ;
In( provF[i] , personF[y] ) ;
In( personF[y] , provF[i] ) ;
} else {
In( provT[i] , personF[y] ) ;
In( personF[y] , provT[i] ) ;
In( provF[i] , personT[y] ) ;
In( personT[y] , provF[i] ) ;
}
In( preT[i] , provT[i] ) ;//前缀和当前证词的关系
In( provF[i] , preF[i] ) ;
if( las[x] ){
In( preT[i] , preT[ las[x] ] ) ;//前缀间关系
In( preF[ las[x] ] , preF[i] ) ;
In( preF[ las[x] ] , provT[i] ) ;//前一个前缀与当前证词的关系
In( provF[i] , preT[ las[x] ] ) ;
}
las[x] = i ;
}
for( int i = 1 ; i <= N ; i ++ )
if( las[i] ){//前缀 和 实际的人的关系
In( personT[i] , preT[ las[i] ] ) ;
In( preF[ las[i] ] , personF[i] ) ;
}
solve() ;
}
附录1:逻辑关系图
(UPD 2018.7.10:貌似有个箭头反了….
箭头表示推导关系,直接按照这个关系建图即可
最后一个供词前缀 和 其他供词前缀 实际上是连起来的(画图太难用了,所以me没有画出来)
me感觉2-sat的题都可以通过画这样的图的关系来理一理思路,但是在画图的时候一定要考虑到所有情况!