「NOI2018」冒泡排序(动态规划+组合计数+树状数组)
Address
Solution
显然合法的排列不能出现长度 \(\geq 3\) 的下降子序列。
证明:如果出现了 \(i<j<k\) 且 \(p_i>p_j>p_k\),那么 \(p_i\) 肯定要和 \(p_j\) 交换一次,\(p_j\) 肯定也要和 \(p_k\) 交换一次。这样 \(p_j\) 向前向后各交换了一次,总交换次数肯定 \(>|j-p_j|\)。
到这里已经 \(44pts\) 了,把 \(p_i=i\) 的打个表发现答案是 \(catalan\) 数减 \(1\),就 \(56pts\) 了。然后你把day1T1,day1T3,day2T1都切了,你就进集训队了。
接下来讲正解,先不考虑字典序的限制。
记 \(f[i][j]\) 表示有多少个 \(p_1=j\) 且长度为 \(i\) 的合法排列,边界 \(f[0][0]=1\)。
当 \(p_1=1\) 时,只要 \(p_2\sim p_i\) 中不出现长度 \(\geq 3\) 的下降子序列(合法)即可,那么有 \(f[i][1]=\sum_{j=1}^{i-1}f[i-1][j]\)。注意 \(f[i-1][j]\) 是 \(p_2\sim p_i\) 在这 \(i-1\) 个数中的相对排名。
当 \(p_1\neq 1\) 时,如果 \(p_2>p_1\) 且 \(p_2\sim p_i\) 合法,那么 \(p_1\sim p_i\) 必定合法。因此 \(f[i][j]+=\sum_{k=j}^{i-1}f[i-1][k]\)。
如果 \(p_2<p_1\), 那么 \(p_2\sim p_i\) 中,所有值在 \([1,j-1]\) 中的数一定是升序排列的。考虑构造 \(p_2=j-1\) 且 \(p_2\sim p_i\) 合法的排列,那么这样 \(j,j-1,1\) 会导致 \(p1\sim p_i\) 非法。而且此时必定不存在 \(3\leq x<y\leq i,1\leq p_x\leq p_y\leq j-2\),否则 \(p_2,p_x,p_y\) 会使得 \(p_2\sim p_i\) 非法。所以我们强制让 \(p_2\) 变成 \(1\),把 \(p_3\sim p_i\) 中值在 \([1,j-2]\) 的数都 \(+1\),就使得 \(p_1\sim p_i\) 合法了。因此这部分的方案数为 \(f[i-1][j-1]\)。
综上所述,\(f[i][j]=\sum_{k=j-1}^{i-1}f[i-1][k]\)。
接下来考虑字典序的限制,枚举 \(i\) 表示 \(p_1\sim p_{i-1}\) 和排列 \(q\) 都相等,从 \(p_i\) 开始字典序大于 \(q\),也就是说 \(p_i>q_i\)。
先考虑非法情况:记 \(bo[j]\) 表示 \(q_j\) 是否为前缀最大值,记 \(val[j]\) 表示 \(q_1\sim q_j\) 中满足 \(bo=0\) 的最大的数,若 \(val[i-1]>q_i\sim q_n\) 的最小值,不管 \(p_i\sim p_n\) 怎么填都是非法的。
排除上述非法情况后,一定不存在 \(x<y<z<i,p_x>p_y>p_z\)。而之前预处理出来的 \(f\) 数组已经保证了 \(i\leq x<y<z,p_x>p_y>p_z\),因此只要考虑 \(x<i\leq y<z,p_x>p_y>p_z\) 或 \(x<y<i\leq z,p_x>p_y>p_z\) 的情况。
对合法情况分类讨论:
- \(p_i\) 是前缀最大值,即不存在 \(x<y<i\leq z,p_x>p_y>p_z\) 。此时 \(p_i\) 不管怎么填都是合法的。因为只要不存在 \(i<y<z,p_i>p_y>p_z\),就一定不会出现 \(x<i\leq y<z,p_x>p_y>p_z\)。那么答案为 \(\sum_{j=k+1}^{n-i+1}f[n-i+1][j]\),其中 \(k\) 是 \(q_i\sim q_n\) 中有多少个数小于等于 \(q_1\sim q_i\) 的最大值。
- \(p_i\) 不是前缀最大值,此时 \(p_i\) 必定是后缀最小值,否则必定非法。但是 \(p_i\) 如果是后缀最小值,就不满足 \(p_i>q_i\),所以不存在这种情况。
综上所述,\(p_i\) 一定是前缀最大值,因此对答案的贡献为 \(\sum_{j=k+1}^{n-i+1}f[n-i+1][j]\)。
但是这个 \(dp\) 是 \(O(n^2)\) 的,只有 \(80pts\),加上 \(p_i=i\) 的有 \(84pts\)。
考虑优化,记 \(s[n][m]=\sum_{i=m}^{n}f[n][i]\),即 \(f[n]\) 的后缀和。
边界 \(s[0][0]=1\)。
考虑 \(O(1)\) 求出 \(s[n][m]\)。
考虑 \(s[n][m]\) 的实际意义:从 \((0,0)\) 走 \(n\) 步到 \((n,m)\)。如果当前位于 \((x,y)\),那么下一步可以走到 \((x+1,y+1)\) 或 \((x,y-1)\),不能走到 \(y<0\) 的地方。
先不考虑 \(y<0\),那么有 \(n\) 次要走到 \((x+1,y+1)\),有 \(n-m\) 次要走到 \((x,y-1)\),方案数为 \(C_{2n-m}^{n-m}\)。
考虑减去 \(y<0\) 的方案数:我们把每一步的走法记成一个 \(01\) 序列,如果走 \((x+1,y+1)\),记 \(0\),否则记 \(1\)。显然合法的 \(01\) 序列必定对于任意前缀,\(0\) 的个数 \(\geq\) \(1\) 的个数。对于非法的序列,考虑找到最短的满足 \(0\) 的个数 \(<\) \(1\) 的个数的前缀 \([1,i]\),然后把 \([1,i]\) 的 \(0\) 变 \(1\),\(1\) 变 \(0\),就形成了一个有 \(n-m-1\) 个 \(1\) 和 \(n+1\) 个 \(0\) 的序列。
而对于任意一个有 \(n-m-1\) 个 \(1\) 和 \(n+1\) 个 \(0\) 的序列,我们只要找到最短的满足 \(0\) 的个数 \(>\) \(1\) 的个数的前缀 \([1,i]\),然后把 \([1,i]\) 的 \(0,1\) 取反, 就能变成一个合法序列。也就是说这种 \(01\) 序列和非法序列一一对应,方案数为 \(C_{2n-m}^{n-m-1}\)。
综上所述,\(s[n][m]=C_{2n-m}^{n-m}-C_{2n-m}^{n-m-1}\)。
时间复杂度 \(O(Tn\log n)\)。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
template <class t>
inline void read(t & res)
{
char ch;
while (ch = getchar(), !isdigit(ch));
res = ch ^ 48;
while (ch = getchar(), isdigit(ch))
res = res * 10 + (ch ^ 48);
}
const int e = 12e5 + 5, mod = 998244353, N = 12e5;
int fac[e], T, inv[e], n, tr[e], ans, p[e], suf[e];
inline int ksm(int x, int y)
{
int res = 1;
while (y)
{
if (y & 1) res = (ll)res * x % mod;
y >>= 1;
x = (ll)x * x % mod;
}
return res;
}
inline int plu(int x, int y)
{
(x += y) >= mod && (x -= mod);
return x;
}
inline int sub(int x, int y)
{
(x -= y) < 0 && (x += mod);
return x;
}
inline int c(int x, int y)
{
if (x < y) return 0;
if (x == y) return 1;
return (ll)fac[x] * inv[x - y] % mod * inv[y] % mod;
}
inline int ask(int n, int m)
{
return sub(c(2 * n - m, n - m), c(2 * n - m, n - m - 1));
}
inline void add(int x, int v)
{
for (int i = x; i <= n; i += i & -i)
tr[i] += v;
}
inline int query(int x)
{
int res = 0;
for (int i = x; i; i -= i & -i)
res += tr[i];
return res;
}
inline void solve()
{
read(n); ans = 0;
int i, val = 0, mx = 0, k;
memset(tr, 0, sizeof(tr));
for (i = 1; i <= n; i++)
read(p[i]), add(p[i], 1);
suf[n] = p[n];
for (i = n - 1; i >= 0; i--)
suf[i] = min(suf[i + 1], p[i]);
for (i = 1; i <= n; i++)
{
if (val > suf[i]) break;
if (mx > p[i]) val = max(val, p[i]);
mx = max(mx, p[i]);
k = query(mx);
ans = plu(ans, ask(n - i + 1, k + 1));
add(p[i], -1);
}
printf("%d\n", ans);
}
inline void init()
{
int i;
fac[0] = 1;
for (i = 1; i <= N; i++)
fac[i] = (ll)fac[i - 1] * i % mod;
inv[N] = ksm(fac[N], mod - 2);
for (i = N - 1; i >= 0; i--)
inv[i] = (ll)inv[i + 1] * (i + 1) % mod;
}
int main()
{
freopen("inverse.in", "r", stdin);
freopen("inverse.out", "w", stdout);
init();
read(T);
while (T--)
solve();
fclose(stdin);
fclose(stdout);
return 0;
}