定义
欧拉回路:指在一个起点出发,能经过图中每条边恰好一次且能回到起点的回路
欧拉路径:和欧拉回路类似,不同的是不回到起点。
性质
欧拉回路:
在有向图中,如果存在欧拉回路,那么每个点入度和出度相同。
在无向图中,如果存在欧拉回路,那么每个点的度都为偶数。
欧拉路径:
在有向图中,如果存在欧拉路径,那么会存在一个入度大于出度恰好为一的点,一个出度大于入度恰好为一的点,其余的点的度都为偶数。
在无向图中,如果存在欧拉路径,那么有两个点度数为奇数,其余的点的度都为偶数。
寻找欧拉回路/路径的算法——Hierholzer算法
该算法的思想是找到一个回路后,任取回路的一个点替换为一个简单回路,直到无法再替换为止。
如果要寻找欧拉路径,则需要从起点开始进行算法。
相关题目
欧拉回路
思路
模板题,先根据欧拉回路的性质判断是否满足,然后从一个度数不为 0 0 0 的点进行 H i e r h o l z e r Hierholzer Hierholzer 算法,如果能遍历到所有边则存在欧拉回路,否则则不存在。(因为图可能不联通)
代码实现
在代码实现中,当找到一个环后,要把环上的边从边集中“删去”,或者说打个标记,表示这条边已经遍历过了。
对于无向边,把无向图的无向边当做两条互反的有向边处理,所以当其中一条有向边“删去”时,要把另外一条也“删去”。
为了快速找到反边,使用链式前向星的写法,建无向边时,将无向边对应的两条有向边挨个插入,使得这两条边的的下标为相邻的一对奇偶数,就可通过一边下标异或一得到另外一条边的下标。
为了保证复杂度是 O ( m ) O(m) O(m) 的,每遍历完一条边,要将邻接表的表头更新为下一条边,这样就不会在多次遍历领接表时遍历到已遍历的边。
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 5;
#define int long long
int n, m, t;
// h[i]为点i的邻接表表头
int h[N], e[N], v[N], id[N], ne[N], st[N], idx;
int d[N], in[N], out[N];
vector<int> ans;
// y为边的符号,正边为1,反边为-1
void add(int a, int b, int x, int y)
{
e[idx] = b, v[idx] = x, id[idx] = y, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int u)
{
for (int i = h[u]; i != -1; i = h[u]) {
if (st[i]){
h[u] = ne[i];
// 将邻接表表头更新
continue;
}
st[i] = 1;
// 标记该边已经走过
if (t == 1) {
st[i ^ 1] = 1;
}
h[u] = ne[i];
// 更新表头
dfs(e[i]);
ans.push_back(id[i]*v[i]);
// 因为是在点e[i]拓展出去替换一个简单环进来
// 所以边i到等替换完(dfs(e[i]回溯)才能加入
}
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin >> t >> n >> m;
memset(h, -1, sizeof(h));
for (int i = 1; i <= m; i++) {
int a, b;
cin >> a >> b;
if (t == 1) {
add(a, b, 1, i);
add(b, a, -1, i);
d[a]++, d[b]++;
} else {
add(a, b, 1, i);
in[b]++, out[a]++;
}
}
int flag = 1;
if (t == 1) {
for (int i = 1; i <= n; i++) {
if (d[i] % 2)
flag = 0;
}
} else {
for (int i = 1; i <= n; i++) {
if (in[i] != out[i])
flag = 0;
}
}
int sx = 1;
for (int i = 1; i <= n; i++) {
if(h[i]!=-1){
sx = i;
break;
}
}
dfs(sx);
// 图可能不连通,遍历完边数不为m
if(ans.size()!=m){
flag = 0;
}
if(!flag){
cout<<"NO\n";
}else{
cout << "YES\n";
// 因为是倒序加边,最后要倒回来
reverse(ans.begin(),ans.end());
for (int i : ans) {
cout << i << ' ';
}
}
}
骑马修栅栏
思路
注意到点不多,可以直接开领接矩阵。
因为要求路径的字典序最小,所以从最小的起点开始出发。位于点
u
u
u 时,优先选择最小的点
v
v
v,从而保证字典序最小。
代码实现
#include <bits/stdc++.h>
using namespace std;
const int N = 5e2 + 5;
int m;
int d[N];
int g[N][N];
vector<int> ans;
void dfs(int u)
{
for (int v = 1; v < N; v++) {
if (g[u][v]) {
g[u][v]--, g[v][u]--;
dfs(v);
}
}
ans.push_back(u);
}
int main()
{
cin >> m;
for (int i = 1; i <= m; i++) {
int a, b;
cin >> a >> b;
d[a]++, d[b]++, g[a][b]++, g[b][a]++;
}
int sx = N;
for (int i = 1; i < N; i++) {
if (d[i]) {
// 如果是欧拉路则找到较小的奇度点作为起点
if (d[i] % 2) {
sx = i;
break;
} else {
// 否则找最小的点为起点
sx = min(sx, i);
}
}
}
dfs(sx);
reverse(ans.begin(), ans.end());
for (int i : ans) {
cout << i << '\n';
}
}
单词游戏
思路
可以把每个字母看做一个点,每个单词为一条边,从单词的首字母向单词的末字母建一条有向边,然后判断是否满足欧拉回路\路径的性质,且跑出的边是恰好遍历所有边即可。
代码实现
#include <bits/stdc++.h>
using namespace std;
const int N = 5e2 + 5;
int m;
int cnt;
multiset<int> h[26];
int in[25], out[26];
void dfs(int u)
{
for (auto it = h[u].begin(); it != h[u].end(); it = h[u].begin()) {
int v = *it;
h[u].erase(it);
cnt++;
dfs(v);
}
}
void solve()
{
for (int i = 0; i < 26; i++) {
h[i].clear();
in[i] = out[i] = 0;
}
cin >> m;
for (int i = 1; i <= m; i++) {
string s;
cin >> s;
int a = s[0] - 'a';
int b = s.back() - 'a';
h[a].insert(b);
in[b]++, out[a]++;
}
int flag = 1, sx = -1, c1 = 0, c2 = 0;
for (int i = 0; i < 26; i++) {
if (in[i] + 1 == out[i]) {
sx = i;
c1++;
} else if (in[i] == out[i] + 1) {
c2++;
} else if (in[i] != out[i]) {
flag = 0;
}
}
if (sx == -1) {
for (int i = 0; i < 26; i++) {
if (h[i].size()) {
sx = i;
}
}
}
if (c1 == c2 && c1 <= 1) {
cnt = 0;
dfs(sx);
if (cnt != m)
flag = 0;
} else {
flag = 0;
}
if (flag)
cout << "Ordering is possible.\n";
else
cout << "The door cannot be opened.\n";
}
int main()
{
int T;
cin >> T;
while (T--) {
solve();
}
}