洛谷P5684 非回文串 题解 组合数学的另一种解法
题目链接:https://www.luogu.com.cn/problem/P5684
原先的解法见:https://www.cnblogs.com/quanjun/p/12396279.html
其实只是换了另外一种思考方式,不过我个人感觉这个比较好理解囧。
题目描述
Alice 有 \(n\) 个字符,它们都是英文小写字母,从 \(1 \sim n\) 编号,分别为 \(c_1,c_2, \cdots , c_n\) 。
Bob 准备将这些字符重新排列,组成一个字符串 \(S\)。Bob 知道 Alice 有强迫症,所以他打算将 \(S\) 组成一个非回文串来折磨 Alice。
现在 Bob 想知道他共有多少种不同的排列字符的方案,能使得 \(S\) 是个非回文串。一种排列字符的方案指的是一个 \(1 \sim n\) 的排列 \(p_i\) ,它所组成的 \(S = c_{p_1}c_{p_2} \cdots c_{p_n}\) 。
一个字符串是非回文串,当且仅当它的逆序串与原串不同。例如 \(abcda\) 的逆序串为 \(adcba\),与原串不同,故 \(abcda\) 是非回文串。而 \(abcba\) 的逆序串与原串相同,是回文串。
由于最后的结果可能很大,你只需要告诉 Bob 总方案数对 \(10^9+7\) 取模后的值。
解题思路
首先判断字符串 \(S\) 能否组成回文串。
判断方法是开一个 \(cnt[]\) 数组,\(cnt[0]\) 用于记录字符 '\(a\)' 出现的次数,\(cnt[1]\) 用于记录字符 '\(b\)' 出现的次数,……,\(cnt[25]\) 用于记录字符 '\(z\)' 出现的次数。
如果 \(cnt[0]\) 到 \(cnt[25]\) 中奇数的个数大于 \(1\) ,那么 \(S\) 没有办法构成回文串。
此时,字符串 \(S\) 的任意排列都是非回文串,答案为 \(A_{n}^{n} = n!\)。
在 \(S\) 可以组成回文串的前提下,我们可以计算有多少种方案可以构成回文串。然后拿总的方案数 \(n!\) 减去回文串的方案数就是我们的答案了。
那么如何计算回文串的排列方案数呢?
首先我们只需要确定前 \(\lfloor \frac{n}{2} \rfloor\) 个数的排列,那么因为回文串是对称的,后 \(\lceil \frac{n}{2} \rceil\) 个数在哪些位置应该放 '\(a\)',哪些位置应该放 '\(b\)',……,哪些位置应该放 '\(z\)'也就确定了。
其次,为了凑这 \(\lfloor \frac{n}{2} \rfloor\) 个数,我们需要:
- 选出 \(\lfloor \frac{cnt[0]}{2} \rfloor\) 个 '\(a\)' \(\Rightarrow\) 对应的方案数为 \(C_{cnt[0]}^{\lfloor \frac{cnt[0]}{2} \rfloor}\);
- 选出 \(\lfloor \frac{cnt[1]}{2} \rfloor\) 个 '\(b\)' \(\Rightarrow\) 对应的方案数为 \(C_{cnt[1]}^{\lfloor \frac{cnt[1]}{2} \rfloor}\);
- 选出 \(\lfloor \frac{cnt[2]}{2} \rfloor\) 个 '\(c\)' \(\Rightarrow\) 对应的方案数为 \(C_{cnt[2]}^{\lfloor \frac{cnt[2]}{2} \rfloor}\);
- ……
- 选出 \(\lfloor \frac{cnt[25]}{2} \rfloor\) 个 '\(z\)' \(\Rightarrow\) 对应的方案数为 \(C_{cnt[25]}^{\lfloor \frac{cnt[25]}{2} \rfloor}\)。
前 \(\lfloor \frac{n}{2} \rfloor\) 个数的排列数为 \(\lfloor \frac{n}{2} \rfloor !\)
对于第 \(i\) 个字符,虽然它在后半部分放的位置确定了,但是这 \(\lceil \frac{cnt[i]}{2} \rceil\) 个元素彼此之间的先后顺序是没有确定的,所以还需要考虑第 \(i\) 个字符在后半部分回文串中的排列方案数 \(\lceil \frac{cnt[i]}{2} \rceil !\)
综上所述,总的方案数为
一些说明
因为代码中涉及到除法取模,所以需要用到扩展欧几里得算法来实现求逆, 代码实现中的 gcd 函数是扩展欧几里得,inv 函数用于求解 a 在模 n 条件下的逆。
实现代码如下:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2020;
const long long MOD = 1000000007LL;
typedef long long ll;
void gcd(ll a , ll b , ll &d , ll &x , ll &y) {
if(!b) {d = a; x = 1; y = 0;}
else { gcd(b , a%b,d,y , x); y -= x * (a/b); }
}
ll inv(ll a , ll n) {
ll d , x , y;
gcd(a , n , d, x , y);
return d == 1 ? (x+n)%n : -1;
}
ll f[maxn];
int n, cnt[26];
string s;
void init() {
f[0] = 1;
for (int i = 1; i < maxn; i ++) f[i] = f[i-1] * i % MOD;
}
ll C(int n, int m) {
ll res = f[n];
res = res * inv(f[n-m], MOD) % MOD;
res = res * inv(f[m], MOD) % MOD;
return res;
}
int main() {
init();
cin >> n >> s;
for (int i = 0; i < n; i ++) cnt[s[i] - 'a'] ++;
int cc = 0;
for (int i = 0; i < 26; i ++) if (cnt[i]%2) cc ++;
if (cc > 1) {
cout << f[n] << endl;
return 0;
}
ll tmp = f[n/2];
for (int i = 0; i < 26; i ++) if (cnt[i]) {
tmp = tmp * C(cnt[i], cnt[i]/2) % MOD;
tmp = tmp * f[cnt[i] - cnt[i]/2] % MOD;
}
cout << (f[n] - tmp + MOD) % MOD << endl;
return 0;
}