专栏名称: Log_x
目录
相关文章推荐
会计雅苑  ·  典型会计案例研究(2024年汇编) ·  昨天  
曾奇峰心理工作室  ·  曾爸付妈,揭秘2025如何养成“吸金体质”, ... ·  5 天前  
曾奇峰心理工作室  ·  曾爸付妈,揭秘2025如何养成“吸金体质”, ... ·  5 天前  
51好读  ›  专栏  ›  Log_x

洛谷P4708 画画

Log_x  · CSDN  ·  · 2020-02-08 12:17

正文

Address

洛谷P4708

Problem

n ( n 50 ) 个点无标号的每个连通块都有欧拉回路的图的个数。
答案对 998244353 取模。

Solution

  • 每个连通块都有欧拉回路即为每个点的度数都为偶数。
  • 为方便起见,先把无标号转成有标号。
  • 定义两个图同构当且仅当存在一种点置换,使得其中一个图在点置换作用下得到的新图和另一个图的连边情况完全相同。
  • 考虑 burnside 引理
    L = 1 G g G F g
  • 因为所有点置换得到的边置换也是一个群,且点置换和得到的边置换构成双射,所以我们可以通过枚举点置换来计算边置换下的不动点个数,且边置换下的不动点个数也恰好满足我们对图同构的定义。
  • 此时群 G 的大小为 n ! ,恰好对应所有的排列。
  • 把某一种点置换 g 分解成若干个轮换的乘积,注意到如果轮换大小构成的多重集相同,对答案产生的贡献也相同。
  • 因此我们只需要枚举轮换大小构成的多重集,设 H 表示给定轮换大小的多重集时不动点的个数,则:
    L = 1 n ! l 1 l 2 . . . l k l 1 + l 2 + . . . + l k = n n ! n i = 1 ( l i 1 ) ! k i = 1 l i ! n i = 1 c i ! H ( l 1 , l 2 , . . . , l k ) = l 1 l 2 . . . l k l 1 + l 2 + . . . + l k = n 1 k i = 1 l i n i = 1 c i ! H ( l 1 , l 2 , . . . , l k )
  • 第一行 H 前面的系数表示轮换大小分别为 l 1 , l 2 , . . . , l k 的排列个数,其中 c i 表示 l 1 , l 2 , . . . , l k i 的出现次数。
  • 具体地,排列个数的计算可以这样考虑:
    1. 枚举所有排列 ( n ! ),先不考虑每个轮换内部的构成,即除以 k i = 1 l i !
    2. 每个轮换可以看做一个有向环, l i 个点构成有向环的方案数是 ( l i 1 ) ! ,所以要乘以 k i = 1 ( l i 1 ) !
    3. 大小相同的轮换之间是无序的,所以要除以 n i = 1 c i !
  • 考虑怎样计算 H ( l 1 , l 2 , . . . , l k )
  • 对于第 i 个轮换,轮换内距离相同的所有点对的连边情况相同,而距离共有 l i 2 种,总的连边情况共有 2 l i 2 种。
    • l i 为奇数,固定距离 x 时,每个点都恰好可以引出两条边,因此对于每一种连边情况都能保证轮换内每个点度数的奇偶性不变。
    • l i 为偶数,
      • x < l i 2 时,情况和 l i 为奇数相同。
      • x = l i 2 时,每个点只能恰好引出一条边,因此每一种连边情况都恰好能使轮换内每个点度数的奇偶性改变。
  • 考虑任意两个轮换 i , j 之间的连边情况,枚举两个轮换上的点对 ( x , y ) ,则将 ( x , y ) 同时顺次移动得到的所有点对和 ( x , y ) 的连边情况相同。
  • 因此共有 l i l j l c m ( l i , l j ) = gcd ( l i , l j ) 组点对,每组点对的连边情况相同,且对于第 i 个轮换上的每一点都恰好可以引出 e 1 = l j gcd ( l i , l j ) 条边,对于第 j 个轮换上的每一点都恰好可以引出 e 2 = l i gcd ( l i , l j ) 条边。
    • e 1 e 2 都为偶数,任意一种连边情况都不会改变轮换 i , j 上每个点度数的奇偶性;
    • e 1 e 2 都为奇数,任意一种连边情况都恰好会使轮换 i , j 上每个点度数的奇偶性改变;
    • e 1 e 2 中恰好有一个为奇数,任意一种连边情况都恰好会使某一个轮换上每个点度数的奇偶性改变。
  • 经过这些讨论,我们发现所有能改变奇偶性的操作都是对于整个轮换,因此我们可以把每个轮换缩成一个点,先将不会影响奇偶性的操作的方案数乘到答案上,把问题简化为:

