树状数组 · 信息学学习手册3-优化LIS

第 4 篇 · LIS 与序列结构

本篇例题(编号沿用全册题单):

编号 题目 来源 主考
G 导弹拦截 洛谷 P1020 最长不升子序列 + Dilworth 定理
H 友好城市 洛谷 P2782 排序 + 严格 LIS
I Why Did the Cow Cross the Road II P 洛谷 P3657 稀疏候选 + 降序枚举求 LIS

本篇与前三篇的唯一差别:树状数组(BIT,Fenwick 树)里存的从"前缀和"换成"前缀最大值"。和可减,max 不可减------所以这里所有写法只能"只增不减",7.4 节专讲。

第七章 LIS 的 O(nlog⁡n)O(n\log n)O(nlogn)

7.1 朴素 DP:两个维度只能省一个

定义 :下标 i1<⋯<iki_1<\dots<i_ki1<⋯<ik 且 ai1<⋯<aika_{i_1}<\dots<a_{i_k}ai1<⋯<aik,称为上升子序列;最长者长度即 LIS 长度。"不降""不升""下降"只换不等号。

fi=1+max⁡{fj: j<i, aj<ai},Ans=max⁡1≤i≤nfif_i=1+\max\{f_j:\ j<i,\ a_j<a_i\},\qquad \text{Ans}=\max_{1\le i\le n}f_ifi=1+max{fj: j<i, aj<ai},Ans=1≤i≤nmaxfi

fif_ifi 是以 iii 结尾 的最长上升子序列长度。朴素转移 O(n2)O(n^2)O(n2),n=105n=10^5n=105 时 5×1095\times10^95×109 次比较,必挂。

式子里两个条件:j<ij<ij<i 是下标维 (已扫过的前缀),aj<aia_j<a_iaj<ai 是值域维 。所有 O(nlog⁡n)O(n\log n)O(nlogn) 做法都在干同一件事:让扫描顺序自动满足 j<ij<ij<i,把值域维交给数据结构------二分法交给数组,BIT 法交给树状数组。

7.2 二分法:tails 数组

tails[k] = 长度为 kkk 的上升子序列中结尾元素的最小值,它关于 kkk 严格递增 (否则长度 k+1k+1k+1 那条的前 kkk 项会给出结尾更小的长度 kkk 子序列,矛盾),故可二分。

找第一个 ≥ai\ge a_i≥ai 的位置 ppp,令 tails[p] = a[i];ppp 超出当前长度则长度加一,最终长度即答案。严格上升用 lower_bound,不降用 upper_bound------选错这一个词答案就错。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
const int N = 500005;
int n, a[N], t[N], len;
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    for (int i = 1; i <= n; i++) {
        int p = lower_bound(t + 1, t + len + 1, a[i]) - t;  // 求"不降"改成 upper_bound
        t[p] = a[i];
        if (p > len) len = p;
    }
    printf("%d\n", len);
}

局限很明确:只回答长度,不回答"以每个位置结尾的最优值"。

7.3 BIT 法:下标交给扫描顺序,值域交给树状数组

扫到 iii 时,BIT 里只有 j<ij<ij<i 的 fjf_jfj。把 fjf_jfj 放在下标 aja_jaj(离散化成排名 rj∈1,mr_j\in1,mrj∈1,m)处,则 max⁡{fj: aj<ai}=query(ri−1)\max\{f_j:\ a_j<a_i\}=\text{query}(r_i-1)max{fj: aj<ai}=query(ri−1),转移只剩三步:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
const int N = 500005;
int n, m, a[N], b[N], c[N];
#define lowbit(x) ((x) & -(x))
void update(int x, int v) { for (; x <= m; x += lowbit(x)) c[x] = max(c[x], v); }  // 只取 max
int query(int x) { int r = 0; for (; x > 0; x -= lowbit(x)) r = max(r, c[x]); return r; }
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); b[i] = a[i]; }
    sort(b + 1, b + n + 1);
    m = unique(b + 1, b + n + 1) - b - 1;
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        int r = lower_bound(b + 1, b + m + 1, a[i]) - b;   // 1-based 排名
        int f = query(r - 1) + 1;                          // 严格上升:只查 < a[i]
        update(r, f);
        ans = max(ans, f);
    }
    printf("%d\n", ans);
}

