【luogu P4708】画画(dfs)(Burnside引理)(分类讨论)(欧拉回路)
画画
题目链接:luogu P4708
题目大意
问你 n 个点的无标号的每个连通块有欧拉回路的图的个数。
思路
你看到无编号,也就代表着可以通过置换把一些有编号的看做同一个。
那两个点之间的边有存在和不存在两种,所以我们考虑用 Burnside 引理:
\(ans=\dfrac{1}{|M|}\sum c(r)\)
其中 \(c(r)\) 为 \(r\) 这个置换中不动点的个数,这道题中 \(|M|=n!\)。
然后你发现这个置换是点的,然而你要统计的 \(c(r)\) 是有关边的。
那你会想到一道叫做有色图的题目。
然后我们假设你已经会了做有色图,然后我们考虑用做那道题的思路来搞这道题。
那你还是考虑 dfs 出点置换的循环,设为 \((a_1,a_2,...)(b_1,b_2,...)...\),然后长度分别为 \(l_1,l_2,...\)。
然后记得每个乘上 \(\dfrac{n!}{\prod l_i\prod c_i!}\),然后 \(n!\) 和外面的 \(\dfrac{1}{|M|}\) 抵消(\(|M|=1\))。
然后你还是分类讨论:
我们搞之前首先要确定一个东西,就是对于一个循环中的边,它们肯定是要么都有这条边,要么都没有。
而且我们再看欧拉回路的性质,就是每个点的度数都是偶数,那我们就要保证你选出来的方案点度数都是偶数。
如果是在同一个循环,那你就像之前一样分奇偶讨论:
- 如果是奇数:
那就是 \(\dfrac{l-1}{2}\) 个循环,然后你会发现这些循环的边要了之后每个点的奇偶度数都不会改变。因为它们都是转一圈,然后两边都会加。
- 如果是偶数:
那就是 \(\dfrac{\binom{l}{2}-\frac{l}{2}}{l}+1\) 个循环,然后前面的 \(\dfrac{\binom{l}{2}-\frac{l}{2}}{l}\) 跟原来一样都是转一圈,所以也是不变。但是右边的 \(1\)(就是两个点的记录是 \(\dfrac{l}{2}\))这个循环跑了一半就每个,那就是每个点都多了 \(1\),度数的奇偶性全改了。
然后如果不是在同一个循环,然后我们来看:
在有色图中我们是 \(\gcd(l_1,l_2)\) 个循环,那我们看看每个循环的样子:
其实你会发现,就对于循环 \(a\) 中,每个点都连出了 \(\dfrac{\gcd(l_1,l_2)}{l_1}\) 条边,然后对于循环 \(b\),就是每个点 \(\dfrac{\gcd(l_1,l_2)}{l_2}\) 条边。
那我们把这两个东西分别设为 \(e_2,e_1\)。
然后我们把这两个东西分奇偶讨论。
- 都是偶数,那你加了之后两个循环的的奇偶每个点都不改。
- 都是偶数,那你加了之后两个循环的的奇偶每个点都改。
- 一个奇数一个偶数,那哪个是奇数,它那边的循环就改了,另一边就没有改。
然后你考虑这些改不改会怎么样。
首先是不改,那它不改那你选也可以不选也行,那就是有多少个方案数就乘上 \(2\) 的它次方。
那接着就是改了的,我们考虑把它一个改奇偶看做是点的一个属性,然后两个点改看做是边的属性。
然后问题就转化成了这个:
你有一些点和一些边,然后每个点有若干次(\(cntp_x\))机会翻转它的属性,每个边有若干次(\(cnte_{x,y}\))机会翻转它连接的两个点的属性,然后问你有多少种方案使得所有点的属性不变。
不难看出每个连通块的答案是独立的,可以分别求之后乘起来。
然后就是考虑怎么求每个连通块的答案:(假设现在的连通块是 \(s\) 大小)
这个弄这个东西方法(个人觉得)非常神奇:
你就随便找一个这个连通块的 dfs 树,然后你在你确定了所有非树边和点的方案,那树边的方案就是唯一确定的(或者没有),然后有的情况当且仅当单点的操作次数和为偶数。
然后证明:(这个的修改值被翻转的次数是奇数)
因为你单点操作保证了只看单点操作的时候有偶数个被修改的点。
然后你非树边操作是不会改被修改点个数的奇偶性。(\(+2,+1-1,-2\))
然后你就可以从 dfs 树的叶子节点开始进行一个树形 DP,每次如果下面的点是被修改的,那你就把用那条树边操作翻转它和它父亲。
然后要么不用操作,要么最后一步就是 \(2\rightarrow 0\),不会出现 DP 到最后只有 \(1\) 是被修改的情况,所以是可以的。
而且显然它这个东西是唯一而且是可以一一对应的。
那我们考虑怎么描写这个统计,那首先是点的选法。
\(2^{\max\{0,cntp_x-1\}}\)
你可以看做是 \(2^{cntp_x}\) 中选的方法,那选中个数是奇数的肯定是其中一半。
当然有个例外就是 \(cntp_x=0\) 的时候方案是是 \(1\),所以要跟 \(0\) 取 \(\max\)。
然后是边的选法,假设你已经确定好一个 dfs 树,那根据我们前面的,非树边随便搞都有一一对应的。
\(2^{\sum cnte_{x,y}-(s-1)}\)
就直接是所有的边减去树上的 \(s-1\) 条边。
所以就是 \(2^{\max\{0,cntp_x-1\}+\sum cnte_{x,y}-(s-1)}\)。
所以答案就是:
代码
#include<cstdio>
#include<iostream>
#define ll long long
#define mo 998244353
using namespace std;
ll n, jc[51], inv[51], inv_q[51];
ll mi[51 * 51 * 51], gcd[51][51];
ll ans, l[51], c[51];
ll fa[51], cntp[51], cnte[51], sz[51];
ll GCD(ll x, ll y) {
if (!y) return x;
return GCD(y, x % y);
}
int find(int now) {
if (fa[now] == now) return now;
return fa[now] = find(fa[now]);
}
void merge(int x, int y) {
int X = find(x), Y = find(y);
if (X == Y) return ;
fa[X] = Y; cntp[Y] += cntp[X]; cnte[Y] += cnte[X]; sz[Y] += sz[X];
}
void slove(int num) {
ll re = 1;
for (int i = 1; i <= num; i++)
re = re * inv[l[i]] % mo;
for (int i = 1; i <= n; i++)
re = re * inv_q[c[i]] % mo;
for (int i = 1; i <= num; i++) {//点的部分
re = re * mi[(l[i] - 1) / 2] % mo;
}
for (int i = 1; i <= num; i++) {
fa[i] = i; sz[i] = 1; cntp[i] = cnte[i] = 0;
}
for (int i = 1; i <= num; i++) {
if (l[i] % 2 == 0) cntp[i]++;
}
for (int i = 1; i <= num; i++)
for (int j = i + 1; j <= num; j++) {
int gcd_ = gcd[l[i]][l[j]];
bool ei = (l[i] / gcd_) & 1, ej = (l[j] / gcd_) & 1;
if (ei && ej) merge(i, j), cnte[find(i)] += gcd_;
else if (ei) cntp[find(j)] += gcd_;
else if (ej) cntp[find(i)] += gcd_;
else re = re * mi[gcd_] % mo;//这里是边的部分
}
for (int i = 1; i <= num; i++)//根据 dfs 树有的统计
if (find(i) == i) re = re * mi[max(0ll, cntp[i] - 1) + cnte[i] - (sz[i] - 1)] % mo;
ans = (ans + re) % mo;
}
void dfs(int num, int els, int L) {
if (!els) {
slove(num);
return ;
}
for (int i = L; i <= els; i++) {
l[++num] = i; c[i]++;
dfs(num, els - i, i);
l[num--] = 0; c[i]--;
}
}
int main() {
scanf("%lld", &n);
jc[0] = 1; for (int i = 1; i <= n; i++) jc[i] = jc[i - 1] * i % mo;
inv[0] = inv[1] = 1; for (int i = 2; i <= n; i++) inv[i] = inv[mo % i] * (mo - mo / i) % mo;
inv_q[0] = 1; for (int i = 1; i <= n; i++) inv_q[i] = inv_q[i - 1] * inv[i] % mo;
mi[0] = 1; for (int i = 1; i <= n * n * n; i++) mi[i] = mi[i - 1] * 2 % mo;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
gcd[i][j] = GCD(i, j);
dfs(0, n, 1);
printf("%lld", ans);
return 0;
}