I'm Back!请自行模拟州长语音。
五百年啦,我已经快忘了我还有个CSDN帐号了,但是今天,我忙里偷闲诈尸一下,写一篇我最近在学的算法的总结——欧拉路。
定义 Definition
欧拉路分两种,欧拉路以及欧拉回路
欧拉路
欧拉路指的是“在连通图中,通过图中所有边的简单路”,他的基本特征是只有两个或没有奇点。奇点指这个点的入度与出度之和为奇数。
欧拉回路
欧拉回路指“没有奇点,且可以最后回到起点的欧拉路”,特征与欧拉路无异。
思路与实现
对于这种问题,我们一般用深搜DFS来解决,实现起来会比较简单。存图的方法可以酌情选择,对于大多数,我们会选择链式前向星,可是对于断子绝孙无向图,如骑马修栅栏,使用邻接矩阵反而会更加方便,因题而异。
对于普通欧拉路,需要考虑起点和终点。一般的我们称"出度=入度+1"的点为起点,而"入度=出度+1"的点则为终点。
对于欧拉回路,我们不需要考虑起点或终点。涉及到欧拉回路的问题一般是要求字典序最小的,所以只需要找出字典序最小的点作为起点即可。这里可以使用sort排序,待会儿代码中会为大家展示。
另外,只要是无向联通图,就一定存在欧拉回路。
代码
欧拉路的代码比较繁琐,因为需要找起点以及终点。例题:P7771 欧拉路径
#include <bits/stdc++.h>
using namespace std;
vector<int>v[200010];
int n,m,in[200010],out[200010];
int st=1,c[200010];
stack<int>sta;
int a,b;
struct node{
int v,u;
}e[200010];
bool cmp(node x,node y){
if(x.u!=y.u){
return x.u<y.u;
}
return x.v<y.v;/*这段CMP代码可以确保字典序最小,如果使用邻接矩阵或者vector的话可以直接写成
这样,不过链式前向星是倒序调用,所以要把x.v<y.v改成大于,这样才对*/
}
void dfs(int p){
for(int i=c[p];i<v[p].size();i=c[p]){
int pig=v[p][i];
if(pig){
v[p][i]=0;
c[p]++;
/*剪枝,当你排除了一些不可用的选项便开始搜下一个,并且这个选项被归回来时,你需要继续
搜索别的选项,程序会又一次搜索你曾经搜索过的选项,所以你就需要记录一下你上次搜索到
了哪个点,下次直接从这个点搜,可以节省不少时间*/
dfs(pig);
}
}
sta.push(p);/*使用栈来记录与输出,因为DFS之后存在数组中的答案数列是倒叙,所以可以使用栈先进
后出的特点来输出。要是不习惯可以用数组*/
}
int main(){
cin >> n >> m;
for(int i=1;i<=m;i++){
int u,vv;
cin >> u >> vv;
e[i].u=u;
e[i].v=vv;
}
sort(e+1,e+1+m,cmp);
// for(int i=1;i<=m;i++){
// cout << "Pig" << e[i].u << " " << e[i].v << endl;
// }
for(int i=1;i<=m;i++){
v[e[i].u].push_back(e[i].v);
out[e[i].u]++;
in[e[i].v]++;
}
for(int i=1;i<=n;i++){
if(in[i]-out[i]>1||out[i]-in[i]>1){//如果不是欧拉路
cout << "No";
return 0;
}
if(out[i]==in[i]+1){//找到起点并确定具备起点特征的数的数量
st=i;
a++;
}else if(out[i]==in[i]-1){//确定具备终点特征的数的数量
b++;
}
}
if(a>1||b>1){//如果有多个起点或终点则不是欧拉路
cout << "No";
return 0;
}
for(int i=1;i<=n;i++){
if(out[i]==in[i]+1){
st=i;
}
}
dfs(st);
while(!sta.empty()){//如果用数组的话可以另开一个变量ind,来记录数组中有几个数,然后倒叙输出
cout << sta.top() << " ";
sta.pop();
}
return 0;
}
欧拉回路的代码就会简单一点,具体思路和欧拉路无异。例题:P6066 Watchcow S
#include<bits/stdc++.h>
using namespace std;
int n,m;
struct node{
int to,next;
}edge[100005];
int tot,ind;
int ans[100005];
int head[100005];
bool vis[100005];
void Add(int v,int u){//链式前向星建边
tot++;
edge[tot].to=v;
edge[tot].next=head[u];
head[u]=tot;
}
void dfs(int cnt){
for(int i=head[cnt];i;i=edge[i].next){
if(!vis[i]){
vis[i]=1;
dfs(edge[i].to);
}
}
ans[ind++]=cnt;
}
int main(){
cin >> n >> m;
for(int i=0;i<m;i++){
int u,v;
cin >> u >> v;
Add(v,u);
Add(u,v);//双向建边
}
dfs(1);
for(int i=0;i<ind;i++){
cout << ans[i] << endl;
}
return 0;
}
最后就是所谓的断子绝孙联通图,指的是建边时双向建,但是只要走过这条边,就要双向删除,受用邻接矩阵存图。例题:P1341 无序字母对,先发一个链式前向星,再发一个邻接矩阵,你们可以有一个对比。
链式前向星
//链式前向星
#include<bits/stdc++.h>
using namespace std;
int n,head[100001],du[200];
int a,b,vis[100001],tot,st=200;
stack<int> sta;
struct node{
int nex,to;
bool vis=0;
}e[100001];
struct Node{
int u,v;
}pig[200001];//将数据暂存于一个结构体中,等排完序再存入另一个
void add(int u,int v){
tot++;
e[tot].nex=head[u];
e[tot].to=v;
head[u]=tot;
}
void dfs(int x){
for(int i=head[x];i;i=head[x]){
if(!e[i].vis){
e[i].vis=1;
for(int j=head[e[i].to];j;j=e[j].nex){
// cout << "PIG" << endl;
if(e[j].to==x&&!e[j].vis){//双向删边
// cout << "Pig" << endl;
e[j].vis=1;
break;
}
}
// cout << "Pig" << x << endl;
head[x]=e[i].nex;
dfs(e[i].to);
}else{
head[x]=e[i].nex;
}
}
sta.push(x);
}
bool cmp(Node x,Node y){
if(x.u!=y.u){
return x.u<y.u;
}
return x.v>y.v;
}
int main(){
cin >> n;
for(int i=1;i<=n;i++){
string str;
cin >> str;
int u=str[0],v=str[1];
pig[i].u=str[0];
pig[i].v=str[1];
pig[n+i].u=str[1];
pig[n+i].v=str[0];
du[u]++;
du[v]++;
st=min(st,u);
}
sort(pig+1,pig+1+2*n,cmp);
// for(int i=1;i<=n;i++){
// cout << "Pig" << char(pig[i].u) << " " << char(pig[i].v) << endl;
// }
for(int i=1;i<=2*n;i++){
add(pig[i].u,pig[i].v);
}
for(int i=200;i>=0;i--){
if(du[i]%2==1){
st=i;
a++;
}
}
if(a!=2&&a!=0){
cout << "No Solution";
return 0;
}
// cout << char(st) << "Pig" << endl;
dfs(st);
while(!sta.empty()){
cout << char(sta.top());
sta.pop();
}
return 0;
}
共85行代码,规模还是挺大的。
//邻接矩阵
#include<bits/stdc++.h>
using namespace std;
int n,du[200];
int a,b,vis[100001],tot,st=200;
stack<int> sta;
int mapp[150][150];
void dfs(int x){
for(int i=1;i<150;i++){
if(mapp[x][i]){
mapp[x][i]=0;
mapp[i][x]=0;
dfs(i);
}
}
sta.push(x);
}
int main(){
cin >> n;
for(int i=1;i<=n;i++){
string str;
cin >> str;
int u=str[0],v=str[1];
mapp[u][v]=1;
mapp[v][u]=1;
du[u]++;
du[v]++;
st=min(st,u);
}
// for(int i=1;i<=n;i++){
// cout << "Pig" << char(pig[i].u) << " " << char(pig[i].v) << endl;
// }
for(int i=200;i>=0;i--){
if(du[i]%2==1){
st=i;
a++;
}
}
if(a!=2&&a!=0){
cout << "No Solution";
return 0;
}
// cout << char(st) << "Pig" << endl;
dfs(st);
while(!sta.empty()){
cout << char(sta.top());
sta.pop();
}
return 0;
}
只有49行代码,可以看出,规模不止缩短了一点,主要就是邻接矩阵的双向删边比链式前向星要方便太多了,所以说遇到这种题就要使用邻接矩阵来做。这种题的特点有:"只能经过一次"、"有多条边"等等,注意辨别,谨防上当受骗。