update 里写的是 c[x] = max(c[x], v) 而非 c[x] = v(理由见 7.4);离散化只改下标不改答案,m≤nm\le nm≤n,BIT 按 mmm 开;时间 O(nlog⁡n)O(n\log n)O(nlogn),空间 O(n)O(n)O(n)。它比二分法多的价值在于:给出每个 fif_ifi,且查询范围换成"任意区间"或"树上子树"时只需改边界(第 5 篇)。

7.4 前缀 max 只能"只增不减"

下标 xxx 管辖 (x−lowbit(x), x](x-\mathrm{lowbit}(x),\ x](x−lowbit(x), x](lowbit(x)=x & (−x)\mathrm{lowbit}(x)=x\ \&\ (-x)lowbit(x)=x & (−x)),c[x] 是这段区间的摘要。求和能随便改,因为加法可逆(add(x,-v) 就还原);max 没有逆运算 :update(x,v) 沿 x+=lowbit(x)x\mathrel{+}=\mathrm{lowbit}(x)x+=lowbit(x) 把 vvv 揉进 O(log⁡n)O(\log n)O(logn) 个区间的摘要,而你并没存区间里其它元素的值------改小、删除、撤销全都做不到。

三句话:① 前缀 max 的 BIT 只支持把某个位置的值变大;② 不支持赋值、删除、撤销;③ 用之前必须验证"只增不减"。

LIS 恰好满足:fif_ifi 算出后永不再改,写入是"在 rir_iri 处取 max"。反过来,求方案数 (max 丢掉"并列最优有几个")、动态改序列 (改小或删除)、回滚这三类需求不能硬套。

7.5 什么时候换成区域最值线段树

口诀:修改只会让值变大,用 BIT;修改可能让值变小,用线段树。

需求 选谁 原因
单点取 max(只增)+ 前缀 max 树状数组 常数小、代码两行
单点赋值(可增可减)+ 区间 max 区域最值线段树 叶子存原值,回溯重算
单点赋值 + 前缀 max 区域最值线段树 BIT 的摘要无法重算
需要撤销 / 回滚 线段树或栈式回滚 max 不可逆

"区域最值线段树"即第二行:modify 递归到叶子赋值 t[k]=v,回溯时 t[k]=max(t[2k],t[2k+1]);ask 同为 O(log⁡n)O(\log n)O(logn)。多一个常数、4 倍内存,换来"任意赋值 + 任意区间";本篇三道题全是"取 max",故全程用 BIT。

7.6 四种严格性对照表(抄在草稿纸上)

rir_iri 是升序排名,ri′=m+1−rir'_i=m+1-r_iri′=m+1−ri 是反转排名。

