欧拉图相关的生成与计数问题探究

最近学了一波国家集训队2018论文的最后一个专题。顺便带上了一些我的注解。
先放一波这个论文

1.基本概念

欧拉图问题是图论中的一类特殊的问题。在本文的介绍过程中,我们将会使用一些图
论术语。为了使本文叙述准确,本节将给出一些术语的定义。

定义 1.1.
图 G 中与顶点 v 关联的边数(自环统计两次)称为图 G 中顶点 v 的度。特别地,
对于有向图 G ,进入顶点 v 的边的条数称为顶点 v 的入度;从顶点v 引出的边的条数称为
顶点 v 的出度。

定义 1.2.
图 G 中度为奇数的点称为奇顶点,度为偶数的点称为偶顶点,度为 0 的点称为孤
立点。

定义 1.3.
对于无向图 G 中的两点 u 和 v ,若存在 u 到 v 的路径,则称 u 和 v 是连通的。如
果图 G 中任意两点都是连通的,则称图 G 为连通图。对于有向图 G ,将所有的有向边替
换为无向边得到图 G 的基图,若图 G 的基图是连通的,则称图 G 是弱连通图。

定义 1.4.
对于图 G 中边 e ,若删去 e 后图 G 的连通分量的数量增加,则称边 e 为 G 的桥。
意思是将桥删掉以后会让这个图分裂成两个图。

定义 1.5.
图 G 中一条路径指一个顶点与边的交替序列。回路指满足 v0 = vm 的一条路径,一般不区分起点。

定义 1.6.
图 G 中经过每条边恰一次的回路称为欧拉回路,经过每条边恰一次的路径称为欧拉路径。

定义 1.7.
存在欧拉回路的图称为欧拉图,存在欧拉路径但不存在欧拉回路的图称为半欧拉
图。

定义 1.8.
不含平行边(也称重边)也不含自环的图称为简单图。

2.欧拉图的判定

注:我并没有照抄论文上的证明,而是按照自己的理解写的,可能并不严谨,意会即可。

2.1无向欧拉图

定理 2.1.
无孤立点的无向图 G 为欧拉图,当且仅当图 G 连通且所有顶点的度都是偶数。
证明:
设一个点的度数是2k,
那么对于非起点,就会有k次进入和k次出去,第一次是进入,所以最后一次操作一定是出去。
对于起点,也会有k次进入和k次出去,第一次是出去,所以最后一次操作一定是进入这个点,即可形成欧拉回路。

定理 2.2.
如果无向连通图有 2k 个奇顶点,则图 G 可以用 k 条路径将图 G 的每一条边经过一次,且至少要使用 k 条路径。
证明:
我们可以将2k个奇顶点两两分组,一共就是k组。那么这样就可以有k条欧拉路径了。
特别地,如果是2k+1个奇顶点,那么至少要k+1条路径。也就是用上面的k条路径在加上另外一条,一共k+1。

2.2 有向欧拉图

其实,有向欧拉图和无向欧拉图的结论类似,证明过程基本相同,因此略过证明过程,请读者自己思考(很好想)。

定理 2.4.
无孤立点的有向图 G 为欧拉图,当且仅当图 G 弱连通且所有顶点的入度等于出度。

定理 2.5.
对于连通有向图,所有顶点入度与出度差的绝对值之和为 2k ,则图 G 可以用 k条路径将图 G 的每一条边经过一次,且至少要使用 k 条路径。

定理 2.6.
无孤立点的有向图 G 为半欧拉图,当且仅当图 G 弱连通,且恰有一个顶点 u 入度比出度小 1 ,一个顶点 v 入度比出度大 1 ,其余顶点入度等于出度。此时存在 u 作为起点, v 作为终点的欧拉路径。

3.欧拉回路的生成

第2章的定理一定要好好的掌握,我们接下来将会经常用到。
既然我们已经学会了判定欧拉图,本章介绍的是如何在一个欧拉图中找到欧拉回路或者欧拉路径。

那么我们先引入一个问题。

问题 3.1.
给定无向欧拉图 G = (V, E) ,求出图 G 的一条欧拉回路。
解决这类问题有两种方法,Fluery算法与Hierholzer
由于前者在在做题和比赛中基本不会用到,所以本节中我们将介绍Hierholzer算法来解决此问题。

hierholzer算法

3.2.1 算法简介

该算法也被称作“套圈算法”或“DFS法”。因为效率高、代码短等优势,该算法成为信息竞赛中最常用的欧拉路径算法。

该算法的思路与定理2.1的构造思路基本相同。任选一起点,沿任意未访问的边走到相邻节点,直至无路可走。此时必然回到起点形成了一个回路,此时图中仍有部分边未被访问。

在退栈的时候找到仍有未访问边的点,从该点为起点求出另一个回路,将该回路与之前求出的回路拼接。如此反复,直至所有的边都被访问。

3.2.2 算法流程

  1. 任取 G 中的一个顶点 v0,将 v0 加入栈S。
  2. 记栈 S 顶端元素为 u ,若 u 不存在未访问的出边,将 u 从栈 S 中弹出,并将 u 插入路径 P 的前端。否则任选一条未访问的出边 (u, v) ,将 v 加入栈 S 。
  3. 重复(2)直到栈 S 为空,此时 P 为所求得的欧拉回路。

在这里插入图片描述
图3(a)-(d)展现了Hierholzer算法的执行过程,不同颜色表示在执行过程中找到的不同回路。图3(e)表示在这个例子上Hierholzer算法得到的最终结果

3.2.3程序的实现

伪代码
在这里插入图片描述
代码

int size,path[M];
void Solve(int u){ 
    for(int i=last[u];i;i=last[u]){
        int v=e[i].v;
		last[u]=e[i].nxt;
        Solve(v);
    }
    path[++size]=id;
}

我们可以使用链表维护边(无向边用两条反向的有向边表示),删除时同时删去两条边。
Hierholzer算法的时间复杂度为 O(m) 。这个算法也可以很容易地按照算法流程中描述的方式改成非递归形式。

我也想到了一个比较合理的证明:
对于无向图,因为存在欧拉回路,所以每个点的度数都是偶数。也就是说假设一开始就从一个点出去,最后一定会回到这个点,同理,如果一开始从这个点进来,那么最后也一定会出去。
所以,我们开始从1一直走,那么一定会回到1。然后我们再回溯的过程中,看哪个点还有边没有访问过的,就假设这个点是起点,然后搜一次,因为度数是偶数,所以最后一定会回到这个点,那么我们就把这个点得到的回路插到栈里面,就这样一直反复,最后得到结果。

3.2.4 算法分析与扩展

该算法同样可以解决有向图的情况与求解欧拉路径:

问题 3.3.
给定有向欧拉图 G = (V, E) ,求出图 G 的一条欧拉回路。

解法. 对于有向图,只需用链表维护有向边即可,删除时只需删除一条边。

问题 3.4.
给定无向(或有向)半欧拉图 G = (V, E) ,求出图 G 的一条欧拉路径。

解法. 对于包含两个奇顶点的无向图,以任意一个奇顶点作为起点。对于有向图,找到入度比出度小 1 的顶点作为起点。使用上述算法就可以得到一条欧拉路径。此时,算法的执行过程就是将一条路径与多个回路依次合并。

我们还可以使用该算法求解对字典序有要求的问题:

问题 3.5.
给定无向(或有向)欧拉图(或半欧拉图) G = (V, E) ,求出图 G 的一条字典序最小的欧拉回路(路径)。

解法. 我们对每个点 u 关联的出边 (u, v) 按照 v 排序,找到图中编号最小的可作为起点的点出发,每一次走到编号最小的相邻节点 v 。在算法执行过程中,先会找到包含起点的一个字典序最小的回路(或路径)。从后向前找到一个仍有未访问边的节点,将一个包含该点的字典序最小的回路拼接到已求得的回路(或路径)中。如此反复就能得到字典序最小的回
路(或路径)。

问题 3.6.
给定无向(或有向)连通图 G = (V, E) ,求出最少的路径将图 G 的每一条边经过一次。

解法. 这一问题在定理2.2中已经给出了构造方案,对奇点两两配对,在每对点之间加入一条边构成欧拉图。求出新图的欧拉回路,将回路从新加入的边处断开,就得到了 k 条路径。

