zoukankan      html  css  js  c++  java
  • 「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;
    }
    
  • 相关阅读:
    对小课堂cpp的用户体验
    面试题 02.07. 链表相交 做题小结
    Leetcode 133. 克隆图 做题小结
    Leetcode 889. 根据前序和后序遍历构造二叉树-105. 从前序与中序遍历序列构造二叉树-106. 从中序与后序遍历序列构造二叉树
    图 的矩阵表示 和邻接表表示
    二叉树 常用函数 小结
    LeetCode 100. 相同的树 做题小结
    LeetCode 897. 递增顺序查找树 做题小结
    Leetcode 814. 二叉树剪枝 做题小结
    Leetcode l872. 叶子相似的树 做题小结
  • 原文地址:https://www.cnblogs.com/cyf32768/p/12195903.html
Copyright © 2011-2022 走看看