当dp的转移常数空间非常大,比如dp100000100000,但是dp内维护的值非常小,比如1-10;我们就可以选择维护dp10000010或dp10100000;,而值代表另一个维度;比如 l r的中间翻倍子序列长度,翻倍子序列很小,所以可以维护从任意 r 结尾,子序列长度为 x 的最小 l 来极大缩小常数。
DP 状态的某一维(通常是"长度")很小(≤60),但每一层的转移涉及"在满足某个权值条件的历史状态中找最优值",需要用线段树/树状数组加速。
一、方法总结
核心思想
| 步骤 | 做什么 | 为什么 |
|---|---|---|
| ① 发现长度上界 | 证明/观察到答案枚举长度 ≤ O(logV) | 可以逐长度枚举,不会 TLE |
| ② 定义 DP 状态 | fi,j = 以 i 结尾、长度为 j 的某种极值(通常是最小起点 / 最大终点) | 把"选子序列"转化为递推 |
| ③ 值域限制 → 区间查询 | 转移条件形如 L≤ak≤R,转化为在离散化权值上的区间查询 | 线段树/树状数组直接上 |
| ④ 控制下标顺序 | 正序/倒序枚举 i,保证查到的都是"前面/后面"的状态 | 不用再管下标维,线段树只管权值 |
| ⑤ 离线/半在线回答询问 | 每算完一层,顺手用后缀/前缀最值更新所有询问的答案 | 免去存所有 fi,j |
适用场景速判
看到以下特征就可以往这个方向想:
-
子序列需要满足某种 乘法/指数型增长条件(2x≤y≤3x、y≥kx、y=y×c 等)//dp值限制在非常小的范围
-
多次区间询问,求最长合法子序列长度
-
转移条件是 "前面找一个满足条件的位置,取某个最值"
常见变体
| 变体 | 改哪里 |
|---|---|
| 条件是 y≥2x(只有下界) | rightId 设为 m(无穷大) |
| 条件是 y=2x 精确相等 | 区间退化为单点查询,用数组/map 就行 |
| 求的是最长长度的具体方案 | 多记一个 pre 指针回溯 |
| 询问强制在线 | 把所有 fi,j 存下来,对每列做 RMQ/线段树 |
| 长度不是 log 级而是 O(n) | 失效,得换思路(单调栈/贪心/决策单调性) |
二、通用板子
定义序列 b = (b1, b2, . . . , bm) 是合法的,当且仅当对于所有 2 ≤ i ≤ m,都有
2bi−1 ≤ bi ≤ 3bi−1.
特别地,长度为 1 的序列一定合法。
给定一个长度为 n 的正整数序列 a,需要回答 q 个区间询问。每次给定区间 l, r,求 al
, al+1, . . . , ar 中最长合法子序列的长度。所选元素不要求连续,但必须保持相对顺序。
1 ≤ n, q ≤ 2 · 10e5,1 ≤ ai ≤ 10e18,1 ≤ l ≤ r ≤ n。
核心思路
1. DP 状态设计
设 fi,j 表示以第 i 个元素结尾 、长度为 j 的合法子序列中,最靠左的开头位置(即最小的起始下标)。
-
当 j=1 时,fi,1=i(单个元素自己就是合法子序列)。
-
当 j>1 时,我们要找一个 k<i,满足 2ak≤ai≤3ak,且 fk,j−1 尽可能小(这样得到的序列整体更靠右,更容易被包含在询问区间里)。转移方程:
fi,j=k<i2ak≤ai≤3akminfk,j−1
2. 用线段树加速转移
转移有两个限制:
-
下标限制:k<i(我们只需从小到大或从大到小枚举 i,保证只用到之前的 k)。
-
权值限制:ak∈⌈ai/3⌉,⌊ai/2⌋,等价于 ai∈2ak,3ak。
代码采用了**倒序枚举 i(从 n 到 1)**的巧妙方式:
-
维护一棵线段树,以离散化后的 a 值 为下标,线段树每个节点存储已经遍历过的元素(下标更大)的 fk,j−1 的最小值。
-
对于当前 i,需要找的是后面的某个 k(k>i),满足 2ai≤ak≤3ai。这正好对应线段树上区间 leftIdi,rightIdi 的查询。
-
查询得到的最小值就是 fi,j(代码中记为
cur[i])。 -
然后把当前的 fi,j−1(即
prev[i])更新进线段树,供更前面的 i 使用。
这样,我们在 O(logm)(m 为离散化后数值种类)时间内完成了单次转移。
3. 如何回答区间询问
对于一个询问 l,r,如果存在某个 i∈l,r 使得 fi,j≥l,那就说明能在 l,r 内找到一个长度为 j 的合法子序列(开头 ≥l,结尾 i≤r)。
代码中使用了一个等价但更易实现的判断:
-
计算出
cur[i](即 fi,j)后,求一个后缀最小值数组suf[i] = min_{k \ge i} cur[k]。 -
如果
suf[l] <= r,说明存在一个结尾 ≥l 且开头 ≤r 的长度为 j 的序列(结合 f 的定义,这个序列整体落在 l,r 内的条件被满足),于是更新答案为 j。
由于我们是按长度 j=2,3,... 依次计算的,并且只要当前长度可行就更新 ans[id] = len,最终每个询问就会留下最大可行长度。
核心结构//涉及权值线段树,离散化,不是最基础的板子,仅面向类似题目
// ============================================================
// 通用板子:逐层DP + 权值线段树优化转移
// 解决:区间询问最长合法子序列长度
// 时间复杂度:O(n log n * max_len)
// ============================================================
#include <bits/stdc++.h>
using namespace std;
const int INF = 1e9;
const int MAXN = 200005;
// ---------- 权值线段树(区间最小值)----------
struct SegMin {
int n;
vector<int> t;
SegMin(int m) {
n = 1; while (n < m) n <<= 1;
t.assign(2 * n, INF);
}
void update(int p, int val) {
if (val >= INF) return;
int x = p + n - 1;
if (t[x] <= val) return;
t[x] = val;
for (x >>= 1; x; x >>= 1)
t[x] = min(t[x<<1], t[x<<1|1]);
}
int query(int l, int r) {
if (l > r) return INF;
int res = INF;
l += n - 1; r += n - 1;
while (l <= r) {
if (l & 1) res = min(res, t[l++]);
if (!(r & 1)) res = min(res, t[r--]);
l >>= 1; r >>= 1;
}
return res;
}
};
// ---------- 主逻辑 ----------
void solve(const vector<long long>& a, const vector<pair<int,int>>& queries) {
int n = a.size() - 1; // a[1..n]
int q = queries.size();
// --- Step 1: 离散化 ---
vector<long long> vals(a.begin() + 1, a.end());
sort(vals.begin(), vals.end());
vals.erase(unique(vals.begin(), vals.end()), vals.end());
int m = vals.size();
auto get_id = [&](long long x) {
return lower_bound(vals.begin(), vals.end(), x) - vals.begin() + 1;
};
auto get_end = [&](long long x) { // upper_bound 版本
return upper_bound(vals.begin(), vals.end(), x) - vals.begin();
};
vector<int> pos(n + 1), L(n + 1), R(n + 1);
for (int i = 1; i <= n; ++i) {
pos[i] = get_id(a[i]);
L[i] = get_id(a[i] * 2); // ← 题目条件:2*a[i] ≤ a[k]
R[i] = get_end(a[i] * 3 - 1); // ← 题目条件:a[k] ≤ 3*a[i]
}
// --- Step 2: 初始化长度为1 ---
vector<int> prev(n + 2);
for (int i = 1; i <= n; ++i) prev[i] = i;
// --- Step 3: 逐层 DP ---
vector<int> cur(n + 2);
vector<int> suf(n + 2);
vector<int> ans(q, 1);
SegMin seg(m);
for (int len = 2; ; ++len) {
seg = SegMin(m); // 重置
fill(cur.begin(), cur.end(), INF);
// 倒序枚举(找后面的 k > i)
for (int i = n; i >= 1; --i) {
if (L[i] <= R[i])
cur[i] = seg.query(L[i], R[i]);
seg.update(pos[i], prev[i]);
}
// 后缀最小值
int best = INF;
for (int i = n; i >= 1; --i) {
best = min(best, cur[i]);
suf[i] = best;
}
if (best >= INF) break; // 这一层已经没有合法序列了
// 更新所有询问
for (int id = 0; id < q; ++id) {
int l = queries[id].first, r = queries[id].second;
if (suf[l] <= r) ans[id] = len;
}
prev.swap(cur);
}
// 输出
for (int x : ans) cout << x << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q; cin >> n >> q;
vector<long long> a(n + 1);
for (int i = 1; i <= n; ++i) cin >> a[i];
vector<pair<int,int>> queries(q);
for (int i = 0; i < q; ++i) cin >> queries[i].first >> queries[i].second;
solve(a, queries);
return 0;
}
权值线段树 SegMin
struct SegMin {
int n;
vector<int> t;
SegMin(int m) {
n = 1; while (n < m) n <<= 1;
t.assign(2 * n, INF);
}
void update(int p, int val) { ... }
int query(int l, int r) { ... }
};
| 普通线段树 | 权值线段树 | |
|---|---|---|
| 下标含义 | 数组的位置(第几个元素) | 元素的值本身 |
| 叶子节点 | 对应 a[i] 这个位置的数 |
对应值为 x 的数有多少个 |
| 典型用途 | 区间求和、区间修改、区间最值 | 查排名、查第 k 大、统计某个值范围内有多少个数 |
这是一个维护区间最小值 的线段树,但它的下标不是数组的位置,而是"数值离散化后的值"。
-
比如原数组里有数字
{3, 5, 12, 100},离散化后变成{1, 2, 3, 4}。 -
那线段树下标
1~4分别对应这些数。 -
在每个下标里,我们存的是:已经遍历过的、值为该数的那些元素,它们的 DP 值的最小值。
两个操作:
-
update(p, val):告诉线段树"值为p的位置,我来报到一个值val,你帮我取个 min"。 -
query(l, r):问线段树"在区间[l, r](也就是值在[l, r]范围内的所有元素)里,最小的 DP 值是多少?"
为什么用线段树?
因为我们要快速在一段"权值区间"里找最小值,暴力扫是 O(n) 的,线段树是 O(logn)。
离散化部分
vector<long long> vals(a.begin() + 1, a.end());
sort(vals.begin(), vals.end());
vals.erase(unique(vals.begin(), vals.end()), vals.end());
int m = vals.size();
原数组 ai 的范围是 1∼10e18,不能直接当下标用。离散化就是把它们压缩到 1∼m(m≤n),让线段树开得下。
auto get_id = [&](long long x) {
return lower_bound(vals.begin(), vals.end(), x) - vals.begin() + 1;
};
二分查找:给我一个数 x,告诉我它在离散化数组里的编号(从 1 开始,方便线段树)。
pos[i] = get_id(a[i]); // a[i] 自己的编号
L[i] = get_id(a[i] * 2); // 2*a[i] 的下界编号
R[i] = get_end(a[i] * 3 - 1); // 3*a[i] 的上界编号
回忆题目要求:2ak≤ai≤3ak(如果我们正序 DP 找前面的 k)。
但我们的板子是倒序 做的,含义反过来:找后面的 k,满足 2ai≤ak≤3ai。
-
L[i]:后面那个数 ak 至少要是 2ai,所以在离散化数组里的最小位置。 -
R[i]:后面那个数 ak 最多是 3ai,所以在离散化数组里的最大位置。 -
也就是说,后面所有合法的 k,它们的
pos[k]一定落在[L[i], R[i]]这个闭区间里。
改成别的题时需要动的地方
| 位置 | 改什么 |
|---|---|
L[i] = get_id(a[i] * 2) |
改成下界条件 |
R[i] = get_end(a[i] * 3 - 1) |
改成上界条件 |
seg.query(L[i], R[i]) |
如果要求最大值就换成 SegMax,相应改 min→max、INF→-INF |
prev[i] = i |
如果 DP 定义变了(比如以 i 开头的),初始化跟着改 |
suf[l] <= r |
判断条件根据你定义的 f 的含义调整 |