例题
caioj1229
【题目】
给出n个点(编号1~n),m条边,有人从1点出发,请输出每条边只经过一次的边的编号。如有多组解,输出编号字典序最小的路径
【输入】
第一行输入两个个整数n(2<=n<=500),m(1<=m<=100000)
接下来m+1行每行输入两个整数x,y表示有一条从x去到y的单向边
【输出】
输出一条路径上边的编号,每个边的编号都以空格隔开,行末不输出空格。 若不存在输出‘NO’
【样例输入】
5 5
1 2
2 3
3 4
4 5
5 1
【样例输出】
1 2 3 4 5
【样例解析】
人从1->2->3->4->5->1形成了一条路径,路径中经过了所有的边。
所以边的编号就是1->2->3->4->5

先要判断是否是欧拉路径。如果是用邻链表,就反着建边,然后跑一遍Hierholzer即可。
注意:如果用dfs的做法的话,在本机评测时要手动扩栈,以免出现爆栈的现象,但在大部分oj和比赛都不用。具体做法如下:打开c++,点击“工具”,点击“编译选项”,找到“编译器”,在“编译时加上以下命令”那一栏里面加入指令“-Wl,--stack=134217728”。就可以了,以下内容亦是如此。
134217728=2^27,方便记忆。
参考代码

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define il inline
#define gc getchar()
#define dg isdigit
using namespace std;
template<typename T>il void read(T &x){
    x=0;int f=0;char s=gc;
    while(!dg(s))f|=s=='-',s=gc;
    while( dg(s))x=x*10+s-48,s=gc;
    x=f?-x:x;
}
const int N=5e2+10,M=1e5+10;
struct edge{
    int v,nxt,id;
}e[M];int tot,last[N];
il void Addedge(int u,int v,int id){
    e[++tot]=(edge){v,last[u],id};
    last[u]=tot;
}
struct bian{int x,y;}b[M];
int n,m,into[N],out[N],fa[N];
int findfa(int x){return x==fa[x]?x:fa[x]=findfa(fa[x]);}
il void Prepare(){
    read(n),read(m);
    for(int i=1;i<=n;i++)fa[i]=i;
    for(int i=1;i<=m;i++){
        read(b[i].x),read(b[i].y);
        into[b[i].y]++,out[b[i].x]++;
        int fx=findfa(b[i].x),fy=findfa(b[i].y);
        if(fx!=fy)fa[fx]=fy;
    }
}
int S,T;
il void Shut(){puts("NO");exit(0);}
il void Check(){
    for(int s=0,i=1;i<=n;i++){
        if(fa[i]==i&&(into[i]||out[i]))s++;
        if(s>1)Shut();
        if(into[i]!=out[i]){
            if(abs(into[i]-out[i])!=1)Shut();
            if(into[i]-out[i]==1){
                if(!T)T=i;else Shut();
            }
            if(out[i]-into[i]==1){
                if(i!=1)Shut();
                if(!S)S=i;else Shut();
            }
        }
    }
    if((S&&!T)||(!S&&T))Shut();
}
il void Build(){
    tot=0,memset(last,0,sizeof(last));
    for(int i=m;i>=1;i--)Addedge(b[i].x,b[i].y,i);
}
int size,path[M];
void Solve(int u,int id){
    for(int i=last[u];i;i=last[u]){
        int v=e[i].v;last[u]=e[i].nxt;
        Solve(v,e[i].id);
    }
    path[++size]=id;
}
void Print(){
    size--;
    for(int i=size;i>=2;i--)
        printf("%d ",path[i]);
    printf("%d\n",path[1]);
}
int main(){
    Prepare();
    Check();
    Build();
    Solve(1,0);
    Print();
    return 0;
}

4.欧拉图相关的性质

通过2、3两节的介绍,我们对欧拉图的判定方法和欧拉回路的生成有了一定了解。下面我们通过解决一些实际问题,分析欧拉图的相关性质。

定理 4.1.
对于任意无向连通图 G ,一定存在回路使得每条边经过恰好两次。进一步地,存在回路使得每条边的两个方向各经过一次。

证明
我们将图 G 的每一条边重复两次,得到无向图 G1 。 G1 是连通图,且所有点的度都
是图 G 中对应点的度的两倍。因此 G1 是欧拉图,存在欧拉回路。
同理,若把图 G 的每条无向边变为两条反向的有向边,得到有向图 G2 。 G2 也存在欧
拉回路,满足图 G 每条边的两个方向各经过一次。

例题 4.1.
caioj 1758
【题目描述】
给定 n 个点, m 条边的无向连通图。其中 m − 2 条边经过两次,余下的两条边各经过一次,经过次数为一次的两条边不同就认为是不同的路径,问有多少种不同的路径。
【输入格式】
第1行输入两个整数n,m
第2至第m+1行每行输入两个整数u,v,表示第i条边
【输出格式】
输出一个整数,表示不同路径的总数
【样例输入】
4 7
1 1
1 1
3 3
1 2
2 3
4 1
4 3
【样例输出】
19

解法:
将图 G 中所有的边重复两次得到图 G’ 。现在需要从 G’ 中删去两条不同的边,使其存在欧拉路径,满足奇顶点个数为 0 或 2 。满足条件的只有以下三种组合方式:
(1)删去的两条边都是自环。
(2)删去的两条边中恰有一条为自环。
(3)删去的两条边均不是自环,但有公共点。分别计算方案即可求出答案。时间复杂度为 O(n + m) 。
由于证明过于简单,所以请读者自己思考。

参考代码

#pragma GCC optimize("Ofast")
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#define il inline
#define dg isdigit
#define gc getchar()
using namespace std;
typedef long long LL;
template<typename T>il void read(T &x){
    x=0;int f=0;char s=gc;
    while(!dg(s))f|=s=='-',s=gc;
    while( dg(s))x=x*10+s-48,s=gc;
    x=f?-x:x;
}
const int N=1e6+10;
int deg[N],n,m;
struct bian{
    int x,y;
}a[N];int tot,ring;
int Rsort[N],sa[N],y[N];
LL ans=0;
int main(){
    read(n),read(m);
    tot=ring=0;int u,v;
    for(int i=1;i<=m;i++){
        read(u),read(v);
        if(u==v)ring++;
        else{
            if(u>v)swap(u,v);
            a[++tot]=(bian){u,v};
            deg[u]++,deg[v]++;
        }
    }
    memset(Rsort,0,sizeof(Rsort));
    for(int i=1;i<=m;i++)Rsort[a[i].y]++;
    for(int i=2;i<=n;i++)Rsort[i]+=Rsort[i-1];
    for(int i=m;i>=1;i--)sa[Rsort[a[i].y]--]=i;
    memset(Rsort,0,sizeof(Rsort));
    memcpy(y,sa,sizeof(sa));
    for(int i=1;i<=m;i++)Rsort[a[i].x]++;
    for(int i=2;i<=n;i++)Rsort[i]+=Rsort[i-1];
    for(int i=m;i>=1;i--)sa[Rsort[a[y[i]].x]--]=y[i];
    ans+=(LL)ring*(ring-1)/2;
    ans+=(LL)ring*(m-ring);
    int now=1;
    for(int i=2;i<=n;i++){
        if(a[sa[i]].x==a[sa[i-1]].x&&a[sa[i]].y==a[sa[i-1]].y)now++;
        else ans-=(LL)now*(now-1)/2,now=1;
    }
    ans-=(LL)now*(now-1)/2;
    for(int i=1;i<=n;i++)ans+=(LL)deg[i]*(deg[i]-1)/2;
    printf("%lld\n",ans);return 0;
}

定理 4.2.
对于无向图 G ,所有顶点的度都是偶数等价于图 G 有圈分解。圈分解即为用若干个圈(不重复经过顶点的回路)使图G的每一条边恰经过一次。

证明.
这一定理我们其实在一开始介绍的定理2.1中就已经证明了,只是因为章节的侧重不同没有将这一定理提出。该定理也可以描述成点度均为偶数等价于存在回路分解。该定理揭示了所有点度为偶数的等价性表述,虽然看似简单,但往往在解题中经常用到。

