好的序列

正难则反:指定坏点后序列碎成段内同值的独立段,容斥计数,再用单调栈维护后缀最小值阶梯把转移压成 O(1)。

OJ: roj

题目 ID: 20025

难度:提高

标签:容斥动态规划单调栈组合计数

日期: 2026-08-29 00:08

形式化题目

给定正整数序列 a1..ana_1..a_n,统计整数序列 b1..bnb_1..b_n 的数量,满足:

  • 1biai1 \leqslant b_i \leqslant a_i(每个位置的上界逐位不同);
  • 任意相邻两项 bibi+1b_i \neq b_{i+1}

答案对 998244353998244353 取模。其中 n106n \leqslant 10^6ai109a_i \leqslant 10^9

样例:a = [2,2,2] 时只有 {1,2,1},{2,1,2}\{1,2,1\},\{2,1,2\},答案是 22

思路

一句话本质:“相邻都不相等"是一个全局耦合约束,正着数很难;反过来把"哪些位置相邻相等”(坏点)当成可指定的集合——一旦指定,序列就碎成若干"段内同值、段间不限"的独立段,每段的取值方案数恰好是该段 aa 的最小值。用容斥把这些计数拼回"零坏点",段数只以奇偶形式出现在容斥系数里,于是把段数奇偶当状态做 DP,最后用单调栈维护"后缀最小值阶梯"+ 前缀和,把每次转移压成 O(1)O(1),整体 O(n)O(n)

先看一个能直接验证想法的朴素解——逐位枚举填数:

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-08-28 23:50
 * update_at: 2026-08-28 23:50
 */
// brute.cpp:小数据暴力解,逐位枚举填数:每层递归给 b[dep] 选一个值 v ∈ [1, a[dep]]。
// 生成完整的 b[1..n] 后,在叶子节点检查"任意相邻两项不等"这个合法条件再计数。
// 只适合小数据(n <= 8,a_i <= 5),用于帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const int MAXN = 15;

int n;
int a[MAXN];   // 输入序列
int b[MAXN];   // 当前填出的完整序列
ll ans;        // 好序列个数

// 检查完整 b[1..n] 是否是好序列:任意相邻两项不等
bool check() {
    for (int i = 1; i < n; i++) {
        if (b[i] == b[i + 1]) return false;
    }
    return true;
}

// 第 dep 层在给 b[dep] 选择一个值,选完后递归下一层
void dfs(int dep) {
    if (dep == n + 1) { // 一条完整序列已经生成
        if (check()) ans++;
        return;
    }
    for (int v = 1; v <= a[dep]; v++) {
        b[dep] = v;
        dfs(dep + 1);
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }

    ans = 0;
    dfs(1);

    cout << ans << '\n';
    return 0;
}

这个暴力第 dep 层给 b[dep][1,adep][1, a_{dep}] 里选一个值,叶子节点检查完整序列是否相邻不等。它枚举的是"完整序列",总数量级 ai\prod a_i,只能处理 n8,ai5n \leqslant 8, a_i \leqslant 5 的小数据。

问题?直接按值做 DP(f[i][j]f[i][j] = 以 bi=jb_i = j 结尾)为什么也不行?

jj 的值域由 aia_i 决定,而 aia_i 最大 10910^9,值域根本枚举不完。所以必须换一种不看具体值、只看"值域分界点"(即 aa 的最小值)的思路。

问题?正着数"所有相邻都不等"到底难在哪?

每个 bib_i 同时受 bi1b_{i-1}bi+1b_{i+1} 两个邻居约束,按"左边不等、右边不等"正向分类会互相重叠,没法拆成独立子问题。

问题?反过来想:如果强制某些位置相邻相等,序列会变成什么?

bi=bi+1b_i = b_{i+1} 的位置叫坏点。指定一个坏点集合 SSbi=bi+1b_i = b_{i+1} 强制成立)后,序列被切成若干连续段:段内所有值相等,段与段之间取值互不限制。坏点个数 = $n - $ 段数。全局耦合约束就这样被拆成了互相独立的段。

问题?一段的取值方案数是多少?

[l,r][l, r] 内所有 bb 取同一个值 vv,要求 vaxv \leqslant a_xlxrl \leqslant x \leqslant r),即 v[1,min(al..ar)]v \in [1, \min(a_l..a_r)],所以一段的取值方案数 = 该段 aa 的最小值。这是整个计数能落地的关键:段内部的选择被压缩成一个"最小值"。

问题?怎么把"指定坏点集合"的计数拼回"零坏点"的答案?