要求的子序列 转移条件 BIT 写法
严格上升 aj<aia_j<a_iaj<ai 原排名,query(r-1)
不降 aj≤aia_j\le a_iaj≤ai 原排名,query(r)
不升 aj≥aia_j\ge a_iaj≥ai 反转排名,query(r')
严格下降 aj>aia_j>a_iaj>ai 反转排名,query(r'-1)

记忆法:"严格"就减一 ;"升变降"就反转排名(把后缀 max 翻成前缀 max)。两条规则独立,可叠加。

例题 G 导弹拦截(洛谷 P1020)

难度 :进阶 主考:最长不升子序列 + Dilworth 定理

题意

依次给出若干枚导弹的高度(正整数,空格分隔,读到文件结束;n≤105n\le 10^5n≤105)。一套系统首发任意高度,之后每发不得高于前一发。第一问:一套系统最多拦多少枚?第二问:拦下全部,至少几套?样例:389 207 155 300 299 170 158 65 → 6 和 2。

触发点

  • n≤105n\le10^5n≤105:朴素 O(n2)O(n^2)O(n2) 的"以 iii 结尾"DP 是 5×1095\times10^95×109,必挂。
  • 转移只依赖"前面值不小于我的位置"------典型的"扫描 + 值域取最值";第二问是"最少分成几组",先想 Dilworth。

★关键结论

第一问 :gig_igi 为以 iii 结尾的最长不升子序列长度,

gi=1+max⁡{gj: j<i, aj≥ai}g_i=1+\max\{g_j:\ j<i,\ a_j\ge a_i\}gi=1+max{gj: j<i, aj≥ai}

是 ≥\ge≥ 不是 >>>:同高度可以连续拦截,所以是不升 不是严格下降。离散化得排名 rir_iri 后 aj≥ai  ⟺  rj≥ria_j\ge a_i\iff r_j\ge r_iaj≥ai⟺rj≥ri,这是后缀 max;取反转排名 ri′=m+1−rir'_i=m+1-r_iri′=m+1−ri,条件变为 rj′≤ri′r'_j\le r'_irj′≤ri′,就成了前缀 max(含相等):gi=1+query(ri′)g_i=1+\text{query}(r'_i)gi=1+query(ri′)。

第二问 :最少系统数 = 最长严格上升子序列长度。推理链:

  1. 一套系统拦下的序列必须不升 (同高度可接着打),故"用 kkk 套拦下全部"   ⟺  \iff⟺ "把原序列划分成 kkk 个不升子序列"。
  2. 定义偏序 i≺j  ⟺  i<ji\prec j\iff i<ji≺j⟺i<j 且 ai≤aja_i\le a_jai≤aj:链 是下标升、值不降,即不升子序列 ;反链 是下标升、值严格上升,即严格上升子序列。
  3. 序列上的 Dilworth 定理:最少链覆盖数 = 最长反链长度 。故第二问 === 最长严格上升子序列长度,hi=1+query(ri−1)h_i=1+\text{query}(r_i-1)hi=1+query(ri−1)。

为什么不能靠贪心模拟糊过去:贪心(每枚放进"能接住且末尾最高"的系统)确实最优,但要高效实现得维护有序 multiset 并二分,正确性还得单独论证;Dilworth 直接把它翻译成第二个 LIS,模板复用、无需再证。

步骤

  1. 读到 EOF,高度存进 a[1..n];排序去重得值域 1,m1,m1,m,lower_bound 求每个 rir_iri。
  2. 第一问:清空 BIT,下标用 ri′=m+1−rir'_i=m+1-r_iri′=m+1−ri,f = query(r'_i) + 1、update(r'_i, f)、取 max。
  3. 第二问:清空 BIT,下标用 rir_iri,f = query(r_i - 1) + 1、update(r_i, f)、取 max,输出两行答案。

代码骨架

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int n, m, a[N], rk[N], b[N], c[N];
#define lowbit(x) ((x) & -(x))
void update(int x, int v) { for (; x <= m; x += lowbit(x)) c[x] = max(c[x], v); }
int query(int x) { int r = 0; for (; x > 0; x -= lowbit(x)) r = max(r, c[x]); return r; }
int solve(bool strictUp) {                 // true: 严格上升(第二问)
    for (int i = 1; i <= m; i++) c[i] = 0;
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        int x = strictUp ? rk[i] : m + 1 - rk[i];   // 不升 -> 反转排名求不降
        int f = query(x - (strictUp ? 1 : 0)) + 1;  // 不升时含相等,故不减 1
        update(x, f);
        ans = max(ans, f);
    }
    return ans;
}
int main() {
    n = 0;
    int x;
    while (scanf("%d", &x) == 1) a[++n] = x;   // 本题输入不定长,读到 EOF
    for (int i = 1; i <= n; i++) b[i] = a[i];
    sort(b + 1, b + n + 1);
    m = unique(b + 1, b + n + 1) - b - 1;
    for (int i = 1; i <= n; i++) rk[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;
    printf("%d\n%d\n", solve(false), solve(true));
    return 0;
}

复杂度

时间 O(nlog⁡n)O(n\log n)O(nlogn)(排序 + 两遍 BIT),空间 O(n)O(n)O(n)。

易错点

  1. 第一问写成严格下降 :条件是 aj≥aia_j\ge a_iaj≥ai(含相等)。样例里没有相等元素,看不出区别;输入改成 155 155 立刻暴露。
  2. 反转排名后仍写 query(x-1) :把相等排除了,第一问偏小。含相等必须是 query(x)。
  3. 两问共用一个 c[] 只清一次 :第二问必须在清零后的 BIT 上跑(写在 solve 里就是防这个)。
  4. 输入读错 :第一行是高度本身 ,不是个数。cin >> n 会把第一个高度当成 nnn,要用 while (scanf("%d", &x) == 1)。

例题 H 友好城市(洛谷 P2782)

难度 :基础 主考:排序 + 严格 LIS

题意

一条河,南北两岸各有一排城市,坐标给出(同岸坐标互不相同)。给出 nnn 对友好城市 (xi,yi)(x_i,y_i)(xi,yi):xix_ixi 北岸坐标,yiy_iyi 南岸坐标。每对之间建一座桥,桥不能相交,问最多建几座。n≤5000n\le 5000n≤5000。样例见题面。

触发点

"配对 + 不能相交"是标准的二维同时递增 约束,看到就该想到"排序 + LIS";n≤5000n\le5000n≤5000 时 O(n2)O(n^2)O(n2) 也能过,但别因此跳过建模------例题 I 就是它的 10510^5105 版本。

★关键结论

把每座桥看成点 (xi,yi)(x_i,y_i)(xi,yi)。设 xi<xjx_i<x_jxi<xj,则两桥不相交   ⟺  yi<yj\iff y_i<y_j⟺yi<yj:yi<yjy_i<y_jyi<yj 时两岸先后顺序一致,不可能相交;yi>yjy_i>y_jyi>yj 就是"左岸靠左连右岸靠右、左岸靠右连右岸靠左",必然交叉。

于是"选一组互不相交的桥"   ⟺  \iff⟺ "按 xxx 排序后选一个 yyy 严格递增的子序列",答案是排序后 yyy 序列的严格 LIS 长度。

排序后为什么不再交叉:① 排序只重排枚举顺序,不改变"选哪些桥";② 排序后任取 i<ji<ji<j 必有 xi<xjx_i<x_jxi<xj(同岸坐标互不相同);③ 于是"不相交"的唯一条件落到 yyy 上,yi<yjy_i<y_jyi<yj------二维几何约束被压成一维大小关系 。本题 yyy 两两不同,严格 LIS 与不降 LIS 同长;写 query(r-1) 更稳,不依赖这一点。

步骤

  1. 读入 nnn 对,按 xxx 升序排序。
  2. 对排序后的 yyy 离散化得排名 ri∈1,mr_i\in1,mri∈1,m,依次 f = query(r_i - 1) + 1、update(r_i, f)、取 max,输出。

代码骨架

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
const int N = 5005;
int n, m, c[N], b[N], rk[N];
struct Node { int x, y; } p[N];
#define lowbit(t) ((t) & -(t))
void update(int x, int v) { for (; x <= m; x += lowbit(x)) c[x] = max(c[x], v); }
int query(int x) { int r = 0; for (; x > 0; x -= lowbit(x)) r = max(r, c[x]); return r; }
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d%d", &p[i].x, &p[i].y);
    sort(p + 1, p + n + 1, [](const Node &u, const Node &v) { return u.x < v.x; });
    for (int i = 1; i <= n; i++) b[i] = p[i].y;
    sort(b + 1, b + n + 1);
    m = unique(b + 1, b + n + 1) - b - 1;
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        rk[i] = lower_bound(b + 1, b + m + 1, p[i].y) - b;
        int f = query(rk[i] - 1) + 1;      // 严格递增:只查 y 更小的
        update(rk[i], f);
        ans = max(ans, f);
    }
    printf("%d\n", ans);
    return 0;
}