例题4.2
caioj2530
【题目描述】
给定 n 个点, m 条边的无向图,边有黑白两种颜色。
现在你可以进行若干次回路反色操作,每次操作从任意点出发,每经过一条边,将其颜色反转,最后回到起点。
判断能否通过若干次操作,使这张图所有边都变成白色。
若可以,求出最少操作次数。否则输出"can’t finish it"
【输入格式】
第1行输入两个整数n,m
第2-m+1行每行输入两个整数x,y表示边
【输出格式】
若可以满足要求,则输出最少操作次数
否则输出"can’t finish it"
【输入样例1】
4 6
1 2 0
2 3 0
3 4 0
4 1 0
1 3 1
2 4 1
【输出样例1】
1
【输入样例2】
4 4
1 2 1
2 3 1
3 4 0
4 1 0
【输出样例2】
can’t finish it
【数据范围】
对于40%的数据,满足1<=n<=1000,1<=m<=100000
对于100%的数据,满足1<=n,m<=10^6

可以发现,要想满足条件。黑边必须经过奇数次,白边必须进过偶数次。否则无法满足条件。
在从贪心的角度思考一下,就可以的出一下的结果。
引理:
在最优的策略下,会黑边都只会经过1次,白边都只会经过0或2次。
证明:
对于黑边,设其连接的两点为u,v。假如从u进入,最终必然会从v出来。既然已经从u进入了,也没有必要再从v折回u走一遍,因此只会进过这条黑边一次,从v进入同理。
对于白边,设其连接的两点为u,v,我们可以选择不从这条边经过,即为0次;或者选择从u进入,搜索与v点相连的边,最终也一定会u点出来,可以证明出这条边只会经过2次。
证毕。
(证明是我自己写的,没有照抄论文,不足之处请谅解)

由此得之,若黑边构成的图每个点的度数均为偶数,则可以达到目标;否则一定不行。(具体地,因为白边度数为偶数,也就是从哪个点进入就从哪个点出来,所以白边仅能减少操作次数,因此黑边必须要构成欧拉图)

所以求解方法如下:
1.判断是否能完成,若不能,则输出“can’t finish it”并结束程序;
2.求出带有黑边的联通分量,并输出这个数。

参考代码

#pragma GCC optimize("Ofast")
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#define il inline
#define dg isdigit
#define gc getchar()
using namespace std;
typedef long long LL;
template<typename T>il void read(T &x){
    x=0;int f=0;char s=gc;
    while(!dg(s))f|=s=='-',s=gc;
    while( dg(s))x=x*10+s-48,s=gc;
    x=f?-x:x;
}
const int N=1e6+10;
int n,m;
struct edge{
    int v,nxt,color;
}e[N<<1];int tot,last[N];
il void Addedge(int u,int v,int color){
    e[++tot]=(edge){v,last[u],color};last[u]=tot;
    e[++tot]=(edge){u,last[v],color};last[v]=tot;
}
bool vis[N];int deg[N];
int dfs(int u){
    vis[u]=1;int ans=1;
    for(int i=last[u];i;i=e[i].nxt){
        int v=e[i].v;ans&=e[i].color;
        if(!vis[v])ans&=dfs(v);
    }
    return ans;
}
int main(){
    read(n),read(m);
    for(int i=1;i<=m;i++){
        int x,y,c;
        read(x),read(y),read(c);
        Addedge(x,y,c);
        if(!c)deg[x]++,deg[y]++;
    }
    bool bk=1;
    for(int i=1;i<=n;i++)
        if(deg[i]&1){bk=0;break;}
    if(!bk){puts("can't finish it");return 0;}
    int ans=0;
    for(int i=1;i<=n;i++)
        if(!vis[i])ans+=(dfs(i)^1);
    printf("%d\n",ans);return 0;
}

定理 4.3.
对于不存在欧拉回路的图 G ,若最少用 a 条路径将图 G 的每一条边经过一次,若最少在图 G 中加入 b 条有向边使之成为欧拉图,则 a 一定等于 b 。

证明.
我们将a条路径分出来,再利用a条边将相邻两个路径收尾连起来,构成一个环,那么b就等于a了。

例题 4.3
caioj2531
【题目描述】
给定n个点,m条边的无向图。求最少添加多少条无向边后,
使得图存在从1号点出发又回到1号点的欧拉回路。
【输入格式】
第1行输入两个整数n,m
第2-m+1行每行输入两个整数x,y表示边
【输出格式】
输出最少添加的边数
【输入样例1】
4 2
1 2
3 4
【输出样例1】
2
【输入样例2】
4 3
1 2
3 4
4 2
【输出样例2】
1
【数据范围】
对于40%的数据,满足1<=n<=103,1<=m<=105
对于100%的数据,满足1<=n,m<=10^6
【提示】
欧拉回路的定义如下:
在图 G 中经过每条边(不是点!!)恰一次的回路称为欧拉回路,
经过每条边恰一次的路径称为欧拉路径。

我们要结合运用定理2.5定理4.3来求解此题。
定义a,b两个整数,初始化为0
对于每一个联通分量(孤立点除外,因为欧拉回路只是说要经过所有的边),如果该联通分量全部都是偶数数点,我们让b+1;否则a加上该连通分量中偶数点的个数。
求完以后,结果就是 a / 2 + b a/2+b a/2+b,特别地,图本来就是欧拉图和1是孤立点的情况要特殊讨论。

参考代码

#pragma GCC optimize("Ofast")
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#define il inline
#define dg isdigit
#define gc getchar()
using namespace std;
template<typename T>il void read(T &x){
    x=0;int f=0;char s=gc;
    while(!dg(s))f|=s=='-',s=gc;
    while( dg(s))x=x*10+s-48,s=gc;
    x=f?-x:x;
}
const int N=1e6+10;
int n,m;
struct edge{
    int v,nxt;
}e[N<<1];int tot,last[N];
il void Addedge(int u,int v){
    e[++tot]=(edge){v,last[u]};last[u]=tot;
    e[++tot]=(edge){u,last[v]};last[v]=tot;
}
int deg[N],bel[N],siz[N],odd[N];int flag;
void Dfs(int u){
    bel[u]=flag;siz[u]=1;odd[u]=deg[u]&1;
    for(int i=last[u];i;i=e[i].nxt){
        int v=e[i].v;if(bel[v])continue;
        Dfs(v);siz[u]+=siz[v];odd[u]+=odd[v];
    }
}
int main(){
    read(n),read(m);
    tot=0;memset(last,0,sizeof(last));
    for(int i=1;i<=m;i++){
        int u,v;read(u),read(v);
        Addedge(u,v);deg[u]++,deg[v]++;
    }
    for(int i=1;i<=n;i++)
        if(!bel[i]){
            flag=i;
            Dfs(i);
        }
    int ansa=0,ansb=0;
    for(int i=1;i<=n;i++)
        if(bel[i]==i&&(i==1||siz[i]>1)){
            if(!odd[i])ansb++;
            else ansa+=odd[i];
        }
    if(!ansa&&ansb==1)puts("0");
    else printf("%d\n",(!ansa?0:((ansa-1)/2+1))+ansb);
    return 0;
     
}

5 欧拉图的生成问题

在解决实际的应用问题时,我们需要先在欧拉图性质的引导下,把问题转换为图论模型。

再借助一些其他知识与算法将图论模型生成出我们所需要的欧拉图。

在本节中,我们将通过几个例子介绍在欧拉图生成问题中的思路与技巧。

5.1 De Bruijn序列

问题5.1

求出一个 2 n 2^n 2n位环形 0/1 串,满足所有 2 n 2^n 2n个长度为 n 的子串恰为所有 2 n 2^n 2n个的 n 位的 0/1 串。

解法

