欧拉环:图中经过每条边一次且仅一次的环;
欧拉路径:图中经过每条边一次且仅一次的路径;
欧拉图:有至少一个欧拉环的图;
半欧拉图:没有欧拉环,但有至少一条欧拉路径的图。
【无向图】
一个无向图是欧拉图当且仅当该图是连通的(注意,不考虑图中度为0的点,因为它们的存在对于图中是否存在欧拉环、欧拉路径没有影响)且所有点的度数都是偶数;一个无向图是半欧拉图当且仅当该图是连通的且有且只有2个点的度数是奇数(此时这两个点只能作为欧拉路径的起点和终点);
证明:因为任意一个点,欧拉环(或欧拉路径)从它这里进去多少次就要出来多少次,故(进去的次数+出来的次数)为偶数,又因为(进去的次数+出来的次数)=该点的度数(根据定义),所以该点的度数为偶数。
【有向图】
一个有向图是欧拉图当且仅当该图的基图(将所有有向边变为无向边后形成的无向图,这里同样不考虑度数为0的点)是连通的且所有点的入度等于出度;一个有向图是半欧拉图当且仅当该图的基图是连通的且有且只有一个点的入度比出度少1(作为欧拉路径的起点),有且只有一个点的入度比出度多1(作为终点),其余点的入度等于出度。
证明:与无向图证明类似,一个点进去多少次就要出来多少次。
【无向图、有向图中欧拉环的求法】
与二分图匹配算法类似,是一个深度优先遍历的过程,时间复杂度O(M)(因为一条边最多被访问一次)。核心代码(边是用边表存储的而不是邻接链表,因为无向图中需要对其逆向的边进行处理,在有向图中,可以用邻接链表存储边):
void dfs(int x)
{
int y;
for (int p=hd[x]; p != -1; p=ed[p].next) if (!ed[p].vst) {
y = ed[p].b;
ed[p].vst = 1;
ed[p ^ 1].vst = 1; //如果是有向图则不要这句
dfs(y);
res[v--] = y + 1;
}
}
要注意的是在res中写入是逆序的,所以初始的v应设成(边数-1)。
但是有一个问题是,这是递归实现的,当点数过多时有爆栈危险,所以最好使用非递归:
void dfs()
{
int x = 0, y, tp = 1; stk[0] = 0;
re(i, n) now[i] = hd[i];
bool ff;
while (tp) {
ff = 0;
for (int p=now[x]; p != -1; p=ed[p].next) if (!ed[p].vst) {
y = ed[p].b;
ed[p].vst = 1;
ed[p ^ 1].vst = 1; //如果是有向图则不要这句
now[x] = p; stk[tp++] = y; x = y; ff = 1; break;
}
if (!ff) {
res[v--] = x + 1;
x = stk[--tp - 1];
}
}
}
当原图是欧拉图时,一定可以求出欧拉回路。当原图是半欧拉图时,求欧拉路径,只要找到起点i和终点j,添加边<j, i>(或(j, i)),求欧拉环,再在求出的欧拉环中删除添加的新边即可。
不过最为BT的还不是这个,而是接下来的——
【混合图】
混合图(既有有向边又有无向边的图)中欧拉环、欧拉路径的判定需要借助网络流!
(1)欧拉环的判定:
一开始当然是判断原图的基图是否连通,若不连通则一定不存在欧拉环或欧拉路径(不考虑度数为0的点)。
其实,难点在于图中的无向边,需要对所有的无向边定向(指定一个方向,使之变为有向边),使整个图变成一个有向欧拉图(或有向半欧拉图)。若存在一个定向满足此条件,则原图是欧拉图(或半欧拉图)否则不是。关键就是如何定向?
首先给原图中的每条无向边随便指定一个方向(称为初始定向),将原图改为有向图G',然后的任务就是改变G'中某些边的方向(当然是无向边转化来的,原混合图中的有向边不能动)使其满足每个点的入度等于出度。
设D[i]为G'中(点i的出度 - 点i的入度)。可以发现,在改变G'中边的方向的过程中,任何点的D值的奇偶性都不会发生改变(设将边<i, j>改为<j, i>,则i入度加1出度减1,j入度减1出度加1,两者之差加2或减2,奇偶性不变)!而最终要求的是每个点的入度等于出度,即每个点的D值都为0,是偶数,故可得:若初始定向得到的G'中任意一个点的D值是奇数,那么原图中一定不存在欧拉环!
若初始D值都是偶数,则将G'改装成网络:设立源点S和汇点T,对于每个D[i]>0的点i,连边<S, i>,容量为D[i]/2;对于每个D[j]<0的点j,连边<j, T>,容量为-D[j]/2;G'中的每条边在网络中仍保留,容量为1(表示该边最多只能被改变方向一次)。求这个网络的最大流,若S引出的所有边均满流,则原混合图是欧拉图,将网络中所有流量为1的中间边(就是不与S或T关联的边)在G'中改变方向,形成的新图G''一定是有向欧拉图;若S引出的边中有的没有满流,则原混合图不是欧拉图。
为什么能这样建图?
考虑网络中的一条增广路径S-->i-->...-->j-->T,将这条从i到j的路径在G'中全部反向,则:i的入度加1出度减1,j的入度减1出度加1,路径中其它点的入度出度均不变。而i是和S相连的,因此初始D[i]>0,即i的出度大于入度,故这样反向之后D[i]减少2;同理,j是和T相连的,这样反向之后D[j]增加2。因此,若最大流中边<S, i>满流(流量为初始D[i]/2),此时D[i]值就变成了0,也就是i的入度等于出度。因此只要使所有S引出的边全部满流,所有初始D值>0的点的D值将等于0,又因为将边变向后所有点的D值之和不变,所有初始D值小于0的点的D值也将等于0,而初始D值等于0的D点既不与S相连也不与T相连,所以它们是网络中的中间点,而中间点的流入量等于流出量,故它们的入度和出度一直不变,即D值一直为0。因此,整个图G'成为欧拉图。
(2)欧拉路径的判定:
首先可以想到的是枚举欧拉路径的起点i和终点j,然后在图中添加边<j, i>,再求图中是否有欧拉回路即可。但是,该算法的时间复杂度达到了O(M * 最大流的时间),需要优化。
前面已经说过,在将边变向的过程中任何点的D值的奇偶性都不会改变,而一个有向图有欧拉路径的充要条件是基图连通且有且只有一个点的入度比出度少1(作为欧拉路径的起点),有且只有一个点的入度比出度多1(作为终点),其余点的入度等于出度。这就说明,先把图中的无向边随便定向,然后求每个点的D值,若有且只有两个点的初始D值为奇数,其余的点初始D值都为偶数,则有可能存在欧拉路径(否则不可能存在)。进一步,检查这两个初始D值为奇数的点,设为点i和点j,若有D[i]>0且D[j]<0,则i作起点j作终点(否则若D[i]与D[j]同号则不存在欧拉路径),连边<j, i>,求是否存在欧拉环即可(将求出的欧拉环中删去边<j, i>即可)。这样只需求一次最大流。
【典型例题】Sightseeing tour(PKU1637,ZJU1992)
本题就是求混合图的欧拉环问题,题目中已经说明图是连通的(Input的最后一句话),故不需判连通。
(本沙茶一开始把DFS中的l0 = aug中的"= aug"写漏了,TLE了N次)
#include <iostream>
#include <stdio.h>
#include <string.h>
using namespace std;
#define re(i, n) for (int i=0; i<n; i++)
const int MAXN = 2002, MAXM = 10000, INF = ~0U >> 2;
struct edge {
int a, b, f, next;
edge () {}
edge (int _a, int _b, int _f): a(_a), b(_b), f(_f), next(-1) {}
} ed[MAXM + MAXM];
int n, m, s, t, D[MAXN], hd[MAXN], tl[MAXN], lb[MAXN], vl[MAXN], flow, lmt;
bool res;
int dfs(int i, int aug)
{
if (i == t) {flow += aug; return aug;}
int l0 = aug, l1, j, f0;
for (int p=hd[i]; p != -1; p=ed[p].next) {
j = ed[p].b; f0 = ed[p].f;
if (lb[i] == lb[j] + 1 && f0) {
l1 = dfs(j, l0 <= f0 ? l0 : f0);
l0 -= l1; ed[p].f -= l1; ed[p ^ 1].f += l1;
if (lb[s] == n || !l0) return aug;
}
}
int minlb = n - 1;
for (int p=hd[i]; p != -1; p=ed[p].next) if (ed[p].f) {
j = ed[p].b;
if (lb[j] < minlb) minlb = lb[j];
}
if (--vl[lb[i]]) vl[lb[i] = minlb + 1]++; else lb[s] = n;
return aug - l0;
}
inline void add_edge(int a, int b, int f)
{
ed[m] = edge(a, b, f);
if (hd[a] != -1) tl[a] = ed[tl[a]].next = m++; else hd[a] = tl[a] = m++;
ed[m] = edge(b, a, 0);
if (hd[b] != -1) tl[b] = ed[tl[b]].next = m++; else hd[b] = tl[b] = m++;
}
void solve()
{
int tests;
scanf("%d", &tests);
int n0, m0, a0, b0, f;
re(testno, tests) {
scanf("%d%d", &n0, &m0);
n = n0 + 2; m = 0; s = 0; t = n - 1;
memset(D, 0, n0 << 2); memset(hd, -1, n << 2); memset(tl, -1, n << 2);
re(i, m0) {
scanf("%d%d%d", &a0, &b0, &f);
D[a0 - 1]++; D[b0 - 1]--;
if (!f) add_edge(a0, b0, 1);
}
res = 1; lmt = 0; flow = 0;
re(i, n0) {
if (D[i] % 2) {res = 0; break;}
if (D[i] > 0) {add_edge(s, i + 1, D[i] >> 1); lmt += D[i] >> 1;}
if (D[i] < 0) add_edge(i + 1, t, -D[i] >> 1);
}
if (res) {
memset(lb, 0, n << 2); vl[0] = n; memset(vl + 1, 0, n << 2);
while (lb[s] < n) dfs(s, INF);
if (flow < lmt) res = 0;
}
puts(res ? "possible" : "impossible");
}
}
int main()
{
solve();
return 0;
}