[Tricks] 记各类欧拉回路问题
以前从来没见过除了板子以外的题,但最近总是做题见到欧拉回路,然后一样的 trick 每次都想不到。
怎么一点举一反三的能力都没有的?
板子
有向图的欧拉回路
dfs,当前弧优化。
Code
stack<int> q;
void dfs(int u)
{
for(int i=head[u];i;i=head[u])
{
head[u]=e[i].nxt;
int v=e[i].to; dfs(v);
}
q.push(u);
}
无向图的欧拉回路
要双向标记已经选过的边。
Code
vector<int> stk;
void dfs(int u)
{
for(int &i=head[u];i;i=e[i].nxt)
{
int v=e[i].to; if(Vis[i]) continue;
Vis[i]=Vis[i^1]=1,dfs(v);
}
stk.push_back(u);
}
混合图的欧拉回路
其实是网络流,跟欧拉回路关系不大。
与有向图、无向图的欧拉回路不同,在混合图中,需要对所有无向边进行合理的定向,使之转化为有向图求解。
先对所有边随机指定一个方向,令点的权值 \(d_i=in_i-out_i\)。我们希望通过调整边的方向,使所有 \(d_i=0\)。
考虑将原本 \(u\to v\) 的边反向,对 \(d\) 数组的影响是 \(d_u\gets d_u+2,d_v\gets d_v-2\)。因此我们把 \(d\) 数组全部除以 \(2\),一次反向操作的贡献变为 \(1\)。
对于 \(d_i>0\),连边 \((s,i,d_i)\);对于 \(d_i<0\),连边 \((i,t,-d_i)\)。对于原图的每条无向边 \(u\to v\),连边 \((u,v,1)\)。跑最大流,流满的边即为需要反向的边。
对所有边定向完毕后,跑有向图的欧拉回路即可。
Code
const int N=1005,inf=1e9;
int n,m,s,t;
struct edge{int nxt,to,w,id;} e[N<<1];
int head[N],cnt=1;
il void add(int u,int v,int w,int id)
{
e[++cnt]={head[u],v,w,id};head[u]=cnt;
e[++cnt]={head[v],u,0,0};head[v]=cnt;
}
int dis[N],now[N];
queue<int> q;
il bool bfs()
{
for(int i=s;i<=t;i++) dis[i]=inf,now[i]=head[i];
dis[s]=0;q.push(s);
while(!q.empty())
{
int u=q.front(); q.pop();
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(e[i].w&&dis[v]==inf) dis[v]=dis[u]+1,q.push(v);
}
}
return dis[t]!=inf;
}
int dfs(int u,int sum)
{
int res=0;
if(u==t) return sum;
for(int i=now[u];i&∑i=e[i].nxt)
{
now[u]=i; int v=e[i].to;
if(!e[i].w||dis[v]!=dis[u]+1) continue;
int k=dfs(v,min(sum,e[i].w));
e[i].w-=k,e[i^1].w+=k,sum-=k,res+=k;
}
return res;
}
bool flag[N];
int U[N],V[N],Tp[N],d[N],st[N],sum;
vector<int> E[N];
stack<int> Q;
void dfs1(int u)
{
for(int i=st[u];i<E[u].size();i=st[u])
{
st[u]++;
int v=E[u][i]; dfs1(v);
}
Q.push(u);
}
int main()
{
int T=read();
while(T--)
{
n=read(),m=read(); s=0,t=n+1,sum=0;
for(int i=0;i<=n+1;i++) d[i]=0,head[i]=0,st[i]=0,E[i].clear(); cnt=1;
for(int i=1;i<=m;i++)
{
int u=read(),v=read(); char c;cin>>c;
U[i]=u,V[i]=v,Tp[i]=(c=='D'?0:1);
d[u]++,d[v]--,flag[i]=0;
}
bool Fg=1;
for(int i=1;i<=n;i++) if(d[i]&1) Fg=0; else d[i]/=2;
if(!Fg) {printf("No euler circuit exist\n\n");continue;}
for(int i=1;i<=n;i++) if(d[i]>0) add(s,i,d[i],0),sum+=d[i]; else if(d[i]<0) add(i,t,-d[i],0);
for(int i=1;i<=m;i++)
{
int u=U[i],v=V[i]; if(!Tp[i]) continue;
add(u,v,1,i);
}
int flw=0;
while(bfs()) flw+=dfs(s,inf);
if(flw!=sum) {printf("No euler circuit exist\n\n");continue;}
for(int u=1;u<=n;u++)
{
for(int i=head[u];i;i=e[i].nxt)
{
if(!e[i].id) continue;
if(!e[i].w) flag[e[i].id]=1;
}
}
for(int i=1;i<=m;i++)
{
if(flag[i]) swap(U[i],V[i]);
E[U[i]].push_back(V[i]);
}
dfs1(1);
while(!Q.empty()) printf("%d ",Q.top()),Q.pop(); printf("\n\n");
}
return 0;
}
CF527E Data Center Drama
发现要求每个点出度和入度都是偶数,那么这个点的总度数必须是偶数。
如果这张图已经有一条长度为偶数的欧拉回路,构造答案是容易的,我们只要在这条路径上每经过一条边反向一次。
反之也可以证明如果不存在一条长度为偶数的欧拉回路,这个图一定找不到解。
所以贪心地将度数为奇数的点两两连边就好了。
最后总边数是奇数就随便找个点加自环,用如上方法构造答案。
Code
const int N=4e5+5;
int n,m;
struct edge{int nxt,to;} e[N<<1];
int head[N],cnt=1;
il void add(int u,int v) {e[++cnt]={head[u],v};head[u]=cnt;}
int d[N],tot,t[N];
int ans[N],vis[N<<1];
void dfs(int u)
{
for(int &i=head[u];i;i=e[i].nxt)
{
if(vis[i]) continue;
int v=e[i].to;
vis[i]=vis[i^1]=1;
dfs(v);
}
ans[++tot]=u;
}
int main()
{
n=read(),m=read();
for(int i=1;i<=m;i++)
{
int u=read(),v=read();
add(u,v),add(v,u),d[u]++,d[v]++;
}
for(int i=1;i<=n;i++) if(d[i]&1) t[++tot]=i;
for(int i=1;i<=tot;i+=2) add(t[i],t[i+1]),add(t[i+1],t[i]),m++;
if(m&1) add(1,1),m++;
tot=0,dfs(1);
printf("%d\n",m);
for(int i=1;i<tot;i++)
{
if(i&1) printf("%d %d\n",ans[i],ans[i+1]);
else printf("%d %d\n",ans[i+1],ans[i]);
}
return 0;
}
CF547D Mike and Fish
将点 \((x,y)\) 看作横坐标为 \(x\) 的点向纵坐标为 \(y\) 的点连无向边。
我们要做的事情就是给这些无向边定向,使每个点的入度和出度至多相差 \(1\)。这看起来就很欧拉回路了:我们先把所有度数为奇数的点向一个虚拟点连边,使所有度数为偶数。
根据边的方向涂颜色即可。
Code
#define pii pair<int,int>
const int N=4e5+5;
int n,L=2e5;
struct edge{int nxt,to;} e[N<<1];
int head[N],cnt=1;
il void add(int u,int v) {e[++cnt]={head[u],v};head[u]=cnt;}
int vis[N<<1],ans[N],tot,d[N],flag[N];
map<pii,int> mp;
void dfs(int u)
{
for(int &i=head[u];i;i=e[i].nxt)
{
if(vis[i]) continue;
int v=e[i].to; vis[i]=vis[i^1]=1;
dfs(v);
}
ans[++tot]=u,flag[u]=1;
}
char col[N];
int main()
{
n=read();
for(int i=1;i<=n;i++)
{
int x=read(),y=read();
add(x,y+L),add(y+L,x),d[x]++,d[y+L]++;
mp[pii(x,y+L)]=i,mp[pii(y+L,x)]=-i;
}
for(int i=1;i<=2*L;i++) if(d[i]&1) add(0,i),add(i,0);
for(int i=0;i<=2*L;i++) if(!flag[i]) dfs(i);
for(int i=1;i<tot;i++)
{
if(!ans[i]||!ans[i+1]) continue;
int id=mp[pii(ans[i],ans[i+1])];
if(id>0) col[id]='b';
else col[-id]='r';
}
for(int i=1;i<=n;i++) printf("%c",col[i]);
printf("\n");
return 0;
}
[省选联考 2020 B 卷] 丁香之路
首先对这个神奇的距离定义式找性质:走 \(x\to y\) 其实等价于走 \(x\to x+1\to \cdots\to y\)。
那么把边划分为题里要求的边和链上的相邻边,最优方案一定可以只经过题里要求的边恰好一次。
设终点为 \(i\),一些边需要恰好经过一次其实就是要走一个 \(s\) 为起点,\(i\) 为终点的欧拉路径。但是起点终点不同不好搞,考虑连个无向边 \(s\to i\),这样就是欧拉回路了。
然而还需要调整度数的奇偶性:从 \(1\) 到 \(n\) 考虑每个点的度数,如果是奇数就连边 \(i\to i+1\)。容易证明这样做最后所有点度数都是偶数。
还有一个问题是图不一定连通。我们要用代价最少的边使这个图连通,本质上是一个最小生成树。看起来有 \(n^2\) 条边不可做,但根据这个代价的性质只需要考虑相邻点的连边。
枚举终点即可,时间复杂度 \(O(n^2\log n)\)。
Code
const int N=2505;
int n,m,s,sum,fa[N],y[N],ydeg[N],deg[N];
il int find(int x) {return fa[x]==x?x:fa[x]=find(fa[x]);}
il void merge(int x,int y) {if(find(x)!=find(y)) fa[find(x)]=find(y);}
struct edge{int u,v,w;} e[N];
il bool cmp(edge x,edge y) {return x.w<y.w;}
il int solve(int t)
{
int ans=sum;
for(int i=1;i<=n;i++) deg[i]=ydeg[i],fa[i]=y[i];
deg[s]++,deg[t]++;
for(int i=1;i<=n;i++) if(deg[i]&1) deg[i]++,deg[i+1]++,ans++,merge(i,i+1);
int lst=0,tot=0;
for(int i=1;i<=n;i++) if(deg[i])
{
if(lst) e[++tot]={lst,i,i-lst};
lst=i;
}
sort(e+1,e+tot+1,cmp);
for(int i=1;i<=tot;i++)
{
int u=e[i].u,v=e[i].v;
if(find(u)!=find(v)) ans+=2*e[i].w,fa[find(u)]=find(v);
}
return ans;
}
int main()
{
n=read(),m=read(),s=read();
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=m;i++)
{
int u=read(),v=read();
ydeg[u]++,ydeg[v]++,sum+=abs(u-v),merge(u,v);
}
for(int i=1;i<=n;i++) y[i]=fa[i];
for(int i=1;i<=n;i++) printf("%d ",solve(i)); printf("\n");
return 0;
}
AGC025E Walking on a Tree
先考虑如果所有树边都被偶数条路径覆盖怎么做。
对于每条路径,连 \(x_i\to y_i\) 的无向边。那么每个点的度数都为偶数,也就是每个连通子图都存在欧拉回路。我们先跑出欧拉回路,再根据回路中每条路径连的边被经过的方向来定向,则所有树边正反通过次数相同。
这个结论的证明就是对于一条树边 \((u,v)\),我们把这棵树上的点划分成不经过这条边的两部分。因为我们没连树边,路径从 \(u\) 一侧到 \(v\) 一侧必须要经过一条路径边,又因为是回路,连接这两部分之间的边正反向通过的次数一定相同。
还需要覆盖次数不全是偶数的情况。这时我们需要补一些边使所有点度数都是偶数。
仿照 [省选联考 2020 B 卷] 丁香之路 的思路,考虑从下到上 dfs 处理原树:如果处理完子树 \(u\),点 \(u\) 的度数仍是奇数,则连一条 \(u\to fa_u\) 的无向边。
和之前同理,可以证明这样每条树边正反通过次数至多差 \(1\),为最优解。
给所有边定向后,树上差分(或直接暴力)即可统计所有树边的权值和。代码长是因为粘了一堆板子。
Code
const int N=10005;
typedef pair<int,int> pir;
map<pir,int> mp;
int n,m;
struct edge{int nxt,to;} e[N<<1];
int head[N],cnt=1;
il void add(int u,int v) {e[++cnt]={head[u],v};head[u]=cnt;}
vector<int> E[N];
struct LCA
{
int dfn[N],fa[N],tot,dep[N],st[20][N];
il int get(int x,int y) {return dep[x]<dep[y]?x:y;}
void dfs(int u,int ff)
{
dfn[u]=++tot,st[0][tot]=ff,fa[u]=ff; dep[u]=dep[ff]+1;
for(auto v:E[u]) if(v^ff) dfs(v,u);
}
il void init()
{
dfs(1,0);
for(int i=1;(1<<i)<=n;i++)
for(int j=1;j<=n-(1<<i)+1;j++)
st[i][j]=get(st[i-1][j],st[i-1][j+(1<<i-1)]);
}
il int lca(int x,int y)
{
if(x==y) return x;
if((x=dfn[x])>(y=dfn[y])) swap(x,y);
int l=__lg(y-x);
return get(st[l][x+1],st[l][y-(1<<l)+1]);
}
}l;
int ans[N],St[N],Ed[N],vis[3][N],Vis[N<<1],deg[N];
int flag[N];
void getedge(int u)
{
for(auto v:E[u]) if(v^l.fa[u])
{
getedge(v);
if(deg[v]&1) deg[v]++,deg[u]++,add(u,v),add(v,u);
}
}
vector<int> stk;
void dfs(int u)
{
flag[u]=1;
for(int &i=head[u];i;i=e[i].nxt)
{
int v=e[i].to; if(Vis[i]) continue;
Vis[i]=Vis[i^1]=1,dfs(v);
}
stk.push_back(u);
}
void getvis(int u,int fa)
{
for(auto v:E[u]) if(v^fa)
getvis(v,u),vis[0][u]+=vis[0][v],vis[1][u]+=vis[1][v];
}
int main()
{
n=read(),m=read();
for(int i=1;i<n;i++)
{
int u=read(),v=read();
E[u].push_back(v),E[v].push_back(u);
}
l.init();
for(int i=1;i<=m;i++)
{
int u=read(),v=read(); St[i]=u,Ed[i]=v;
add(u,v),add(v,u); deg[u]++,deg[v]++;
mp[pir(u,v)]=i,mp[pir(v,u)]=-i;
}
getedge(1);
for(int i=1;i<=n;i++) if(!flag[i]) dfs(i);
reverse(stk.begin(),stk.end());
for(int i=0;i+1<stk.size();i++)
if(mp.count(pir(stk[i],stk[i+1])))
{
int x=mp[pir(stk[i],stk[i+1])];
ans[abs(x)]=x>0?0:1;
}
for(int i=1;i<=m;i++)
{
int u=St[i],v=Ed[i],x=ans[i];
vis[x][u]++,vis[x][l.lca(u,v)]--;
vis[x^1][v]++,vis[x^1][l.lca(u,v)]--;
}
getvis(1,0);
int res=0;
for(int i=1;i<=n;i++) res+=(vis[0][i]!=0)+(vis[1][i]!=0);
printf("%d\n",res);
for(int i=1;i<=m;i++)
if(ans[i]==0) printf("%d %d\n",St[i],Ed[i]);
else printf("%d %d\n",Ed[i],St[i]);
return 0;
}
先写这些,剩下的再做到这类题再说。
本文来自博客园,作者:樱雪喵,转载请注明原文链接:https://www.cnblogs.com/ying-xue/p/Euler-circuit.html