DP 杂题
Trick
-
优化 dp 状态找题目的特殊性质。(XXYX Binary Tree)
-
对于有多组修改,但只是独立修改一个位置权值的 dp 题,可以考虑去讨论是否选择被修改的这个位置。(Contest with Drinks Hard)
-
对于不好优化的序列 dp,可以考虑放在分治上做。(Yet Another Partiton Problem)
-
对于 \(\min(B_j+C_k-i,A_i+C_k-j,A_i+B_j-k)\) 这样的形式,可以变成 \(A_i+B_j+C_k-\max(i+A_i,j+B_j,k+C_k)\) 进行求解。(赌局)
-
\(dp_i\leftarrow \min dp_j+cost(j+1,i)\),形如这样的式子,可以考虑分析 \(cost(j+1,i)\) 的取值上限,然后枚举 \(cost(j+1,i)\) 来进行转移。(Inverse Minimum Partition)
-
CDQ 分治是优化 1d 问题的大手子。(Say Hello to the Future)
题目
[ARC157E] XXYX Binary Tree
一个很显然的暴力,\(f_{u,a,b,c}\) 表示在在 \(u\) 的子树中,是否有 \(a\) 个 XX,\(b\) 个 XY,\(c\) 个 YX。
这个状态 \(O(n^4)\) 的,考虑优化,可以先省去一个 \(c\),变成 \(f_{u,a,b}\),因为 \(a+b+c\) 的总和是知道的。
然后优化不了了……
只能重新设计,发现都没有用上二叉树的条件,显然要找一点性质的。
可以发现 \(Y\) 的个数好像可以确定,如果 \(Y\) 为非根节点,那么 \(Y\) 的数量有 \(B\) 个,否则就有 \(B+1\) 个,因为一个除了根节点的 \(Y\) 肯定可以贡献一个 XY。
还可以发现非叶子的 \(Y\) 可以贡献两个 YX。
然后就可以刻画 XY 和 YX 的数量了,这里分根节点是否为 \(Y\) 两种情况。
如果根不为 \(Y\),那么一共有 \(B\) 个 \(Y\),记叶子节点为 \(Y\) 的个数为 \(sum\),那么 YX 的数量就是 \(2(B-sum)\),所以 \(sum\) 满足 \(sum=B-\frac{C}{2}\)。
如果根为 \(Y\),那么一共有 \(B+1\) 个 \(Y\),记叶子节点为 \(Y\) 的个数为 \(sum\),那么 YX 的数量就是 \(2(B+1-sum)\),所以 \(sum\) 满足 \(sum=B+1-\frac{C}{2}\)。
所以设 \(f_{u,i,0/1}\) 表示 \(u\) 的子树中有 \(i\) 个叶子为 \(Y\) 且 \(u\) 是否是 \(Y\) 时最多有多少个非叶子的 \(Y\)。
这里可以 \(O(n^2)\) 树上背包转移。
代码
#include <bits/stdc++.h>
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 1e4 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;
int n, A, B, C;
int f[N][N][2], siz[N];
vector< int> G[N];
void dfs( int u) {
siz[u] = 0;
if (! G[u].size()) {
siz[u] = 1;
f[u][0][0] = 0, f[u][1][1] = 0;
return ;
}
f[u][0][0] = 0, f[u][0][1] = 1;
for ( auto v : G[u]) {
dfs(v);
// 这里注意,必须要倒叙枚举,这样才能保证不会用到已经被转移过的信息
for ( int i = siz[u]; i >= 0; i --)
for ( int j = siz[v]; j >= 0; j --) {
f[u][i + j][0] = max(f[u][i + j][0], f[u][i][0] + max(f[v][j][0], f[v][j][1]));
f[u][i + j][1] = max(f[u][i + j][1], f[u][i][1] + f[v][j][0]);
}
siz[u] += siz[v];
}
}
void solve() {
cin >> n >> A >> B >> C;
for ( int i = 1; i <= n; i ++) {
G[i].clear();
for ( int j = 0; j <= B + 1; j ++)
f[i][j][0] = f[i][j][1] = -inf;
}
for ( int i = 2, fa; i <= n; i ++)
cin >> fa, G[fa].push_back(i);
if (C & 1) return cout << "No\n", void();
dfs(1);
if (B - C / 2 >= 0 && f[1][B - C / 2][0] >= C / 2) {
cout << "Yes\n";
return ;
}
if (B + 1 - C / 2 >= 0 && f[1][B + 1 - C / 2][1] >= C / 2) {
cout << "Yes\n";
return ;
}
cout << "No\n";
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int t; cin >> t;
while (t --) solve();
return 0;
}
[ARC066F] Contest with Drinks Hard
它这个贡献形式就已经表明了这是一个 1d 问题。
设 \(f_i\) 表示考虑了前 \(i\) 题的最大值。
转移就是枚举 \(j\),\((j+1,i]\) 的题目都要做。
当然也可以不做第 \(i\) 题,直接从 \(f_{i-1}\) 转移,记 \(cost(l,r)=\dfrac{(r-l+1)(r-l+2)}{2}-sum_r+sum_{l-1}\)。
那么 \(f_i\) 转移就有:
把 \(cost(j+1,i)\) 拆开,可以直接斜率优化。
但是这个题麻烦的是有多组修改,直接做 \(O(nm)\) 过不了。
但特殊在于,每个修改都是独立的,所以考虑讨论是否选择被修改的那道题。
记 \(f_i\) 表示考虑了前 \(i\) 个题的最大值;\(g_i\) 表示考虑了后 \(i\) 个题的最大值;\(h_i\) 表示考虑了所有题,但是钦定第 \(i\) 题必做的最大值。
假设题目 \(i\) 被修改后,考虑是否选择它,如果不选择 \(i\),那么答案就是 \(f_{i-1}+g_{i+1}\);如果选择,答案就是 \(h_i-x+a_i\)。
所以最后的答案就是就是 \(\max(f_{i-1}+g_{i+1},h_i-x+a_i)\)。
\(f_i\) 转移和上述一样,斜率优化即可;\(g_i\) 也就是倒序 dp,和 \(f_i\) 相似。
考虑 \(h_i\) 怎么求?直接转移就是:
这里转移是 \(O(n^3)\) 的。
因为 \(l,r\) 要跨过 \(i\),那么不妨考虑分治。
设 \(dp_i\) 左端点为 \(i\) 且 \(i\in[l,mid]\),右端点为 \(j-1\) 且 \(j\in[mid+1,r+1]\) 时,\(f_{i-1}+g_{j}+cost(i,j-1)\) 的最大值。
这里也是可以斜率优化去求的,求出来后 \(dp_i\) 可以给 \(h_{i\sim mid}\) 贡献最大值。
当然,因为 \(dp_i\) 只能给 \(h_{i\sim mid}\) 贡献最大值,那么再设一个和 \(dp_i\) 相反的能够给 \(h_{mid+1\sim i}\) 贡献最大值的 \(dp'_i\) 即可。
实现这块,我用的是李超线段树,多带一个 \(\log\) 但是无伤大雅。
代码
#include <bits/stdc++.h>
#define int long long
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 3e5 + 10, M = 2e5 + 10, inf = 1e18, mod = 998244353;
int n, m;
int a[N], sum[N];
int f[N], g[N], h[N];
int tot, rt;
struct sgt {
#define mid ((l + r) >> 1)
int tag[N * 18], ls[N * 18], rs[N * 18];
struct line {
int k, b;
} l[N];
int cal( int id, int x) {
return l[id].k * x + l[id].b;
}
void insert( int u, int & k, int l = 1, int r = n) {
if (! k) return k = ++ tot, ls[k] = 0, rs[k] = 0, tag[k] = u, void();
int & v = tag[k];
if (cal(v, mid) < cal(u, mid)) swap(v, u);
if (cal(u, l) > cal(v, l)) insert(u, ls[k], l, mid);
if (cal(u, r) > cal(v, r)) insert(u, rs[k], mid + 1, r);
}
int ask( int x, int k, int l = 1, int r = n) {
if (! k) return -inf;
return max(cal(tag[k], x), x <= mid ? ask(x, ls[k], l, mid) : ask(x, rs[k], mid + 1, r));
}
#undef mid
} tr;
void solve( int l, int r) {
if (l == r) {
h[l] = max(h[l], 1 - a[l]);
return ;
}
int mid = (l + r) >> 1;
solve(l, mid), solve(mid + 1, r);
rt = tot = 0;
for ( int i = mid + 1; i <= r + 1; i ++) {
tr.l[i] = {-i, g[i] - sum[i - 1] + (i * i + i) / 2};
tr.insert(i, rt);
}
int mx = -inf;
for ( int i = l; i <= mid; i ++) {
mx = max(mx, f[i - 1] + tr.ask(i, rt) + (i * i - i) / 2 + sum[i - 1]);
h[i] = max(h[i], mx);
}
rt = tot = 0;
for ( int i = mid; i >= l - 1; i --) {
tr.l[i] = {-i, f[i] + sum[i] + (i * i - i) / 2};
tr.insert(i, rt);
}
mx = -inf;
for ( int i = r; i > mid; i --) {
mx = max(mx, g[i + 1] + tr.ask(i, rt) + (i * i + i) / 2 - sum[i]);
h[i] = max(h[i], mx);
}
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
for ( int i = 1; i <= n; i ++) cin >> a[i], sum[i] = sum[i - 1] + a[i];
memset(h, 128, sizeof h);
rt = tot = 0;
tr.l[0] = {0, 0};
tr.insert(0, rt);
for ( int i = 1; i <= n; i ++) {
f[i] = f[i - 1];
f[i] = max(f[i], tr.ask(i, rt) + (i * i + i) / 2 - sum[i]);
tr.l[i] = {-i, f[i] + sum[i] + (i * i - i) / 2};
tr.insert(i, rt);
}
rt = tot = 0;
tr.l[n + 1] = {-(n + 1), -sum[n] + ((n + 1) * (n + 1) + n + 1) / 2};
tr.insert(n + 1, rt);
for ( int i = n; i; i --) {
g[i] = g[i + 1];
g[i] = max(g[i], tr.ask(i, rt) + (i * i - i) / 2 + sum[i - 1]);
tr.l[i] = {-i, g[i] - sum[i - 1] + (i * i + i) / 2};
tr.insert(i, rt);
}
solve(1, n);
cin >> m;
while (m --) {
int i, x; cin >> i >> x;
cout << max(f[i - 1] + g[i + 1], h[i] - x + a[i]) << '\n';
}
return 0;
}
Yet Another Partiton Problem
2d 问题。
暴力的 dp 是简单的,设 \(f_{i,l}\) 表示考虑到第 \(i\) 个数,已经分了 \(l\) 段的最小代价。
转移是 \(O(n^2k)\) 的,所以要优化转移的复杂度。
这个东西,显然是没有决策单调性的,数据结构啥的也优化不了,主要是最大值的贡献太难表示了。
遇到不会优化的序列题就分治一下,考虑 \([l,mid]\) 转移到 \((mid,r]\)。
这里设 \(g_i\) 为上一轮的 dp 值,\(f_i\) 为此轮的。
记 \(mx1_i\) 为 \((i,mid]\) 的最大值,\(mx2_i\) 为 \((mid,i]\) 的最大值。
那么转移有 \(f_i\leftarrow g_j+(i-j)\times\max(mx1_j,mx2_i)\)。
这里显然可以讨论 \(mx1_j\) 和 \(mx2_i\) 的大小。
如果 \(mx1_j\le mx2_i\),那么就有 \(f_i\leftarrow g_j-j\times mx2_i+i\times mx2_i\)。
这个东西可以直接上李超树去做,因为 \(mx1_j\) 与 \(mx2_i\) 本来就有序,就可以像 CDQ 那样搞:对于 \(mx1_j \le mx2_i\) 的 \(j\),把 \(y=-jx+g_j\) 这条直线插入李超,询问 \(x=mx2_i\) 处的最小值即可。
如果 \(mx1_j\gt mx2_i\),做法和上述同理。
代码
#include <bits/stdc++.h>
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 2e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;
int Mx;
int n, k;
int a[N];
int f[N], g[N];
struct line {
int k, b;
} l[N];
int cal( int id, int x) {
return l[id].k * x + l[id].b;
}
int tag[N * 10], ls[N * 10], rs[N * 10];
int rt, tot;
#define mid ((l + r) >> 1)
void insert( int & k, int l, int r, int u) {
if (! k) return k = ++ tot, ls[k] = rs[k] = 0, tag[k] = u, void();
int & v = tag[k];
if (cal(v, mid) > cal(u, mid)) swap(v, u);
if (cal(u, l) < cal(v, l)) insert(ls[k], l, mid, u);
if (cal(u, r) < cal(v, r)) insert(rs[k], mid + 1, r, u);
}
int ask( int k, int l, int r, int x) {
if (! k) return inf;
return min(cal(tag[k], x), x <= mid ? ask(ls[k], l, mid, x) : ask(rs[k], mid + 1, r, x));
}
#undef mid
int mx1[N], mx2[N];
void CDQ( int l, int r) {
if (l == r) return ;
int mid = (l + r) >> 1;
CDQ(l, mid), CDQ(mid + 1, r);
rt = tot = 0;
mx1[mid] = mx2[mid] = 0;
for ( int i = mid - 1; i >= l; i --)
mx1[i] = max(mx1[i + 1], a[i + 1]);
for ( int i = mid + 1; i <= r; i ++)
mx2[i] = max(mx2[i - 1], a[i]);
int j = mid;
for ( int i = mid + 1; i <= r; i ++) {
while (mx1[j] <= mx2[i] && j >= l) {
:: l[j] = {-j, g[j]};
insert(rt, 1, Mx, j);
j --;
}
f[i] = min(f[i], ask(rt, 1, Mx, mx2[i]) + i * mx2[i]);
}
rt = tot = 0;
j = l;
for ( int i = r; i > mid; i --) {
while (mx1[j] > mx2[i] && j <= mid) {
:: l[j] = {mx1[j], g[j] - j * mx1[j]};
insert(rt, 1, n, j);
j ++;
}
f[i] = min(f[i], ask(rt, 1, n, i));
}
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> k;
for ( int i = 1; i <= n; i ++) cin >> a[i];
for ( int i = 1; i <= n; i ++)
Mx = max(Mx, a[i]), f[i] = Mx * i;
k -= 1;
while (k --) {
for ( int i = 1; i <= n; i ++) g[i] = f[i];
for ( int i = 1; i <= n; i ++) f[i] = inf;
CDQ(1, n);
}
cout << f[n] << '\n';
return 0;
}
赌局
没有原题。

可以考虑背包去做。
设 \(A_i\) 表示 \(A\) 赢的时候亏损 \(i\) 元时,\(A\) 输了可以获得的最大价值,\(B_i\)、\(C_i\) 同理。
转移是 \(O(n^2V)\) 的。
那么答案就是 \(\min(B_j+C_k-i,A_i+C_k-j,A_i+B_j-k)\)。
发现求答案的复杂度是 \(O(n^3V^3)\) 的,有点炸。
考虑把答案形式化一下,变成 \(A_i+B_j+C_k-\max(i+A_i,j+B_j,k+C_k)\),这就很典了。
考虑钦定 \(\max(i+A_i,j+B_j,k+C_k)\) 是哪一个,然后用树状数组维护一个前缀最大值即可。
复杂度 \(O(n^2V+nV\log{nV})\)。
代码
#include <bits/stdc++.h>
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 5e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;
int n;
int A[N], B[N], C[N];
struct bit {
int tr[N];
void init() {
for ( int u = 1; u < N; u ++) tr[u] = -inf;
}
void add( int u, int v) {
for (; u < N; u += (u & -u)) tr[u] = max(tr[u], v);
}
int ask( int u, int res = -inf) {
for (; u; u -= (u & -u)) res = max(res, tr[u]);
return res;
}
} ta, tb, tc;
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
memset(A, 128, sizeof A);
memset(B, 128, sizeof B);
memset(C, 128, sizeof C);
A[0] = B[0] = C[0] = 0;
ta.init(), tb.init(), tc.init();
cin >> n;
for ( int i = 1; i <= n; i ++) {
int t, a, b; cin >> t >> a >> b;
for ( int j = n * 500; j >= 0; j --)
if (j - a >= 0) {
if (t == 1) A[j] = max(A[j], A[j - a] + b);
else if (t == 2) B[j] = max(B[j], B[j - a] + b);
else C[j] = max(C[j], C[j - a] + b);
}
}
int ans = 0;
for ( int i = 0; i <= n * 500; i ++) {
if (A[i] > -inf) ta.add(i + A[i] + 1, A[i]);
if (B[i] > -inf) tb.add(i + B[i] + 1, B[i]);
if (C[i] > -inf) tc.add(i + C[i] + 1, C[i]);
}
for ( int i = 0; i <= n * 500; i ++) {
if (A[i] > -inf) ans = max(ans, tb.ask(i + A[i] + 1) + tc.ask(i + A[i] + 1) - i);
if (B[i] > -inf) ans = max(ans, ta.ask(i + B[i] + 1) + tc.ask(i + B[i] + 1) - i);
if (C[i] > -inf) ans = max(ans, ta.ask(i + C[i] + 1) + tb.ask(i + C[i] + 1) - i);
}
cout << ans << '\n';
return 0;
}
Inverse Minimum Partition (Hard Version)
先考虑 \(f(1\sim n)\) 怎么做,这也是 Easy Version。
不难写出一个 \(O(n^2)\) 的 dp:
考虑如何优化这个。
首先从一些性质入手,即每次划分时一定会让一个后缀最小值为右端点,这是显然的。
那么考虑把所有后缀最小值取出来,从后往前考虑,对于一个右端点 \(a_i\),如果把所有 \(\ge \frac{a_i}{2}\) 的划分进 \(a_i\) 对应的段里面,那么这个段的 \(cost\) 肯定是 \(\le 2\) 的,而且下一次的右端点的大小会减小一半,所以最多 \(\log V\) 段。
根据这个,就可以得到 \(f(1\sim n)\) 的上界是 \(2\log V\)。
那么转移时就可以枚举 \(cost(j+1,i)\),二分出对应的 \(j\) 即可。
现在考虑 Hard Version 的做法。
记 \(f_{r,j}\) 表示最小的一个 \(l\) 满足 \([l,r]\le j\),如果能够求出 \(f\),计算答案就是简单的。这也是处理子区间问题的一个手段。
转移这个的复杂度是 \(O(n\log^2 V)\) 的,过不了。
但是打表出来,可以发现对于一个最优的划分,每一段的 \(cost\) 不会超过 \(3\),这是为什么?
如果有一个段的 \(cost\) 为 \(x\),那么可以拆出一个 \(cost\le 2\) 的段和一个 \(cost\le\frac{x}{2}\) 的一段,这样 \(cost\) 变成 \(2+\frac{x}{2}\),显然可以这样一直操作直到 \(cost\le 3\)。
得到这个后,转移就可以优化一个 \(\log V\)(只会从 \(j-1\)、\(j-2\)、\(j-3\) 转移到 \(j\))。
具体实现可以 dp 时同时维护一个单调栈,可以得到右端点为 \(i\) 时的后缀最小值。
代码
#include <bits/stdc++.h>
#define int long long
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 4e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;
int n;
int a[N];
int sta[N], top;
int f[N][129];
int search( int x, int y) {
int l = 1, r = top;
while (l < r) {
int mid = (l + r) >> 1;
if ((x - 1) / a[sta[mid]] + 1 <= y) r = mid;
else l = mid + 1;
}
return r;
}
void solve() {
cin >> n;
for ( int i = 1; i <= n; i ++) cin >> a[i];
int ans = 0;
top = 0;
for ( int i = 1; i <= n; i ++) {
while (top && a[sta[top]] >= a[i]) top --;
sta[++ top] = i;
int l2 = search(a[i], 2);
int l3 = search(a[i], 3);
for ( int j = 0; j < 129; j ++) {
f[top][j] = top + 1;
if (j >= 1) f[top][j] = min(f[top][j], f[top - 1][j - 1]);
if (j >= 2) f[top][j] = min(f[top][j], f[l2 - 1][j - 2]);
if (j >= 3) f[top][j] = min(f[top][j], f[l3 - 1][j - 3]);
if (j >= 1) {
ans += j * (sta[f[top][j - 1] - 1] + 1 - (sta[f[top][j] - 1] + 1));
}
}
}
cout << ans << '\n';
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int T; cin >> T;
while (T --) solve();
return 0;
}
Say Hello to the Future
先考虑没有修改时的 dp 怎么做。
\(O(n^2)\) 是简单的,记 \(f_i\) 表示把 \([1,i]\) 划分的方案数,那么有:
这东西显然不好用数据结构优化,但是不要忘了 CDQ 也是处理 1d 的一个大手子。
就考虑 \([j,i]\) 跨过当前区间 \([L,R]\) 的中点 \(mid\) 的转移。
记 \(mxL_j=\max(a_{mid},a_{mid-1},\dots,a_j)\)、\(mxR_i=\max(a_{mid+1},a_{mid+2},\dots,a_{i})\)。
那么 \(i\) 能从 \(j\) 转移就要满足 \(i-j+1\ge\max(mxL_j,mxR_i)\),式子拆开有 \(i-j+1\ge mxL_j\wedge i-j+1\ge mxR_i\)。
变型有 \(i\ge mxL_j+j-1\wedge j\le i-mxR_i+1\),这显然是一个二维数点,直接排序加树状数组即可。
边界是当 \(L=R\) 时,只要 \(a_L=1\) 就有 \(f_L\leftarrow f_{L-1}\)。
那么考虑当有修改时怎么做,显然将一个 \(a_i\) 变成 \(1\),只会让若干不合法的划分变得合法,考虑去枚举包含 \(i\) 的一个区间 \([l,r]\),满足 \([l,r]\) 本身不合法,但将 \(a_i\) 变成 \(1\) 后此区间合法。
考虑计算出一个 \(g_i\) 表示 \([i,n]\) 划分的方案数,求法和 \(f_i\) 同理。
那么将一个 \(a_i\) 变成 \(1\) 后,它的增量为 \(\sum_{所有初始不合法、修改后合法的区间 [l,r]}f_{l-1}g_{r+1}\)。
可以发现只有当 \(a_i\) 为最大值的时候,才可能使得区间变合法,记最大值为 \(mx\),次大值为 \(mxx\),那么就有 \(mx\gt r-l+1\ge mxx\)。
怎么维护呢?还是考虑分治处理,处理跨过 \(mid\) 的所有区间。
这里要分成两部分做,即最大值在 \([L,mid]\) 中和最大值在 \((mid,R]\) 中,这两部分的处理是相似的,以 \([L,mid]\) 为例子。
还是先处理出 \(mxR_r\) 的信息,然后在左区间从 \(mid\) 枚举到 \(l\),动态维护 \(mx\) 与 \(mxx\),记 \(mx\) 的编号为 \(p\),那么在 \(p\) 处的增量就是 \(f_{l-1}\sum_{所有合法的 r}g_{r+1}\)。
那么此时要满足三个条件:
- \(r-l+1\ge mxR_r\)。
- \(r-l+1\ge mxx\)。
- \(r-l+1\lt mx\)。
给它变形就有 \(l\le mxR_r-r+1\wedge mxx+l-1\le r \lt mx+l-1\)。
这还是一个二维数点的形式,CDQ 做即可。
边界是当 \(L=R\) 时,只要 \(a_L\neq 1\),在 \(l\) 处的增量加 \(f_{L-1}g_{L+1}\)。
代码
#include <bits/stdc++.h>
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 2e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;
int n;
int a[N];
int f[N], tmp[N], g[N];
int mxL[N], mxR[N], id[N];
struct bit {
int tr[2 * N];
void add( int u, int v) {
if (u < 1 || u >= 2 * N) return ;
for (; u < 2 * N; u += (u & -u)) tr[u] = (tr[u] + v) % mod;
}
int ask( int u, int res = 0) {
if (u < 1 || u >= 2 * N) return 0;
for (; u; u -= (u & -u)) res = (res + tr[u]) % mod;
return res;
}
} B;
void DP( int l, int r, int * dp) {
if (l == r) {
if (a[l] == 1) dp[l] = (dp[l] + dp[l - 1]) % mod;
return ;
}
int mid = (l + r) >> 1;
DP(l, mid, dp);
mxL[mid + 1] = 0;
for ( int i = mid; i >= l; i --) mxL[i] = max(mxL[i + 1], a[i]), id[i] = i;
sort(id + l, id + mid + 1, [&]( int a, int b) {
return mxL[a] + a - 1 < mxL[b] + b - 1;
});
int ptr = l, mx = 0;
for ( int i = mid + 1; i <= r; i ++) {
mx = max(mx, a[i]);
while (mxL[id[ptr]] + id[ptr] - 1 <= i && ptr <= mid) B.add(id[ptr], dp[id[ptr] - 1]), ptr ++;
dp[i] = (dp[i] + B.ask(i - mx + 1)) % mod;
}
for ( int i = l; i < ptr; i ++) B.add(id[i], mod - dp[id[i] - 1]);
DP(mid + 1, r, dp);
}
int ans[N];
void solve( int l, int r) {
if (l == r) {
if (a[l] != 1) ans[l] = (ans[l] + 1ll * f[l - 1] * g[l + 1] % mod) % mod;
return ;
}
int mid = (l + r) >> 1;
solve(l, mid), solve(mid + 1, r);
mxR[mid] = 0;
for ( int i = mid + 1; i <= r; i ++) mxR[i] = max(mxR[i - 1], a[i]), id[i] = i;
sort(id + mid + 1, id + r + 1, [&]( int a, int b) {
return a - mxR[a] + 1 > b - mxR[b] + 1;
});
int ptr = mid + 1, mx = 0, mxx = 0, ID = 0;
for ( int i = mid; i >= l; i --) {
if (mx <= a[i]) mxx = mx, mx = a[i], ID = i;
else if (mxx <= a[i]) mxx = a[i];
while (id[ptr] - mxR[id[ptr]] + 1 >= i && ptr <= r) B.add(id[ptr], g[id[ptr] + 1]), ptr ++;
ans[ID] = (ans[ID] + 1ll * f[i - 1] * (B.ask(mx + i - 2) - B.ask(mxx + i - 2) + mod) % mod) % mod;
}
for ( int i = mid + 1; i < ptr; i ++) B.add(id[i], mod - g[id[i] + 1]);
mxL[mid + 1] = 0;
for ( int i = mid; i >= l; i --) mxL[i] = max(mxL[i + 1], a[i]), id[i] = i;
sort(id + l, id + mid + 1, [&]( int a, int b) {
return mxL[a] + a - 1 < mxL[b] + b - 1;
});
ptr = l, mx = 0, mxx = 0, ID = 0;
for ( int i = mid + 1; i <= r; i ++) {
if (mx <= a[i]) mxx = mx, mx = a[i], ID = i;
else if (mxx <= a[i]) mxx = a[i];
while (mxL[id[ptr]] + id[ptr] - 1 <= i && ptr <= mid) B.add(id[ptr], f[id[ptr] - 1]), ptr ++;
ans[ID] = (ans[ID] + 1ll * (B.ask(i - mxx + 1) - B.ask(i - mx + 1) + mod) * g[i + 1] % mod) % mod;
}
for ( int i = l; i < ptr; i ++) B.add(id[i], mod - f[id[i] - 1]);
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
for ( int i = 1; i <= n; i ++) cin >> a[i];
f[0] = 1, DP(1, n, f);
reverse(a + 1, a + n + 1);
tmp[0] = 1, DP(1, n, tmp);
for ( int i = 0; i <= n; i ++) g[n - i + 1] = tmp[i];
reverse(a + 1, a + n + 1);
solve(1, n);
for ( int i = 1; i <= n; i ++) cout << (f[n] + ans[i]) % mod << ' ';
return 0;
}

浙公网安备 33010602011771号