复杂度

时间 O(nlog⁡n)O(n\log n)O(nlogn),空间 O(n)O(n)O(n)。

易错点

  1. 两边都排序:只排一侧就对,两边一起排会拆散配对关系,答案全错。
  2. 排一侧、对另一侧忘了"严格" :本题数据上 query(r) 与 query(r-1) 可能同结果,但有重复坐标就会多算。
  3. 按 yyy 排序、对 xxx 求 LIS 同样正确------只要"排序与 LIS 作用在不同两维上"。

例题 I Why Did the Cow Cross the Road II P(洛谷 P3657)

难度 :提高 主考:稀疏候选 + 降序枚举 + LIS

题意

给出两个长度均为 nnn 的序列 AAA、BBB,二者都是 1..n1..n1..n 的排列(n≤105n\le10^5n≤105)。若 ∣Ai−Bj∣≤4|A_i-B_j|\le4∣Ai−Bj∣≤4,可在 AiA_iAi 与 BjB_jBj 间连一条线;每个元素至多连一条,且任意两条不交叉(对 (i,j)(i,j)(i,j) 与 (i′,j′)(i',j')(i′,j′),i<i′i<i'i<i′ 必须 j<j′j<j'j<j′)。求最多连多少条。样例见题面。

触发点

  • n≤105n\le10^5n≤105,但合法配对极其稀疏 :AiA_iAi 只能与 BBB 中值落在 Ai−4, Ai+4A_i-4,\\,A_i+4Ai−4,Ai+4 的位置配对,至多 999 个候选,总数 ≤9n\le 9n≤9n。
  • 点这么少,就不该走"值域 BIT 批量数点"(第 3 篇逆序对的打法),而应把候选点全列出来求最长链 :iii、jjj 同时严格递增的最长链 = 排序后的一维 LIS。

