「NOI2018」冒泡排序(动态规划+组合计数+树状数组)

Address

LOJ#2719

Luogu#4769

BZOJ#5416

UOJ#394

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\) 的情况。

对合法情况分类讨论:

  1. \(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\) 的最大值。
  2. \(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\)

\[s[n][m]=\sum_{i=m}^{n}\sum_{j=i-1}^{n-1}f[n-1][j] \]

\[s[n][m]=\sum_{j=m-1}^{n-1}f[n-1][j]+\sum_{i=m+1}^{n}\sum_{j=i-1}^{n-1}f[n-1][j] \]

\[s[n][m]=s[n-1][m-1]+\sum_{i=m+1}^{n}f[n][i] \]

\[s[n][m]=s[n-1][m-1]+s[n][m+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;
}
posted @ 2020-01-15 11:46  花淇淋  阅读(208)  评论(0编辑  收藏  举报