[UOJ210]-寻找罪犯-前缀边优化2-SAT

说在前面

学了一天2-SAT
从不会到寻找罪犯,感觉成就感满满
顺便还嘴巴AC了 NOI2017游戏 (虽然这就是一道水题,真的是水题)


题目

UOJ210传送门
题面me就不贴了
要看的话可以戳传送门


解法

Emmmm网上的做法为什么都那么简单啊woc?感觉全世界就me建边最多
这道题的限制条件还是很清晰了:
1. 一句话,不是真的就是假的
2. 一个人,不是真人(好人)就是假人(坏人)
3. 一个人如果是假人,那么至多有一句话是假的

然而发现这第3个条件十分坑,数据规模太大,直接建边是会爆炸的…
于是对于每个人,再定义一个前缀真话,表示这个人的 这句话以及这句话之前的所有话中,是否全是真话
建边很好想的,画个图梳理逻辑结构(图me会附在后面),然后建好了跑就是。

最后输出方案的时候,选择所在强连通分量编号较小的那个即可。关于正确性:如果A和!A在不同的图里面,那么选谁都是一样的。如果A和!A在同一张图里,就需要选拓扑序靠后的,而先dfs到的强连通分量,深度更深,拓扑序也相对靠后。


下面是me自带大常数的代码

#include 
#include 
#include 
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:逻辑关系图

[UOJ210]-寻找罪犯-前缀边优化2-SAT_第1张图片
(UPD 2018.7.10:貌似有个箭头反了….
箭头表示推导关系,直接按照这个关系建图即可
最后一个供词前缀 和 其他供词前缀 实际上是连起来的(画图太难用了,所以me没有画出来)

me感觉2-sat的题都可以通过画这样的图的关系来理一理思路,但是在画图的时候一定要考虑到所有情况!

你可能感兴趣的:(2-SAT)