[Tricks] 记各类欧拉回路问题

以前从来没见过除了板子以外的题,但最近总是做题见到欧拉回路,然后一样的 trick 每次都想不到。
怎么一点举一反三的能力都没有的?

板子

有向图的欧拉回路

dfs,当前弧优化。

Code
stack 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 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 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 E[N];
stack Q;
void dfs1(int u)
{
    for(int i=st[u];i>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

CF547D Mike and Fish

将点 \((x,y)\) 看作横坐标为 \(x\) 的点向纵坐标为 \(y\) 的点连无向边。
我们要做的事情就是给这些无向边定向,使每个点的入度和出度至多相差 \(1\)。这看起来就很欧拉回路了:我们先把所有度数为奇数的点向一个虚拟点连边,使所有度数为偶数。
根据边的方向涂颜色即可。

Code
#define pii pair
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 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;i0) 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

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 pir;
map 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 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](y=dfn[y])) swap(x,y);
        int l=__lg(y-x);
        return get(st[l][x+1],st[l][y-(1< 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;i0?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;
}

先写这些,剩下的再做到这类题再说。

你可能感兴趣的:(深度优先,图论,算法)