标准容斥:设事件 AiA_i = “bi=bi+1b_i = b_{i+1}”,

Ans=S{1..n1}(1)SN(S) Ans = \sum_{S \subseteq \{1..n-1\}} (-1)^{|S|} \cdot N(S)

其中 N(S)N(S) = 强制 SS 中坏点出现的方案数。因为任意一个真实坏点集合为 TT 的序列 bbN(S)N(S) 计数当且仅当 STS \subseteq T,它对右侧的贡献是 ST(1)S=(11)T\sum_{S \subseteq T} (-1)^{|S|} = (1-1)^{|T|},只有 T=T = \varnothing(好序列)时是 11,其余全是 00

按坏点数分组:设 fn,jf_{n,j} = 把前 nn 项分成 jj 段(段内同值、段间不限)的方案总数,它恰好是所有 S=nj|S| = n - jN(S)N(S) 之和(段数 j    j \iff 坏点数 njn - j),于是

Ans=j=1n(1)njfn,j Ans = \sum_{j=1}^{n} (-1)^{n-j} \, f_{n,j}

问题?fi,jf_{i,j} 怎么递推?

枚举最后一段的起点 kk:前 k1k-1 项分成 j1j-1 段,最后一段 [k,i][k, i] 取值为 min(ak..ai)\min(a_k..a_i),乘法原理:

fi,j=k=1ifk1,j1min(ak..ai) f_{i,j} = \sum_{k=1}^{i} f_{k-1,j-1} \cdot \min(a_k..a_i)

下面用样例 11a=[2,2,2]a = [2,2,2])的 fi,jf_{i,j} 表验证。这张表展示 jj 段分段 DP 的完整状态:行是"已处理到第 ii 项",列是"分成 jj 段",单元格是 fi,jf_{i,j}

i\ji \backslash j 1 2 3
1 2 0 0
2 2 4 0
3 2 8 8

观察 f3,2=8f_{3,2} = 8 这一格(至少一轮转移来源):它由三段拼成—— f3,2=f0,1min(a1..a3)+f1,1min(a2..a3)+f2,1min(a3)=02+22+22=8f_{3,2} = f_{0,1}\min(a_1..a_3) + f_{1,1}\min(a_2..a_3) + f_{2,1}\min(a_3) = 0\cdot 2 + 2\cdot 2 + 2\cdot 2 = 8。 最后答案 =f3,1f3,2+f3,3=28+8=2= f_{3,1} - f_{3,2} + f_{3,3} = 2 - 8 + 8 = 2,正是样例输出。

问题?容斥系数 (1)nj(-1)^{n-j}jj 的具体值重要吗?

nn 是常数,所以只差 jj奇偶。按段数奇偶合并两维:

f[i][p]=jp(mod2)fi,j(p=0,1) f[i][p] = \sum_{j \equiv p \pmod 2} f_{i,j} \quad (p = 0,1)

多分一段就翻转一次奇偶,转移变成:

f[i][p]=k=1if[k1][1p]min(ak..ai) f[i][p] = \sum_{k=1}^{i} f[k-1][1-p] \cdot \min(a_k..a_i)

答案 Ans=(1)n(f[n][0]f[n][1])Ans = (-1)^n \big(f[n][0] - f[n][1]\big)nn 为奇输出 f[n][1]f[n][0]f[n][1]-f[n][0],为偶输出 f[n][0]f[n][1]f[n][0]-f[n][1]。这就是 O(n2)O(n^2) 的 60pts 做法。

问题?O(n2)O(n^2) 的瓶颈在哪?

对每个 ii 都要枚举所有 kk,并且每次重新求 min(ak..ai)\min(a_k..a_i)。这两个都是 O(n)O(n),必须同时优化。

问题?固定 ii 时,min(ak..ai)\min(a_k..a_i)kk 怎么变化?

kkii 往左走,区间变大,最小值只会单调不升,且只在"遇到新的更小值"的位置改变——它是一个阶梯函数,拐点恰好构成一个 aa 值严格递增的单调栈。以 a=[2,1,3]a = [2,1,3]i=3i = 3 为例,这张文本图展示后缀最小值阶梯与单调栈的对应关系:

text
固定 i = 3,a = [2, 1, 3],m(k) = min(a[k..3]) 的阶梯:

m(k)
3 ┤              ┌───┐
  │              │ 3 │   ← k = 3 这一段:min = a[3] = 3
1 ┤──────────────└───┘
  │      min = 1:k = 1..2 这一段(最小值是 a[2] = 1)
  └──────────────────→ k
     1     2     3

