DP 杂题

Trick

  1. 优化 dp 状态找题目的特殊性质。(XXYX Binary Tree)

  2. 对于有多组修改,但只是独立修改一个位置权值的 dp 题,可以考虑去讨论是否选择被修改的这个位置。(Contest with Drinks Hard)

  3. 对于不好优化的序列 dp,可以考虑放在分治上做。(Yet Another Partiton Problem)

  4. 对于 \(\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)\) 进行求解。(赌局)

  5. \(dp_i\leftarrow \min dp_j+cost(j+1,i)\),形如这样的式子,可以考虑分析 \(cost(j+1,i)\) 的取值上限,然后枚举 \(cost(j+1,i)\) 来进行转移。(Inverse Minimum Partition)

  6. 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

然后就可以刻画 XYYX 的数量了,这里分根节点是否为 \(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\) 转移就有:

\[\begin{cases} f_i\leftarrow f_{i-1} \\ f_i\leftarrow f_j+cost(j+1,i),j\lt i \end{cases} \]

\(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\) 怎么求?直接转移就是:

\[h_i\leftarrow f_{l-1}+g_{r+1}+cost(l,r),i\in(l,r) \]

这里转移是 \(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:

\[f_i\leftarrow \min f_j+cost(j+1,i) \]

考虑如何优化这个。

首先从一些性质入手,即每次划分时一定会让一个后缀最小值为右端点,这是显然的。

那么考虑把所有后缀最小值取出来,从后往前考虑,对于一个右端点 \(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]\) 划分的方案数,那么有:

\[f_i\leftarrow f_{j-1},i-j+1\ge \max_{k=j}^i a_k \]

这东西显然不好用数据结构优化,但是不要忘了 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}\)

那么此时要满足三个条件:

  1. \(r-l+1\ge mxR_r\)
  2. \(r-l+1\ge mxx\)
  3. \(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;
}
posted @ 2025-09-12 20:36  咚咚的锵  阅读(10)  评论(0)    收藏  举报