把按「上一个数是谁」转移的 O(n^2) 子序列 DP 弱化成按二进制位建 30 个桶, 每次读自身 1 位的桶最大值再加一写回, 做到 O(n log A).
OJ: luogu
题目 ID: P4310
难度:普及+/提高-
标签:动态规划位运算状态设计
创建: 2026-10-26 13:06
更新: 2026-10-26 16:36
形式化题目
给定长度为
求最长的下标递增子序列
输出最大的
样例
样例
, 接不上. , 与 能接上. , 与 也能接上.
所以能取到长度
暴力解法
思路
按定义做子序列 DP. 令
初值
这个转移不漏方案: 任何合法子序列的最后两项一定是一对能接上的
代码
/**
* 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-10-26 13:20
* update_at: 2026-10-26 16:36
*/
// brute.cpp:小数据暴力解,直接枚举子序列的上一个元素,O(n^2)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 5005;
ll n;
ll a[MAXN]; // 输入的数列
ll f[MAXN]; // f[i]:以第 i 个数结尾的最长合法子序列长度
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (ll i = 1; i <= n; i++) {
cin >> a[i];
}
ll ans = 1;
// 枚举每一对 (j, i),j < i 且 a[j] & a[i] != 0 时可以把 i 接在 j 后面。
for (ll i = 1; i <= n; i++) {
f[i] = 1;
for (ll j = 1; j < i; j++) {
if ((a[j] & a[i]) != 0 && f[j] + 1 > f[i]) {
f[i] = f[j] + 1;
}
}
if (f[i] > ans) {
ans = f[i];
}
}
cout << ans << '\n';
return 0;
}复杂度
时间
瓶颈
瓶颈是内层扫描要为每个
不过转移只用到历史元素的一个性质: 它和
正解
关键观察
把状态从"末项的具体数值"放宽成"末项含有哪一位".
令
为什么会想到这样合并? 因为"
于是转移变成两步:
- 读出:
. 没有可用位时( )取 . - 写回: 对
的每个 位 , 令 .
样例推演
表里用
| 处理到 | 读到的 |
写回后 |
答案 | ||
|---|---|---|---|---|---|
| 初始 | — | — | 0 | ||
| 1 | |||||
| 1 | |||||
| 2 |
第三行最能说明问题:
思路
对每个读入的数
顺序不能交换. 若写成边读边写, 处理
正确性由两条夹逼给出:
不超过 : 设 给出最大值 . 按归纳, 前缀里存在一个下标 、含第 位、长度为 的合法子序列. 与 共享第 位, 所以 , 接上 就得到长度 的合法子序列. 不超过 : 取达到 的合法子序列, 前一项是 . , 取一个公共位 . 由归纳 , 而 也是 的 1 位, 所以 . - 两条合起来
, 写回操作与 的定义一致, 归纳成立.
代码
main.cpp 是正式主解. main.py 是同一算法的 Python 实现, 复杂度同阶, 与 C++ 并列放在这一节.
/**
* 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-10-26 13:20
* update_at: 2026-10-26 16:36
*/
// main.cpp:按二进制位维护最长合法子序列长度,把 O(n^2) 的枚举压成 O(n * 30)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXB = 31; // a_i <= 1e9 < 2^30,最多用到第 30 位
ll n;
ll best[MAXB]; // best[b]:以某个含第 b 位的数结尾的、最长合法子序列的长度
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
ll ans = 0;
for (ll i = 1; i <= n; i++) {
ll x;
cin >> x;
// 第 i 个数能接在任何一个末项与它有公共位的子序列后面,取这些长度里的最大值。
ll cur = 0;
for (int b = 0; b < MAXB; b++) {
if (((x >> b) & 1) && best[b] > cur) {
cur = best[b];
}
}
cur = cur + 1; // 接上自己
// 以 x 结尾的最优子序列长度 cur,可以更新 x 的每一个二进制位。
for (int b = 0; b < MAXB; b++) {
if (((x >> b) & 1) && cur > best[b]) {
best[b] = cur;
}
}
if (cur > ans) {
ans = cur;
}
}
cout << ans << '\n';
return 0;
}#!/usr/bin/env python3
# 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-10-26 13:30
# update_at: 2026-10-26 13:30
import sys
BITS = 30 # a_i <= 1e9 < 2^30,只需要看低 30 位
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
seq = [next(data) for _ in range(n)]
best = [0] * BITS # best[b]:以某个含第 b 位的数结尾的最长合法子序列长度
ans = 0
for x in seq:
# 所有与 x 有公共位的末项都能接上 x,取其中最长的那个
length = 1 + max((best[b] for b in range(BITS) if x >> b & 1), default=0)
for b in range(BITS):
if x >> b & 1:
best[b] = max(best[b], length)
ans = max(ans, length)
print(ans)
if __name__ == "__main__":
solve()复杂度
每个元素只扫
- 时间
, 其中 . - 空间
, 只需要 30 个桶; C++ 实现读一个处理一个, 不保存整个序列.
总结
本题的关键不是位运算本身, 而是把 DP 转移条件里"用不到的信息"删掉.
同一个套路可以迁移到其他"末项只需满足某个弱条件"的子序列问题, 例如按值域分桶的 LIS 优化.