洛谷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 !\)

综上所述,总的方案数为

\[n! - \lfloor \frac{n}{2} \rceil ! \times \prod_{i=0}^{25} C_{cnt[i]}^{\lfloor \frac{cnt[i]}{2} \rfloor} \cdot \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;
}
posted @ 2020-03-03 15:20  quanjun  阅读(502)  评论(0编辑  收藏  举报