基本概念
定義: 度(自環統計計兩次),入度,出度。奇頂點,偶頂點,孤立點。連通,連通圖,弱連通圖。橋。路徑,回路。歐拉回路(每條邊經過恰一次),歐拉路徑。歐拉圖(存在歐拉回路),半歐拉圖(存在歐拉路徑)。簡單圖。
歐拉圖的判定
無向歐拉圖
定理: 一張無向圖為歐拉圖當且僅當該圖連通且無奇頂點。
定理: 一張無向圖為半歐拉圖當且僅當該圖連通且奇頂點數恰為兩個。此時兩個奇頂點為歐拉路徑的起點和終點。
定理: 若一張無向連通圖有 \(2k\) 個奇頂點,則可以用 \(k\) 條路經將該圖的每條邊經過一次,且至少要用 \(k\) 條路徑。
有向歐拉圖
定理: 一張有向圖為歐拉圖當且僅當該圖弱連通且所有點的入度等於出度。
定理: 一張有向圖為半歐拉圖當且僅當該圖弱連通且恰有一個點 \(u\) 入度比出度小 \(1\),一個點 \(v\) 入度比出度大 \(1\)。此時 \(u\) 和 \(v\) 分別為歐拉路徑的起點和終點。
定理: 若一張有向弱連通圖中所有節點的入度與出度之差的絕對值之和為 \(2k\),則可以用 \(k\) 條路經將該圖的每條邊經過一次,且至少要用 \(k\) 條路徑。
歐拉回路的生成
問題: 給定無向歐拉圖 \(G\),求出 \(G\) 的一條歐拉回路。
Fleury 算法
維護當前經過 \(k\) 條邊的歐拉路徑 \(p_k\),並且保證任意時刻未訪問的邊構成的子圖 \(G_k=G\setminus p_k\) 除去孤立點后連通。具體地,可以每次暴力枚舉待擴展的邊,刪掉它然后 bfs/dfs 判斷連通性。時間復雜度 \(O(m^2)\)。
同時,該算法可以通過回溯來求一張無向歐拉圖(半歐拉圖)的所有歐拉回路(路徑)。
Hierholzer 算法
隨意選取起始點進行 dfs,沿着任意未訪問的邊走到相鄰節點,直至無路可走,此時必然會回到起點形成回路。若仍有邊為訪問過,則退棧時找到有未訪問邊的節點,以它開始求出另一回路並與之前回路拼接。如此反復直到所有邊都被訪問。時間復雜度 \(O(m)\)。
同時,該算法可以用於求解有向圖的情況、求解歐拉路徑、求解對字典序有要求的問題、求最少路徑數將圖的所有邊經過一次。
歐拉圖相關的性質
定理: 對於任意無向連通圖,一定存在回路使得每條邊經過恰好兩次。進一步地,存在回路使得每條邊的兩個方向各經過一次。
證明: 將該圖的每一條邊變成兩條重邊,能夠得到無向歐拉圖;將該圖的每一條邊變成兩條方向相反的有向邊,能夠得到有向歐拉圖。
例題: (CF 788 B)
定理: 一張無向圖有圈分解當且僅當該圖無奇頂點。
例題: (BZOJ 3706)
定理: 對於不存在歐拉回路的圖,若最少用 \(a\) 條路徑將圖中的每條邊經過一次,最少在圖中中加入 \(b\) 條邊使其成為歐拉圖,那么 \(a=b\)。
例題: (CF 209 C)
歐拉圖的生成問題
De Bruijin 序列
問題: 求出一個 \(2^n\) 位的環形 0/1 串,滿足所有 \(2^n\) 個長為 \(n\) 的子串恰為所有 \(2^n\) 個 \(n\) 位 0/1 串。
解法: 構造一個 \(2^{n-1}\) 個點,\(2^n\) 次方條邊的有向圖。其中每個點代表一個 \(n-1\) 位 0/1 串,每條邊代表一個 \(n\) 位 0/1 串。其中有向邊 \(x_1x_2\ldots x_n\) 連接 \((x_1,x_2\ldots x_{n-1},x_2x_3\ldots x_n)\)。於是原問題等價於求此圖上的歐拉回路。由於此圖的每個節點都恰有兩條出邊和兩條入邊,所以解一定存在。
同時,該序列可以擴展到 \(k\) 進制。
例題: (POJ 1780)
混合圖歐拉回路
問題: 給定包含有向邊和無向邊的弱連通圖,求出該圖的一條歐拉回路或判斷無解。
解法: 將所有無向邊定向之后就容易解決,但要滿足定向之后所有點的入度等於出度。考慮先隨意定向,然后通過網絡流進行調整、構造。
例題: (Luogu 3511)
中國郵遞員問題
問題: 給定有向帶權連通圖,求出一條總邊權和最小的回路,使得經過每條邊至少一次。
解法: 原問題等價於復制一些邊若干次,使得所有點入度等於出度,最小化總邊權。可以轉化成費用流問題進行求解。
問題: 給定無向帶權連通圖,求出一條總邊權和最小的回路,使得經過每條邊至少一次。
解法: 原問題等價於復制一些邊若干次,使得所有點度數為偶,最小化總邊權。容易發現每次選定兩個奇頂點,復制它們最短路上的邊是使它們變為偶頂點的最佳方案。於是對所有奇頂點進行一般圖最小權完美匹配即可。
歐拉圖相關的計數
歐拉圖計數
問題: 求 \(n\) 個節點無奇頂點的有標號簡單無向圖個數。
解法: 考慮 \(n\) 號點的連邊,與 \(n-1\) 個節點的有編號任意簡單無向圖進行雙射。答案為 \(2^{\binom{n-1}{2}}\)。
問題: 求 \(n\) 個節點的有標號簡單連通歐拉圖個數。
解法: 容斥(枚舉 \(n\) 號點所在連通塊的大小)/生成函數(ln/求逆)。
歐拉子圖計數
問題: 給定 \(n\) 個節點 \(m\) 條邊的無向連通圖,求該圖有多少無奇頂點的支撐子圖。
解法: 考慮一棵生成樹。每條非樹邊都有選或不選兩種選擇,所有非樹邊都確定后樹邊只有一種可能的選擇方案。答案為 \(2^{m-n+1}\)。
問題: 給定 \(n\) 個節點 \(m\) 條邊的無向圖,求該圖有多少無奇頂點的支撐子圖。
解法: 對每個連通塊單獨計算,再累乘。
歐拉回路計數
問題: 給定 \(n\) 個節點 \(m\) 條邊的有向歐拉圖,求從 \(1\) 號點開始的歐拉路徑數。
解法: 考慮一種構造方案。找到一棵以 \(1\) 號點為根的內向樹,對於一個點所有不在樹上的出邊指定順序。此方案與從 \(1\) 號點開始的歐拉路徑構成雙射:
- 此方案 \(\rightarrow\) 歐拉路徑:考慮 Fleury 算法的過程。對於每個點,先按照指定順序訪問不在樹上的出邊,再訪問樹邊。容易證明方案合法。
- 歐拉路徑 \(\rightarrow\) 此方案:將除了一號點之外的所有點在路徑中訪問的最后一條出邊設為樹邊,其余邊按訪問次序決定順序。容易證明選擇的樹邊無環。
答案為 \(T_1d_1!\prod_{i=2}^n(d_i-1)!\)。其中 \(d_i\) 為 \(i\) 號點的出度,\(T_i\) 為以 \(i\) 號點為根的內向樹個數(可以用矩陣樹定理求出)。
問題: 給定 \(n\) 個節點 \(m\) 條邊的有向歐拉圖,求歐拉回路數。 (BEST 定理)
解法: 考慮用同樣的方法求出。為了去重,欽定 \(1\) 號點的一條出邊為第一條訪問邊。答案為 \(T_1\prod_{i=1}^n(d_i-1)!\)。
問題: 給定 \(n\) 個節點 \(m\) 條邊的有向半歐拉圖,求歐拉路徑數。
解法: 將該圖添加一條有向邊變成歐拉圖,新圖的歐拉回路和原圖的歐拉路徑構成雙射,用 BEST 定理計算即可。
例題
CF 788 B
簡要題意
給定 \(n\) 個點,\(m\) 條邊的無向連通圖。求出有多少條路徑滿足經過其中 \(m-2\) 條邊各兩次,剩下兩條邊各一次。兩條路徑不同當且僅當存在一條邊在兩條路徑中的經過次數不同。
題解
將該圖的每一條邊變成兩條重邊,原問題轉化為刪除兩條邊使得奇頂點個數 \(\leq 2\)。分類討論然后計數即可。
code
#include<cstdio>
#include<algorithm>
#define ll long long
#define N 1000005
int n,m;
int hd[N],_hd;
struct edge{
int v,nxt;
}e[N<<1];
inline void addedge(int u,int v){
e[++_hd]=(edge){v,hd[u]};
hd[u]=_hd;
}
bool vis[N];
inline void dfs(int u){
vis[u]=1;
for(int i=hd[u];i;i=e[i].nxt)
if(!vis[e[i].v])
dfs(e[i].v);
}
int deg[N],cnt;
inline ll C2(int x){
return 1ll*x*(x-1)/2;
}
ll ans;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
vis[i]=1;
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
if(u==v)
cnt++;
else{
deg[u]++;
deg[v]++;
}
addedge(u,v);
addedge(v,u);
vis[u]=vis[v]=0;
}
for(int i=1;i<=n;i++)
if(!vis[i]){
dfs(i);
break;
}
for(int i=1;i<=n;i++)
if(!vis[i]){
puts("0");
return 0;
}
ans=C2(cnt)+1ll*cnt*(m-cnt);
for(int i=1;i<=n;i++)
ans+=C2(deg[i]);
printf("%lld\n",ans);
}
BZOJ 3706
簡要題意
給定 \(n\) 個點,\(m\) 條邊的無向圖,邊有黑白兩種顏色。你每次操作可以選擇一條回路將其反色。求出將所有邊變成白色的最小操作次數或判斷無解。會進行若干次對一條邊反色,每次反色后求出答案。
題解
要滿足條件當且僅當所有黑邊經過奇數次,所有白邊經過偶數次。於是可以將圖的每一條白邊變成兩條重邊,有解當且僅當新圖可以圈分解。最小操作數為包含黑邊的連通塊數量。
code
#include<cstdio>
#include<algorithm>
#define N 1000005
int n,m,q;
struct Edge{
int u,v,col;
}E[N];
int deg[N];
int f[N];
inline int fnd(int x){
return f[x]?f[x]=fnd(f[x]):x;
}
int cnt,sz[N];
int ans;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int u,v,col;
scanf("%d%d%d",&u,&v,&col);
E[i]=(Edge){u,v,col};
int fu=fnd(u),fv=fnd(v);
if(fu!=fv)
f[fu]=fv;
if(col){
deg[u]^=1;
deg[v]^=1;
}
}
for(int i=1;i<=n;i++)
cnt+=deg[i];
for(int i=1;i<=m;i++)
sz[fnd(E[i].u)]+=E[i].col;
for(int i=1;i<=n;i++)
if(!f[i]&&sz[i])
ans++;
scanf("%d",&q);
while(q--){
int opt;
scanf("%d",&opt);
if(opt==1){
int i;
scanf("%d",&i);
i++;
int u=E[i].u,v=E[i].v,col=E[i].col;
E[i].col^=1;
int fu=fnd(u);
ans-=sz[fu]>0;
sz[fu]+=E[i].col-col;
ans+=sz[fu]>0;
cnt-=deg[u]+deg[v];
deg[u]^=1,deg[v]^=1;
cnt+=deg[u]+deg[v];
}
else{
if(cnt)
puts("-1");
else
printf("%d\n",ans);
}
}
}
CF 209 C
簡要題意
給定 \(n\) 個點,\(m\) 條邊的無向圖。求最少添加多少條無向邊后,圖中存在從號點 1 出發的歐拉回路。
題解
轉化為將圖中每條邊經過一次的最少路徑數,對每個連通塊分別考慮即可。注意特判 1 號點為孤立點的情況。
code
#include<cstdio>
#include<algorithm>
#define N 1000005
int n,m;
int hd[N],_hd;
struct edge{
int v,nxt;
}e[N<<1];
inline void addedge(int u,int v){
e[++_hd]=(edge){v,hd[u]};
hd[u]=_hd;
}
int deg[N],cnt;
bool vis[N];
inline void dfs(int u){
vis[u]=1;
cnt+=deg[u]&1;
for(int i=hd[u];i;i=e[i].nxt)
if(!vis[e[i].v])
dfs(e[i].v);
}
int co,ce;
int ans;
int main(){
scanf("%d%d",&n,&m);
if(!m){
puts("0");
return 0;
}
for(int i=1;i<=n;i++)
vis[i]=1;
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
deg[u]++;
deg[v]++;
addedge(u,v);
addedge(v,u);
vis[u]=vis[v]=0;
}
for(int i=1;i<=n;i++)
if(!vis[i]){
cnt=0;
dfs(i);
if(!cnt){
ans++;
ce++;
}
else{
ans+=cnt/2;
co++;
}
}
if(ce==1&&co==0&°[1])
ans=0;
if(!deg[1])
ans++;
printf("%d\n",ans);
}
POJ 1780
簡要題意
給定 \(n\),求出一個字典序最小的長度為 \(10^n+n-1\) 的 \(10\) 進制串,滿足所有 \(10^n\) 個長為 \(n\) 的子串恰為所有 \(10^n\) 個 \(n\) 位 \(10\) 進制串。
題解
仿照 0/1 串的 De Bruijin 序列的解法,建出 \(n\) 位 \(10\) 進制串對應的圖,找到字典序最小的歐拉路徑即可。
code
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<stack>
#define pii std::pair<int,int>
#define mp std::make_pair
#define fir first
#define sec second
#define M 1000005
int n,m;
int vis[M][10];
std::stack<pii> st;
std::vector<int> ans;
int main(){
while(1){
scanf("%d",&n);
if(!n)
break;
m=1;
for(int i=1;i<=n;i++)
m*=10;
ans.clear();
memset(vis,0,sizeof(vis));
vis[0][0]=1;
st.push(mp(0,0));
while(st.size()){
int u=st.top().fir,c=st.top().sec;
int flg=0;
for(int i=0;i<10;i++)
if(!vis[u][i]){
vis[u][i]=1;
st.push(mp((u+i*m/10)/10,i));
flg=1;
break;
}
if(!flg){
ans.push_back(c);
st.pop();
}
}
std::reverse(ans.begin(),ans.end());
for(int i=1;i<n;i++)
putchar('0');
for(int i=0;i<ans.size();i++)
putchar('0'+ans[i]);
puts("");
}
}
Luogu 3511
簡要題意
給定 \(n\) 個點,\(m\) 條邊的圖,每條邊正着走和反着走有不同的邊權。求一條最大邊權最小的從 \(1\) 號點出發的歐拉回路。
題解
二分答案后轉化為混合圖歐拉回路問題。
code
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<queue>
#define inf 0x3f3f3f3f
#define N 1005
#define M 2005
namespace MF{
int n,s,t;
int hd[N],_hd;
struct edge{
int v,f,nxt;
}e[(M+N)<<1];
inline void addedge(int u,int v,int f){
e[++_hd]=(edge){v,f,hd[u]};
hd[u]=_hd;
e[++_hd]=(edge){u,0,hd[v]};
hd[v]=_hd;
}
inline void init(int n_,int s_,int t_){
for(int i=1;i<=n;i++)
hd[i]=0;
_hd=1;
n=n_,s=s_,t=t_;
}
std::queue<int> q;
int cur[N],dis[N];
inline bool bfs(){
for(int i=1;i<=n;i++)
cur[i]=hd[i];
for(int i=1;i<=n;i++)
dis[i]=inf;
dis[s]=0;
q.push(s);
while(q.size()){
int u=q.front();
q.pop();
for(int i=hd[u];i;i=e[i].nxt){
int v=e[i].v,f=e[i].f;
if(f&&dis[v]>dis[u]+1){
dis[v]=dis[u]+1;
q.push(v);
}
}
}
return dis[t]<inf;
}
inline int dfs(int u,int lmt){
if(u==t||!lmt)
return lmt;
int res=0;
for(int i=cur[u];i;i=e[i].nxt){
cur[u]=i;
int v=e[i].v,f=e[i].f;
if(dis[v]!=dis[u]+1)
continue;
f=dfs(v,std::min(lmt,f));
e[i].f-=f,e[i^1].f+=f;
lmt-=f,res+=f;
if(!lmt)
break;
}
return res;
}
inline int sol(){
int res=0;
while(bfs())
res+=dfs(s,inf);
return res;
}
}
int n,m;
struct Edge{
int u,v,a,b;
}E[M];
int cnt;
namespace DSU{
int f[N],sz[N];
inline int fnd(int x){
return f[x]?f[x]=fnd(f[x]):x;
}
inline void mrg(int u,int v){
int fu=fnd(u),fv=fnd(v);
if(fu==fv)
return;
if(sz[fu]<sz[fv]){
f[fu]=fv;
sz[fv]+=sz[fu];
}
else{
f[fv]=fu;
sz[fu]+=sz[fv];
}
}
inline void init(){
memset(f,0,sizeof(f));
for(int i=1;i<=n;i++)
sz[i]=1;
}
}
int hd[N],_hd;
struct edge{
int v,nxt;
}e[M];
inline void addedge(int u,int v){
e[++_hd]=(edge){v,hd[u]};
hd[u]=_hd;
}
bool vis[M];
std::vector<int> p;
inline void dfs(int u,int c){
for(int i=hd[u];i;i=e[i].nxt)
if(!vis[i]){
vis[i]=1;
dfs(e[i].v,i);
}
if(c)
p.push_back(c);
}
int deg[N],id[M];
inline bool chk(int x,int tp){
memset(deg,0,sizeof(deg));
MF::init(n+2,n+1,n+2);
for(int i=1;i<=m;i++){
int u=E[i].u,v=E[i].v,a=E[i].a,b=E[i].b;
if(a<=x&&b<=x){
deg[u]--,deg[v]++;
MF::addedge(v,u,1);
if(tp)
id[i]=MF::_hd;
}
else if(a<=x)
deg[u]--,deg[v]++;
else if(b<=x)
deg[u]++,deg[v]--;
else
return 0;
}
int sum=0;
for(int i=1;i<=n;i++){
if(deg[i]&1)
return 0;
if(deg[i]>0){
MF::addedge(MF::s,i,deg[i]/2);
sum+=deg[i]/2;
}
else
MF::addedge(i,MF::t,-deg[i]/2);
}
if(MF::sol()!=sum)
return 0;
if(!tp)
return 1;
for(int i=1;i<=m;i++){
int u=E[i].u,v=E[i].v,a=E[i].a,b=E[i].b;
if(a<=x&&b<=x){
if(MF::e[id[i]].f)
addedge(v,u);
else
addedge(u,v);
}
else if(a<=x)
addedge(u,v);
else if(b<=x)
addedge(v,u);
}
dfs(1,0);
std::reverse(p.begin(),p.end());
for(auto i:p)
printf("%d ",i);
puts("");
return 1;
}
int main(){
scanf("%d%d",&n,&m);
DSU::init();
for(int i=1;i<=m;i++){
scanf("%d%d%d%d",&E[i].u,&E[i].v,&E[i].a,&E[i].b);
DSU::mrg(E[i].u,E[i].v);
}
for(int i=2;i<=n;i++)
if(!DSU::f[i]&&DSU::sz[i]==1)
cnt++;
if(DSU::sz[DSU::fnd(1)]!=n-cnt){
puts("NIE");
return 0;
}
int l=1,r=1000,ans=-1;
while(l<=r){
int mid=(l+r)>>1;
if(chk(mid,0)){
ans=mid;
r=mid-1;
}
else
l=mid+1;
}
if(ans==-1)
puts("NIE");
else{
printf("%d\n",ans);
chk(ans,1);
}
}