标签:解题报告 图论
原题见POJ 3207
有n个点围成一个圈,给出m条可以弯曲的边,使得点两两相连。这些边可以在圆内画,也可以在圆外画。问是否有可以让边不相交的画法。
分析
每条边都有两种选择,画在内部或者外部。以这两种状态来标记边,共有m对状态,对第i条边,以2i
, 2i+1
来标记两种状态。而且这种状态必须选一种作为结果。这符合2-sat的说法。
判断是否有两条边i,j会相交。若相交,则在图中添加四条有向边:2j+1<->2i
, 2i+1<->2j
,形成有向图。对于有向图中的u->v
表示选了u必须选择v。
对强连通分量进行缩点,表示如果2i
, 2i+1
在同一强连通分量中,则无解。否则有解。
怀疑太简单?缩完点的有向无环图一定有解吗?
假设无解,那必然是因为2i
, 2i+1
在一个连通图中。不妨设存在一个子图a,有2i->a
,a->2i+1
,则必存在边2i<->a
,a<->2i+1
,形成有环图,矛盾。
Tarjon+2sat版本代码
/*--------------------------------------------
* File Name: POJ 3207
* Author: Danliwoo
* Mail: Danliwoo@outlook.com
* Created Time: 2016-10-12 16:13:50
--------------------------------------------*/
#include <cstdio>
#include <iostream>
#include <cstring>
#include <queue>
#include <algorithm>
#include <cmath>
using namespace std;
#define N 510
int n, m;
struct edge
{
int u, v;
void sc() {
scanf("%d%d", &u, &v);
}
}p[N];
std::vector<int> G[N<<1];
int out[N<<1];
void add(int x, int y) {
G[x].push_back(y);
out[x]++;
}
int dt(int x) {
return x == 0 ? 0 : (x > 0 ? 1 : -1);
}
bool cross(edge a, edge b) {
return dt((a.u - b.u) * (a.v - b.u))
* dt((a.u - b.v) * (a.v - b.v)) < 0;
}
void make() {
memset(G, 0, sizeof(G));
memset(out, 0, sizeof(out));
for(int i = 0;i < m;i++)
for(int j = 0;j < i;j++) {
if(cross(p[i], p[j])) {
add(i<<1, j<<1|1);
add(j<<1, i<<1|1);
add(i<<1|1, j<<1);
add(j<<1|1, i<<1);
}
}
}
int vis[N<<1], dfn[N<<1], low[N<<1], stk[N<<1], ins[N<<1], clus[N<<1];
int top, clk, cn;
void Tarjon(int x) {
dfn[x] = low[x] = clk++;
stk[top++] = x;
vis[x] = ins[x] = 1;
for(int j = 0;j < G[x].size();j++) {
int y = G[x][j];
if(!vis[y]) {
Tarjon(y);
low[x] = min(low[x], low[y]);
} else if(ins[y]) {
low[x] = min(low[x], dfn[y]);
}
}
if(low[x] == dfn[x]) {
do {
ins[stk[top-1]] = 0;
clus[stk[top-1]] = cn;
top--;
} while(stk[top] != x);
cn++;
}
}
bool solve() {
for(int i = 0;i < m;i++)
if(clus[i<<1] == clus[i<<1|1])
return false;
return true;
}
int main() {
while(~scanf("%d%d", &n, &m)) {
for(int i = 0;i < m;i++)
p[i].sc();
make();
memset(vis, 0, sizeof(vis));
cn = top = clk = 0;
for(int i = 0;i < (m<<1);i++)
if(!vis[i]) Tarjon(i);
if(solve()) printf("panda is telling the truth...\n");
else printf("the evil panda is lying again\n");
}
return 0;
}
并查集+2sat版本
实际上,这题由于双向对称的缘故,连的都是无向边,经过缩点后即形成多个点,点与点之间若有边,则两点又被合并为一个点。用并查集也能搞缩点。
/*--------------------------------------------
* File Name: POJ 3207
* Author: Danliwoo
* Mail: Danliwoo@outlook.com
* Created Time: 2016-10-12 17:05:44
--------------------------------------------*/
#include <cstdio>
#include <iostream>
#include <cstring>
#include <queue>
#include <algorithm>
#include <cmath>
using namespace std;
#define N 510
int n, m;
struct edge
{
int u, v;
void sc() {
scanf("%d%d", &u, &v);
}
}ed[N];
int p[N << 1];
int get(int x) {
if(x != p[x]) p[x] = get(p[x]);
return p[x];
}
void un(int x, int y) {
get(x); get(y);
if(p[x] != p[y]) p[p[x]] = p[y];
}
int dt(int x) {
return x == 0 ? 0 : (x > 0 ? 1 : -1);
}
bool cross(edge a, edge b) {
return dt((a.u - b.u) * (a.v - b.u))
* dt((a.u - b.v) * (a.v - b.v)) < 0;
}
void make() {
for(int i = 0;i < 2*m;i++)
p[i] = i;
for(int i = 0;i < m;i++)
for(int j = 0;j < i;j++) {
if(cross(ed[i], ed[j])) {
un(i<<1, (j<<1|1));
un(j<<1, (i<<1|1));
}
}
}
bool solve() {
for(int i = 0;i < m;i++)
if(p[i<<1] == p[i<<1|1])
return false;
return true;
}
int main() {
while(~scanf("%d%d", &n, &m)) {
for(int i = 0;i < m;i++)
ed[i].sc();
make();
if(solve()) printf("panda is telling the truth...\n");
else printf("the evil panda is lying again\n");
}
return 0;
}