/* 2-sat 题意:m个门,每个门上有两把锁,打开一个就可以通过 2n个钥匙,每两个绑在一起,只能选用一个 问最多可以通过几扇门? 2-sat问题关键在建图,2-sat对每个事物都有两个选项 可以这么建: 每把钥匙有两个状态(用或不用),把这作为2-sat的两个选项 然后是加条件,a、b绑在一起,则选a就不选b,选b就不选a,建边a->!b,b->!a c、d在同一个门上,则不开c就开d,不开d就开c,建边!c->d,!d->c 然后二分答案都可以了 代码是根据上一题改的,可能有的代码和变量没有用到 */ #include<stdio.h> #include<string.h> #include<queue> #include<stack> using namespace std; struct edge { int yong; int v[1000000]; int next[1000000]; int head[1000000]; edge() { clear(); } void clear() { yong=1; memset(head,0,sizeof(head)); } void add(int u,int w) { v[yong]=w; next[yong]=head[u]; head[u]=yong; yong++; } }e1,e2; int dfn[5000],low[5000]; int n,m,index,scc; queue<int>q; stack<int>ss; int ins[5000],belong[5000],dui[5000],ind[5000],fang[5000]; int a[1100],b[1100]; int c[2500],d[2500]; void tarjan(int u)//tarjan求强连通分量 { int i,v; dfn[u]=low[u]=index++; ins[u]=1; ss.push(u);//之前误写成队列 for(i=e1.head[u];i;i=e1.next[i]) { v=e1.v[i]; if(dfn[v]==0) { tarjan(v); if(low[v]<low[u]) low[u]=low[v]; }else if(ins[v]&&dfn[v]<low[u]) low[u]=dfn[v]; } if(low[u]==dfn[u]) { do{ v=ss.top(); ss.pop(); ins[v]=0; belong[v]=scc;//标记所属强连通分量的标号 }while(v!=u); scc++; } } int havetry(int mid) { //每次都要从新建图 e1.clear(); int i; for(i=1;i<=n;i++) { e1.add(a[i]*2,b[i]*2+1);//a[]b[]c[]d[]里村的都是钥匙编号,因为每个钥匙都有两个状态,*2表示被选状态,*2+1表示不被选状态 e1.add(b[i]*2,a[i]*2+1); } for(i=1;i<=mid;i++) { e1.add(c[i]*2+1,d[i]*2); e1.add(d[i]*2+1,c[i]*2); } index=1;//dfn[]标记 scc=0;//强连通分量个数,从0开始计数 memset(dfn,0,sizeof(dfn)); memset(ins,0,sizeof(ins)); for(i=0;i<4*n;i++)//有n组钥匙,2*n个钥匙,4*n个钥匙状态 { if(!dfn[i]) tarjan(i); } for(i=0;i<2*n;i++) {//belong[i]表示i所属的强连通分量的标号 if(belong[2*i]==belong[2*i+1])//若有夫妇在同一强连通分量中,无解 break; } if(i==2*n) return 1; return 0; } int main() { int i,mid,max,min; while(scanf("%d%d",&n,&m),n+m) { //读入数据 for(i=1;i<=n;i++) scanf("%d%d",&a[i],&b[i]); for(i=1;i<=m;i++) scanf("%d%d",&c[i],&d[i]); //二分答案 min=0; max=m+1; for(;;) { mid = (max + min) / 2; if(mid == min) break; if(havetry(mid)) min = mid; else max = mid; } printf("%d\n",mid); } return 0; }
第二种建图方法:
/* 也可以这样建图: 每组钥匙有两个选择,把这作为2-sat的两个选择 c、d在同一个门上,则不开c(c和某个钥匙在一组,假设a,不开c,那么就一定开a)就开d, 不开d(d和某个钥匙在一组,假设b,不开d,那么就一定开b)就开c, 建边a->d,b->c 或者这么说 !c<=>a,!c->d,故a->d !d<=>b,!d->c,故b->c */ #include<stdio.h> #include<string.h> #include<queue> #include<stack> using namespace std; struct edge { int yong; int v[1000000]; int next[1000000]; int head[1000000]; edge() { clear(); } void clear() { yong=1; memset(head,0,sizeof(head)); } void add(int u,int w) { v[yong]=w; next[yong]=head[u]; head[u]=yong; yong++; } }e1,e2; int dfn[5000],low[5000]; int n,m,index,scc; queue<int>q; stack<int>ss; int ins[5000],belong[5000],dui[5000],ind[5000],fang[5000]; int id[2500]; int c[2500],d[2500]; void tarjan(int u)//tarjan求强连通分量 { int i,v; dfn[u]=low[u]=index++; ins[u]=1; ss.push(u);//之前误写成队列 for(i=e1.head[u];i;i=e1.next[i]) { v=e1.v[i]; if(dfn[v]==0) { tarjan(v); if(low[v]<low[u]) low[u]=low[v]; }else if(ins[v]&&dfn[v]<low[u]) low[u]=dfn[v]; } if(low[u]==dfn[u]) { do{ v=ss.top(); ss.pop(); ins[v]=0; belong[v]=scc;//标记所属强连通分量的标号 }while(v!=u); scc++; } } int havetry(int mid) { e1.clear(); int i; for(i=1;i<=mid;i++) { e1.add(c[i]^1,d[i]);//注意这儿啊 a^1表示与a同一组的另外的那个节点,见开头的建图原则 e1.add(d[i]^1,c[i]); } index=1;//dfn[]标记 scc=0;//强连通分量个数,从0开始计数 memset(dfn,0,sizeof(dfn)); memset(ins,0,sizeof(ins)); for(i=0;i<2*n;i++) { if(!dfn[i]) tarjan(i); } for(i=0;i<n;i++) {//belong[i]表示i所属的强连通分量的标号 if(belong[2*i]==belong[2*i+1])//若有夫妇在同一强连通分量中,无解 break; } if(i==n) return 1; return 0; } int main() { int i,mid,max,min,numed,a,b; while(scanf("%d%d",&n,&m),n+m) { numed=0; for(i=1;i<=n;i++) { scanf("%d%d",&a,&b);//读到的是钥匙的编号,但是进行强连通的时候节点是按组安排在一起的,所以要按节点顺序保存, id[a]=numed++;//故用id[i]表示编号为i的钥匙的节点序号 id[b]=numed++; } for(i=1;i<=m;i++) { scanf("%d%d",&a,&b); c[i]=id[a];//c[]d[]表示的是门上的锁所对应的钥匙的节点序号 d[i]=id[b]; } //二分答案 min=0; max=m+1; for(;;) { mid = (max + min) / 2; if(mid == min) break; if(havetry(mid)) min = mid; else max = mid; } printf("%d\n",mid); } return 0; }