2025.7.7 建模杂题
欧拉路径与欧拉回路的判定
一个图存在欧拉回路当且仅当:
- 非零度点互相(强)连通.
- 点的度数都是偶数(或出度入度).
一个图存在欧拉路径当且仅当:
- 非零度点互相(强)连通.
- 恰好有 \(2\) 个奇度点(或恰好存在两点 \(u,v\) 满足一个出度比入度多 \(1\),一个入度比出度多 \(1\)).
P6628 [省选联考 2020 B 卷] 丁香之路
Hint:欧拉回路 + 最小生成树.
欧拉回路的考察可能比较冷门,但是在考场上遇到不会还是很致命的,因为知识点本身并不算困难.
根据判定定理,可以考虑人为加一条 \((s,i)\) 的边,把找欧拉路径变成找欧拉回路. 考虑用并查集维护连通性,必须走的 \(m\) 条边求出每个点所属的连通块 bel[u].
对于每个起点,要使存在欧拉回路必须让每个点度数为偶数,于是考虑把奇度点单独拉出来,相邻两个连边代价最小. 值得注意的是由于欧拉回路要保证连通,所以这两个点连边在代价不增的情况下覆盖更多的点一定是更优的,即把一条边拆成多条长度为 \(1\) 的边,两端度数 \(+1\),中间度数 \(+2\).
现在所有点都是偶度点,最终要使得偶度点全部连通,而且由于度数为 \(0\) 的边不必考虑其连通性,所以我们将相邻但不连通非零偶度点的边(一定是最优的边集)全部加入待选边中,排序后跑 Kruskal 生成树来加边. 注意每次应连两条重边来避免改变度数的奇偶性.
总时间复杂度 $O(n^2\log n). 另外每个起点相当于多测,记得必要的清空.
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 2e5 + 10, inff = 1e9 + 1;
int n, m, s[maxn], t[maxn];
int cnt, a[maxn << 1], d[maxn << 1]; ll ans;
int tot;
struct Edge{int u, v, w;} e[maxn];
inline bool cmp(Edge x, Edge y) {return x.w < y.w;}
int fa[maxn << 1];
inline int findf(int u) {if(fa[u] == u) return u; else return fa[u] = findf(fa[u]);}
inline void merge(int u, int v) {int fu = findf(u), fv = findf(v); fa[fu] = fv; return;}
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> s[i] >> t[i];
s[++n] = inff, t[n] = 1;
for(int i = 1; i <= n; i++) a[++cnt] = s[i], a[++cnt] = t[i];
sort(a + 1, a + cnt + 1); cnt = unique(a + 1, a + cnt + 1) - a - 1;
for(int i = 1; i <= cnt; i++) fa[i] = i;
for(int i = 1; i <= n; i++) {
s[i] = lower_bound(a + 1, a + cnt + 1, s[i]) - a; d[s[i]]++;
t[i] = lower_bound(a + 1, a + cnt + 1, t[i]) - a; d[t[i]]--;
merge(s[i], t[i]);
}
for(int i = 1; i <= cnt; i++) d[i] += d[i - 1];
for(int i = 1; i < cnt; i++) {
if(d[i] != 0) {
merge(i, i + 1);
if(d[i] > 0) ans += 1ll * d[i] * (a[i + 1] - a[i]);
}
if(findf(i) != findf(i + 1)) e[++tot] = (Edge){i, i + 1, a[i + 1] - a[i]};
}
sort(e + 1, e + tot + 1, cmp);
for(int i = 1; i <= tot; i++) {
int fu = findf(e[i].u), fv = findf(e[i].v);
if(fu != fv) ans += e[i].w, fa[fu] = fv;
} cout << ans;
return 0;
}
P6168 [IOI 2016] railroad
Hint:考虑增速不计代价,加强限制,并新增 \((inf,1)\) 的点连向起点,转化成欧拉回路.
如果题目没理解错那么思路应该就很清晰了. 原题的小于等于限制太松,全部连边会炸空间,转换题意后代价不变. 考虑直接把 \(s_i,t_i\) 合并排序并离散化. 原来 \(s_i\rightarrow t_i\) 的边不变,代价为 \(0\). 现在我们要进行加边使图强连通,认为从左往右连边代价为 \(0\),从右往左连边代价为速度之差.
一个重要的转化是排序之后的有向图存在欧拉回路当且仅当跨过每条相邻两点之间边向左和向右的边数相同. 证明考虑如果边数不同,那么回路一定会无法遍历所有边,因为向左向右的边在欧拉回路中使一一配对的. 那么向左向右边数之差可以差分 \(O(n)\) 求得(注意差分一闭一开). 对于向右边数更多的要向左连边,相邻两点之间代价最小直接连肯定是最优的;对于向左边数更多的要向右连边但是根据转化题意边权不计所以不用考虑.
同时也要用并查集维护连通性. 当加完上述边后,再扫一遍若相邻两点不连通则加入待选边集中,仿照 P6628 使用 Kruskal 生成树求得最小的连边代价使图连通. 注意这里实际上是相邻两点互相连边,只不过向左边权不计所以总代价就是减速的代价.
时间复杂度 \(O(n\log n)\).
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 2.5e3 + 10, maxm = 3.2e6;
int n, m, ed;
ll sum, ans, d[maxn];
int tot;
struct Edge{int u, v, w;} e[maxm];
bool cmp(Edge x, Edge y) {return x.w < y.w;}
int fa[maxn], bel[maxn];
inline void init() {for(int i = 1; i <= n; i++) fa[i] = i; return;}
inline int findf(int u) {if(fa[u] == u) return u; else return fa[u] = findf(fa[u]);}
inline void merge(int u, int v) {int fu = findf(u), fv = findf(v); fa[fu] = fv; return;}
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m >> ed; init();
for(int i = 1, u, v; i <= m; i++) cin >> u >> v, merge(u, v), d[u]++, d[v]++, sum += abs(u - v);
for(int i = 1; i <= n; i++) bel[i] = findf(i);//每次统计答案继承必须走的边形成的连通块
for(int st = 1; st <= n; st++) {
init(), ans = sum, tot = 0;
merge(bel[st], bel[ed]), d[st]++, d[ed]++;
for(int i = 1, lst = 0; i <= n; i++) if(d[i] & 1) {
if(lst) {for(int j = lst; j < i; j++) merge(bel[j], bel[j + 1]), ans++; lst = 0;}
else lst = i;//间隔加边
}
for(int i = 1, lst = 0; i <= n; i++) if(d[i]) {
if(lst && findf(bel[i]) != findf(bel[lst])) e[++tot] = (Edge){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, w = e[i].w;
if(findf(bel[u]) != findf(bel[v])) ans += 2 * w, merge(bel[u], bel[v]);
} cout << ans << " ";
d[st]--, d[ed]--;
}
return 0;
}

浙公网安备 33010602011771号