经过观察可以发现,沿着这个环往下走一位,那么就等于将原来的左移一位,并在最后加上0或1。
设原来这个串为 x 1 x 2 . . . x n x_1x_2...x_n x1x2...xn。那么变化以后就是 x 2 x 3 . . . x n − 1 0 x_2x_3...x_{n-1}0 x2x3...xn10 x 2 x 3 . . x n − 1 1 x_2x_3..x_{n-1}1 x2x3..xn11,公共部分是 x 2 x 3 . . . x n − 1 x_2x_3...x_{n-1} x2x3...xn1
一个很容易想到的思路是,将所有的0/1串看成 2 n 2^n 2n个点,每个点向 x 2 x 3 . . . x n − 1 0 x_2x_3...x_{n-1}0 x2x3...xn10 x 2 x 3 . . x n − 1 1 x_2x_3..x_{n-1}1 x2x3..xn11连一条有向边,所以只需要每个点经过一次,就能满足结果了。即哈密顿路径。然而,求解这个问题是相当困难的。所以我们要尝试转化这个模型。

我们不妨将点和边所代表的意义互换。即用边表示0/1串。

那么,我们用 2 n − 1 2^{n-1} 2n1个点,表示公共部分,然后连接 2 n 2^n 2n条边。每条边的编号就代表了0/1串,那么连的有向边的顶点就是这个0/1串的前n-1位和后n-1位组成的0/1串。建完模型以后跑一遍Hierholzer算法即可。

例题5.1
caioj2532

保险箱
【题目描述】
一个保险箱有 n 位数字密码,正确输入密码后保险箱就会打开。
目前输入的最后n位数字与密码相同就算正确输入密码。
请求出一个长度为 10^n+n−1字典序最小的数字序列,
满足依次输入序列的每一位数字,一定可以打开保险箱。
【输入格式】
仅一行,输入一个整数n
【输出格式】
输出这个序列
【输入样例1】
1
【输出样例1】
0123456789
【输入样例2】
2
【输出样例2】
00102030405060708091121314151617181922324252627282933435363738394454647484955657585966768697787988990
【数据范围】
1<=n<=6

可以发现本题与上述的问题再本质上是一样的,唯一不同点就是二进制变成了十进制。
换汤不换药,我们先建立 1 0 n − 1 10^{n-1} 10n1个点,表示相同的部分,然后建立 1 0 n 10^n 10n条边,表示密码。
然后就跑一次欧拉路径即可。如果要使字典序最小,仅需要让每个点先搜索与它相连且字典序最小的边即可。时间复杂度 O ( 1 0 n ) O(10^n) O(10n)

参考代码

#include<iostream>
using namespace std;
const int N=1e6+10;
int n;
struct edge{
    int v,nxt,id;
}e[N];int tot,last[N];
inline void Addedge(int u,int v,int id){
    e[++tot]=(edge){v,last[u],id};last[u]=tot;
}
int fac[11],len,a[11];
void get(int now,int &x,int &y){
    len=0;for(int i=n;i>=0;i--)a[i]=0;
    while(now)a[++len]=now%10,now/=10;
    for(int i=n;i>=2;i--){
        x=x*10+a[i];
        y=y*10+a[i-1];
    }
}
int size,path[N];
void Dfs(int u,int id){
    for(int i=last[u];i;i=last[u]){
        int v=e[i].v;last[u]=e[i].nxt;
        Dfs(v,e[i].id);
    }
    path[++size]=id;
}
int ans[N];
int main(){
    cin>>n;
    fac[0]=1;for(int i=1;i<=n;i++)
        fac[i]=fac[i-1]*10;
    for(int i=fac[n]-1;i>=0;i--){
        int u=0,v=0;get(i,u,v);
        Addedge(u,v,i);
    }
    Dfs(0,-1);size--;
    for(int i=size;i>=1;i--)ans[n+size-i]=path[i]%10;
    for(int i=1,tt=fac[n]+n-1;i<=tt;i++)putchar(ans[i]+'0');
}

5.2 混合图欧拉回路

问题5.2

caioj2533
混合图欧拉回路
【题目描述】
给定包含有向边与无向边的弱连通图 G = (V, E)。
判断图G是否为欧拉图。若是,请求按边的编号任意输出一条欧拉回路;否则输出"No"
【输入格式】
第1行输入三个整数n,m1,m2,分别表示点、有向边、无向边的个数。
接下来m1行,每行两个整数u,v表示有向边
最后m2行,每行两个整数u,v表示无向边
【输出格式】
若有,则第1行输出起点,第二行按边的编号任意输出一条欧拉回路,否则输出"No"。
对于边的编号,我们定义有向边1m1的编号为1m1,无向边1m2的编号为m1+1m1+m2
【输入样例1】
5 4 2
2 1
3 2
4 5
3 4
1 3
5 3
【输出样例1】
1
1 5 4 3 6 2
【输入样例2】
5 4 2
1 2
3 2
4 5
3 4
1 3
5 3
【输出样例2】
No
【数据范围】
1<=n<=300,1<=m1,m2<=1000

因为如果要在一个有向图中求出欧拉回路,必须保证每一条边的入度等于出度。然而,在这一题中有无向边。
因此,我们需要确定这些无向边的方向。
s ( i ) s(i) s(i)表示点i出度减去入度的值,若要满足条件,必须使得 s ( i ) 1 < = i < = n = 0 s(i)_{1<=i<=n}=0 s(i)1<=i<=n=0。我们发现,加入将一条u->v的边反转成v->u那么 s ( u ) s(u) s(u)就会减小2, s ( v ) s(v) s(v)就会增加2。
想到这种匹配问题,我们就可以把这个看作多源汇的最大流模型。
建图方式如下,先假设无向边都是u->v。对于一个点如果 s ( u ) > 0 s(u)>0 s(u)>0,则从源点向u连一条容量为 a b s ( s ( u ) / 2 ) abs(s(u)/2) abs(s(u)/2)的边;若 s ( u ) < 0 s(u)<0 s(u)<0,则从u向汇点连一条容量为 a b s ( s ( u ) / 2 ) abs(s(u)/2) abs(s(u)/2)的边。对于每条无向边,按照定向的方向连边,容量为1。
跑一次最大流,如果不能流满,则不存在合法方案。如果有哪条无向边的有流量,那么就将这条边反转。
然后跑欧拉回路即可。

值得一提的是,如果存在 s ( i ) 1 < = i < = n = 2 k + 1 s(i)_{1<=i<=n}=2k+1 s(i)1<=i<=n=2k+1(其中k为整数)那么一定不存在欧拉回路。
还有就是关于流量可以随意流,不用割点这个问题的解释:假设有a->b->c,s(a)=2,s(b)=0,s©=-2。那么我们可以将三条边都反转,也就是a<-b<-c,s(a)=s(b)=s(c )=0。(论文中并没有对此给予解释)