给定 k 个点和若干条边,每个点的初始点权为 0 ,每个点 x 可以有 c n t p x 次机会使点权异或 1 ,每条边 ( x , y ) 可以有 c n t e ( x , y ) 次机会使 x , y 两个端点的点权同时异或 1 ,询问使得每个点最终的点权仍然都为 0 的方案数。

  • 依次考虑每个连通块的方案,将答案累乘。
  • 对于每个连通块,任意找出一棵 DFS树 ,注意到如果我们确定了所有非树边和点的方案,树边的方案是唯一确定的,并且存在合法的方案当且仅当单点的操作次数和为偶数。
  • 证明:
    1. 单点的操作使得连通块中恰好有偶数个点权为 1 的点;
    2. 非树边的操作不会改变连通块中点权为 1 的点的个数的奇偶性;
    3. DFS树 的叶子结点开始往上推出每条树边的状态,对一个点 x 指向其父结点的边操作当且仅当 x 的点权为 1 ,并且每次只能使点权为 1 的点的个数减少 2 或不变,因此树边的方案唯一确定且合法。
  • 于是一个点数为 s 的连通块的方案为 2 max { ( c n t p x ) 1 , 0 } + ( c n t e ( x , y ) ) s + 1
  • p n 表示 n 的拆分数,时间复杂度 O ( n 2 p n ) ,可以通过。

Code

#include <bits/stdc++.h>

const int N = 55;
const int M = 125e3 + 5;
const int mod = 998244353;

int n, m, ans;
int fra[N], inv_fra[N], inv[N];
int ex[M], g[N][N], l[N], c[N];
int sze[N], fa[N], cnt_p[N], cnt_e[N];

template <class T>
inline T Max(T x, T y) {return x > y ? x : y;}

inline void add(int &x, int y)
{
	x += y;
	x >= mod ? x -= mod : 0;
}

inline void mul(int &x, int y)
{
	x = 1ll * x * y % mod;
}

inline int quick_pow(int x, int k)
{
	int res = 1;
	while (k)
	{
		if (k & 1)
			res = 1ll * res * x % mod;
		x = 1ll * x * x % mod;
		k >>= 1;
	}
	return res;
}

inline int ufs_find(int x)
{
	return fa[x] == x ? x : fa[x] = ufs_find(fa[x]);
}

inline void ufs_merge(int x, int y, int e)
{
	int tx = ufs_find(x),
		ty = ufs_find(y);
	if (tx != ty)
	{
		if (sze[tx] > sze[ty])
			std::swap(tx, ty);
		fa[tx] = ty;
		sze[ty] += sze[tx];
		cnt_p[ty] += cnt_p[tx];
		cnt_e[ty] += cnt_e[tx] + e;
	}
	else
		cnt_e[ty] += e;
}

inline void solve(int k)
{
	int res = 1;
	for (int i = 1; i <= k; ++i)
		mul(res, inv[l[i]]);
	for (int i = 1; i <= n; ++i)
		mul(res, inv_fra[c[i]]);
	for (int i = 1; i <= k; ++i)
	{
		fa[i] = i;
		cnt_e[i] = 0;
		cnt_p[i] = 0;
		sze[i] = 1;
	}
	for (int i = 1; i <= k; ++i)
	{
		if (!(l[i] & 1))
			++cnt_p[i];
		mul(res, ex[l[i] - 1 >> 1]);
	}
	for (int i = 1; i < k; ++i)
		for (int j = i + 1; j <= k; ++j)
		{
			int e = g[l[i]][l[j]];
			bool e1 = (l[j] / e) & 1,
				 e2 = (l[i] / e) & 1;
			if (e1 && e2)
				ufs_merge(i, j, e);
			else if (e1)
				cnt_p[ufs_find(i)] += e;
			else if (e2)
				cnt_p[ufs_find(j)] += e;
			else 
				mul(res, ex[e]);
		}
	for (int i = 1; i <= k; ++i)
		if (ufs_find(i) == i)
			mul(res, ex[Max(0, cnt_p[i] - 1) + cnt_e[i] - sze[i] + 1]);
	add(ans, res);
}

inline void dfs(int k, int s, int t)
{
	if (!s)
		return solve(k - 1);
	for (int i = t; i <= s; ++i)
	{
		l[k] = i;
		++c[i];
		dfs(k + 1, s - i, i);
		--c[i];
	}
}

int main()
{
	scanf("%d", &n);
	fra[0] = 1;
	for (int i = 1; i <= n; ++i)
		fra[i] = 1ll * fra[i - 1] * i % mod;
	inv_fra[n] = quick_pow(fra[n], mod - 2);
	for (int i = n; i >= 1; --i)
		inv_fra[i - 1] = 1ll * inv_fra[i] * i % mod;
	inv[0] = inv[1] = 1;
	for (int i = 2; i <= n; ++i)
		inv[i] = 1ll * (mod - mod / i) * inv[mod % i] % mod;
	ex[0] = 1;
	for (int i = 1, im = n * n * n; i <= im; ++i)
		add(ex[i] = ex[i - 1], ex[i - 1]);
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= n; ++j)
			g[i][j] = std::__gcd(i, j);
	dfs(1, n, 1);
	printf("%d\n", ans);
}
  • 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
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135