★关键结论

每条可连的线是点 (i,j)(i,j)(i,j);两线不交叉   ⟺  \iff⟺ iii 与 jjj 同时严格递增。所以答案是候选点集在"两维都严格递增"下的最长链:按 iii 递增枚举,对 jjj 求严格 LIS。

唯一的坑:同一个 iii 内的候选必须按 jjj 从大到小处理。

  • 按 iii 递增枚举保证第一维严格递增,"每个 AiA_iAi 至多一条线"也由这点保证。
  • 若同一 iii 内按 jjj 升序 :先更新的小 j1j_1j1 留在 BIT 里,处理 j2>j1j_2>j_1j2>j1 时 query(j_2-1) 读到 f(j_1),就把"同一个 AiA_iAi 用了两次"当成合法链,答案偏大。
  • 按 jjj 降序 :先更新较大的 jjj,之后处理较小 jjj 时前缀查询 1,j−11,j-11,j−1 覆盖不到它,自链被天然排除。

"每个 BjB_jBj 至多一条线"自动成立:不同 iii 若用同一个 jjj,jjj 就不严格递增,LIS 不会同时收下。

候选枚举:BBB 是排列,值 vvv 在 BBB 中的位置唯一,预处理 posB[v] = j;对每个 iii 枚举 v∈Ai−4, Ai+4∩1,nv\inA_i-4,\\,A_i+4\cap1,nv∈Ai−4,Ai+4∩1,n,令 j=posBvj=\text{posB}vj=posBv 即得全部候选。BIT 下标直接用 j∈1,nj\in1,nj∈1,n,不需要离散化。

步骤

  1. 读入 A1..nA1..nA1..n、B1..nB1..nB1..n,填 posB[B[j]] = j;BIT 全 0。
  2. 对 i=1..ni=1..ni=1..n:把 v∈max⁡(1,Ai−4), min⁡(n,Ai+4)v\in\\max(1,A_i-4),\\ \\min(n,A_i+4)v∈max(1,Ai−4), min(n,Ai+4) 对应的 posB[v] 收进临时数组 → 按 jjj 降序 排序 → 依次 f = query(j-1)+1、update(j,f)、取 max。
  3. 输出 ans。

