[组合数学题单][LibreOJ NOI Round #2]不等关系

某道题的弱化版(从树上搬到链上面去了),但是即使是弱化版也强得离谱......

\[\color{red}{\textsf{小游者,真神人也,左马桶,右永神,会执利笔破邪炁,何人当之?}} \\ \begin{array}{|} \hline \color{pink}{\text{A small swimmer is a God.}} \\ \color{pink}{\text{The left toilet and the right eternal God}} \\ \color{pink}{\text{can break the evil energy with a sharp pen.}} \\ \color{pink}{\text{Who can resist him? }} \\ \hline \end{array} \\ \begin{array}{|} \hline \color{green}{\text{小遊者は、神であり、左便器、右永神であり}} \\ \color{green}{\text{鋭いペンを持って真実を突き刺している。誰が彼に抵抗できるだろうか? }} \\ \hline \end{array} \\ \begin{array}{|} \hline \color{lightblue}{\text{Petit voyageur, est Dieu aussi, toilettes gauche, Dieu éternel droit,}} \\ \color{lightblue}{\text{peut tenir un stylo tranchant pour briser le mal, qui devrait le faire?}} \\ \hline \end{array} \\ \begin{array}{|} \hline \color{purple}{\text{Der Direktor ist wirklich ein Gott}} \\ \color{purple}{\text{mit einer Toilette links und Yongshen rechts}} \\ \color{purple}{\text{der einen spitzen Stift hält}} \\ \color{purple}{\text{um die Wahrheit zu durchdringen.}} \\ \color{purple}{\text{Wer kann ihm widerstehen? }} \\ \hline \end{array} \\ \begin{array}{|} \hline \color{cyan}{\text{Ein kleiner Schwimmer ist ein Gott.}} \\ \color{cyan}{\text{Die linke Toilette und der rechte ewige Gott können }} \\ \color{cyan}{\text{die böse Energie mit einem scharfen Stift brechen.}} \\ \color{cyan}{\text{Wer sollte es sein?}} \\ \hline \end{array} \\ \color{red}{\textsf{对曰:“无人,狗欲当之,还请赐教!”}} \\ \newcommand\brak[1]{\left({#1}\right)} \newcommand\Brak[1]{\left\{{#1}\right\}} \newcommand\d[0]{\text{d}} \newcommand\string[2]{\genfrac{\{}{\}}{0pt}{}{#1}{#2}} \newcommand\down[2]{{#1}^{\underline{#2}}} \newcommand\ddiv[2]{\left\lfloor\frac{#1}{#2}\right\rfloor} \newcommand\udiv[2]{\left\lceil\frac{#1}{#2}\right\rceil} \newcommand\lcm[0]{\operatorname{lcm}} \newcommand\set[1]{\left\{{#1}\right\}} \newcommand\ceil[1]{\left\lceil{#1}\right\rceil} \newcommand\floor[1]{\left\lfloor{#1}\right\rfloor} \newcommand\rhs[1]{\;\text{Rhs}\;#1} \newcommand\lhs[1]{\;\text{Lhs}\;#1} \newcommand\Vec[1]{\vec{\mathbf{#1}}} \newcommand\rank[0]{\text{rank}} \newcommand\group[1]{\left\langle\right\rangle} \newcommand\norm[1]{\left|{#1}\right|} \]

  不知道以前写的都是些什么,把这个文章大改特改了一番,于是有了现在这个东西。

一个部分分算法

  对于 \(n\) 比较小的情况,我们可以使用类似排列 DP 的方法,记 \(f(i,j)\) 表示前 \(i\) 个位置,填入了 \([1,i]\) 的数,且最后一个位置是 \(j\) 的方案数。

  如果当前位置要填入一个比 \(j\) 小的,假设填入的这个数为 \(x\in [1,j]\),那么,我们让前 \(i\) 个位置的 \([1,i]\) 的数字中 \([j,i]\) 的数字给 \(x\) 挪个位置 —— 所有 \([j,i]\) 的数字都 \(+1\),不难发现,这样整体增加 \(1\) 之后,并不会违反前面位置的大小关系。

  这个算法的复杂度是 \(\mathcal O(n^3)\),用前缀和优化可以到达 \(\mathcal O(n^2)\),不过再往下也难走了。

  这个算法是基于一般的排列 DP,它无法脱离限制本身(必须拿一维来记录前一个数字填的啥),因此,\(\mathcal O(n^2)\) 的负责度已经和状态复杂度相同,没有办法再进行优化了。

  写这个方法是因为,可能有的时候并不能想到下面的做法,这个做法可以算个次级做法。

另一个算法与正解

  在说正解之前,先说一个部分分,这个算法比上面稍微劣一些,但是更靠近正解 —— 暴力容斥。

  可以对 <> 进行容斥,比如,我们忽略 >,只考虑有 < 的情况,那么我们算的就是某些 > 的位置变成了任意符号,另外某些 > 的位置被填成 < 的方案数。

  更加正式地,设 \(p_i\) 表示不满足第 \(i\)< 的方案数,那么我们算的就是 \(\norm{p_i\cap p_j\cap\cdots}\) 的方案数,但是,目标是计算 \(\displaystyle \norm{\bigcap _{i=1}^m\bar{p_i}}\),其中 \(m\)< 的数量。

  由容斥原理,不难得到

\[\displaystyle \norm{\bigcap _{i=1}^m\bar{p_i}}=\norm U-\norm{\bigcup_{i=1}^m p_i} \]

  我们需要计算 \(\displaystyle \norm{\bigcup_{i=1}^m p_i}\),由于这个并集不是这么容易,考虑继续使用容斥原理转成交集:

\[\displaystyle \norm{\bigcup_{i=1}^m p_i}=\sum_i \norm{p_i}-\sum_{i<j}\norm{p_i\cap p_j}+\sum_{i<j<k}\norm{p_i\cap p_i\cap p_k}-\cdots+(-1)^{m-1}\norm {p_1\cap p_2\cdots\cap p_m} \]

  考虑交集的具体意义:有些位置强制填入 <,有些位置任意填数的方案数 —— 这就相当于将 \(1\sim n\) 分成若干没有联系的上升序列计数,这个很简单吧。

  因此,总复杂度为 \(\mathcal O(2^n\times n)\).

  不过,我们发现这个做法实际上大有前途,它不像最开始的做法,需要结合数列具体填了什么数,而是将他们抽象出来变成了几个上升序列,而我们也不需要使用暴力容斥来计算上面那个柿子。我们只需要计算将原序列划分为若干上升序列的方案数,这显然是可以直接 DP 做的。

  具体地,设 \(f(i)\) 表示满足前 \(i\) 个位置的方案数,记录下所有 > 的位置之后就可以直接转移:

\[f(i)=\sum_{j\in \mathscr P} f(j)\times \frac{1}{(i-j)!}\times (-1)^{cnt(j,i-1)} \]

  其中 \(\mathscr P\) 即所有 > 的位置集合,\(cnt(l,r)\) 表示 \([l,r]\) 中有多少 >. 边界是 \(f(0)=1\),并且令 \(0\in \mathscr P\),最后答案乘上一个 \(n!\).

  这个柿子暴力做的复杂度是 \(\mathcal O(n^2)\) 的,但是还可以进行优化。不妨将上述柿子写得更正式一些:

\[f(i)=\sum_{j=0}^{j<i}[s(j)=\texttt{">"}]f(j)\times \frac{1}{(i-j)!}\times (-1)^{cnt(i-1)-cnt(j)} \]

  其中 \(cnt(i)\) 表示前 \(i\) 个位置的 > 数量,我们尝试将其做一点变化:

\[\begin{aligned} (*)&=(-1)^{cnt(i-1)}\times \sum_{j=0}^{j<i}[s(j)=\texttt{">"}]f(j)\times (-1)^{-cnt(j)}\times \frac{1}{(i-j)!} \end{aligned} \]

  这个就是 \(A(x)=[s(x)=\texttt{">"}]f(x)\times (-1)^{-cnt(x)}\)\(B(x)=\frac{1}{x!}\) 的卷积再多 一个 \((-1)^{cnt(i-1)}\) 了。由于 \(A(x)\)\(f\) 有关,因此只能使用分治 NTT,复杂度 \(\mathcal O(n\log^2 n)\).

参考代码

  这个是 \(\mathcal O(n^2)\) 的。

using namespace Elaina;

const int Maxn = 5000;
const int mod = 998244353;

inline void chkadd(int& x, int y) { if ((x += y) >= mod) x -= mod; }

int fac[Maxn + 5], inv[Maxn + 5], ifac[Maxn + 5];
inline void prelude() {
    fac[0] = fac[1] = inv[0] = inv[1] = ifac[0] = ifac[1] = 1;
    rep (i, 2, Maxn) {
        fac[i] = (int)(1ll * fac[i - 1] * i % mod);
        inv[i] = (int)(1ll * (mod - mod / i) * inv[mod % i] % mod);
        ifac[i] = (int)(1ll * ifac[i - 1] * inv[i] % mod);
    }
}

int f[Maxn + 5], cnt[Maxn + 5], n;
char s[Maxn + 5];

inline void input() {
    std::cin >> (s + 1);
    n = (int)strlen(s + 1);
    s[0] = '>', cnt[0] = 1;
    rep (i, 1, n) cnt[i] = cnt[i - 1] + (s[i] == '>');
}

inline void solve() {
    f[0] = 1;
    rep (i, 1, n + 1) {
        repf (j, 0, i) if (s[j] == '>') {
            int ss = (int)(1ll * f[j] * ifac[i - j] % mod);
            if (cnt[i - 1] - cnt[j] & 1) ss = (mod - ss) % mod;
            chkadd(f[i], ss);
        }
    }
    writln(1ll * f[n + 1] * fac[n + 1] % mod);
}

signed main() {
    std::cin.tie(NULL)->sync_with_stdio(false);
    prelude();
    input();
    solve();
    return 0;
}

  这是 \(\mathcal O(n\log^2 n)\)

/** @author __Elaina__ */

#include <bits/stdc++.h>

#define USING_FREAD
// #define NDEBUG
#include <cassert>

namespace Elaina {
/** その可憐な少女は魔女であり、旅人でした。 ―― そう、私です! */

#define rep(i, l, r) for(register int i = (l), i##_end_ = (r); i <= i##_end_; ++i)
#define repf(i, l, r) for (register int i = (l), i##_end_ = (r); i < i##_end_; ++i)
#define drep(i, l, r) for(register int i = (l), i##_end_ = (r); i >= i##_end_; --i)
#define fi first
#define se second
#define mp(a, b) make_pair(a, b)
#define Endl putchar('\n')
#define whole(v) ((v).begin()), ((v).end())
#define bitcnt(s) (__builtin_popcount(s))
/** @warning no forced type conversion */
#define rqr(x) ((x) * (x))
#define y0 FUCK_UP
#define y1 MOTHER_FUCKER
#define masdf(...) fprintf(stderr, __VA_ARGS__)

typedef long long ll;
typedef unsigned long long ull;
typedef std::pair<int, int> pii;

template<class T> inline T fab(T x) { return x < 0 ? -x : x; }
template<class T> inline void chkmin(T& x, const T& rhs) { x = std::min(x, rhs); }
template<class T> inline void chkmax(T& x, const T& rhs) { x = std::max(x, rhs); }

#ifdef USING_FREAD
inline char qkgetc() {
# define BUFFERSIZE 1 << 20
    static char BUF[BUFFERSIZE], *p1 = BUF, *p2 = BUF;
    return p1 == p2 && (p2 = (p1 = BUF) + fread(BUF, 1, BUFFERSIZE, stdin), p1 == p2) ? EOF : *p1++;
# undef BUFFERSIZE
}
# define CHARRECEI qkgetc()
#else
# define CHARRECEI ((char)getchar())
#endif

template<class T> inline T readret(T x) {
    x = 0; int f = 0; char c;
    while (!isdigit(c = CHARRECEI)) if(c == '-') f = 1;
    for (x = (c ^ 48); isdigit(c = CHARRECEI); x = (x << 1) + (x << 3) + (c ^ 48));
    return f ? -x : x;
}
template<class T> inline void readin(T& x) {
    x = 0; int f = 0; char c;
    while (!isdigit(c = CHARRECEI)) if (c == '-') f = 1;
    for (x = (c ^ 48); isdigit(c = CHARRECEI); x = (x << 1) + (x << 3) + (c ^ 48));
    if (f) x = -x;
}
template<class T, class... Args> inline void readin(T& x, Args&... args) {
    readin(x), readin(args...);
}
template<class T> inline void writln(T x, char c = '\n') {
    if (x < 0) putchar('-'), x = -x;
    static int __stk[55], __bit = 0;
    do __stk[++__bit] = (int)(x % 10), x /= 10; while (x);
    while (__bit) putchar(__stk[__bit--] ^ 48);
    putchar(c);
}
template<class T> inline T listMax(const T& x) { return x; }
template<class T, class... Args> inline T listMax(const T& x, const Args&... args) {
    return std::max(x, listMax(args...));
}
template<class T> inline T listMin(const T& x) { return x; }
template<class T, class... Args> inline T listMin(const T& x, const Args&... args) {
    return std::min(x, listMin(args...));
}

} // namespace Elaina
using namespace Elaina;

const int Maxn = 1e5 * 4; // four times
const int mod = 998244353;

inline int qkpow(int a, int n) {
    int ret = 1;
    for (; n; n >>= 1, a = 1ll * a * a % mod)
        if (n & 1) ret = 1ll * ret * a % mod;
    return ret;
}

namespace _poly {

int G[35];
struct _poly_builtin_init {
    inline _poly_builtin_init() {
        rep (i, 1, 30) G[i] = qkpow(3, mod - 1 >> i);
    }
} _init_func;
int rev[Maxn + 5], n, invn;
inline void init(int len) {
    for (n = 1; n < len; n <<= 1);
    repf (i, 0, n) rev[i] = (rev[i >> 1] >> 1) | ((i & 1)? n >> 1: 0);
    invn = qkpow(n, mod - 2);
}
inline void ntt(std::vector<int>& f, int op = 1) {
    f.resize(n);
    repf (i, 0, n) if (i < rev[i]) std::swap(f[i], f[rev[i]]);
    for (int p = 2, lev = 1; p <= n; p <<= 1, ++lev) {
        int len = p >> 1, w = G[lev];
        for (int k = 0; k < n; k += p) {
            int buf = 1, tmp;
            repf (i, k, k + len) {
                tmp = (int)(1ll * f[i + len] * buf % mod);
                f[i + len] = (f[i] + mod - tmp) % mod;
                f[i] = (f[i] + tmp) % mod;
                buf = (int)(1ll * buf * w % mod);
            }
        }
    }
    if (op != 1) {
        std::reverse(f.begin() + 1, f.end());
        repf (i, 0, n) f[i] = (int)(1ll * f[i] * invn % mod);
    }
}

} // namespace _poly

int fac[Maxn + 5], inv[Maxn + 5], ifac[Maxn + 5];
inline void prelude() {
    fac[0] = fac[1] = inv[0] = inv[1] = ifac[0] = ifac[1] = 1;
    rep (i, 2, Maxn) {
        fac[i] = (int)(1ll * fac[i - 1] * i % mod);
        inv[i] = (int)(1ll * (mod - mod / i) * inv[mod % i] % mod);
        ifac[i] = (int)(1ll * ifac[i - 1] * inv[i] % mod);
    }
}

int n;
char s[Maxn + 5];
int cnt[Maxn + 5];

std::vector<int> f, A, B;
#define sign(i) (((i) & 1)? (-1): (1))
void solve(int l, int r) { // [l, r)
    if (l + 1 == r) {
        if (l == 0) f[l] = 1;
        else f[l] = (mod + 1ll * sign(cnt[l - 1]) * f[l]) % mod;
        return ;
    }
    int mid = l + r >> 1, len = r - l;
    solve(l, mid), _poly::init(len << 1);
    A.clear(), B.clear();
    A.resize(_poly::n), B.resize(_poly::n);
    repf (i, 0, mid - l) {
        if (s[i + l] == '>')
            A[i] = (mod + 1ll * sign(cnt[i + l]) * f[i + l]) % mod;
        else A[i] = 0;
    }
    repf (i, 0, len) B[i] = ifac[i];
    _poly::ntt(A), _poly::ntt(B);
    repf (i, 0, _poly::n) A[i] = (int)(1ll * A[i] * B[i] % mod);
    _poly::ntt(A, -1);
    repf (i, mid - l, r - l) f[l + i] = (f[l + i] + A[i]) % mod;
    solve(mid, r);
}

signed main() {
    std::cin.tie(NULL)->sync_with_stdio(false);
    std::cin >> (s + 1);
    prelude();
    n = strlen(s + 1);
    s[0] = '>', cnt[0] = 1;
    rep (i, 1, n) cnt[i] = cnt[i - 1] + (s[i] == '>');
    _poly::init(n + 2);
    f.resize(_poly::n);
    solve(0, _poly::n);
    writln(1ll * f[n + 1] * fac[n + 1] % mod);
    return 0;
}

/**
 * 
 * @warning
 *  1. pay attention to the size of static arrays;
 *  2. when change the array with type int to long long;
 *    check the calculation of memories(functions like memset,memcpy and so on);
 *  3. whether the function have returned;
 * 
*/
posted @ 2021-02-15 19:46  Arextre  阅读(85)  评论(0编辑  收藏  举报