首先了解两个概念:
1:欧拉路径:对于一个图要经过图中的每一条边且只能经过一次。
2:欧拉回路:对于一个图要经过图中的每一条边且只能经过一次且最后要回到起点即终点和起点相同.
1:对于所有边都连通的无向图。
1.1:存在欧拉路径的充分必要条件:度数为奇数的点只能由0个或2个
1.2:存在欧拉回路的充分必要条件:度数为奇数的点只能由0个
2:对于所有边都连通的有向图。
2.1:存在欧拉路径的充分必要条件:所有点入度等于出度。或者对于起点而言出度比入度多一,对于终点而言入度比出度多一,其他点入度与出度都相等。
2.2:存在欧拉回路的充分必要条件:所有点出度等于入度。
证明1.1:
对于起点入度必需比出度多一,对于终点,出度比入度必需多一,对于其他点,入度等于出度。因此起点和终点入度 + 出度 == 奇数,其他点入度 + 出度 == 偶数。
证明1.2:
对于一个欧拉回路所有点出度入度都必须相等所以出度 + 入度 == 偶数(两个相同的数相加必为偶数)
证明2.1:
什么时候出度等于入度呢?
如图:
对于这种图入度和出度相等:
什么时候所有点入度等于出度。或者对于起点而言出度比入度多一,对于终点而言入度比出度多一,其他点入度与出度都相等??
对于上图符合上述条件。
证明2.2:
因为起点和终点一样所以所于中间所有点入度都等于出度,又因为从起点出发要回到终点所以起点的入度也等于出度
例题:铲雪车。
来源:信息学奥赛一本通。
随着白天越来越短夜晚越来越长,我们不得不考虑铲雪问题了。 整个城市所有的道路都是双向车道,道路的两个方向均需要铲雪。因为城市预算的削减,整个城市只有 1 辆铲雪车。 铲雪车只能把它开过的地方(车道)的雪铲干净,无论哪儿有雪,铲雪车都得从停放的地方出发,游历整个城市的街道。 现在的问题是:最少要花多少时间去铲掉所有道路上的雪呢? 输入格式 输入数据的第 1 行表示铲雪车的停放坐标 (x,y),x,y 为整数,单位为米。 下面最多有4000行,每行给出了一条街道的起点坐标和终点坐标,坐标均为整数,所有街道都是笔直的,且都是双向车道。 铲雪车可以在任意交叉口、或任何街道的末尾任意转向,包括转 U 型弯。 铲雪车铲雪时前进速度为 20 千米/时,不铲雪时前进速度为 50 千米/时。 保证:铲雪车从起点一定可以到达任何街道。 输出格式 输出铲掉所有街道上的雪并且返回出发点的最短时间,精确到分钟,四舍五入到整数。 输出格式为”hours:minutes”,minutes不足两位数时需要补前导零。 具体格式参照样例。 数据范围 −106≤x,y≤106 所有位置坐标绝对值不超过 106。 输入样例: 0 0 0 0 10000 10000 5000 -10000 5000 10000 5000 10000 10000 10000 输出样例: 3:55 样例解释 输出结果表示共需3小时55分钟。
由题意一条路要来回铲两次每去一次出度加一,回来一次入度加一所以入度 == 出度符合欧拉路径。因此一定有解
又因为每条路径必走两遍铲雪车从某一个点开始因此我们是需要算出每个点直接的路径 * 2最后转换时间单位即可。
#include <cmath> #include <cstdio> #include <iostream> using namespace std; int main() { double sum; double x1, y1, x2, y2; cin >> x1 >> y2;//没用的变量 while (cin >> x1 >> y1 >> x2 >> y2) { double dx = x1 - x2, dy = y1 - y2; sum += sqrt(dx * dx + dy * dy) * 2;//一条道路要走两遍 } int minutes = round(sum / 1000 / 20 * 60);//转化为分钟:sum单位为米 ÷ 1000 转化为千米 // ÷20 转化为千米/小时 * 60转化为分钟,四舍五入 int hours = minutes / 60; minutes %= 60; printf("%d:%02d\n", hours, minutes); return 0; }
补充:欧拉路径(模板):
题目链接:
P7771 【模板】欧拉路径 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include <cstdio> #include <cstring> #include <iostream> #include <vector> #include <algorithm> using namespace std; const int N = 100010, M = 200010; int n, m; int st, ed; int del[N];//表示已经删除了几条边 int cnt, ans[M];//注意ans村的是边数 int din[N], dout[N];//din, dout分别表示入度和出度 vector<int> map[N];//存边 void dfs(int u) { for (int i = del[u]; i < map[u].size(); i = del[u])//del[u]记录的是u这个点被删除了del[u]条边 { del[u] ++ ;//删除的边数加1 dfs(map[u][i]);//递归下一个点 } ans[cnt ++ ] = u; } int main() { cin >> n >> m; for (int i = 0; i < m; i ++ ) { int a, b; scanf("%d %d", &a, &b); map[a].push_back(b);//从a到b连一条边 dout[a] ++ , din[b] ++ ;//a的出度加一,b的入度加1 } int u = 1, res = 0; for (int i = 1; i <= n; i ++ ) { if (din[i] == dout[i] + 1) ed ++ ;//若入读比出度多一,说明是终点 if (dout[i] == din[i] + 1)//若出度比入度多一说明是起点 { st ++ ; u = i; } if (din[i] != dout[i]) res ++ ;//记录出度不等于入度的点的数量 } if (res != 0 && res != 2)//若入度不等于出度的点不是0,2说明无解 { puts("No"); return 0; } if (!res)//当所有点的入度等于出度时即起点等于终点 { if (ed || st)//ed, st都为0 { puts("No"); return 0; } } if (res == 2)//起点不等于终点 { if (ed != 1 && st != 1)//若起点和终点不止一个说明无解 { puts("No"); return 0; } } for (int i = 1; i <= n; i ++ ) sort(map[i].begin(), map[i].end());//按访问的点从小到大排序 dfs(u); for (int i = cnt - 1; i >= 0; i -- ) printf("%d ", ans[i]);//因为dfs先遍历后存数,所以要逆序输出 return 0; }
例题2:
欧拉回路模板;
题目:欧拉回路:
来源:信息学奥赛一本通
给定一张图,请你找出欧拉回路,即在图中找一个环使得每条边都在环上出现恰好一次。 输入格式 第一行包含一个整数 t,t∈{1,2},如果 t=1,表示所给图为无向图,如果 t=2,表示所给图为有向图。 第二行包含两个整数 n,m,表示图的结点数和边数。 接下来 m 行中,第 i 行两个整数 vi,ui,表示第 i 条边(从 1 开始编号)。 如果 t=1 则表示 vi 到 ui 有一条无向边。 如果 t=2 则表示 vi 到 ui 有一条有向边。 图中可能有重边也可能有自环。 点的编号从 1 到 n。 输出格式 如果无法一笔画出欧拉回路,则输出一行:NO。 否则,输出一行:YES,接下来一行输出 任意一组 合法方案即可。 如果 t=1,输出 m 个整数 p1,p2,…,pm。令 e=|pi|,那么 e 表示经过的第 i 条边的编号。如果 pi 为正数表示从 ve 走到 ue,否则表示从 ue 走到 ve。 如果 t=2,输出 m 个整数 p1,p2,…,pm。其中 pi 表示经过的第 i 条边的编号。 数据范围 1≤n≤105, 0≤m≤2×105 输入样例1: 1 3 3 1 2 2 3 1 3 输出样例1: YES 1 2 -3 输入样例2: 2 5 6 2 3 2 5 3 4 1 2 4 2 5 1 输出样例2: YES 4 1 3 5 2 6
模板题:
#include <cstdio> #include <cstring> #include <iostream> using namespace std; const int N = 100010, M = 400010; int cnt; int n, m; int type; int ans[M / 2]; bool used[M]; int din[N], dout[N]; int h[N], e[M], ne[M], idx; void add(int a, int b) { e[idx] = b; ne[idx] = h[a]; h[a] = idx; idx ++ ; } void dfs(int u) { for (int &i = h[u]; i != -1; ) { if (used[i])//若该边使用过 { i = ne[i];//删除这条边 continue; } used[i] = true;//标记这条边被使用过 if (type == 1) used[i ^ 1] = true;//若为无向图,则要把他的反边标记 int t; if (type == 1)//若为无向图 { t = i / 2 + 1;//i这条边所对应的点t(邻接表以边0,1为一条无向边,边2,3为一条无向边,) if (i & 1) t = -t; } else t = i + 1;//若为有向图,i这条边所对应的边为i + 1因为我们存的边下标从0开始即初始时idx = 0; int j = e[i];//在递归之前删掉边,而不是将这两条写在for里 i = ne[i];//若放在for则当它再次遍历到这个点的时候边i仍然还未被删去,所以会tle dfs(j); ans[++ cnt] = t;//先遍历后存数,所以存的是边的倒叙 } } int main() { cin >> type; cin >> n >> m; memset(h, -1, sizeof h); for (int i = 0; i < m; i ++ ) { int a, b; scanf("%d %d", &a, &b); add(a, b); if (type == 1) add(b, a); din[b] ++ , dout[a] ++ ; } if (type == 1) { for (int i = 1; i <= n; i ++ )//若是无向图欧拉回路每个点的度要为偶数 if (din[i] + dout[i] & 1) { puts("NO"); return 0; } } else { for (int i = 1; i <= n; i ++ )//若是有向图欧拉回路每个点入度要等于出度 if (din[i] != dout[i]) { puts("NO"); return 0; } } for (int i = 1; i <= n; i ++ ) if (h[i] != -1)//找到第一个存在边的点开始遍历 { dfs(i); break; } if (cnt < m) //若遍历到的遍小于m条即不能遍历所有的边。即边不连通 { puts("NO"); return 0; } puts("YES");//排除了一切不可能即为正确答案 for (int i = cnt; i ; i -- ) printf("%d ", ans[i]);//ans存的是边的倒序 puts(""); return 0; }
例题:骑马修栅栏
来源:《信息学奥赛一本通》 , usaco training 3.3
农民John每年有很多栅栏要修理。 他总是骑着马穿过每一个栅栏并修复它破损的地方。 John是一个与其他农民一样懒的人。 他讨厌骑马,因此从来不两次经过一个栅栏。 你必须编一个程序,读入栅栏网络的描述,并计算出一条修栅栏的路径,使每个栅栏都恰好被经过一次。 John能从任何一个顶点(即两个栅栏的交点)开始骑马,在任意一个顶点结束。 每一个栅栏连接两个顶点,顶点用 1 到 500 标号(虽然有的农场并没有 500 个顶点)。 一个顶点上可连接任意多( ≥1 )个栅栏。 所有栅栏都是连通的(也就是你可以从任意一个栅栏到达另外的所有栅栏)。 你的程序必须输出骑马的路径(用路上依次经过的顶点号码表示)。 我们如果把输出的路径看成是一个500进制的数,那么当存在多组解的情况下,输出500进制表示法中最小的一个 (也就是输出第一个数较小的,如果还有多组解,输出第二个数较小的,等等)。 输入数据保证至少有一个解。 输入格式 第 1 行:一个整数 F,表示栅栏的数目; 第 2 到 F+1 行:每行两个整数 i,j 表示这条栅栏连接 i 与 j 号顶点。 输出格式 输出应当有 F+1 行,每行一个整数,依次表示路径经过的顶点号。 注意数据可能有多组解,但是只有上面题目要求的那一组解是认为正确的。 数据范围 1≤F≤1024, 1≤i,j≤500 输入样例: 9 1 2 2 3 3 4 4 2 4 5 2 5 5 6 5 7 4 6 输出样例: 1 2 3 4 2 5 4 6 5 7
代码:
#include <cstdio> #include <iostream> using namespace std; const int N = 510, M = 1100; int d[N]; int g[N][N]; int n = 500, m; int ans[M], cnt;//ans数组存的数目与边的数目有关 void dfs(int u) { for (int i = 1; i <= n; i ++ )//从小到大遍历所有点 if (g[u][i])//若 u 到 i 直接有边 { g[u][i] -- ;//删掉边,防止重复遍历 g[i][u] -- ; dfs(i); } ans[++ cnt] = u; } int main() { cin >> m; for (int i = 0; i < m; i ++ ) { int a, b; scanf("%d %d", &a, &b); g[a][b] ++ , g[b][a] ++ ;//读入双向边 d[a] ++ , d[b] ++ ;//a, b的度++ } int start = 1;//首先要从最小的点开始遍历,因为先遍历后输出,最后欧拉回路输出的是逆序 //即先遍历最小的点则在逆序中它存放在最后,那么正序就是从小到大排序 while (!d[start]) start ++;//若没用奇数点,则找到最小的偶数点 for (int i = 1; i <= n; i ++ )//若起点终点不一样,则必需从奇数点开始出发才有解。 if (d[i] & 1) { start = i; break; } dfs(start); for (int i = cnt; i ; i -- ) printf("%d\n", ans[i]); return 0; }
题目:单词游戏
来源:信息学奥赛一本通
有 N 个盘子,每个盘子上写着一个仅由小写字母组成的英文单词。 你需要给这些盘子安排一个合适的顺序,使得相邻两个盘子中,前一个盘子上单词的末字母等于后一个盘子上单词的首字母。 请你编写一个程序,判断是否能达到这一要求。 输入格式 第一行包含整数 T,表示共有 T 组测试数据。 每组数据第一行包含整数 N,表示盘子数量。 接下来 N 行,每行包含一个小写字母字符串,表示一个盘子上的单词。 一个单词可能出现多次。 输出格式 如果存在合法解,则输出”Ordering is possible.”,否则输出”The door cannot be opened.”。 数据范围 1≤N≤105, 单词长度均不超过1000 输入样例: 3 2 acm ibm 3 acm malform mouse 2 ok ok 输出样例: The door cannot be opened. Ordering is possible. The door cannot be opened.
解题思路:
将每个单词的首尾字母看做一条边如:
acm:从a 到 m 连一条边,表示a可以到m。
然后将所有边都加到并查集中判断是否连通。
有向图欧拉回路成立条件:
所有点入度 == 出度
出起点终点外的点入度 == 出度。
代码如下:
#include <cstdio> #include <cstring> #include <iostream> using namespace std; const int N = 30; int n; int p[N]; bool st[N]; int din[N], dout[N]; int find(int x)//并查集模板 { if (p[x] != x) p[x] = find(p[x]); return p[x]; } int main() { int T; cin >> T; while (T -- ) { char str[1010]; scanf("%d", &n); memset(st, 0, sizeof st);//多组测试数据初始化 memset(din, 0, sizeof din); memset(dout, 0, sizeof dout); for (int i = 0; i < 26; i ++ ) p[i] = i;//初始化并查集 for (int i = 0; i < n; i ++ ) { scanf("%s", str); int len = strlen(str); int a = str[0] - 'a', b = str[len - 1] - 'a'; st[a] = st[b] = true;//标记点a, b dout[a] ++ , din[b] ++ ;//对于一条从a走到b的边,a的出边 + 1, b的入边加1 p[find(a)] = find(b);//将a, b加到并查集中 } bool success = true; int start = 0, last = 0; for (int i = 0; i < 26; i ++ ) if (din[i] != dout[i])//说明入度不等于出度 { if (din[i] == dout[i] + 1) last ++ ;//入度比出度多1为终点 else if (din[i] + 1 == dout[i]) start ++ ;//起点 else//否则说明无解 { success = false; break; } } //入度和出度都不等于0,或起点和终点都不唯一。说明无解 if (success && !(start == 0 && last == 0 || start == 1 && last == 1)) success = false; int rsp = -1; for (int i = 0; i < 26; i ++ )//判断所有点是否连通 if (st[i])//若该点出现过 { if (rsp == -1) rsp = find(i); else if (rsp != find(i))//说明不连通则无解 { success = false; break; } } if (success) puts("Ordering is possible."); else puts("The door cannot be opened."); } return 0; }