您的位置:首页 > 其它

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

2018-01-19 20:48 363 查看

说在前面

学了一天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:逻辑关系图



箭头表示推导关系,直接按照这个关系建图即可

最后一个供词前缀 和 其他供词前缀 实际上是连起来的(画图太难用了,所以me没有画出来)

me感觉2-sat的题都可以通过画这样的图的关系来理一理思路,但是在画图的时候一定要考虑到所有情况!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: