【状态机DP、权值树状数组】ABC439F

· · 题解

【状态机DP、权值树状数组】ABC439F

原题链接

简要题意

给你一个 1N 的排列 P,求其好子序列的个数。

一个序列 a_{1...n} 是好的,当且仅当其峰的个数大于谷的个数,峰就是满足 2 \le i \le n - 1a_i > a_{i - 1},a_i > a_{i + 1}i 的个数,谷就是满足 2 \le i \le n - 1a_i < a_{i - 1},a_i < a_{i + 1}i 的个数。

## 思路 这种计数题要么 DP 要么推式子,本题把排列 $P$ 通过输入给出来了,所以会倾向于往 DP 上思考。 我们首先应该发现一个事情,$P$ 的任何一个子序列,其峰和谷的个数至多差 $1$。 为什么呢,我们考虑序列的升降趋势,发现只有下面四种情况: - **升降升降...升**:由于每个“升降”的改变点会带来一个峰,每个“降升”的改变点会带来一个谷,所以这种情况下峰的个数等于谷的个数。 - **升降升降...升降**:分析可知峰的个数比谷的个数恰好多一个。 - **降升降升...降**:和第一种情况一样,峰的个数等于谷的个数。 - **降升降升...降升**:分析可知峰的个数比谷的个数恰好少一个。 - **单点**:**这个很容易忘记**!因为为了形成升或者降的趋势,**至少是有两个数才行**,而只有一个数时是没有趋势的,所以这种情况要单列出来。 因此,这道题其实就是计数**第二种情况的子序列有多少**。 我们考虑设: - $dp[i][0]$ 为以第 $i$ 个数结尾的**只有一个数**的序列个数。 - $dp[i][1]$ 为以第 $i$ 个数结尾的,趋势是**升降升降...升**的序列个数。 - $dp[i][2]$ 为以第 $i$ 个数结尾的,趋势是**升降升降...升降**的序列的个数。 对于 $dp[i][0]$: - $dp[i][0] = 1$。 对于 $dp[i][1]$: - $dp[i][1] += dp[j][0]$,其中 $p[j] < p[i]$。 - $dp[i][1] += dp[j][1]$,其中 $p[j] < p[i]$。 - $dp[i][1] += dp[j][2]$,其中 $p[j] < p[i]$。 对于 $dp[i][2]$: - 由于**先升了才能降**,所以单点序列 $dp[i][0]$ 不能转移过来。 - $dp[i][2] += dp[j][1]$,其中 $p[j] > p[i]$。 - $dp[i][2] += dp[j][2]$,其中 $p[j] > p[i]$。 显然这个转移可以用权值树状数组进行优化,我们开 3 棵树状数组,分别维护 $dp[i][0]$ 、$dp[i][1]$ 和 $dp[i][2]$ 的和即可。 实现时注意取模,以及涉及到取模减法的话记得最后把答案调整成非负数。 ## 代码 ```cpp #include <bits/stdc++.h> #define x first #define y second using namespace std; typedef long long LL; typedef unsigned long long ULL; typedef pair<int, int> PII; const int N = 3e5 + 10; const int mod = 998244353; struct Fenwick { LL tr[N], n; int lowbit(int x) { return x & -x; } void add(int pos, LL x) { while (pos <= n) { tr[pos] += x; tr[pos] %= mod; pos += lowbit(pos); } } LL query(int pos) { LL res = 0; while (pos > 0) { res += tr[pos]; res %= mod; pos -= lowbit(pos); } return res; } void init(int sz) { n = sz; for (int i = 0; i <= n; i++) { tr[i] = 0; } } void clear() { for (int i = 0; i <= n; i++) { tr[i] = 0; } } }; Fenwick fen0, fen1, fen2; void solve() { int n; cin >> n; vector<int> a(n + 1, 0); for (int i = 1; i <= n; i++) { cin >> a[i]; } LL res = 0; fen0.init(n + 2); fen1.init(n + 2); fen2.init(n + 2); vector<vector<LL>> dp(n + 2, vector<LL>(2, 0)); for (int i = 1; i <= n; i++) { LL down_cnt = 0, up_cnt = 0; up_cnt += fen0.query(a[i] - 1) + fen1.query(a[i] - 1) + fen2.query(a[i] - 1); down_cnt += fen1.query(n) - fen1.query(a[i]) + fen2.query(n) - fen2.query(a[i]); up_cnt %= mod; down_cnt %= mod; dp[i][0] = 1; dp[i][1] = up_cnt; dp[i][2] = down_cnt; fen0.add(a[i], dp[i][0]); fen1.add(a[i], dp[i][1]); fen2.add(a[i], dp[i][2]); } for (int i = 1; i <= n; i++) { res += dp[i][2]; res %= mod; } res = (res % mod + mod) % mod; cout << res << "\n"; } int main() { #ifdef LOCAL_TEST freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); #endif ios::sync_with_stdio(false); cin.tie(0); int T = 1; // cin >> T; while (T--) { solve(); } return 0; } ``` ## Bonus 通过上面的分析,我们其实注意到一个事情:**一个序列(长度不小于 $4$)是好的,当且仅当序列开头俩元素递增,结尾俩元素递减**。 假如题目**改成**问有多少个 $1$ 到 $n$ 的排列 $P$ 是好的,等价于只要考虑构造最开始 $2$ 个数递增,最后 $2$ 个数递减,中间随便排的方案了($n \ge 4$),似乎是 $C(n, 4) \times C(4, 2) \times (n - 4)!$,化简一下就是 $\frac{n!}{4}$。 特判更小的情况即可解决 $n \le 3$ 。