拐点位置: 2, 3  ⟺  单调栈 stk = [2, 3](栈内 a 值严格递增)

图中每段水平线上的 min\min 值就是栈内对应拐点的 aa。栈顶被弹出,意味着它被右边更小的值"接管";压入 ii 则产生新的一段。

问题?有了阶梯结构,怎么把 kf[k1][1p]m(k)\sum_k f[k-1][1-p]\cdot m(k) 增量维护成 O(1)O(1)

处理到 ii 时先弹出所有 aaia \geqslant a_i 的栈顶,设 cur = 弹出后的栈顶(栈空为 0)。弹出前那部分阶梯和 curcur 时刻完全相同,其加权和就是 f[cur][p]f[cur][p](已经算过);新产生的只有最后一段 k(cur,i]k \in (cur, i],其上 min=ai\min = a_i,用前缀和 g[i][p]=t=0if[t][p]g[i][p] = \sum_{t=0}^{i} f[t][p] 相减即可:

f[i][p]=f[cur][p]+ai(g[i1][1p]g[cur1][1p]) f[i][p] = f[cur][p] + a_i \cdot \big(g[i-1][1-p] - g[cur-1][1-p]\big)

栈空时 f[cur][p]=0f[cur][p] = 0。下表用 a=[2,1,3]a = [2,1,3] 走一遍奇偶 DP(ffgg 两个状态都列出,ggff 的奇偶前缀和),重点看 i=3i = 3 行如何从 i=2i = 2 行增量得到:

ii f[i][0]f[i][0] f[i][1]f[i][1] g[i][0]g[i][0] g[i][1]g[i][1] 说明
0 1 0 1 0 空前缀分 0 段(偶数)
1 0 2 1 2 栈空,f[1][1]=a1g[0][0]=2f[1][1] = a_1\cdot g[0][0] = 2g[1][1]=g[0][1]+f[1][1]=2g[1][1] = g[0][1] + f[1][1] = 2
2 2 1 3 3 弹出 a1a_1,栈空,f[2][0]=a2g[1][1]=12f[2][0] = a_2\cdot g[1][1] = 1\cdot2g[2][0]=g[1][0]+f[2][0]=1+2=3g[2][0] = g[1][0] + f[2][0] = 1+2 = 3
3 5 7 8 10 [2,3][2,3]f[3][0]=f[2][0]+a3(g[2][1]g[1][1])=2+31=5f[3][0] = f[2][0] + a_3(g[2][1]-g[1][1]) = 2+3\cdot1 = 5f[3][1]=1+3(31)=7f[3][1] = 1+3\cdot(3-1) = 7

验证 i=3i=3f[3][0]=kf[k1][1]m(k)=f[0][1]1+f[1][1]1+f[2][1]3=0+2+3=5f[3][0] = \sum_k f[k-1][1]\,m(k) = f[0][1]\cdot1 + f[1][1]\cdot1 + f[2][1]\cdot3 = 0+2+3 = 5,与增量式一致;答案(nn 奇)=f[3][1]f[3][0]=2= f[3][1]-f[3][0] = 2,直接枚举 {(2,1,2),(2,1,3)}\{(2,1,2),(2,1,3)\} 也是 2。注意 f[i][0]f[i][0] 引用的是奇数前缀和 g[][1]g[\cdot][1]f[i][1]f[i][1] 引用偶数前缀和——"段数加一翻转奇偶"的体现,这是最容易写错的地方。

实现上:f[MAXN][2]g[MAXN][2] 存奇偶两维,stk[] 存单调栈(弹出条件 a[stk[top]]aia[\text{stk}[top]] \geqslant a_i,保证栈内严格递增),cur 就是弹出后的栈顶。所有运算取模,前缀和相减可能为负,乘 aia_i 前先取正;nn 可达 10610^6 需要快读。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-08-28 23:50
 * update_at: 2026-08-28 23:50
 */
// main.cpp:T5 好序列(GoodSeq) 最终解。
// 思路:正难则反 + 容斥(坏点 = 相邻相等的位置)-> 分段 DP(段内同值、段间不限)
//       -> 容斥系数只依赖段数奇偶,丢掉 j 维 -> 单调栈 + 前缀和优化 min 枚举,O(n)。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const int MOD = 998244353;
const int MAXN = 1000005;

int n;
int a[MAXN]; // 输入序列