参考代码

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cstdlib>
#include<queue>
#define il inline
#define gc getchar()
#define dg isdigit
using namespace std;
template<typename T>il void read(T &x){
    x=0;int f=0;char s=gc;
    while(!dg(s))f|=s=='-',s=gc;
    while( dg(s))x=x*10+s-48,s=gc;
    x=f?-x:x;
}
const int N=3e2+10,M=1e3+10;
int n,m1,m2;
struct node{
    int y,c,nxt;
}a[(N+M)<<1];int len,head[N],S,T;
il void ins(int x,int y,int c){
    len++;a[len].y=y;a[len].c=c;
    a[len].nxt=head[x];head[x]=len;
    len++;a[len].y=x;a[len].c=0;
    a[len].nxt=head[y];head[y]=len;
}
int h[N];
il bool bt_h(){
    memset(h,0,sizeof(h));h[S]=1;
    queue<int>q;q.push(S);
    while(!q.empty()){
        int x=q.front();q.pop();
        for(int k=head[x];k;k=a[k].nxt){
            int y=a[k].y;
            if(a[k].c&&!h[y])
                h[y]=h[x]+1,q.push(y);
        }
    }
    return h[T];
}
int cur[N];
int dfs(int x,int f){
    if(x==T)return f;
    int role=0,t;
    for(int &k=cur[x];k;k=a[k].nxt){
        int y=a[k].y;
        if(a[k].c&&h[y]==h[x]+1){
            role+=(t=dfs(y,min(a[k].c,f-role)));
            a[k].c-=t,a[k^1].c+=t;
            if(role==f)break;
        }
    }
    if(!role)h[x]=0;return role;
}
il int Dicnic(){
    int s=0;
    while(bt_h()){
        memcpy(cur,head,sizeof(head));
        s+=dfs(S,1e9);
    }
    return s;
}
struct bian{
    int x,y;
}b[M<<1];int s[N];
int size,path[M<<1];
struct edge{
    int v,nxt;
}e[M<<1];int tot,last[N];
il void Addedge(int u,int v){
    e[++tot]=(edge){v,last[u]};
    last[u]=tot;
}
void Dfs(int u,int id){
    for(int i=last[u];i;i=last[u]){
        int v=e[i].v;
        last[u]=e[i].nxt;
        Dfs(v,i);
    }
    path[++size]=id;
}
int main(){
    read(n),read(m1),read(m2);
    for(int x,y,c,i=1;i<=m1;i++){
        read(x),read(y);
        b[i]=(bian){x,y};
        s[x]++,s[y]--;
    }
    for(int x,y,c,i=1;i<=m2;i++){
        read(x),read(y);
        b[m1+i]=(bian){x,y};
        s[x]++,s[y]--;
    }
    int s1=0,s2=0;
    for(int i=1;i<=n;i++){
        if(abs(s[i])&1){
            puts("No");return 0;
        }
        if(s[i]<0)s2+=s[i];
        else s1+=s[i];
    }
    if(s1+s2!=0){puts("No");return 0;}
    len=1;memset(head,0,sizeof(head));
    S=n+1,T=n+2;
    for(int i=1;i<=n;i++){
        if(s[i]<0)ins(i,T,abs(s[i])>>1);
        else ins(S,i,s[i]>>1);
    }
    int pos=len+1;
    for(int i=m1+1;i<=m1+m2;i++)
        ins(b[i].x,b[i].y,1);
    int ans=Dicnic();
    if(ans*2!=s1){puts("No");return 0;}
    for(int i=m1+1;i<=m1+m2;i++,pos+=2){
        if(!a[pos].c)swap(b[i].x,b[i].y);
    }
    for(int i=1;i<=m1+m2;i++)Addedge(b[i].x,b[i].y);
    Dfs(1,-1);size--;
    for(int i=size;i>1;i--)printf("%d ",path[i]);
    printf("%d\n",path[1]);return 0;
}

5.3 中国邮递员问题

问题 5.3.
给定有向带权连通图 G = (V, E) ,求出一条总边权和最小的回路,使得经过每一条边至少一次。

解法.
这道题目看起来比较复杂,我们可以先考虑一些简单情况。如果给定的图 G 是欧拉图,显然不需要重复走任意一条边,欧拉回路就是符合要求的路线。如果图G为半欧拉图,存在从 v1 到 v2 的一条欧拉路径 P ,那么我们需要加入一些重边使得图中的所有点入度等于出度。最优方案是找到一条从 v2 到 v1 的最短路 Q ,将 P 与 Q 拼接就得到了一条符合要求的路线。

通过以上的分析可以看出,本题的实质是在图 G 中增加一些重复边,使新图的每个点入度等于出度,并且增加的重复边总边权和最小。我们先对每个点 u 求出入度减去出度的值 s(u) 。如果将一条边 (u, v) 重复一次,就会使得 s(u) 减小 1 ,而 s(v) 增加 1 ,同时产生边权的代价。这就是一个多源汇的最小费用最大流模型。具体的建图方式如下:对于点 u ,若s(u) > 0 ,则从源点向 v 连容量为 |s(u)| ,费用为 0 的边;若 s(u) < 0 ,则从 u 向汇点连容量为 |s(u)| ,费用为 0 的边。对于每条图 G 中的有向边 (u, v) ,则从 u 向 v 连一条容量为正无穷,费用为该边边权的边。求出该图源点到汇点的最小费用最大流。若在源点连出的
边中存在未满流的边,则图 G 不存在每条边至少经过一次的回路。

若存在方案,图G的边权和加上求出的最小费用就是该方案的总边权。对于每条边 (u, v),对应的边在最大流中的流量,就是该边需要额外重复的次数。可以利用有向图欧拉回路算法求出符合要求的路径。

由于该题和上一题的代码相差不大,就不再加以详细的叙述了。

我们可以对应地给出该问题的无向图版本,可以通过同样的方式分析此题。

问题 5.4.
给定无向带权连通图 G = (V, E) ,求出一条总边权和最小的回路,使得经过每一条边至少一次。

解法.
无向图可以类比有向图的解法。我们要在图 G 中增加一些重复边,使新图的每个点都成为偶顶点,并且增加的重复边总边权和最小。若图中有 2k 个奇顶点,则我们要加入 k条以奇顶点为端点路径。每个点最多只会为一条路径的端点,否则我们可以将两条路径合并为一条。

每一条路径必然选取两个端点间的最短路。因此,我们需要将 2k 个奇顶点分为k 对,使得每对点的最短路长度之和最小。我们要求出一个最小权完美匹配,这可以通过一般图最小权完美匹配算法求解。(由于本人实力有限,不会带权带花树,所以等以后再来填坑把)

6 欧拉图相关的计数

计数问题是图论中一类重要的问题。欧拉图,代表了点度为偶以及连通这两个条件。
两个简单的条件却产生了很多有趣的计数问题。在本节的介绍过程中,我们将会利用上文
中得到的欧拉图的相关性质,构造有利于我们求解的模型,最后使用我们熟知的方法得到
答案。
我们将通过几个经典的例子,探究欧拉图相关的计数问题的常用方法,并提出一些
有用的结论。

本章是最难的一章,前四题已经达到了洛谷较难的蓝题和紫题的难度,后面的三道题也绝对有顶级难度的紫题甚至黑题的难度。我也只能以提供代码为主了。

6.1 欧拉图计数

问题6.1

caioj2534
无向图计数
【题目描述】
给定 n ,求包含 n 个点的所有点度为偶数的有标号简单无向图个数。
【输入格式】
仅一行,输入一个整数n
【输出格式】
仅一行,输出一个整数,表示包含 n 个点的所有点度为偶数的有标号简单无向图个数。
最后的结果记得 mod 10^9+7
【输入样例1】
1
【输出样例1】
1
【输入样例2】
2
【输出样例2】
1
【输入样例3】
3
【输出样例3】
2
【数据范围】
1<=n<=2*10^9
【提示】
不含平行边(也称重边)也不含自环的图称为简单图

解法.
为了方便表示,记 s n s_n sn 表示 n 个点所有点度为偶数的有标号简单无向图个数。除去 n号点以及 n 号点关联的边,此时其余这 n − 1 个点中必然有偶数个奇顶点,那么 n 号点就必须与这偶数个奇顶点各连一条边。我们只需求 n − 1 个点包含偶数个奇顶点的简单无向图的个数。而任意一个无向图中奇顶点的个数一定是偶数, s n s_n sn 就是 n − 1 个点简单无向图的数量。
因为n-1个点之间有 C n − 1 2 C_{n-1}^2 Cn12个位置可以连边,形象理解就是 n ∗ ( n − 1 ) / 2 n*(n-1)/2 n(n1)/2,所以 s n = 2 C n − 1 2 s_n=2^{C_{n-1}^2} sn=2Cn12

参考代码

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL P=1e9+7;
LL ksm(LL a,LL b){
    LL ans=1;
    while(b){
        if(b&1)ans=(LL)ans*a%P;
        a=(LL)a*a%P;b>>=1;
    }
    return ans;
}
LL inv[21];
LL C(LL y,LL x){
    if(y<0||x<0||y<x)return 0;
    y%=P;if(!y||!x)return 1;
    LL ans=1;
    for(int i=0;i<x;i++)ans=(LL)ans*(y-i)%P;
    for(int i=1;i<=x;i++)ans=(LL)ans*inv[i]%P;
    return ans;
}
int main(){
    LL n;cin>>n;
    for(int i=1;i<=11;i++)inv[i]=ksm(i,P-2);
    printf("%lld\n",ksm(2,C(n-1,2)));return 0;
}

问题6.2

caioj2535
【题目描述】
给定 n ,求包含 n 个点的有标号简单连通无向欧拉图个数。
【输入格式】
仅一行,输入一个整数n
【输出格式】
仅一行,输出一个整数,表示包含 n 个点的欧拉图个数。
最后的结果记得 mod 10^9+7
【输入样例1】
1
【输出样例1】
1
【输入样例2】
2
【输出样例2】
0
【输入样例3】
3
【输出样例3】
4
【数据范围】
1<=n<=2000

