赛前告诉小朋友们去年区域赛出了两题2-sat,今年不会出了,然后我就错了。而且集训队的2-sat是我讲的,结果导致他们三个队都没过,何等罪过啊 T_T
题目意思很简单,两个人石头剪刀布,一个人的出法是确定的,另一个人的出法有一定约束,某两次要相同或者不同,问你第二个人能否全部都不失败。题目还是很容易想到2-sat的,不过按照官方题解里说的每次出拳拆分成两个相对点的建图方法相当麻烦,小明哥按照那种方法建图就写了5k那么长,比较容易理解的方法是拆六个点:
石头(R), 非石头(~R)
布(P), 非布(~P)
剪刀(S), 非剪刀(~S)
每次只能三者取一个,那么就是如果出了一个,另外两个就不能出
R->~P, R->~S, P->~R, P->~S, S->~R, S->~P
然后根据对方的出法,就只能出平局和胜局两种出法
比如:如果对方是石头(R),那么只能出R和P,非R即P,非P即R
~R->P, ~P->R
最后就是加入约束关系,如果两次必须相同:
R1<->R2, P1<->P2, S1<->S2 均是双向边
如果不同也是一样
R1->~R2, R2->~R1, ......
这样建好图之后跑一边2-sat模板就能A掉了
比赛时候模板敲错,查了三个多小时,还好最后A掉,不然就去卧轨了 T_T
1 #include<cstdio>
2 #include<cstring>
3 #include<algorithm>
4 using namespace std;
5 #define N 60005
6 #define M 999999
7
8 struct Edge{
9 int vtx,next;
10 }E[M];
11 int head[N],size;
12 int dfn[N],low[N],blg[N],step,scc;
13 int stk[N],top;
14 bool ins[N];
15
16 void Init(){
17 memset(head,-1,sizeof(head));
18 memset(dfn,-1,sizeof(dfn));
19 memset(ins,false,sizeof(ins));
20 step=size=0; top=-1;
21 }
22
23 void Insert(int u,int v){
24 E[size].vtx=v;
25 E[size].next=head[u];
26 head[u]=size++;
27 }
28
29 void Tarjan(int u){
30 stk[++top]=u; ins[u]=true;
31 dfn[u]=low[u]=step++;
32 for(int i=head[u];~i;i=E[i].next){
33 int v=E[i].vtx;
34 if(dfn[v]==-1){
35 Tarjan(v);
36 low[u]=min(low[u],low[v]);
37 }else if(ins[v]){
38 low[u]=min(low[u],dfn[v]);
39 }
40 }
41 if(low[u]==dfn[u]){
42 for(int v=-1;v!=u;top--){
43 v=stk[top];
44 ins[v]=false;
45 blg[v]=scc;
46 }
47 scc++;
48 }
49 }
50
51 int main(){
52 int t,cas=0;
53 scanf("%d",&t);
54 while(t--){
55 int n,m;
56 scanf("%d%d",&n,&m);
57
58 Init();
59 for(int i=0;i<n;i++){
60 Insert(i*6+0,i*6+3);
61 Insert(i*6+0,i*6+5);
62 Insert(i*6+2,i*6+1);
63 Insert(i*6+2,i*6+5);
64 Insert(i*6+4,i*6+1);
65 Insert(i*6+4,i*6+3);
66 }
67
68 for(int i=0;i<n;i++){
69 int v;
70 scanf("%d",&v);
71 if(v==1){
72 Insert(i*6+3,i*6+0);
73 Insert(i*6+1,i*6+2);
74 }else if(v==2){
75 Insert(i*6+5,i*6+2);
76 Insert(i*6+3,i*6+4);
77 }else if(v==3){
78 Insert(i*6+5,i*6+0);
79 Insert(i*6+1,i*6+4);
80 }
81 }
82 for(int i=0;i<m;i++){
83 int a,b,v;
84 scanf("%d%d%d",&a,&b,&v);
85 a--; b--;
86 if(v){
87 Insert(a*6+0,b*6+1);
88 Insert(a*6+2,b*6+3);
89 Insert(a*6+4,b*6+5);
90
91 Insert(b*6+0,a*6+1);
92 Insert(b*6+2,a*6+3);
93 Insert(b*6+4,a*6+5);
94 }else{
95 Insert(a*6+0,b*6+0);
96 Insert(a*6+2,b*6+2);
97 Insert(a*6+4,b*6+4);
98
99 Insert(b*6+0,a*6+0);
100 Insert(b*6+2,a*6+2);
101 Insert(b*6+4,a*6+4);
102 }
103 }
104 for(int i=0;i<n*6;i++){
105 if(dfn[i]==-1) Tarjan(i);
106 }
107 bool flag=true;
108 for(int i=0;i<n*3;i++){
109 if(blg[i<<1]==blg[i<<1|1]){
110 flag=false;
111 break;
112 }
113 }
114
115 printf("Case #%d: ",++cas);
116 puts(flag?"yes":"no");
117 }
118 }
两个点的建图方法也很容易理解,每一次只有两种出法,平局或是胜局,即如果对方是S,那么只能是R或者P,R和P是两个相对点,这样就省去了上面方法中的前两种建图。不过这时要记下对应的两个点是什么,然后建图就要讨论很多很多东西了:
比如说如果两次分别是RP1和SP2,两次必须相同时候建图
第一次有P,第二次也有P,那么第一个P和第二个P变为强连通,P1<->P2
第一次有R,第二次没R,那么就是R->P1,表示这种情况不可能
S同样,S->P2建边,不可能存在的情况
各种情况讨论下去,然后做强连通
这样做时间和内存都会少很多,但是代码量大了很多,对于O(n+m)的这种算法作用不大...