// f[i][0/1]:前 i 项分成 偶数段 / 奇数段(段内同值、段间取值不限)的方案总数。
//            即 f[i][p] = Σ_{j ≡ p (mod 2)} f_{i,j},其中 f_{i,j} 是分成 j 段的总方案。
// g[i][0/1]:f 的按奇偶分别做前缀和,g[i][p] = Σ_{t=0..i} f[t][p]。
int f[MAXN][2], g[MAXN][2];

int stk[MAXN], top; // 单调栈:栈内 a 值严格递增(从栈底到栈顶),
                    // 栈中位置就是"后缀最小值"阶梯函数的所有拐点

// 快速读入一个整数(n 可达 1e6,关闭同步后仍建议快读)
int read_int() {
    int x = 0;
    char c = getchar();
    while (c < '0' || c > '9') c = getchar();
    while (c >= '0' && c <= '9') {
        x = x * 10 + (c - '0');
        c = getchar();
    }
    return x;
}

void read_input() {
    n = read_int();
    for (int i = 1; i <= n; i++) {
        a[i] = read_int();
    }
}

// 模意义下取正数:x 可能在 (-MOD, 2*MOD) 之间
ll M(ll x) { return (x % MOD + MOD) % MOD; }

void solve() {
    // 空序列分成 0 段:0 是偶数,算 1 种(空划分)
    f[0][0] = 1;
    g[0][0] = 1;

    for (int i = 1; i <= n; i++) {
        // 弹出所有 a >= a[i] 的栈顶:它们不再能作为 (k..i) 的最小值拐点
        while (top && a[stk[top]] >= a[i]) top--;
        int cur = top ? stk[top] : 0; // 弹出后栈顶 cur:最后一个 a < a[i] 的拐点位置
        stk[++top] = i;               // 压入 i 作为新的拐点

        // 关键转移:f[i][p] = Σ_{k=1..i} f[k-1][1-p] * min(a[k..i])
        // 后缀最小值 min(a[k..i]) 是阶梯函数:
        //   k ∈ (stk[s-1], stk[s]] 时,min(a[k..i]) = a[stk[s]]
        // 因此总贡献 = f[cur][p](沿用 cur 处已算好的前 top 层)
        //            + a[i] * (g[i-1][1-p] - g[cur-1][1-p])(新增的最后一段 (cur+1..i))
        if (cur == 0) {
            // 栈空:阶梯只有最后一段 (1..i),min = a[i]
            f[i][0] = M((ll)g[i - 1][1] * a[i]);
            f[i][1] = M((ll)g[i - 1][0] * a[i]);
        } else {
            f[i][0] = M(f[cur][0] + M((ll)M(g[i - 1][1] - g[cur - 1][1]) * a[i]));
            f[i][1] = M(f[cur][1] + M((ll)M(g[i - 1][0] - g[cur - 1][0]) * a[i]));
        }

        // 更新按奇偶分组的前缀和
        g[i][0] = M(g[i - 1][0] + f[i][0]);
        g[i][1] = M(g[i - 1][1] + f[i][1]);
    }

    // 答案 = Σ_j (-1)^{n-j} f_{n,j} = (-1)^n * (f[n][0] - f[n][1])
    ll ans;
    if (n & 1) ans = M(f[n][1] - f[n][0]);
    else ans = M(f[n][0] - f[n][1]);
    cout << ans << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    read_input();
    solve();

    return 0;
}

复杂度

  • 时间:每个元素至多入栈/出栈一次,每次转移 O(1)O(1),总计 O(n)O(n)。实测 n=106n=10^6 约 0.18s。
  • 空间:a,f,g,stka, f, g, stk 四个 O(n)O(n) 数组,约 24MB。

总结

  • 思维链:正向计数卡在"相邻不等的全局耦合" → 正难则反,指定坏点集合把序列切成独立段(段取值数 = 段内最小值)→ 容斥拼回答案 → 系数只差奇偶,丢掉 jj 维 → 瓶颈在枚举 kkmin(ak..ai)\min(a_k..a_i) → 阶梯函数 + 单调栈 + 前缀和增量维护。
  • 两个易错点:①奇偶交叉引用(f[i][0]f[i][0]g[][1]g[\cdot][1]);②容斥系数符号:答案 =(1)n(f[n][0]f[n][1])= (-1)^n(f[n][0]-f[n][1]),奇数 nn 要反过来减。
  • 可迁移:任何"强制一段内相同"的计数问题都可以用同样的坏点/段划分 + 容斥框架;"最小值随区间端点变化的阶梯结构用单调栈维护"是 O(n2)O(n^2) 区间 DP 转 O(n)O(n) 的通用手法。