浅谈欧拉路径,欧拉回路
引入
前有哈密顿路径,表示经过每个点恰好一次,现有欧拉路径,表示经过每条边恰好一次。
许多题目重要的是建模,往往最浅的建模就是点之间的连边,表示可以到达。如果说需要满足到达每个点一次,这就变成了 $\tt NPC$ 问题。但是我们往往可以将一个信息拆分成若干个信息,变成边之间的关系,这样就有多项式复杂度的解法,同样这个是可以求方案的。
本篇会向读者介绍欧拉路径和欧拉回路以及其判定方法,还有常用的套路。
欧拉图
具有欧拉回路的图叫欧拉图,如果只有欧拉路径就叫做半欧拉图。
欧拉路径
定义:
经过每条边恰好一次,起点和终点不一定相同。
无向图
每个点的度数都是偶数,或者只有两个节点度数是奇数。
有向图
设一个点的权值 $v_i$ 表示其出度减去入度。
那么存在的欧拉路径的条件是 $v_i = 0$ 或者同时存在一个 $v_i = 1, v_j = -1, i \ne j$。
欧拉回路
定义
经过每条边恰好一次,起点和终点相同。
无向图
每个点的度数都是偶数。
有向图
设一个点的权值 $v_i$ 表示其出度减去入度。
那么存在的欧拉路径的条件是 $v_i = 0$。
具体实现
如果说对于一条路径起点和终点是不同的,作为开始的节点需要是出度较大的节点。
我们考虑直接进行暴力 $\tt dfs$,每次删除一条边。在遍历完其相邻的所有点之后将当前点加入答案。
复杂度是 $O(n + m)$ 的。
1 2 3 4 5 6 7
| void dfs(int p) { while(!vc[p].empty()) { int v = vc[p].back(); vc[p].pop_back(); dfs(v); } ans[++ ed] = p; }
|
为什么要最后加入当前点呢?
如果说出现环的情况,而且我们对于当前点 $u$ 并不是第一次遍历环,那么直接记录答案显然是不行的。
但是我们可以先走这个环,再走链。这种时候就可以考虑最后加入点,这样就是倒着走,环并不会影响答案。
读者可以尝试画画图,因为笔者个人博客从来不放图,请见谅。
因为这样我们的答案是反着的,所以我们可以翻转一下数组。
之后的答案都指翻转数组之后的。
套路
字典序要求
- 字典序最小:贪心走最小的点即可。
- 字典序最大:贪心走最大的点即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| #include <bits/stdc++.h> using namespace std;
namespace Legendgod {
namespace Read {
#ifdef Fread const int Siz = (1 << 21) + 5; char buf[Siz], *iS, *iT; #define gc() (iS == iT ? (iT = (iS = buf) + fread(buf, 1, Siz, stdin), iT == iS ? EOF : *iS ++ ) : *iS ++) #define getchar gc #endif
template <typename T> void r1(T &x) { x = 0; int f(1); char c(getchar()); for(; !isdigit(c); c = getchar()) if(c == '-') f = -1; for(; isdigit(c); c = getchar()) x = (x * 10) + (c ^ 48); x *= f; } template <typename T, typename...Args> void r1(T &x, Args&...arg) { r1(x), r1(arg...); }
}
using namespace Read;
const int maxn = 2e5 + 5;
struct Graph { vector<int> vc[maxn]; int del[maxn]; void add(int u,int v) { vc[u].push_back(v); } }G;
int n, m, deg[maxn]; int st[maxn], ed(0);
void dfs(int p) { for(int &i = G.del[p]; i < (int)G.vc[p].size(); ) { int to = G.vc[p][i]; ++ i; dfs(to); } st[++ ed] = p; }
signed main() { int i, j; r1(n, m); for(i = 1; i <= m; ++ i) { int u, v; r1(u, v), G.add(u, v); -- deg[v], ++ deg[u]; } int sum(0), ps(0); for(i = 1; i <= n; ++ i) { if(deg[i] == 1) ++ sum, ps = i; else if(deg[i] == -1) ++ sum; else if(deg[i] != 0) return puts("No"), 0; } if(sum > 2) return puts("No"), 0; if(ps == 0) ps = 1; for(i = 1; i <= n; ++ i) sort(G.vc[i].begin(), G.vc[i].end()); dfs(ps); if(ed - 1 != m) return puts("No"), 0; for(i = ed; i >= 1; -- i) printf("%d%c", st[i], " \n"[i == 1]); return 0; }
}
signed main() { return Legendgod::main(), 0; }
|
拆点成边
例题1
求一个最短的字符串,使得其包含所有的 $n$ 位 $k$ 进制数。
最好的方法就是考虑每次增加一个字符,但是显然直接来做是不方便的。
可能会考虑对于每一个 $k$ 进制数,其能到达的数连边加边权,边权是需要的字符数。
这样转换只能用 $2^x$ 指数级暴力,状压来写。
我们考虑将每个 $n$ 位数,拆分成两个 $n - 1$ 位的数。 $(a_1a_2a_3a_4a_5\dots a_{n - 1})_k \to (a_2 a_3 a_4 a_5 a_6 \dots a_n)_k$ 进行连边。
这样拆出来的每条边都需要经过恰好一次,每次正好是增加一个字符,可以保证答案最小。
如何保证有解?
显然对于最终的一个字符串肯定能拆分成若干个上述形式的 $n - 1$ 位的数。
因为字符串长度是最小的,所以两个 $n - 1$ 位的数之间的边不会重复经过。
别忘了是欧拉路径,所以经过多次同一个点是合法的。
更好的理解
考虑最终答案字符串的更新过程,也就是每次选取当前区间的 $n$ 的字符进行匹配。
那么显然对于上一个区间更新到这个区间本质上就是$\large\color{red}\text{去掉上一个字符,加上当前的字符}$。
显然对于一个状态我们并不需要长度为 $n$ 的区间,而是长度为 $n- 1$ 的区间。
那么相同的部分就是中间的 $n - 2$ 个字符,那么上述的东西可以看做一个状态的转移,从一个 $n - 1$ 长度的区间转移到另外一个长度为 $n - 1$ 的区间。总共有多少个可以转移的区间呢?
对于每一个长度为 $n$ 的数,正好意味着这样的转移。
之后直接进行跑欧拉回路即可。
例题2
设一种拆分为对于一个字符串取出其所有长度为 $k$ 连续子串。
给定 $n$ 个长度为 $k$ 的字符串,问能否复原一个长度为 $n + k - 1$ 的字符串,构造任意方案即可。
考虑一下直接将字符串作为点,还是一个哈密顿回路。
那么看到这个问题我们不妨将一个长度为 $k$ 的字符串变成两个长度为 $k - 1$ 的字符串进行连边。
同样也可以考虑最终字符串的转移,对于每个长度为 $k$ 的区间可以拆分成两个长度为 $k - 1$ 的区间。
每个字符串意味着这样的转移。
进行欧拉路径的处理即可。
Best 定理
限制
$\huge\color{red}\text{必须是欧拉图}$!!!
前置:矩阵树定理
求一张图的生成树个数。
设 $D(G)$ 是度数矩阵,$A(G)$ 是邻接矩阵,拉普拉斯矩阵 $L(G) = D(G) - A(G)$。
重边也可以计算,但是自环不计邻接矩阵但是计 入度数。
然后随意去掉行号和列号相同的一行一列,得到的就是 $n - 1$ 阶主子式,求出其行列式就是方案数。
有向图
其实上面那个东西的行列式其实就是 $\sum_{T \in Tree} \prod_{w \in Edge} w$。
也就是每个生成树的边权的乘积。
所以如果要求上面的那个东西,将邻接矩阵 $A_{i, j}$ 定义成 $i \to j$ 的边权即可。
具体部分
如果图 $G$ 是有向欧拉图,那么 $G$ 不同的欧拉回路总数 $ec(G)$ 是:
$$
ec(G) = t^{root}(G, k) \prod_{v \in V}(deg(v) - 1) !
$$
其中 $t^{root}(G, k)$ 表示根向树形图的方案数,其中 $k$ 是任意的根。
本质上对于任意的根其欧拉回路的总数都是相同的。
因为是欧拉回路,所以对于一个点的入度和出度是相同的。
无向图是 $\tt NPC$ 问题。
一些细节
- 图是否联通。
- 欧拉回路本质上就是若干个环构成,如果说环的顺序不同得到的方案也不同,那么还要乘上 $deg_{rt}$。
具体来说就是考虑每个点第 $i$ 次经过他的时候的贡献是 $deg_u’ - i + 1$,其中 $deg_u’ = deg_u - 1$ 因为每个欧拉回路的最后一条边都可以作为内向生成树的一条边,$deg_u’$ 本质上就是除了内向生成树边以外的所有边。
对于根的情况我们特判因为其没有这样的边,也就是 $deg’_u = deg_u$,所以最后计算贡献的时候少算了一个 $deg_u$ 我们最后乘上即可。
例题
Which Dreamed It
就是板子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
| #include <bits/stdc++.h> using namespace std;
#define Getmod
#ifdef Fread char buf[1 << 21], *iS, *iT; #define gc() (iS == iT ? (iT = (iS = buf) + fread (buf, 1, 1 << 21, stdin), (iS == iT ? EOF : *iS ++)) : *iS ++) #define getchar gc #endif
template <typename T> void r1(T &x) { x = 0; char c(getchar()); int f(1); for(; c < '0' || c > '9'; c = getchar()) if(c == '-') f = -1; for(; '0' <= c && c <= '9';c = getchar()) x = (x * 10) + (c ^ 48); x *= f; }
template <typename T,typename... Args> inline void r1(T& t, Args&... args) { r1(t); r1(args...); }
#ifdef Getmod const int mod = 1000003; template <int mod> struct typemod { int z; typemod(int a = 0) : z(a) {} inline int inc(int a,int b) const {return a += b - mod, a + ((a >> 31) & mod);} inline int dec(int a,int b) const {return a -= b, a + ((a >> 31) & mod);} inline int mul(int a,int b) const {return 1ll * a * b % mod;} typemod<mod> operator + (const typemod<mod> &x) const {return typemod(inc(z, x.z));} typemod<mod> operator - (const typemod<mod> &x) const {return typemod(dec(z, x.z));} typemod<mod> operator * (const typemod<mod> &x) const {return typemod(mul(z, x.z));} typemod<mod>& operator += (const typemod<mod> &x) {*this = *this + x; return *this;} typemod<mod>& operator -= (const typemod<mod> &x) {*this = *this - x; return *this;} typemod<mod>& operator *= (const typemod<mod> &x) {*this = *this * x; return *this;} int operator == (const typemod<mod> &x) const {return x.z == z;} int operator != (const typemod<mod> &x) const {return x.z != z;} }; typedef typemod<mod> Tm; #endif
const int maxn = 1e2 + 5; const int maxm = 2e5 + 5;
Tm fac[maxm]; Tm a[maxn][maxn]; int deg[maxn]; Tm ksm(Tm x,int mi) { Tm res(1); while(mi) { if(mi & 1) res *= x; mi >>= 1; x *= x; } return res; }
void init(int x) { fac[0] = 1; for(int i = 1; i <= x; ++ i) fac[i] = fac[i - 1] * i; } int n; Tm Guess() { Tm res(1), c(mod - 1); for(int i = 1; i <= n - 1; ++ i) { int pos(i); for(int j = i; j <= n - 1; ++ j) if(a[j][i].z > 0) { pos = j; break; } if(pos != i) swap(a[pos], a[i]), res *= c; Tm inv = ksm(a[i][i], mod - 2); res *= a[i][i]; for(int j = 1; j <= n - 1; ++ j) a[i][j] *= inv; for(int j = i + 1; j <= n - 1; ++ j) { for(int k = n - 1; k >= i; -- k) { a[j][k] -= a[i][k] * a[j][i]; } } }
return res; }
signed main() {
int i, j; init(2e5); while(scanf("%d", &n) != EOF) { if(n == 0) break; for(i = 1; i <= n; ++ i) for(j = 1; j <= n; ++ j) a[i][j] = 0; for(i = 1; i <= n; ++ i) { int s; r1(s); deg[i] = s; for(j = 1; j <= s; ++ j) { int x; r1(x); if(x != i) a[i][x] -= Tm(1), a[i][i].z ++; } } if(n == 1) { printf("%d\n", fac[deg[1]].z); continue; } Tm ans = Guess(); for(i = 1; i <= n; ++ i) if(deg[i] > 0) ans *= fac[deg[i] - 1]; else ans = 0; ans *= deg[1]; printf("%d\n", ans.z); } return 0; }
|
AGC051D C4
这是一个无向图的欧拉回路计数,这个是 $\tt NPC$ 的,但是我们发现因为其满足欧拉回路的基本性质,也就是意味着如果钦定任意一条边比如 $u \to v$ 的个数,就可以推出所有边的限制。
之后直接进行 $\tt best$ 定理的计算了。
但是这里不同的就是,这里每条边是本质相同的,我们就不能直接使用排列,我们使用组合即可。
对于去除第一条边不能使用的情况,我们直接除以其方案数即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
| #include <bits/stdc++.h> using namespace std;
#define Getmod
#ifdef Fread char buf[1 << 21], *iS, *iT; #define gc() (iS == iT ? (iT = (iS = buf) + fread (buf, 1, 1 << 21, stdin), (iS == iT ? EOF : *iS ++)) : *iS ++) #define getchar gc #endif
template <typename T> void r1(T &x) { x = 0; char c(getchar()); int f(1); for(; c < '0' || c > '9'; c = getchar()) if(c == '-') f = -1; for(; '0' <= c && c <= '9';c = getchar()) x = (x * 10) + (c ^ 48); x *= f; }
template <typename T,typename... Args> inline void r1(T& t, Args&... args) { r1(t); r1(args...); }
#ifdef Getmod const int mod = 998244353; template <int mod> struct typemod { int z; typemod(int a = 0) : z(a) {} inline int inc(int a,int b) const {return a += b - mod, a + ((a >> 31) & mod);} inline int dec(int a,int b) const {return a -= b, a + ((a >> 31) & mod);} inline int mul(int a,int b) const {return 1ll * a * b % mod;} typemod<mod> operator + (const typemod<mod> &x) const {return typemod(inc(z, x.z));} typemod<mod> operator - (const typemod<mod> &x) const {return typemod(dec(z, x.z));} typemod<mod> operator * (const typemod<mod> &x) const {return typemod(mul(z, x.z));} typemod<mod>& operator += (const typemod<mod> &x) {*this = *this + x; return *this;} typemod<mod>& operator -= (const typemod<mod> &x) {*this = *this - x; return *this;} typemod<mod>& operator *= (const typemod<mod> &x) {*this = *this * x; return *this;} int operator == (const typemod<mod> &x) const {return x.z == z;} int operator != (const typemod<mod> &x) const {return x.z != z;} }; typedef typemod<mod> Tm; #endif
const int maxn = 1e6 + 5; const int maxm = 4;
Tm fac[maxn], inv[maxn], iv[maxn];
Tm ksm(Tm x,int mi) { Tm res(1); while(mi) { if(mi & 1) res *= x; mi >>= 1; x *= x; } return res; }
void init(int x = 1e6) { fac[0] = 1; for(int i = 1; i <= x; ++ i) fac[i] = fac[i - 1] * i; inv[x] = ksm(fac[x], mod - 2); for(int i = x - 1; i >= 0; -- i) inv[i] = inv[i + 1] * (i + 1); iv[0] = 0; for(int i = 1; i <= x; ++ i) iv[i] = inv[i] * fac[i - 1]; }
int a, b, c, d;
Tm C(int a,int b) { return a < b ? 0 : fac[a] * inv[b] * inv[a - b]; }
signed main() {
int i, j; init(); Tm ans(0); r1(a, b, c, d); if((a & 1) == (b & 1) && (b & 1) == (c & 1) && (c & 1) == (d & 1)) { for(int ia = 0; ia <= a; ++ ia) { int ib = (b - a) / 2 + ia; int ic = (c - b) / 2 + ib; int id = (d - c) / 2 + ic; if(ib < 0 || ic < 0 || id < 0 || ib > b || ic > c || id > d) continue;
int A1 = 1ll * (ia + b - ib) * (ib + c - ic) % mod * (ic + d - id) % mod; int C1 = 1ll * (ia + b - ib) * (ic) % mod * (ic - c) % mod; int B1 = 1ll * (ib) * (ib - b) % mod * (ic + d - id) % mod; Tm Del = ((A1 + B1 + C1) % mod + mod) % mod;
Tm T1 = iv[ia + b - ib] * iv[ib + c - ic] * iv[ic + d - id]; Tm T2 = C(ia + b - ib, ia) * C(ib + c - ic, ib) * C(ic + d - id, ic) * C(id + a - ia, id); ans += T1 * T2 * Del; } } printf("%d\n", ans.z); return 0; }
|