解法. 记 f n f_n fn 为问题所求的 n 个点的有标号简单连通无向欧拉图个数。同时处理连通与度为偶数两个约束较为困难,但我们在前面已经解决了度为偶数的问题。接下来,我们利用容斥原理的思路解决连通这一限制。 f n f_n fn 等于 s n s_n sn 减去不连通的方案数。而不连通的方案数可以通过枚举 1 号点所在连通分量大小计算。当连通分量大小为 i 时,该连通分量内点的集
合有 C n − 1 i − 1 C_{n-1}^{i-1} Cn1i1 种不同组合方式,这些点之间的连边方法有 f i f_i fi 种,剩余点之间就不需要保证连通
性了,有 s n − i s_{n−i} sni 种连边方法。公式如下:
f n = s n − ∑ i = 1 n − 1 C n − 1 i − 1 f i s n − i f_n=s_n-\sum_{i=1}^{n-1}C_{n-1}^{i-1}f_is_{n-i} fn=sni=1n1Cn1i1fisni
运用该递推公式可以在 O ( n 2 ) O(n^2) O(n2) 的时间内求出答案。当然,也可以运用CDQ分治+FFT或
是多项式求逆等优化技巧得到 O ( n l o g 2 n ) O(n log^2n) O(nlog2n) O ( n l o g n ) O(n log n) O(nlogn) 的更高效率的解法。
由于本人能力有限,仅能写出朴素算法

参考代码

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL P=1e9+7;
const int N=2e3+10;
LL ksm(LL a,LL b){
    LL ans=1;
    while(b){
        if(b&1)ans=(LL)ans*a%P;
        a=(LL)a*a%P;b>>=1;
    }
    return ans;
}
LL inv[N],s[N],C[N][N],f[N];
//C[i][j]表示i选j个数的组合总数 
int main(){
    for(int i=1;i<=2000;i++)
        inv[i]=ksm(i,P-2);
    C[1][0]=C[1][1]=1;
    for (int i=2;i<=2000;i++){C[i][0]=1;
        for(int j=1;j<=2000;j++)
            C[i][j]=(C[i-1][j]+C[i-1][j-1])%P;
    }
    for(int i=1;i<=2000;i++)
        s[i]=ksm(2,C[i-1][2]);
    LL n;cin>>n;
    for(int i=1;i<=n;i++){
        f[i]=s[i];LL sum=0;
        for(int j=1;j<=n-1;j++)
            sum=(sum+C[i-1][j-1]*f[j]%P*s[i-j]%P)%P;
        f[i]=(f[i]+P-sum)%P;
    }
    printf("%lld\n",f[n]);
    return 0;
}

6.2 欧拉子图计数

问题6.3

caioj2536
【题目描述】
给定一个无向连通图 G = (V, E) ,
求有多少个支撑子图G’=(V,E’),E’属于E,满足每个点的度都是偶数。
该无向图含有n个点,m条边。
最后的结果mod 1e9+7
【输入格式】
第1行输入两个整数n,m
第2-m+1行每行输入两个整数x,y表示边
【输出格式】
输出满足条件的支撑子图的总数 mod 1e9+7
【输入样例】
4 4
1 2
2 3
1 3
3 4
【输出样例】
2
【数据范围】
对于40%的数据,满足1<=n<=200,1<=m<=400,且n<=m-1
对于100%的数据,满足1<=n,m<=10^6,且n<=m-1

这个图论问题不好直接入手,我们可以将这个问题可以转化为代数问题。用变量表
示每条边选或不选,再把度的约束转化为等式,这样可以列出一组方程描述此问题。对于
标号为 i 的边,这条边是否出现在子图中用 0/1 变量 xi 表示。对于点 v ,与 v 关联的边分
别为 e 1 , e 2 , . . . , e d e_1, e_2, ..., e_d e1,e2,...,ed,这些边在子图中出现的条数必为偶数,得到 x e 1 x o r x e 2 x o r . . . x o r x e d x_{e_1} xor x_{e_2} xor ... xor x_{e_d} xe1xorxe2xor...xorxed = 0 ,
其中 xor 表示异或运算。由此我们得到了 n 个等式, m 个变量的异或方程组。通过高斯消
元算法,我们可以求出该方程组的自由元的数量s ,则满足题意的子图数量为 2 s 2^s 2s 。这一解
法的时间复杂度为 O ( n 2 m ) O(n^2m) O(n2m)

上述方法虽然已经解决了本问题,但在转化为代数模型的过程中,我们忽略了一些图自身的性质,例如图 G 是连通的,每一条边所代表的变量只在方程组中出现了两次。那么我们能否利用这些性质得到更简便的解法呢?

我们任意求出图 G 的一棵生成树,将该生成树上的边对应的变量作为主元,此时非树边所对应的变量恰为自由元。我们可以找到这一过程的组合意义。依次决定每条非树边是否选择,容易发现,对于一组非树边的选法,树边只有唯一的选法。
我们任意指定一个点为根,按照从叶子到树根的顺序依次决定每个点与其父亲间的树边是否选择。
所以,一组非树边变量的取值,就对应了一组方程的解。
事实上,若把一条非树边与其两端点在树上的路径看做该树边所代表的环,将所有选择的非树边代表的环异或(一条边出现奇数次则选择,否则不选择),就得到了一个满足条件的子图。我们可以很容易表示出满足条件的子图的数量为 2 m − n + 1 2^{m−n+1} 2mn+1

辅助解释:
因为题目只是说点的度数为偶数,并没有要求存在欧拉回路。所以我们可以构建任意一颗生成树,一共用掉n-1条边。因为一组非树边都对应了唯一一组树边,且一定存在树边能够使得每个点度都是偶数。所以一共有 2 m − ( n − 1 ) 2^{m-(n-1)} 2m(n1) 2 m − n + 1 2^{m-n+1} 2mn+1种情况。
还有一种解释,就是直接从方程式入手。对于一个n*m的方程组,且每条边只会出现两次,所以高斯消元以后一定会剩余(m-n+1)个自由元,所以结果一定是 2 m − n + 1 2^{m-n+1} 2mn+1与内部连接方式无关。

参考代码

#include<iostream>
using namespace std;
typedef long long LL;
const LL P=1e9+7;
LL n,m;
LL power(LL a,LL b){
    LL ans=1;
    while(b){
        if(b&1)ans=ans*a%P;
        a=a*a%P;b>>=1;
    }
    return ans;
}
int main(){
    cin>>n>>m;
    cout<<power(2,m-n+1)<<endl;
    return 0;
}

我们还可以将该问题扩展到一般的无向图:

问题6.4

caioj2537
【题目描述】
给定一个无向图(不一定连通)G = (V, E) ,
求有多少个支撑子图G’=(V,E’),E’属于E,满足每个点的度都是偶数。
该无向图含有n个点,m条边。
最后的结果mod 1e9+7
【输入格式】
第1行输入两个整数n,m
第2-m+1行每行输入两个整数x,y表示边
【输出格式】
输出满足条件的支撑子图的总数 mod 1e9+7
【输入样例】
4 4
1 2
2 3
1 3
3 4
【输出样例】
2
【数据范围】
对于40%的数据,满足1<=n<=200,1<=m<=400
对于100%的数据,满足1<=n,m<=10^6

解法.
容易发现,不同的连通分量之间是独立的。若图的连通分量数量为 c ,对每一个连
通分量使用上述公式,得到满足顶点为度为偶数的支撑子图数量为 2 m − n + c 2^{m−n+c} 2mn+c 。可以发现这一数值与每一连通分量内部边的连接方式无关,在 O(n + m) 的时间内就可以求出 c ,计算出本题的答案。

