题解:洛谷 AT_abc470_f Googol Swaps
【题目来源】
洛谷:AT_abc470_f [ABC470F] Googol Swaps
【题目描述】
给定一个由小写英文字母组成的长度为 \(N\) 的字符串 \(S\)。
请计算将下述操作恰好执行 \(10^{100}\) 次后,\(S\) 能变成的不同字符串的个数,并对 \(998244353\) 取模。
- 可以选择一个 \(i\),满足 \(1 \leq i \leq M\),并交换 \(S\) 的第 \(A_i\) 个字符和第 \(B_i\) 个字符的位置。
【输入】
输入通过标准输入给出,格式如下:
\(N\) \(M\)
\(S\)
\(A_1\) \(B_1\)
\(\vdots\)
\(A_M\) \(B_M\)
【输出】
输出答案。
【输入样例】
5 3
miria
1 3
2 5
4 5
【输出样例】
6
【核心思想】
-
问题分析:给定字符串 \(S\) 和 \(M\) 对可交换位置,求恰好执行 \(10^{100}\) 次交换后能得到的字符串数量(模 \(998244353\))。由于 \(10^{100}\) 是极大的偶数,核心观察是:交换操作生成一个置换群,恰好执行 \(10^{100}\) 次等价于在群中取 \(10^{100}\) 次幂。通过并查集找到交换操作生成的连通块,每个连通块内字符可任意重排,但偶数次操作限制了某些排列的可达性。
-
算法选择:
- 并查集:找到由交换操作连接的连通块,每个连通块内的位置可自由交换
- 多重集排列:每个连通块内的字符串排列数为 \(\frac{siz!}{\prod cnt_c!}\)
- 群论分析:若连通块内无重复字符,所有排列的阶为 \(2\)(交换生成对称群 \(S_{siz}\),但 \(10^{100}\) 为偶数,只有偶排列可达,方案数需除以 \(2\))
-
关键步骤:
- 初始化:读取 \(N\)、\(M\)、\(S\),并查集初始化
- 建图合并:读入 \(M\) 对交换位置 \((A_i, B_i)\),用并查集合并
- 预处理阶乘和逆元:\(fac[i] = i! \bmod mod\),\(invfac[i] = (i!)^{-1} \bmod mod\)(费马小定理)
- 按连通块分组:统计每个连通块包含的位置和字符频率
- 计算答案:
- 对每个连通块,计算多重集排列数 \(ways = \frac{siz!}{\prod cnt_c!}\),累乘到 \(ans\)
- 检查连通块内是否有重复字符:若有,\(flag = false\)(存在不动点,偶数次操作不影响计数)
- 若所有连通块内字符均不重复(\(flag = true\)):\(ans = ans \times 2^{-1} \bmod mod\)(只有偶排列可达)
- 输出答案 \(ans\)
-
时间/空间复杂度:
- 时间复杂度:\(O(N \alpha(N) + N \log mod)\),并查集近乎线性,阶乘预处理和快速幂为 \(O(N \log mod)\)
- 空间复杂度:\(O(N)\),并查集、阶乘数组、连通块分组
-
置换群与组合数学的核心思想:
- 连通块独立性:不同连通块的位置互不连通,字符无法跨块交换,总方案数为各连通块方案数的乘积
- 多重集排列:连通块内字符可任意排列,但相同字符不可区分,用多重集排列公式计算
- 偶数次操作的约束:当连通块内无重复字符时,交换操作生成完整的对称群,但 \(10^{100}\) 为偶数意味着只有偶排列(可分解为偶数个对换)可达,方案数减半
- 有重复字符的特殊性:若连通块内有重复字符,存在非恒等排列在偶数次操作后仍可达(因交换相同字符不改变字符串),无需额外除法
- 适用于置换群计数、组合数学、并查集连通性分析类问题
【算法标签】
提高 #排列组合
【代码详解】
#include <bits/stdc++.h>
using namespace std;
#define int long long // 使用long long防止中间计算溢出
const int N = 200005, mod = 998244353; // N:最大字符串长度, mod:模数
int n, m; // n:字符串长度, m:可交换的位置对数
string s; // 原始字符串
int a[N], b[N]; // a[i]/b[i]:第i对可交换的位置(本题中未直接使用数组)
int p[N]; // 并查集父节点数组
int fac[N], invfac[N]; // fac:阶乘数组, invfac:阶乘逆元数组
map<int, vector<int>> groups; // 每个连通块包含的所有位置
// 并查集查找:带路径压缩
int find(int x)
{
if (p[x]!=x) p[x] = find(p[x]);
return p[x];
}
// 并查集合并
void merge(int a, int b)
{
a = find(a), b = find(b);
if (a != b)
p[a] = b;
}
// 快速幂:计算a^b % mod
int qmi(int a, int b)
{
int res = 1;
while (b)
{
if (b&1) res = res * a % mod;
a = a * a % mod;
b >>= 1;
}
return res;
}
signed main()
{
cin >> n >> m >> s; // 读入字符串长度、交换对数和字符串
s = " " + s; // 字符串下标从1开始
// 初始化并查集
for (int i=1; i<=n; i++)
p[i] = i;
// 读入m对可交换位置,并在并查集中合并
for (int i=1; i<=m; i++)
{
int a, b;
cin >> a >> b;
merge(a, b);
}
// 预处理阶乘数组
fac[0] = 1;
for (int i=1; i<=n; i++)
fac[i] = fac[i-1] * i % mod;
// 预处理阶乘逆元数组(费马小定理)
invfac[n] = qmi(fac[n], mod-2);
for (int i=n-1; i>=1; i--)
invfac[i] = invfac[i+1] * (i+1) % mod;
// 按连通块分组:每个根节点对应一个位置向量
for (int i=1; i<=n; i++)
groups[find(i)].push_back(i);
int ans = 1; // 最终答案
bool flag = true; // 标记是否所有连通块内字符都不重复
// 遍历每个连通块,计算该连通块内的排列数
for (auto t : groups)
{
int root = t.first; // 连通块的根节点
vector<int> vec = t.second; // 该连通块包含的所有位置
int siz = vec.size(); // 连通块大小
map<char, int> cnt; // 统计该连通块内每种字符的出现次数
for (auto idx : vec)
cnt[s[idx]]++;
// 计算该连通块内的不同排列数:多重集排列公式 siz! / (c1! * c2! * ...)
int ways = fac[siz];
for (auto t : cnt)
{
char c = t.first;
int cnum = t.second;
ways = ways * invfac[cnum] % mod;
}
ans = ans * ways % mod; // 各连通块独立,答案相乘
// 检查该连通块内是否有重复字符
bool hasDuplicate = false;
for (auto t : cnt)
{
char c = t.first;
int cnum = t.second;
if (cnum>=2)
{
hasDuplicate = true;
break;
}
}
// 如果有重复字符,说明存在不动点(某些排列执行偶数次后回到原串)
if (hasDuplicate)
flag = false;
}
// 如果所有连通块内字符都不重复,则每个排列的阶都是2
// 执行10^100次(偶数次)后,只有恒等排列能回到原串
// 所以方案数需要除以2(乘以2的逆元)
if (flag)
ans = ans * qmi(2, mod-2) % mod;
cout << ans << endl; // 输出最终答案
return 0;
}
【运行结果】
5 3
miria
1 3
2 5
4 5
6
浙公网安备 33010602011771号