[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&&sum;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;
}

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

  • 16
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值