代码骨架

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int n, A[N], B[N], posB[N], c[N], cand[16];
#define lowbit(x) ((x) & -(x))
void update(int x, int v) { for (; x <= n; x += lowbit(x)) c[x] = max(c[x], v); }
int query(int x) { int r = 0; for (; x > 0; x -= lowbit(x)) r = max(r, c[x]); return r; }
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", &A[i]);
    for (int j = 1; j <= n; j++) { scanf("%d", &B[j]); posB[B[j]] = j; }
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        int cnt = 0;
        for (int v = max(1, A[i] - 4); v <= min(n, A[i] + 4); v++)
            cand[++cnt] = posB[v];                 // 值 -> B 中的位置
        sort(cand + 1, cand + cnt + 1, greater<int>());   // 同一 i 内 j 降序!
        for (int t = 1; t <= cnt; t++) {
            int f = query(cand[t] - 1) + 1;        // j 严格递增
            update(cand[t], f);
            ans = max(ans, f);
        }
    }
    printf("%d\n", ans);
    return 0;
}

复杂度

每个 iii 至多 999 个候选,每个候选一次 O(log⁡n)O(\log n)O(logn) 的查询加修改:时间 O(nlog⁡n)O(n\log n)O(nlogn)(常数约 9×29\times29×2),空间 O(n)O(n)O(n)。

易错点

  1. 同一 iii 内按 jjj 升序处理 :答案偏大,本题最高频的错。删掉 greater<int>() 随手就能对拍出差异。
  2. 把值当位置用 :vvv 是值 ,坐标必须经 posB[v] 映射成位置 jjj;直接拿 vvv 当 jjj 同样高频。
  3. 边界越界 :Ai−4A_i-4Ai−4 可能小于 111、Ai+4A_i+4Ai+4 可能大于 nnn,必须 clamp 到 1,n1,n1,n,否则 posB 读到未定义值。
  4. 用 query(j) 而不是 query(j-1) :会允许两条线共用同一个 BjB_jBj,答案偏大。

本篇易错点

  1. 严格性与查询边界必须一一对应 :严格上升 query(r-1),不降 query(r),不升先反转排名再 query(r')。四种组合错一个,往往只错个别数据,按 7.6 的表逐条确认。
  2. 前缀 max 初值取 0,且要清楚它表示"长度 0"而非"非法" :fi=1+query(⋅)f_i=1+\text{query}(\cdot)fi=1+query(⋅),空查询返回 000 恰好得到 fi=1f_i=1fi=1。若另设"不可达"标记,别和这个 0 混用。
  3. 坐标一律 1-based :lower_bound(...) - b 得到的就是 1..m1..m1..m(b 从 1 开始),别再加 1;posB 存 1..n1..n1..n;update 从 1 起步------x = 0 时 lowbit(0) = 0,循环永不前进,直接死循环。
  4. 修改只能是"取 max" :写成 c[x] = v(赋值)或试图"撤回"某次 update,BIT 立刻不正确;需要赋值、删除、回滚时换区域最值线段树(7.5)。
  5. LIS 长度不是方案数 :c[x] = max(c[x], v) 把并列最优的个数全丢掉;要计数得把 max 换成"对等于最优值的状态求和",还要处理重复元素。
  6. BIT 的下标含义每题都可能不同 :例题 G 是离散化排名 1,m1,m1,m,循环上界是 m;例题 I 是**BBB 中的位置** 1,n1,n1,n,上界是 n。都叫"下标",混用就漏值或越界。
相关推荐
果壳science1 小时前
关于光子、量子猜想
笔记·科技·学习·创业创新·学习方法
fanfan_hongyun1 小时前
【UE5.1 创建C++文件】
c++·ue5
动词ing1 小时前
【题目练习】外观数列
算法
余额瞒着我当琳1 小时前
C++11,发展历史浅谈,初始化列表和对象初始化,右值引用和移动语义
java·开发语言·c++·算法
小弥儿1 小时前
GitHub今日热榜 | 2026-10-22:智能体工具退潮,基础层集中上新
学习·开源·github
suhan1 小时前
【数据结构】六、图:4.图的遍历(深度优先算法DFS、广度优先算法BFS)
数据结构·算法·排序算法
suhan1 小时前
【数据结构】六、图:3.十字链表、邻接多重表、边集数组
数据结构·算法·排序算法
Dola_Zou1 小时前
工商业储能系统核心策略
算法·自动化·软件工程
Tsuki_tl1 小时前
【优选算法:双指针】——复写零题解
java·c++·算法·leetcode·数组·双指针·复写零