欧拉回路:图 G G G中经过每条边一次的回路。
欧拉路径:图 G G G中经过每条边一次的路径。
欧拉图:存在欧拉回路的图。
半欧拉图:存在欧拉路径且不存在欧拉回路的图。
定理1:无向图 G G G为欧拉图,当且仅当 G G G连通且每个点的度数均为偶数。
推论1:无向图 G G G为半欧拉图,当且仅当 G G G连通且除了两个点度数为奇数外,其余个点度数均为偶数。
定理2:有向图 G G G为欧拉图,当且仅当 G G G的基图连通且每个点入度等于出度。
推论2:有向图 G G G为半欧拉图,当且仅当 G G G连通且除了 u u u入度比出度大 1 1 1, v v v出度比入度大一外,其余个点入度均等于出度。
性质1:设 C C C为欧拉图 G G G中一条简单回路,将 C C C中的边从图 G G G中删去后得到 G ′ G' G′, G ′ G' G′的每个极大连通子图均为欧拉图。
证明:
性质2:设 C 1 、 C 2 C_1、C_2 C1、C2为图 G G G中没有公共边,但至少有一个公共点的简单回路,则 C 1 、 C 2 C_1、C_2 C1、C2可以合成一个新的回路。
证明:略。。。
bool vis[M << 1]; // 每条边是否被访问
void dfs(int u) {
for (int i = G[u]; i != 0; i = e[i].nxt) {
int v = e[i].v;
if (vis[i]) continue;
vis[i] = vis[i ^ 1] = true;
dfs(v);
}
}
Tarjan算法基于图的DFS遍历。
在DFS时,按照每个点第一次被访问的顺序,标记每个结点。该标记称为时间戳,记作 d f n ( u ) dfn(u) dfn(u)。
在DFS时,每个结点只会被遍历一次。若图连通,则所有连接一个未访问结点的边会构成一棵树,不连通则构成一个森林。
从 u u u出发能访问到的点中, d f n dfn dfn的最小值,称为追溯值,记作 l o w ( u ) low(u) low(u)。
有向图的极大强连通子图,称为强连通分量,简称SCC。
int scc_num; // 强连通分量个数
int belong[N]; // 每个点所属的强连通分量编号
int dfn[N], low[N]; // 时间戳、追溯值
int S[N], top; // 栈
bool inS[N]; // 是否在栈中
void Tarjan(int u) {
static int now = 0;
dfn[u] = low[u] = ++now;
S[++top] = u, inS[u] = true;
for (int i = G[u]; i != 0; i = e[i].nxt) {
int v = e[i].v;
if (dfn[v] == 0) {
Tarjan(v);
low[u] = min(low[u], low[v]);
} else if (inS[v]) {
low[u] = min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u]) {
++scc_num;
int v;
do {
v = S[top--], inS[v] = false;
belong[v] = scc_num;
} while (u != v);
}
}
在一个无向图中,如果有一个顶点集合,删除这个顶点集合以及这个集合中所有顶点相关联的边以后,图的连通分量增多,就称这个点集为割点集合。
如果某个割点集合只含有一个顶点 v v v(即{ v v v}是一个割点集合),那么 v v v称为一个割点。
int root;
bool cut[N]; // 判断是否为割点
void Tarjan(int u) {
static int now = 0;
dfn[u] = low[u] = ++now;
for (int i = G[u], cnt = 0; i != 0; i = e[i].nxt) {
int v = e[i].v;
if (dfn[v] == 0) {
++cnt;
Tarjan(v);
low[u] = min(low[u], low[v]);
if ((u == root && 1 < cnt) ||
(u != root && dfn[u] <= low[v]))
cut[u] = true;
} else {
low[u] = min(low[u], dfn[v]);
}
}
}
在一个无向图中,如果有一个边集,删除这个边集以后,图的连通分量增多,就称这个边集为割边集合。
如果某个割边集合只含有一条边 ( u , v ) (u,v) (u,v),那么 ( u , v ) (u,v) (u,v)称为一条割边。
一条边 ( u , v ) (u,v) (u,v)为割边,当且仅当 ( u , v ) (u,v) (u,v)在搜索树中且 d f n ( u ) < l o w ( v ) dfn(u)<low(v) dfn(u)<low(v)
int from[N]; // 到达该结点通过的边的编号
bool cut[M << 1]; // 判断是否为割边
void Tarjan(int u) {
static int now = 0;
dfn[u] = low[u] = ++now;
for (int i = G[u]; i != 0; i = e[i].nxt) {
if (i == (from[u] ^ 1)) continue;
int v = e[i].v;
if (dfn[v] == 0) {
from[v] = i;
Tarjan(v);
low[u] = min(low[u], low[v]);
if (dfn[u] < low[v])
cut[i] = cut[i ^ 1] = true;
} else {
low[u] = min(low[u], dfn[v]);
}
}
}
对于一个连通图,若任意两点之间至少存在两条“点不重复”的路径,则称该图是点双连通的。
对于一个图,点双连通的极大子图称为点双连通分量。
不同的点双连通分量最多只有一个公共点,而这个公共点一定是割点。
任意的割点至少是两个不同点双连通分量的公共点。
仿照求解强连通分量的方法,在dfs时维护一个栈,若当前边 ( u , v ) (u,v) (u,v)满足 d f n ( u ) ≤ l o w ( v ) dfn(u) \leq low(v) dfn(u)≤low(v),
说明 u u u为割点。将栈中的结点依次弹出,但 u u u不弹出。
注意割点属于多个点双连通分量。
void Tarjan(int u) {
dfn[u] = low[u] = ++now;
S[++top] = u;
for (int i = G[u], cnt = 0; i != 0; i = e[i].nxt) {
int v = e[i].v;
if (dfn[v] == 0) {
++cnt;
Tarjan(v);
low[u] = min(low[u], low[v]);
if ((u == root && cnt > 1) ||
(u != root && dfn[u] <= low[v]))
cut[u] = true;
if (dfn[u] <= low[v]) {
+vbcc_num;
b[v_dcc].clear();
do {
vbcc[vbcc_num].push_back(S[top--]);
} while (S[top + 1] != v);
vbcc[vbcc_num].push_back(u);
}
} else {
low[u] = min(low[u], dfn[v]);
}
}
}
对于一个连通图,若任意两点之间至少存在两条“边不重复”的路径,则称该图是边双连通的。
对于一个图,边双连通的极大子图称为边双连通分量。
割边不属于任何一个边双连通分量。
根据变双联通分量的定义,只需求一遍割边。然后删去割边,原图会分裂成若干个连通块,每个连通块就是一个边双连通分量。
有向无环图。。。
void Topological_Sort() {
static queue<int> Q;
for (int i = 1; i <= n; ++i)
if (d[i] == 0) Q.push(i);
while (!Q.empty()) {
int u = Q.front(); Q.pop();
for (int i = G[u]; i != 0; i = e[i].nxt) {
int v = e[i].v;
--d[v];
if (d[v] == 0)
Q.push(v);
}
}
}
在一个DAG上
最小反链覆盖等于最长链长度
最小链覆盖等于最长反链长度
反链:一个点集,没有任何一对点是前驱后继关系