最小字典序输出
题意:
现在有n个党派,每个党派有2个代表,我们需要从每个党派中选一个代表出来,构成一个n个人的立法委员会.但是可能有一些代表互相讨厌,所以他们不能同时出现在立法委员会中.现在问你是否存在一个合理的方案,且输出所有可能立法委员会的最小字典序结果.
分析:
要输出最小字典序的2-SAT问题,用刘汝佳 训练指南上的方法是最简单的(即按字典序顺序构造每个选择即可). 这里我们把每个党派看出一个点,从0到n-1. 如果对于a(a从0到2*n-1)代表与b代表不和,那么G[a].push_back(b^1) 且 G[b].push_back(a^1).
AC代码:
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
const int maxn= 8000+10;
struct TwoSAT
{
int n;
vector<int> G[maxn*2];
int S[maxn*2], c;
bool mark[maxn*2];
bool dfs(int x)
{
if(mark[x^1]) return false;
if(mark[x]) return true;
mark[x]= true;
S[c++]=x;
for(int i=0;i<G[x].size();i++)
if(!dfs(G[x][i])) return false;
return true;
}
void init(int n)
{
this->n=n;
for(int i=0;i<2*n;i++) G[i].clear();
memset(mark,0,sizeof(mark));
}
void add_clause(int x,int y)//注意这里的修改
{
G[x].push_back(y^1);
G[y].push_back(x^1);
}
bool solve()
{
for(int i=0;i<2*n;i+=2)
if(!mark[i] && !mark[i+1])
{
c=0;
if(!dfs(i))
{
while(c>0) mark[S[--c]]=false;
if(!dfs(i+1)) return false;
}
}
return true;
}
void print()
{
if(!solve()) printf("NIE\n");
else
{
for(int i=0;i<2*n;i++)if(mark[i])
printf("%d\n",i+1);
}
}
}TS;
int main()
{
int n,m;
while(scanf("%d%d",&n,&m)==2)
{
TS.init(n);
while(m--)
{
int a,b;
scanf("%d%d",&a,&b);
a--, b--;
TS.add_clause(a,b);
}
TS.print();
}
return 0;
题意:
一个N个顶点和M条边的有向图,每个顶点能取0或1两个值.现在每条边被一个操作符(or,and,xor)以及一个值(0或1)标记了,表示a与b按操作符运算的结果是值(0或1).问你该有向图是否有可行解?
分析:
由于每个点只能取0或1两个值,所以我们把该问题转化为2-SAT问题.原图中的每个点对应2-SAT中的每个点.对于每种运算有下列转换方式:
a and b = 0 转换为 a=0 或 b=0
a and b = 1 转换为 a=1 且 b=1 即添加边 2*a->2*a+1 2*b->2*b+1(只要a为0或b为0必然引起矛盾)
a or b = 0 转换为 2*a+1->2*a 2*b+1->2*b(只要a为1或b为1必然引起矛盾)
a or b = 1 转换为 a=1 或b=1
a xorb=0转换为 a=1且b=1 或 a=0且b=0 即连下面的边:
2*a->2*b 2*b->2*a 2*a+1->2*b+1 2*b+1->2*a+1.
a xor b=1 转换为a=1且b=0 或a=0且b=1 则连下面的边:
2*a+1->2*b 2*b->2*a+1 2*a->2*b+1 2*b+1->2*a
AC代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int maxn=1000+10;
struct TwoSAT
{
int n;
vector<int> G[maxn*2];
int S[maxn*2],c;
bool mark[maxn*2];
bool dfs(int x)
{
if(mark[x^1]) return false;
if(mark[x]) return true;
mark[x]=true;
S[c++]=x;
for(int i=0;i<G[x].size();i++)
if(!dfs(G[x][i])) return false;
return true;
}
void init(int n)
{
this->n=n;
for(int i=0;i<n*2;i++) G[i].clear();
memset(mark,0,sizeof(mark));
}
void add_clause(int x,int xval,int y,int yval)//这里的函数做了修改,只加单向边
{
x=x*2+xval;
y=y*2+yval;
G[x].push_back(y);
}
bool solve()
{
for(int i=0;i<2*n;i+=2)
if(!mark[i] && !mark[i+1])
{
c=0;
if(!dfs(i))
{
while(c>0) mark[S[--c]]=false;
if(!dfs(i+1)) return false;
}
}
return true;
}
}TS;
int main()
{
int n,m;
scanf("%d%d",&n,&m);
TS.init(n);
int a,b,c;
char op[10];
for(int i=0;i<m;i++)
{
scanf("%d%d%d%s",&a,&b,&c,op);
if(op[0]=='A')
{
if(c==0)
{
TS.add_clause(a,1,b,0);
TS.add_clause(b,1,a,0);
}
else if(c==1)
{
TS.add_clause(a,0,a,1);
TS.add_clause(b,0,b,1);
}
}
else if(op[0]=='O')
{
if(c==0)
{
TS.add_clause(a,1,a,0);
TS.add_clause(b,1,b,0);
}
else if(c==1)
{
TS.add_clause(a,0,b,1);
TS.add_clause(b,0,a,1);
}
}
else if(op[0]=='X')
{
if(c==0)
{
TS.add_clause(a,0,b,0);
TS.add_clause(a,1,b,1);
TS.add_clause(b,0,a,0);
TS.add_clause(b,1,a,1);
}
else if(c==1)
{
TS.add_clause(a,0,b,1);
TS.add_clause(a,1,b,0);
TS.add_clause(b,0,a,1);
TS.add_clause(b,1,a,0);
}
}
}
if(TS.solve()) printf("YES\n");
else printf("NO\n");
return 0;
}
输出方案
题意:
有N对新人举行婚礼,且每次婚礼需要持续d时间,从s时间到t时间之间举行且只能选择s到s+d时间或t-d到t时间这两个完整的时间段举行.现在只有一个神父,问他有没有可能参加所有新人的婚礼(待完整段时间且任意两对新人的婚礼时间不重叠)? 输出一个可行的方案.
分析:
每对新人的婚礼时间只有两种选择,直接就可以转化为2-SAT问题.其中如果对于第i个婚礼与第j个婚礼来说:
假设i先办的时间区间为[a,b]而j后办的时间区间为[c,d],如何判断[a,b]与[c,d]是否发生了冲突呢?(边界相交不算).
只有下面两种情况下区间[s1,e1]与区间[s2,e2]才规范相交.
1. s1<e2 且 s2<e1
2. s2<e1 且 s1<e2
仔细一看上面两种情况是相同的,只要相交的两个区间的e1 e2 > s1 s2 即可保证这两个区间相交.
(仔细想想上面情况)
然后对于冲突的每对新人添加边即可.
AC代码:
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
const int maxn=1000+10;
struct Time
{
int s,e,d;//开始,结束,持续
Time(){}
Time(int s,int e,int d):s(s),e(e),d(d){}
}t[maxn];
struct TwoSAT
{
int n;
vector<int> G[maxn*2];
int S[maxn*2],c;
bool mark[maxn*2];
bool dfs(int x)
{
if(mark[x^1]) return false;
if(mark[x]) return true;
mark[x]=true;
S[c++]=x;
for(int i=0;i<G[x].size();i++)
if(!dfs(G[x][i])) return false;
return true;
}
void init(int n)
{
this->n=n;
for(int i=0;i<n*2;i++) G[i].clear();
memset(mark,0,sizeof(mark));
}
void add_clause(int x,int xval,int y,int yval)//这里做了修改,指x与y值有冲突
{
x=x*2+xval;
y=y*2+yval;
G[x].push_back(y^1);
G[y].push_back(x^1);
}
bool solve()
{
for(int i=0;i<2*n;i+=2)if(!mark[i] && !mark[i+1])
{
c=0;
if(!dfs(i))
{
while(c>0) mark[S[--c]]=false;
if(!dfs(i+1)) return false;
}
}
return true;
}
}TS;
int main()
{
int n;
scanf("%d",&n);
for(int i=0;i<n;i++)
{
int sh,sm,eh,em,d;
scanf("%d:%d %d:%d %d",&sh,&sm,&eh,&em,&d);
t[i]=Time(sh*60+sm,eh*60+em,d);
}
TS.init(n);
for(int i=0;i<n;i++)
for(int j=i+1;j<n;j++)
{
if(t[i].s < t[j].s+t[j].d && t[j].s < t[i].s+t[i].d )
TS.add_clause(i,0,j,0);
if(t[i].s < t[j].e && t[j].e-t[j].d < t[i].s+t[i].d )
TS.add_clause(i,0,j,1);
if(t[i].e-t[i].d < t[j].s+t[j].d && t[j].s < t[i].e)
TS.add_clause(i,1,j,0);
if(t[i].e-t[i].d < t[j].e && t[j].e-t[j].d < t[i].e)
TS.add_clause(i,1,j,1);
}
if(!TS.solve()) printf("NO\n");
else
{
printf("YES\n");
for(int i=0;i<n;i++)
{
if(TS.mark[i*2])
printf("%02d:%02d %02d:%02d\n",t[i].s/60,t[i].s%60,(t[i].s+t[i].d)/60,(t[i].s+t[i].d)%60);
else
printf("%02d:%02d %02d:%02d\n",(t[i].e-t[i].d)/60,(t[i].e-t[i].d)%60,t[i].e/60,t[i].e%60);
}
}
return 0;
}
TWO-SAT输出
题意:
有一对新人结婚,n-1对夫妇去参加婚礼.有一个很长的座子,新娘与新郎坐在座子的两边(相反).接下来n-1对夫妇就坐,其中任何一对夫妇都不能坐在同一边,且(有一些人有奸情)这些有奸情的两个人不能同时坐在新娘对面.(只能分开做,或者都坐到新娘一边去)。对于每个输入实例,输出应该坐在新娘同一边的人编号。
分析:
由于有n对夫妇(0号表示新婚夫妻).所以我们这里用0表示第0对的妻子,1表示第0对的丈夫. 2*i表示第i对的夫人,2*i+1表示第i对的丈夫.一共就有2*n个人了.
然后对于每个人来说,把他分成两个节点,如果该人在做左边就mark[i*2],如果该人坐右边就mark[i*2+1].
我令新娘直接坐左边即第0个人mark[0]=true,新郎直接坐右边即第1个人mark[1*2+1]=true.
然后对于每对夫妻,因为他们不能在同一边,所以第i对夫妻中a= 2*i表示妻子,b=2*i+1表丈夫. 有这样的关系:
a在左边,那么b就在右边,a*2->b*2+1
a在右边,那么b就在左边,a*2+1->b*2
b在左边,那么a就在右边,b*2->a*2+1
b在右边,那么a就在左边,b*2+1->a*2
然后对于每对有奸情的人a与b,因为它们不能同时在新娘对面(右边),所以:
a*2+1->b*2
b*2+1->a*2
注意首先我们定了新娘(0号)在左边,新郎(第1号人)一定在右边,所以我们要先加上:
0*2+1->0*2 和 1*2->1*2+1
这样就保证了新娘和新郎在固定的那边不动.
AC代码:
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
using namespace std;
const int maxn = 1000*2+100;
struct TwoSAT
{
int n;
vector<int> G[maxn*2];
int S[maxn*2],c;
bool mark[maxn*2];
bool dfs(int x)
{
if(mark[x^1]) return false;
if(mark[x]) return true;
mark[x]=true;
S[c++]=x;
for(int i=0;i<G[x].size();i++)
if(!dfs(G[x][i])) return false;
return true;
}
void init(int n)
{
this->n=n;
for(int i=0;i<2*n;i++) G[i].clear();
memset(mark,0,sizeof(mark));
}
void add_clause(int x,int xval,int y,int yval)
{
x=x*2+xval;
y=y*2+yval;
G[x].push_back(y);
}
bool solve()
{
for(int i=0;i<2*n;i+=2)
if(!mark[i] && !mark[i+1])
{
c=0;
if(!dfs(i))
{
while(c>0) mark[S[--c]]=false;
if(!dfs(i+1)) return false; //注意细节,这里写成了return true;
}
}
return true;
}
}TS;
int main()
{
int n,m;
while(scanf("%d%d",&n,&m)==2)
{
if(n==0&&m==0) break;
TS.init(n*2);
TS.add_clause(0,1,0,0);//新娘放左
TS.add_clause(1,0,1,1);//新郎放右
for(int i=1;i<n;i++)
{
int a=i*2; //妻子
int b=i*2+1;//丈夫
TS.add_clause(a,0,b,1);
TS.add_clause(a,1,b,0);
TS.add_clause(b,0,a,1);
TS.add_clause(b,1,a,0);
}
for(int i=0;i<m;i++)
{
int a,b;
char s1[10],s2[10];
scanf("%d%s%d%s",&a,s1,&b,s2);
if(s1[0]=='w') a=a*2;
else a=a*2+1;
if(s2[0]=='w') b=b*2;
else b=b*2+1;
TS.add_clause(a,1,b,0);
TS.add_clause(b,1,a,0);
}
if(!TS.solve()) printf("bad luck\n");
else
{
for(int i=2;i<2*n;i+=2)
{
if(TS.mark[i*2])
printf("%dw ",i/2);
if(TS.mark[(i+1)*2])
printf("%dh ",i/2);
}
printf("\n");
}
}
return 0;
}
题意:n 个人选举 m 条民意结果,编码方式如下:
若 i 与 j 至少一人当选,我会高兴 +i +j
若 i 与 j 至少一人不当选,我会高兴 -i -j
若 i 当选或 j 不当选,我会高兴 +i -j
若 i 不当选 或 j 当选,我会高兴 -i +j
现在要根据给出的 m 条民意结果,判断是否符合存在相应的选举结果,输出 1 或 0
思路:
对于 n 个参选人,每个人都有两种状态,当选或不当选,根据民意编码结果可知,+ 代表当选,- 代表不当选
设对于参选人 i、j,0 代表不当选,1 代表当选,则有:
i 与 j 至少一人当选:or(i,1,j,1)
i 与 j 至少一人不当选:or(i,0,j,0)
i 当选或 j 不当选:or(i,1,j,0)
i 不当选或 j 当选:or(i,0,j,1)
根据当选与不当选的编码方式可以看出,每条民意都是或的关系,因此直接根据 m 条民意结果进行建图即可,要注意的是,候选人是从 1 开始的,因此处理是要将编号由 1~n 改为由 0~n-1
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int maxn = 1000+10;
struct TwoSAT
{
int n;
vector<int> G[maxn*2];
int S[maxn*2],c;
bool mark[maxn*2];
bool dfs(int x)
{
if(mark[x^1]) return false;
if(mark[x]) return true;
mark[x]= true;
S[c++]=x;
for(int i=0;i<G[x].size();i++)
if(!dfs(G[x][i])) return false;
return true;
}
void init(int n)
{
this->n=n;
for(int i=0;i<n*2;i++) G[i].clear();
memset(mark,0,sizeof(mark));
}
void add_clause(int x,int xval,int y,int yval)
{
x = x*2+xval;
y = y*2+yval;
G[x^1].push_back(y);
G[y^1].push_back(x);
}
bool solve()
{
for(int i=0;i<2*n;i+=2)
if(!mark[i] && !mark[i+1])
{
c=0;
if(!dfs(i))
{
while(c>0) mark[S[--c]]=false;
if(!dfs(i+1)) return false;
}
}
return true;
}
}TS;
int main()
{
int n,m;
while(scanf("%d%d",&n,&m)==2)
{
TS.init(n);
for(int i=0;i<m;i++)
{
int a,b;
scanf("%d%d",&a,&b);
TS.add_clause(abs(a)-1,a<0?1:0,abs(b)-1,b<0?1:0); //记得abs(a)-1
}
printf("%d\n",TS.solve()?1:0);
}
return 0;
}
2-SAT问题
现有一个由N个布尔值组成的序列A,给出一些限制关系,比如A[x]AND A[y]=0、A[x] OR A[y] OR A[z]=1等,要确定A[0..N-1]的值,使得其满足所有限制关系。这个称为SAT问题,特别的,若每种限制关系中最多只对两个元素进行限制,则称为2-SAT问题。
由于在2-SAT问题中,最多只对两个元素进行限制,所以可能的限制关系共有11种:
A[x]
NOT A[x]
A[x] AND A[y]
A[x] AND NOT A[y]
A[x] OR A[y]
A[x] OR NOT A[y]
NOT (A[x] AND A[y])
NOT (A[x] OR A[y])
A[x] XOR A[y]
NOT (A[x] XOR A[y])
A[x] XOR NOT A[y]
进一步,A[x] ANDA[y]相当于(A[x]) AND (A[y])(也就是可以拆分成A[x]与A[y]两个限制关系),NOT(A[x]OR A[y])相当于NOT A[x] AND NOT A[y](也就是可以拆分成NOT A[x]与NOT A[y]两个限制关系)。因此,可能的限制关系最多只有9种。
在实际问题中,2-SAT问题在大多数时候表现成以下形式:有N对物品,每对物品中必须选取一个,也只能选取一个,并且它们之间存在某些限制关系(如某两个物品不能都选,某两个物品不能都不选,某两个物品必须且只能选一个,某个物品必选)等,这时,可以将每对物品当成一个布尔值(选取第一个物品相当于0,选取第二个相当于1),如果所有的限制关系最多只对两个物品进行限制,则它们都可以转化成9种基本限制关系,从而转化为2-SAT模型。
(引自:http://www.cnblogs.com/kuangbin/archive/2012/10/05/2712429.html)
在程序实现中,我们把初始的n个物品变成2n个节点,然后从0开始编号到2*n-1号。其中原始第i个物品对应节点i*2和i*2+1。如果我们mark[i*2]节点,那么表示我们i节点设为假,如果我们mark[i*2+1]节点,那么我们i节点设为真。同一个节点只能mark一种结果(即对于原始i来说,我们只能mark[i*2]或mark[i*2+1]其中之一)。
然后加入存在i假或j假的论述,我们就引一条图中从2*i+1到2*j的边,再引一条2*j+1到2*i的边,表示如果i是真的,那么j肯定是假的(否则之前的结论不成立)。且如果j是真的,那么i肯定是假的(否则之前的结论也不成立)。
如果存在i为真的论述,那么我们直接mark[i*2+1]即可。
最终判断整个问题是否有解,就是做多次dfs来设置每个节点可能的值(真或假),看看是否所有可能取值情况都会冲突。如果不冲突,那么有解。(这里并不需要暴力枚举所有的可能,具体请看刘汝佳<<训练指南>>P323)
注意:下面解题的过程中,比如我们要设定i为假,那么我们不是mark[i*2]=true,而是添加一条i*2+1->i*2的边,即只要i设为真了,那么就会使得导出矛盾。因为每个节点只有两种选择,所以上面添加边的思路更直观。反而每次都去转换成一条或语句更不直观。我基本上所有的题解都是以添加边的角度来做的。
下面给出2-SAT的模板代码:
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
const int maxn=10000+10;
struct TwoSAT
{
int n;//原始图的节点数(未翻倍)
vector<int> G[maxn*2];//G[i]==j表示如果mark[i]=true,那么mark[j]也要=true
bool mark[maxn*2];//标记
int S[maxn*2],c;//S和c用来记录一次dfs遍历的所有节点编号
void init(int n)
{
this->n=n;
for(int i=0;i<2*n;i++) G[i].clear();
memset(mark,0,sizeof(mark));
}
//加入(x,xval)或(y,yval)条件
//xval=0表示假,yval=1表示真
void add_clause(int x,int xval,int y,int yval)
{
x=x*2+xval;
y=y*2+yval;
G[x^1].push_back(y);
G[y^1].push_back(x);
}
//从x执行dfs遍历,途径的所有点都标记
//如果不能标记,那么返回false
bool dfs(int x)
{
if(mark[x^1]) return false;//这两句的位置不能调换
if(mark[x]) return true;
mark[x]=true;
S[c++]=x;
for(int i=0;i<G[x].size();i++)
if(!dfs(G[x][i])) return false;
return true;
}
//判断当前2-SAT问题是否有解
bool solve()
{
for(int i=0;i<2*n;i+=2)
if(!mark[i] && !mark[i+1])
{
c=0;
if(!dfs(i))
{
while(c>0) mark[S[--c]]=false;
if(!dfs(i+1)) return false;
}
}
return true;
}
};
模板
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
const int maxn=10000+10;
struct TwoSAT
{
int n;
vector<int> G[maxn*2];
bool mark[maxn*2];
int S[maxn*2],c;
bool dfs(int x)
{
if(mark[x]) return true;
if(mark[x^1]) return false;
mark[x]=true;
S[c++]=x;
for(int i=0;i<G[x].size();i++)
if(!dfs(G[x][i])) return false;
return true;
}
void init(int n)
{
this->n=n;
for(int i=0;i<2*n;i++) G[i].clear();
memset(mark,0,sizeof(mark));
}
void add_clause(int x,int xval,int y,int yval)
{
x=x*2+xval;
y=y*2+yval;
G[x^1].push_back(y);
G[y^1].push_back(x);
}
bool solve()
{
for(int i=0;i<2*n;i+=2)
if(!mark[i] && !mark[i+1])
{
c=0;
if(!dfs(i))
{
while(c>0) mark[S[--c]]=false;
if(!dfs(i+1)) return false;
}
}
return true;
}
};