参考代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#define il inline
#define dg isdigit
#define gc getchar()
using namespace std;
template<typename T>il void read(T &x){
    x=0;int f=0;char s=gc;
    while(!dg(s))f|=s=='-',s=gc;
    while( dg(s))x=x*10+s-48,s=gc;
    x=f?-x:x;
}
typedef long long LL;
const LL P=1e9+7;
int n,m;
LL Power(LL a,LL b){
    LL ans=1;
    while(b){
        if(b&1)ans=ans*a%P;
        a=a*a%P;b>>=1;
    }
    return ans;
}
const int N=1e6+10;
struct edge{
    int v,nxt;
}e[N<<1];int tot,last[N];
il void Addedge(int u,int v){
    e[++tot]=(edge){v,last[u]};last[u]=tot;
    e[++tot]=(edge){u,last[v]};last[v]=tot;
}
bool vis[N];int num=0;
void dfs(int u){
    vis[u]=1;num++;
    for(int i=last[u];i;i=e[i].nxt){
        int v=e[i].v;
        if(!vis[v])dfs(v);
    }
}
int main(){
    read(n),read(m);int c=0,last;
    for(int i=1;i<=m;i++){
        int u,v;read(u),read(v);
        Addedge(u,v);
    }
    for(int i=1;i<=n;i++)
        if(!vis[i]){
            last=num;dfs(i);
            if(last+1<num)c++;
            else if(last+1==num)num--;
        }
    cout<<Power(2,m-num+c)<<endl;
    return 0;
}

6.3 欧拉回路计数

问题6.5
caioj2538
【题目描述】
.给定一个有向欧拉图 G = (V, E) ,求以1号点为起点的欧拉路径的数量。
结果记得mod 10^9+7
【输入格式】
第一行两个整数n,m,表示该欧拉图的点数和边数。
接下来m行,每行两个整数x,y,表示一条有向边。
【输出格式】
输出一个整数,表示求以1号点为起点的欧拉路径的数量mod 10^9+7后的值。
【输入样例】
3 6
1 2
1 2
2 3
2 3
3 1
3 1
【输出样例】
8
【数据范围】
1<=n<=500,1<=m<=200000

解法
我们先提出一种构造方案。找到一棵以1号点为根的内向树(即每个点有唯一的一条路径到达1号点),对于一个点的所有不再树上的出边指定一个顺序。接下来,我们来证明上述方案与欧拉路径一一对应。

先证明一个方案唯一对于一条欧拉路径。对于一个方案,我们从1号点出发,对于每一个点,按照非树边指定的顺序走,这条路径就是一条欧拉路径。回顾Fleury算法的执行过程,只需证明走一条非树边 (u, v) 的时候 u 与 v 在树上是弱连通的。如果一个点有未访问的出边,则这个点出发的树边一定未访问过。走一条非树边 (u, v) 的时候, u 与 v 的树边都未访问过,它们的父亲节点出发的树边一定也未访问过。以此类推,它们到根节点的路径的每一条边都未访问过。因此,走一条非树边 (u, v) 的时候 u 与 v 在树上是弱连通的。按照上述方式得到的路径一定是欧拉路径。

再证明一条欧拉路径唯一对应一个方案。对于以 1 号点为起点和终点的欧拉路径,除1 号点外的每个点最后访问的出边是“树边”,其余出边按照访问次序确定顺序,便得到一个上述方案。需要证明得到 n − 1 条“树边”中不存在环。我们可以反证,假设“树边”形成了环,由于 1 号节点不选择树边,环上不存在一号节点。我们任选环上一点出发,沿着“树边”走,最后又回到了这个点。因为树边为最后访问的一条边,因此欧拉路径终止于该点了,这与欧拉路径的终点为 1 号点矛盾。这样我们就证明了方案与欧拉路径一一对应。接下来,我们来考虑“方案”的数量。对
于图 G 记 Ti 为以 i 为根的内向树的数量, di 为 i 号点的出度。对于一棵内向树,与1 号点关联的出边有 d1! 种,其余节点 i 对非树边指定顺序,有 di! 种。以 1 号点为起点和终点的欧拉路径的数量为:
T 1 d 1 ∏ i = 2 n ( d i − 1 ) ! T_1d_1 \prod_{i=2}^n(d_i-1)! T1d1i=2n(di1)!
其中 T1 可以利用矩阵树定理求出。即出度矩阵减去邻接矩阵去掉第一行第一列后行列
式的值。时间复杂度为 O(n^3) 。

参考代码

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#define il inline
#define gc getchar()
#define dg isdigit
using namespace std;
template<typename T>il void read(T &x){
    x=0;int f=0;char s=gc;
    while(!dg(s))f|=s=='-',s=gc;
    while( dg(s))x=x*10+s-48,s=gc;
    x=f?-x:x;
}
const int N=5e2+10,M=2e5+10;
const int P=1e9+7;
typedef long long LL;
int power(int a,int b){
    int ans=1;
    for(;b;b>>=1,a=(LL)a*a%P)
        if(b&1)ans=(LL)ans*a%P;
    return ans;
}
int n,m,a[N][N],b[N][N],jc[M],d[M];
int gauss(){
    int k=1,ff=1,ans=1;
    for(int i=1;i<=n;i++){
        int now=k;
        while(now<=n&&!(a[now][i]))++now;
        if(now==n+1)continue;
        if(now!=k)ff*=-1;
        for(int j=1;j<=n;j++)swap(a[now][j],a[k][j]);
        for(int inv=power(a[k][i],P-2),j=i+1;j<=n;j++)
        for(int t=(LL)a[j][i]*inv%P,p=1;p<=n;p++)
            a[j][p]=(a[j][p]-(LL)a[k][p]*t%P+P)%P;
        ++k;
    }
    for(int i=1;i<=n;i++)ans=(LL)ans*a[i][i]%P;
    if(ff==-1)return P-ans;else return ans;
}
int  main(){
//  freopen("5.in","r",stdin);
//  freopen("5.out","w",stdout);
    read(n),read(m);
    for(int x,y,i=1;i<=m;i++){
        read(x),read(y);d[x]++;
        a[y][x]--,a[x][x]++;
    }
    for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
        b[i][j]=a[i][j];
    for(int i=1;i<n;i++)
    for(int j=1;j<n;j++)
        a[i][j]=b[i+1][j+1];
    n--;int ans=gauss();jc[0]=1;
    for(int i=1;i<=m;i++)jc[i]=(LL)jc[i-1]*i%P;
    ans=(LL)ans*jc[d[1]]%P;++n;
    for(int i=2;i<=n;i++)
        ans=(LL)ans*jc[d[i]-1]%P;
    printf("%d\n",ans);return 0;
}

问题6.6

caioj2539
【题目描述】
.给定一个有向欧拉图 G = (V, E) ,求欧拉回路的数量。
结果记得mod 10^9+7
【输入格式】
第一行两个整数n,m,表示该欧拉图的点数和边数。
接下来m行,每行两个整数x,y,表示一条有向边。
【输出格式】
输出一个整数,表示欧拉回路的数量mod 10^9+7后的值。
【输入样例】
3 6
1 2
1 2
2 3
2 3
3 1
3 1
【输出样例】
4
【数据范围】
1<=n<=500,1<=m<=200000

我们可以通过同样的方式求出不同欧拉回路的数量,即不考虑起点,路径循环同构。
为了避免重复计算同一种欧拉回路,我们需要将一个欧拉回路转化为唯一的一条欧拉路径,
即从 1 号点出发的标号最小的边作为第一条访问边。我们就能够得到不同的欧拉回路的数
量:

