2 条题解
-
0

#include <cstdio> #include <iostream> using namespace std; const int M = 2000005; #define int long long const int MOD = 998244353; int read() { int x=0,f=1;char c; while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;} while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();} return x*f; } int T,n,m,ans,fac[M],inv[M],b[M]; void init(int n) { fac[0]=inv[0]=inv[1]=1; for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%MOD; for(int i=2;i<=n;i++) inv[i]=inv[MOD%i]*(MOD-MOD/i)%MOD; for(int i=2;i<=n;i++) inv[i]=inv[i]*inv[i-1]%MOD; } int C(int n,int m) { if(n<m || m<0) return 0; return fac[n]*inv[m]%MOD*inv[n-m]%MOD; } int walk(int x,int y,int n,int m) { if(x>n || y>m || x<0 || y<0) return 0; return C(n+n-x-y,n-x)-C(n+n-x-y,n-x+1); } void add(int x) { for(int i=x;i<=n;i+=(i&-i)) b[i]++; } int ask(int x) { int r=0; for(int i=x;i>0;i-=(i&-i)) r+=b[i]; return r; } void work() { n=read();ans=0; for(int i=1;i<=n;i++) b[i]=0; for(int i=1,mx=0,fl=1;i<=n;i++) { int x=read();mx=max(mx,x); if(fl) ans=(ans+walk(i-1,mx+1,n,n))%MOD; add(x); if(x!=mx && ask(x)<x) fl=0; } printf("%lld\n",(ans+MOD)%MOD); } signed main() { T=read();init(2e6); while(T--) work(); } -
0
感谢给我讲了这个题的神仙跳瓜 。
首先看给的提示,我们可以发现在这样的排序方式下,对于每一个数都只向目标位置方向走,不换向。那么对于一个数 ,如果有一个比它大的数在它前面,那么它必须向左走;如果有一个比它小的数在它后面,那么它必须向右走。这样的排列是不合法的,即要求不存在长度超过 的下降子序列。
根据 定理,最长下降子序列的长度不超过 ,即整个排列最多被划分成 个上升子序列。
先不考虑字典序严格大于 的限制。
我们记 为前 个的最大值为 后面 个位置的方案数。我们考虑第 个数填什么。注意
如果填比 大的数,那么一定可以接在 的后面;如果要填比 小的数,那么必须填当前还没有填的中最小的,否则上升子序列将不止 个,所以
$$\begin{aligned} f_{i, j} &= \begin{cases} f_{i + 1, k} \ \ \ \ \ (k > j) \\ f_{i + 1, j} \ \ \ \ \ \end{cases} \\ &= \sum_{k = j}^n f_{i + 1, k} \end{aligned}$$但是直接这样递推是 的。我们把它以图像的形式表示, 即表示从点 开始,每次向右走一步,向上走 步,不与直线 (因为 ) 相交,走到点 的方案数。

如图,即从点 走到 的不与直线 相交的方案数。
首先,如果不考虑与直线不相交,即为走 次,每次选择向上走 步,一共走了 步的方案数。模仿插板法,因为 可以取 ,我们把总个数加上划分数 ,变成 个物品划分成 块的方案数,即 。
再模仿 数的推法,看第一个与直线 相交的位置。找点 关于直线 的对称点 , 由于方案一一对应,所以,从点 出发,经过直线的方案数即为从点 出发到点 的方案数。
得到
$$\begin{aligned} f_{i, j} &= calc(i, j) - calc(j + 1, i - 1) \\ &= \dbinom{2n - i - j - 1}{n - i - 1} - \dbinom{2n - i - j - 1}{n - j - 2} \\ \end{aligned}$$这样就得到 的 求解啦!(然而 有足足 分,真香)
可以发现 即为 数,可以得到 分。
回到有限制字典序严格大于 的原题上来。考虑一位一位枚举,假设当前枚举到第 项,我们计数证前 项与 相同,第 项大于 的排列个数。
记 , 为当前还没有用过的最小的数,。第 位只能填 或大于 的数。分类讨论
-
(因为 是最小可以填的,所以 的下界是 。)
此时,第 项不能填 ,只能大于 ,故后面 项的填法有 种( 为第 位填的数)。
-
此时第 位没有可以填的,后面不再存在合法方案。(但是还是要读完)
-
此时第 位可以填 或大于 的数,方案数为 。
问题又来了,怎么求 的前缀和呢?考虑 的递推式 ,这正是一个前缀和的形式。所以
不用像其他题解上说的要用树状数组,复杂度 (还好写)
完结撒花~
代码
注意数组要开 ,写起来很简单,但是最开始由于没彻底搞懂想了半天
#include <bits/stdc++.h> using namespace std; namespace TYC { typedef long long ll; const int N = 1.2e6, p = 998244353; int fac[N + 5], inv[N + 5], vis[N + 5]; inline int read() { int v = 0, fl = 0, ch = getchar(); while (!isdigit(ch)) fl |= (ch == '-'), ch = getchar(); while (isdigit(ch)) v = v * 10 + ch - '0', ch = getchar(); return fl ? -v : v; } inline int qpow(int v, int tim) { int ans = 1; for (; tim; tim >>= 1, v = (ll)v * v % p) if (tim & 1) ans = (ll)ans * v % p; return ans; } void init() { fac[0] = 1; for (int i = 1; i <= N; i++) fac[i] = (ll)fac[i - 1] * i % p; inv[N] = qpow(fac[N], p - 2); for (int i = N; i; i--) inv[i - 1] = (ll)inv[i] * i % p; } inline int C(const int n, const int m) { return (n < 0 || m < 0 || n < m) ? 0 : int((ll)fac[n] * inv[m] % p * inv[n - m] % p); } int n; inline int F(const int i, const int j) { return i <= j && j <= n ? (C(2 * n - i - j - 1, n - i - 1) - C(2 * n - i - j - 1, n - j - 2) + p) % p : 0; } void work() { init(); int T = read(); while (T--) { n = read(); memset(vis, 0, sizeof(int[n + 1])); int ans = 0, mx = 0, mn = 1, flag = 0, v; for (int i = 1; i <= n; i++) { v = read(); if (flag) continue; ans = (ans + (F(i - 1, max(mx, v) + 1) + p) % p) % p; if (mx > v && v > mn) flag = 1; mx = max(mx, v); vis[v] = 1; while (vis[mn]) mn++; } printf("%d\n", ans); } } } int main() { TYC::work(); return 0; }P.S.
upd 2023.8.5 我已经退役很多年了,一次碰巧上洛谷看到有同学提醒组合数写反了,现特为纠正,希望同学们都能看明白。 -
- 1
信息
- ID
- 2568
- 时间
- 1000ms
- 内存
- 512MiB
- 难度
- 10
- 标签
- 递交数
- 7
- 已通过
- 3
- 上传者