欧拉路径学习笔记
\(\bigstar\) 欧拉路径
若 \(G=(V,\ E)\) 中的一条路径包含了 \(E\) 中的所有边且不重复,则称其为 欧拉路径(\(\textbf{Eulerian Path}\))。
若该路径的起点与终点相同,则称其为 欧拉回路(\(\textbf{Eulerian Circuit}\))。
欧拉路径的存在条件:
-
此图连通;
-
对于无向图,当且仅当奇点个数为 \(0\) 或 \(2\);
-
对于有向图,当且仅当
-
入度与出度不同的点的个数为 \(0\) 或 \(2\);
-
当入度与出度不同的点的个数为 \(2\) 时,则存在一个点(即路径终点)入度比出度多 \(1\),一个点(即路径起点)出度比入度多 \(1\)。
-
\(\bigstar\) 欧拉路径的求法
Hierholzer 算法
根据欧拉路径的存在条件,找到一个可能的起点,从其出发进行 dfs:
- 遍历与该点相邻的边,经过之后就将其删除;
- 遍历结束后,将该点加入一个栈内并回到上一层。
最后依次将点从栈中取出,即可得到一条欧拉路径。
\(\bigstar\) 例题
【模板题 1】(无向图)洛谷 P2731 - [USACO3.3] 骑马修栅栏 Riding the Fences
求一个至多 \(500\) 个点,\(m\) 条边的无向图字典序最小的欧拉路径。
\(\texttt{Data Range: }\ 1\leq m\leq 1024.\)
解 首先根据欧拉路径的存在条件判断是否存在欧拉路径,若存在,就求出可能的起点。
题目中要求字典序最小的欧拉路径,每次取尽可能小的编号。
考虑使用邻接矩阵存图,由于可能有重边,故用 \(Map_{u,\ v}\) 存储 \(u\) 与 \(v\) 之间的边数。每取一条边 \(u\to v\),就将 \(Map_{u,\ v}\) 减少 \(1\)。然后只需按照 Hierholzer 算法的做法操作即可。
程序
#include<bits/stdc++.h>
using namespace std;
int m,Map[501][501],deg[501];
stack<int>answer;
void dfs(int id){
for(int i=1;i<=500;i++)
if(Map[id][i]){
Map[id][i]--,Map[i][id]--;
dfs(i);
}
answer.push(id);
}
void print(){
while(!answer.empty())
printf("%d\n",answer.top()),answer.pop();
}
int main(){
scanf("%d",&m);
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
Map[u][v]++,Map[v][u]++;
deg[u]++,deg[v]++;
}
vector<int>oddid; // 表示奇点的集合
for(int i=1;i<=500;i++)
if(deg[i]&1)oddid.push_back(i);
if(oddid.size()>2){
/* 奇点数量超过 2,则不可能存在欧拉路径,
图中奇点个数必为偶数,故无需考虑奇点数量为 1 的情况 */
printf("-1\n");
return 0;
}
if(oddid.size()==0){
int tmp=0;
while(deg[tmp]==0)tmp++;
// 奇点数量为 0,取编号最小的节点为起点
dfs(tmp); print();
}
else{
// 奇点数量为 2,取编号较小的奇点为起点
dfs(oddid[0]); print();
}
return 0;
}
【模板题 2】(有向图)洛谷 P7771 - 【模板】欧拉路径
求一个 \(n\) 个点,\(m\) 条边的有向图字典序最小的欧拉路径。
\(\texttt{Data Range: }1\leq n\leq 10^5,\ 1\leq m\leq 2\times 10^5.\)
解 首先根据欧拉路径的存在条件判断是否存在欧拉路径,若存在,就求出可能的起点。
题目中要求字典序最小的欧拉路径,就将每个点有边相连的点的编号排序,每次取尽可能小的编号。
令 \(n\) 个点编号为 \(1\sim n\),不妨用一个数组 \(t\) 表示一个点下一个要取的边,每取一条边 \(u\to v\),就将 \(t_u\) 增加 \(1\)。然后只需按照 Hierholzer 算法的做法操作即可。
时间复杂度 \(\Theta(n+m)\)。
程序
#include<bits/stdc++.h>
using namespace std;
int n,m;
vector<int>edge[100001];
int in[100001],out[100001];
stack<int>answer;
int t[100001];
void dfs(int id){
for(int i=t[id];i<edge[id].size();i=t[id]){
t[id]=i+1; dfs(edge[id][i]);
}
answer.push(id);
}
void print(){
while(!answer.empty())
printf("%d ",answer.top()),answer.pop();
printf("\n");
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
edge[u].push_back(v);
in[v]++,out[u]++;
}
// 将将每个点有边相连的点的编号排序,使得以后每次取的点都是编号最小的那个
for(int i=1;i<=n;i++)
sort(edge[i].begin(),edge[i].end());
vector<int>tmp; // 表示入度与出度不同的点的集合
for(int i=1;i<=n;i++)
if(in[i]!=out[i])tmp.push_back(i);
if(tmp.size()==0){
/* 入度与出度不同的点个数为 0,此时可以选任意点作为起点,
为了字典序最小,我们选 1 号点作为起点 */
dfs(1); print();
}
else if(tmp.size()==2){
// 入度与出度不同的点个数为 2,此时只能选出度比入度大 1 的点作为起点
if(out[tmp[0]]==in[tmp[0]]+1){
dfs(tmp[0]); print();
}
else if(out[tmp[1]]==in[tmp[1]]+1){
dfs(tmp[1]); print();
}
else printf("No\n"); // 找不到起点
}
else printf("No\n"); // 入度与出度不同的点个数不是 0 或 2
return 0;
}
【题 1】洛谷 P1341 - 无序字母对
有 \(n\) 个互不相同的无序字母对(区分大小写),请构造一个字典序最小的长为 \(n+1\) 的字符串使得这些字母对都在字符串中出现。
\(\texttt{Data Range: }1\leq n\leq\dbinom{52}2.\)
解 把不同的字符设为点,对于每个无序字母对,将这两个字符对应的点之间连边。那么让这些字符对出现且长度为 \(n+1\) 的字符串只能为图的一条欧拉路径。
程序
#include<bits/stdc++.h>
using namespace std;
int n;
inline int f(char ch){
if(ch<'a')return ch-'A'+1;
else return ch-'a'+27;
}
inline char g(int ch){
if(ch<=26)return ch-1+'A';
else return ch-27+'a';
}
vector<int>edge[53];
int deg[53];
bool p[53][53];
int t[53];
stack<int>answer;
void print(){
while(!answer.empty())
printf("%c",g(answer.top())),answer.pop();
printf("\n");
}
void dfs(int id){
for(int i=t[id];i<edge[id].size();i=t[id]){
t[id]++;
if(!p[id][edge[id][i]]){
p[id][edge[id][i]]=p[edge[id][i]][id]=true;
dfs(edge[id][i]);
}
}
answer.push(id);
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
char c1,c2;
scanf(" %c%c",&c1,&c2);
edge[f(c1)].push_back(f(c2));
edge[f(c2)].push_back(f(c1));
deg[f(c1)]++,deg[f(c2)]++;
}
for(int i=1;i<=52;i++)
sort(edge[i].begin(),edge[i].end());
int odd=0;
for(int i=1;i<=52;i++)
if(deg[i]&1)odd++;
if(odd==0){
int tmp=1;
while(!deg[tmp])tmp++;
dfs(tmp); print();
}
else if(odd==2){
int tmp=1;
while((deg[tmp]&1)==0)tmp++;
dfs(tmp); print();
}
else printf("No Solution\n");
return 0;
}
【题 2】洛谷 P3520 - [POI2011] SMI-Garbage
给定一个有 \(n\) 个点和 \(m\) 条边的无向图,每条边都有一个状态 \(0\) 或 \(1\) 以及一个目标状态,垃圾车每次经过都会改变状态。有若干辆垃圾车,每辆垃圾车的路径上不能经过同一个点 \(2\) 次(起点除外),且最后必须回到起点。找出一个合法的垃圾车路径设定方案。
\(\texttt{Data Range: }1\leq n\leq 10^5,\ 1\leq m\leq 10^6.\)
解 若一条边初始状态与目标状态相同,则我们直接忽视此边。此时图被分为若干连通块,对每个连通块找一条欧拉路径即可。
程序
#include<bits/stdc++.h>
using namespace std;
int n,m;
struct Edge{
int to,nxt;
}edge[2000001];
int head[100001],edgecnt;
inline void addEdge(int u,int v){
edge[++edgecnt]={v,head[u]},head[u]=edgecnt;
}
int deg[100001],t[100001];
vector<vector<int> > answer;
vector<vector<int> > odd;
set<pair<int,int> > p;
int blockid[100001],blockcnt;
void blockdfs(int id,int block){
blockid[id]=block;
if(deg[id]&1)odd[block].push_back(id);
for(int i=head[id];i;i=edge[i].nxt)
if(deg[edge[i].to]&&blockid[edge[i].to]==0)
blockdfs(edge[i].to,block);
}
void dfs(int id){
for(int i=t[id];i;i=t[id]){
t[id]=edge[i].nxt;
if(p.find({id,edge[i].to})==p.end()){
p.insert({id,edge[i].to});
p.insert({edge[i].to,id});
dfs(edge[i].to);
}
}
answer[blockid[id]].push_back(id);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int a,b,s,t;
scanf("%d%d%d%d",&a,&b,&s,&t);
if(s==t)continue;
addEdge(a,b);
addEdge(b,a);
deg[a]++,deg[b]++;
}
answer.push_back({});
odd.push_back({});
for(int i=1;i<=n;i++)
t[i]=head[i];
for(int i=1;i<=n;i++)
if(deg[i]&&blockid[i]==0){
answer.push_back({});
odd.push_back({});
// 划分连通块
blockdfs(i,++blockcnt);
}
int cnt=0;
for(int i=1;i<=n;i++){
if(!deg[i])continue;
if(blockid[i]<=cnt)continue;
cnt++;
// 找欧拉路径
if(odd[blockid[i]].size()>2){
printf("NIE\n");
return 0;
}
if(odd[blockid[i]].size()==0)dfs(i);
else dfs(odd[blockid[i]][0]);
}
printf("%d\n",blockcnt);
for(int i=1;i<=blockcnt;i++){
printf("%d ",answer[i].size()-1);
for(int j=answer[i].size()-1;j>=0;j--)
printf("%d ",answer[i][j]);
printf("\n");
}
return 0;
}
【题 3】CodeForces 36E - Two Paths
给定一个有 \(m\) 条无向边,至多有 \(10^4\) 个点,可能有重边,不保证联通的图,请在这个图上找出两条路径,使得这两条路径覆盖了图中每一条边仅一次。
\(\texttt{Data Range: }1\leq m\leq 10^4.\)
分析 两条路径产生的奇点数为 \(0\) 或 \(2\),则两条路径产生的奇点数只能为 \(0,\ 2\) 或 \(4\)。
此外,如果我们能找到一条路径覆盖图中每一条边仅一次,那么只需将这条路径从一个断点处断开即可。
解 对整个图的连通块数量与奇点数量分类讨论。
-
若整个图连通,
-
奇点数为 \(0\) 或 \(2\),则必能找到一条路径覆盖图中每一条边仅一次。此时只需路径长度 \(\geq 2\),就可以将其分为两条路径;
-
奇点数为 \(4\),此时任取两个没有连边的奇点,将它们连上一条临时边。此时有 \(2\) 个奇点,必能找到一条路径覆盖图中每一条边仅一次,由于临时边的端点都变成了偶点,故不可能成为这条路径的第一条边或最后一条边,也就可以从临时边处断开路径,得到答案。
-
-
若整个图分为了两个连通块,只要两个连通块奇点数均为 \(0\) 或 \(2\),就可以在两个连通块各找到一条路径,即为最终答案。
-
若整个图分为了三个或更多的连通块,显然无解。
我们使用 Hierholzer 算法找到合法的路径,由于最后输出的是边的编号,输出时需要对任意相邻两个点找到边的编号。
时间复杂度 \(\Theta(n+m)\),其中 \(n\) 为点数,即 \(10^4\)。
程序
#include<bits/stdc++.h>
using namespace std;
void error(){
//由于此代码用到的地方较多,故封装成一个函数
printf("-1\n"); exit(0);
}
int n=10000,m;
struct Edge{
int to,nxt;
}edge[20003];
int head[10001],cntEdge;
inline void addEdge(int u,int v){
edge[++cntEdge]={v,head[u]},head[u]=cntEdge;
}
int deg[10001]; // 每个点的度
int blockid[10001],blockcnt,blockf[10001];
/* blockid 表示点所在的连通块编号
blockcnt 表示连通块数目
blockf 表示点所在连通块中编号最小的点 */
void dfs_block(int id,int block){
blockid[id]=block;
for(int i=head[id];i;i=edge[i].nxt)
if(blockid[edge[i].to]==0&°[edge[i].to])
dfs_block(edge[i].to,block);
}
bool used[10001]; // 这个边输出答案时是否被使用过,可以避免重边情况
int findedge(int u,int v){
// 找到 u 和 v 之间的边
for(int i=head[u];i;i=edge[i].nxt)
if(edge[i].to==v&&!used[i+1>>1])return i;
return -1;
}
bool vis[10002];
void dfs(int id,vector<int> &tmp){
// 找欧拉路径,答案存储在 tmp 里
if(deg[id]==0){
// 这个点已经没有出边
tmp.push_back(id); return;
}
for(int i=head[id];i;i=edge[i].nxt){
if(vis[i+1>>1])continue;
vis[i+1>>1]=true; // 标记走过的边
deg[edge[i].to]--,deg[id]--;
dfs(edge[i].to,tmp);
}
tmp.push_back(id);
}
void print(vector<int> answer,int l,int r){
for(int i=l;i<=r;i++){
int tmp=findedge(answer[i],answer[i+1])+1>>1;
printf("%d ",tmp),used[tmp]=true;
}
printf("\n");
}
int main(){
scanf("%d",&m);
if(m==1)error();
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);
}
vector<int>oddid;
for(int i=1;i<=n;i++){
if(deg[i]&&blockid[i]==0)
blockf[blockcnt+1]=i,
dfs_block(i,++blockcnt);
if(deg[i]&1)oddid.push_back(i); // 找到一个奇点
}
if(blockcnt>2)error(); // 连通块数量超过 2,无解
if(blockcnt==1){
if(oddid.size()==4){
bool flag=false; int t1=0,t2=0;
// 找没有连边的两个奇点
for(int i=0;i<4&&!flag;i++)
for(int j=i+1;j<4&&!flag;j++)
if(findedge(oddid[i],oddid[j])==-1){
swap(oddid[0],oddid[i]);
swap(oddid[1],oddid[j]);
flag=true;
}
// 连边
addEdge(oddid[0],oddid[1]);
addEdge(oddid[1],oddid[0]);
deg[oddid[0]]++,deg[oddid[1]]++;
vector<int>tmp;
dfs(oddid[2],tmp);
int t=0;
// 找到临时添加的边
while((tmp[t]!=oddid[0]||tmp[t+1]!=oddid[1])&&
(tmp[t]!=oddid[1]||tmp[t+1]!=oddid[0]))t++;
printf("%d\n",t);
print(tmp,0,t-1);
printf("%d\n",m-t);
print(tmp,t+1,tmp.size()-2);
}
else{
vector<int>tmp;
// 找一条路径遍历整个图
if(oddid.size()==0)
dfs(blockf[1],tmp);
else if(oddid.size()==2)
dfs(oddid[0],tmp);
else error();
// 断开第一条边输出
printf("1\n");
print(tmp,0,0);
printf("%d\n",tmp.size()-2);
print(tmp,1,tmp.size()-2);
}
}
if(blockcnt==2){
// 有两个连通块时,分别找路径
vector<int>odd1,odd2;
for(int i:oddid)
if(blockid[i]==1)odd1.push_back(i);
else odd2.push_back(i);
if(odd1.size()!=0&&odd1.size()!=2)error();
if(odd2.size()!=0&&odd2.size()!=2)error();
vector<int>ans1,ans2;
if(odd1.size()==0){
int t=1;
while(blockid[t]!=1)t++;
dfs(t,ans1);
}
else dfs(odd1[0],ans1);
printf("%d\n",ans1.size()-1);
print(ans1,0,ans1.size()-2);
if(odd2.size()==0){
int t2=1;
while(blockid[t2]!=2)t2++;
dfs(t2,ans2);
}
else dfs(odd2[0],ans2);
printf("%d\n",ans2.size()-1);
print(ans2,0,ans2.size()-2);
}
return 0;
}
【题 4】CodeForces 209C - Trails and Glades
给定 \(n\) 个点,\(m\) 条边的无向图,添加若干条无向边,使得图存在从 \(1\) 号点出发又回到
\(1\) 号点,且覆盖图中每一条边仅一次的路径,求添加边的数量的最小值。
\(\texttt{Data Range: }1\leq n,\ m\leq 10^6.\)
解 图中存在若干奇点,容易想到奇点之间两两配对连边。
题目要求存在整个图的欧拉回路,而图可能分为若干个连通块。
设一个连通块中有 \(t\) 个奇点。
若 \(t>0\),则只需与其它连通块的奇点连线,也就是 \(t\) 个奇点参与两两配对连边。
若 \(t=0\),为了使图连通,则必须从该连通块连出去一条边,此时产生一个奇点,为了存在欧拉回路,必定又要连一条边。此时相当于增加两个点参与配对。
故答案为
注意到当只有 \(1\) 个连通块且没有奇点时,不需要连出去边,所以上述公式会得出错误的答案,只需在输出时特判即可。
说明 由于题目要求从 \(1\) 开始存在欧拉回路,故我们可以在 \(1\) 号点没有边的情况下,把 \(1\) 号点也视为一个独立的连通块以避免 \(1\) 号点没有连边的情况的错误。
程序
#include<bits/stdc++.h>
using namespace std;
int n,m,deg[1000001];
int oddcnt[1000001],f[1000001];
bool p[1000001];
int get(int id){
return f[id]==id?id:f[id]=get(f[id]);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)f[i]=i;
for(int i=1;i<=m;i++){
int u,v; scanf("%d%d",&u,&v);
deg[u]++,deg[v]++;
f[get(u)]=get(v); // 合并连通块
}
int total=0,block=0;
// 统计奇点
for(int i=1;i<=n;i++)
if(deg[i]&1)oddcnt[get(i)]++,total++;
int answer=0;
for(int i=1;i<=n;i++){
int tmp=get(i);
if(!p[tmp]&&(i==1||deg[i])){
// 遇到一个新的连通块,计算对答案的贡献
p[tmp]=true,block++;
if(oddcnt[tmp]==0)answer++;
else answer+=oddcnt[tmp]>>1;
}
}
if(block==1&&total==0)answer=0; // 特判
printf("%d\n",answer);
return 0;
}
【题 5】洛谷 P5921 - [POI1999] 原始生物
设 \(K=(a_1,\ a_2,\ \cdots,\ a_n)\),满足 \(n\) 个条件,对于第 \(i\) 个条件 \((l_i,\ r_i)\),满足 \(\exists\ 1\leq i<n\),\(a_i=l_i,\ a_{i+1}=r_i\)。给定这些条件,求满足条件的 \(n\) 的最小值。
\(\texttt{Data Range: }1\leq n\leq 10^6,\ 1\leq l_i,\ r_i\leq 10^3.\)
解 本题等价于:
有一张有 \(m\) 条边的有向图,在图中补上若干条边使得存在一条欧拉路径可以覆盖图中每一条边仅一次。求补边后整张图边数的最小值。
题目要求存在整个图的欧拉路径,而图可能分为若干个连通块。
设一个连通块中,每个点入度减去出度的值之和为 \(t\)。
若 \(t>0\),则为了让它符合欧拉回路的存在条件,至少再连 \(t\) 条边。
若 \(t=0\),为了使图连通,至少连 \(1\) 条边。
故答案为
程序
#include<bits/stdc++.h>
using namespace std;
int n=1000,m,in[1001],out[1001];
int f[1001]; bool p[1001];
int a[1001];
int get(int id){
if(f[id]!=id)f[id]=get(f[id]);
return f[id];
}
int main(){
scanf("%d",&m);
for(int i=1;i<=n;i++)f[i]=i;
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
in[v]++,out[u]++;
p[u]=p[v]=true;
f[get(u)]=get(v); // 合并连通块
}
for(int i=1;i<=n;i++)
if(in[i]>out[i])
a[get(i)]+=in[i]-out[i];
int answer=0;
for(int i=1;i<=n;i++)
if(p[i]&&get(i)==i)
answer+=max(1,a[i]);
printf("%d\n",answer+m);
return 0;
}
【题 6】CodeForces 547D - Mike and Fish
给定 \(n\) 个整点的坐标 \((x_i,\ y_i)\),你要给这 \(n\) 个点染成红色或者蓝色,要求 \(x\) 坐标或 \(y\) 坐标相同的点中两色点数差值 \(\leq 1\)。
\(\texttt{Data Range: }1\leq n,\ x_i,\ y_i\leq 2\times 10^5.\)
解 首先把此图转化。
为每个 \(x\) 坐标和每个 \(y\) 坐标建立一个点,若存在点 \((x_0,\ y_0)\),就给表示 \(x\) 坐标为 \(x_0\) 的点与表示 \(y\) 坐标为 \(y_0\) 的点之间连上一条边。
首先考虑转化后的图中的每一个连通块。显然每个连通块都是二分图(只需按照这个点表示的是 \(x\) 坐标还是 \(y\) 坐标。
若图中所有点均为偶点,则必然存在一条欧拉回路,此时只需给路径交错染色,则此时每个点所连的边中,两种颜色的边数量相等(由于图是二分图,路径中走进一个点则下一步必然走到另一集合的点中)。
若存在奇点,我们考虑将所有奇点都与一个特殊点连边,不妨设都与编号为 \(0\) 的点连边,则此时所有点都为偶点(由于奇点个数必为偶数,故编号为 \(0\) 的点也是偶点),接着按照上述全为偶点的操作方法操作即可(由于每个点只会向 \(0\) 连至多 \(1\) 条边,故两种颜色边数量之差 \(\leq 1\))。
程序
#include<bits/stdc++.h>
using namespace std;
int n;
struct Edge{
int to,nxt;
}edge[1600001];
int head[400001],cntEdge;
inline void addEdge(int u,int v){
edge[++cntEdge]={v,head[u]},head[u]=cntEdge;
}
int deg[400001];
bool color[400001],p[400001];
int t[400001];
void dfs(int id){
for(int i=t[id];i;i=t[id]){
t[id]=edge[i].nxt;
if(!p[i+1>>1]){
p[i+1>>1]=true;
if((i+1>>1)<=n)color[i+1>>1]=i&1;
dfs(edge[i].to);
}
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
int x,y;
scanf("%d%d",&x,&y);
addEdge(x,y+200000);
addEdge(y+200000,x);
deg[x]++,deg[y+200000]++;
}
for(int i=1;i<=400000;i++)
if(deg[i]&1){
// 奇点与 0 号点连边
addEdge(0,i);
addEdge(i,0);
deg[0]++,deg[i]++;
}
for(int i=0;i<=400000;i++)
t[i]=head[i];
for(int i=1;i<=400000;i++)dfs(i);
for(int i=1;i<=n;i++)
if(color[i])printf("r");
else printf("b");
printf("\n");
return 0;
}
【题 7】洛谷 P6628 - [省选联考 2020 B 卷] 丁香之路
给定一个有 \(n\) 个点,\(m\) 条边的无向图以及一个起点 \(s\)。对于任意一个点,找出最短的一条从 \(s\) 到这个点的路径经过这 \(m\) 条边,路径中可以从任意一个点 \(i\) 走到另一个点 \(j\),无论是否在给定的 \(m\) 条边中,边权均为 \(|i-j|\)。
\(\texttt{Data Range: }50\leq n\leq 2500,\ 0\leq m\leq\dbinom n2.\)
解 本题中要求求出相同起点,不同终点的结果。不妨将起点和终点连一条边,这样就变成了求一条欧拉回路。
为了存在欧拉回路,图中必须全为偶点。我们考虑将奇点两两配对连边。由于此题边权设置的特殊性,可以优先考虑将相邻的奇点连边。即按编号从小到大,第 \(1\) 个奇点与第 \(2\) 个奇点配对,第 \(3\) 个奇点与第 \(4\) 个奇点配对,以此类推。考虑到欧拉回路的另一个存在条件为图是连通图,尽可能在连边的时候多连通几个点,所以可以在为 \(u\) 和 \(v\) 连边时转化成编号相邻的点连边(不妨设 \(u<v\),即 \(u\) 与 \(u+1\) 连边,\(u+1\) 与 \(u+2\) 连边,以此类推直到 \(v-1\) 与 \(v\) 连边。
下面处理依然未被连通的块。不妨将每个连通块视为一个点,两个连通块之间的边权为连通这两个连通块所需的最小边权,接着需要求出一种连边方式,使得图连通且边权总和尽可能小,也就转化成了求最小生成树的问题。
事实上,整个计算过程中都无需使用给定的 \(m\) 条边的具体数据,于是我们可以不记录边的信息,只记录每个点的度数,再用并查集维护连通块的情况。
算法中为奇点配对时间复杂度 \(\Theta(n)\),连边时虽然要一个一个点连,但是至多连 \(n-1\) 条边,时间复杂度也为 \(\Theta(n)\);连通块之间连至多 \(n\) 条边,按边权从小到大排序时间复杂度 \(\Theta(n\log n)\),求最小生成树时间复杂度也为 \(\Theta(n)\)。上述过程要对每个终点都求一遍,时间复杂度为 \(\Theta(n^2\log n)\)。在输入 \(m\) 条边统计信息时还有 \(\Theta(m)\) 的时间复杂度。综上,总时间复杂度为 \(\Theta(n^2\log n+m)\)。
程序
#include<bits/stdc++.h>
using namespace std;
int n,m,s;
struct Edge{
int u,v,w;
inline bool operator<(Edge tmp)const{
return w<tmp.w;
}
};
vector<Edge>edge;
int deg[2501];
struct{
int f[2501];
void init(int N){
for(int i=1;i<=N;i++)f[i]=i;
}
int find(int id){
if(f[id]!=id)f[id]=find(f[id]);
return f[id];
}
void merge(int u,int v){
f[find(u)]=find(v);
}
}a,b;
long long sum;
int main(){
scanf("%d%d%d",&n,&m,&s);
a.init(n);
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
deg[u]++,deg[v]++;
a.merge(u,v);
sum+=abs(u-v);
}
for(int i=1;i<=n;i++)
a.f[i]=a.find(i);
for(int i=1;i<=n;i++){
b.init(n);
deg[s]++,deg[i]++;
b.merge(a.f[s],a.f[i]); // 起点和终点连边
long long answer=sum;
// 将奇点两连配对连边
int Last=0;
for(int j=1;j<=n;j++)
if(deg[j]&1)
if(Last==0)Last=j;
else{
answer+=abs(j-Last);
for(int k=Last;k<j;k++)
b.merge(a.f[k],a.f[k+1]);
Last=0;
}
// 连通块之间连边
edge.clear();
Last=0;
for(int j=1;j<=n;j++)
if(deg[j]){
if(Last&&b.find(a.f[Last])!=b.find(a.f[j]))
edge.push_back({a.f[Last],a.f[j],abs(j-Last)});
Last=j;
}
// 求最小生成树
sort(edge.begin(),edge.end());
for(Edge e:edge)
if(b.find(e.u)!=b.find(e.v)){
b.merge(e.u,e.v);
answer+=e.w*2;
}
printf("%lld ",answer);
deg[s]--,deg[i]--;
}
return 0;
}