T 1 ∏ i = 1 n ( d i − 1 ) ! T_1 \prod_{i=1}^n(d_i-1)! T1i=1n(di1)!

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#define il inline
#define gc getchar()
#define dg isdigit
using namespace std;
template<typename T>il void read(T &x){
    x=0;int f=0;char s=gc;
    while(!dg(s))f|=s=='-',s=gc;
    while( dg(s))x=x*10+s-48,s=gc;
    x=f?-x:x;
}
const int N=5e2+10,M=2e5+10;
const int P=1e9+7;
typedef long long LL;
int power(int a,int b){
    int ans=1;
    for(;b;b>>=1,a=(LL)a*a%P)
        if(b&1)ans=(LL)ans*a%P;
    return ans;
}
int n,m,a[N][N],b[N][N],jc[M],d[M];
int gauss(){
    int k=1,ff=1,ans=1;
    for(int i=1;i<=n;i++){
        int now=k;
        while(now<=n&&!(a[now][i]))++now;
        if(now==n+1)continue;
        if(now!=k)ff*=-1;
        for(int j=1;j<=n;j++)swap(a[now][j],a[k][j]);
        for(int inv=power(a[k][i],P-2),j=i+1;j<=n;j++)
        for(int t=(LL)a[j][i]*inv%P,p=1;p<=n;p++)
            a[j][p]=(a[j][p]-(LL)a[k][p]*t%P+P)%P;
        ++k;
    }
    for(int i=1;i<=n;i++)ans=(LL)ans*a[i][i]%P;
    if(ff==-1)return P-ans;else return ans;
}
int  main(){
    read(n),read(m);
    for(int x,y,i=1;i<=m;i++){
        read(x),read(y);d[x]++;
        a[y][x]--,a[x][x]++;
    }
    for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
        b[i][j]=a[i][j];
    for(int i=1;i<n;i++)
    for(int j=1;j<n;j++)
        a[i][j]=b[i+1][j+1];
    n--;int ans=gauss();jc[0]=1;n++;
    for(int i=1;i<=m;i++)jc[i]=(LL)jc[i-1]*i%P;
    for(int i=1;i<=n;i++)
        ans=(LL)ans*jc[d[i]-1]%P;
    printf("%d\n",ans);return 0;
}

问题6.7

caioj2540
【题目描述】
.给定一个有向半欧拉图 G = (V, E) ,求欧拉路径的数量。
结果记得mod 10^9+7
【输入格式】
第一行两个整数n,m,表示该欧拉图的点数和边数。
接下来m行,每行两个整数x,y,表示一条有向边。
【输出格式】
输出一个整数,表示求以1号点为起点的欧拉路径的数量mod 10^9+7后的值。
【输入样例】
3 5
2 2
1 3
2 2
3 2
2 2
【输出样例】
6
【数据范围】
1<=n<=500,1<=m<=200000

解法.
对于不好处理的半欧拉图问题,最直接的办法就是通过加边转化为欧拉图问题。我
们在半欧拉图中添加一条有向边,将半欧拉图 G 变为欧拉图 G‘。那么,图 G’中的欧拉回
路就与图 G 中的欧拉路径一一对应。利用BEST定理给出的公式,计算图 G‘的欧拉回路的
数量,就可以算出图 G 的欧拉路径的数量。

参考代码

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#define il inline
#define gc getchar()
#define dg isdigit
using namespace std;
template<typename T>il void read(T &x){
    x=0;int f=0;char s=gc;
    while(!dg(s))f|=s=='-',s=gc;
    while( dg(s))x=x*10+s-48,s=gc;
    x=f?-x:x;
}
const int N=5e2+10,M=2e5+10;
const int P=1e9+7;
typedef long long LL;
int power(int a,int b){
    int ans=1;
    for(;b;b>>=1,a=(LL)a*a%P)
        if(b&1)ans=(LL)ans*a%P;
    return ans;
}
int n,m,a[N][N],b[N][N],jc[M],d[N],d2[N];
int gauss(){
    int k=1,ff=1,ans=1;
    for(int i=1;i<=n;i++){
        int now=k;
        while(now<=n&&!(a[now][i]))++now;
        if(now==n+1)continue;
        if(now!=k)ff*=-1;
        for(int j=1;j<=n;j++)swap(a[now][j],a[k][j]);
        for(int inv=power(a[k][i],P-2),j=i+1;j<=n;j++)
        for(int t=(LL)a[j][i]*inv%P,p=1;p<=n;p++)
            a[j][p]=(a[j][p]-(LL)a[k][p]*t%P+P)%P;
        ++k;
    }
    for(int i=1;i<=n;i++)ans=(LL)ans*a[i][i]%P;
    if(ff==-1)return P-ans;else return ans;
}
int  main(){
    read(n),read(m);int S,T;
    for(int x,y,i=1;i<=m;i++){
        read(x),read(y);
        d[x]++;d2[y]++;
        a[y][x]--,a[x][x]++;
    }
    for(int i=1;i<=n;i++){
        if(d[i]==d2[i])continue;
        if(d[i]-d2[i]==1)S=i;
        else if(d2[i]-d[i]==1)T=i;
    }
    //T -> S  x=T y=S
    a[S][T]--,a[T][T]++,d[T]++;
    for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
        b[i][j]=a[i][j];
    for(int i=1;i<n;i++)
    for(int j=1;j<n;j++)
        a[i][j]=b[i+1][j+1];
    n--;int ans=gauss();jc[0]=1;n++;
    for(int i=1;i<=m;i++)jc[i]=(LL)jc[i-1]*i%P;
    for(int i=1;i<=n;i++)
        ans=(LL)ans*jc[d[i]-1]%P;
    printf("%d\n",ans);return 0;
}

7 总结

欧拉图问题是图论中十分重要的一类问题。本文从欧拉图本质分析着手,介绍了欧拉
图的判定方法和欧拉回路的生成算法。在分析性质和设计算法的过程中,图的连通性与顶
点的度起到了关键的作用,这两个要素是我们分析欧拉图问题的基础。以这两个要素入手,
我们总结出了更一般的结论与解题思路。

运用欧拉图模型可以解决一些应用问题。本文分析了两种常见的解题思路。一是对于
表面上看似是哈密尔顿问题的题目,不妨考虑一下将边与点的关系对调,转化为欧拉图问
题。二是对于构造或贪心难以处理的欧拉图生成问题,可以考虑将约束转化为网络流模型
或其它经典问题。模型转化往往是这类题目的突破口。

欧拉图的计数问题与生成问题类似,需要灵活运用模型转化,基于欧拉图性质分析,
构造组合意义,抽象出代数模型,并且善用计数技巧进行解决。

总而言之,深入理解欧拉图相关性质,灵活运用模型转化,巧妙借助其他知识与算法,
是解决欧拉图相关问题的关键。希望本文可以起到抛砖引玉的作用,使更多人了解并继续
发掘欧拉图问题的奥秘。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
欧拉图问题是指判断一个图中是否存在一条通过所有边恰好一次的路径或回路。在C语言中可以使用邻接矩阵或邻接表表示图,并通过深度优先搜索或广度优先搜索判断图是否为欧拉图。 以下是使用邻接矩阵和深度优先搜索判断图是否为欧拉图的示例代码: ```c #include <stdio.h> #define MAXN 100 int G[MAXN][MAXN]; // 邻接矩阵表示图 int vis[MAXN]; // 标记数组,记录节点是否被访问过 int dfs(int u, int n) { vis[u] = 1; // 标记当前节点已被访问 int cnt = 1; // 统计当前连通块的节点数 for (int v = 0; v < n; v++) { if (G[u][v] && !vis[v]) // 如果u和v有边相连且v未被访问 cnt += dfs(v, n); // 继续搜索v所在连通块 } return cnt; } int main() { int n, m; scanf("%d%d", &n, &m); // 输入节点数和边数 for (int i = 0; i < m; i++) { int u, v; scanf("%d%d", &u, &v); // 输入每条边的起点和终点 G[u][v] = G[v][u] = 1; // 标记这条边 } int cnt = dfs(0, n); // 统计从节点0开始所在的连通块的节点数 if (cnt != n) { // 如果不是连通图,就不可能存在欧拉回路或欧拉路径 printf("Not Eulerian\n"); return 0; } int odd = 0; // 统计度数为奇数的节点数 for (int i = 0; i < n; i++) { int degree = 0; // 当前节点的度数 for (int j = 0; j < n; j++) { if (G[i][j]) degree++; // 统计i的度数 } if (degree % 2 == 1) odd++; // 如果度数为奇数,计数器加1 } if (odd == 0) { printf("Eulerian circuit\n"); // 所有节点的度数均为偶数,存在欧拉回路 } else if (odd == 2) { printf("Eulerian path\n"); // 只有两个节点的度数为奇数,存在欧拉路径 } else { printf("Not Eulerian\n"); // 其他情况都不是欧拉图 } return 0; } ``` 在输入中,第一个整数为节点数,第二个整数为边数,接下来m行每行输入两个整数表示一条边的起点和终点。输出结果有三种情况:欧拉回路、欧拉路径和不是欧拉图

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值