树状数组优化动态规划
树状数组优化动态规划:从入门到精通(C++实现)
一、树状数组基础回顾
1.1 树状数组概述
树状数组(Fenwick Tree)是一种高效处理前缀和查询和单点更新的数据结构,由Peter Fenwick于1994年提出。其核心优势在于:
- 简洁的实现(仅需约10行代码)
- O(log n)的单点更新和前缀查询复杂度
- 极低的时间常数(位运算优化)
1.2 标准树状数组实现
class FenwickTree {
private:vector<int> tree;int n;public:FenwickTree(int size) : n(size), tree(size + 1, 0) {}// 单点更新:位置index增加deltavoid update(int index, int delta) {for (; index <= n; index += index & -index)tree[index] += delta;}// 前缀查询:[1, index]的和int query(int index) {int sum = 0;for (; index > 0; index -= index & -index)sum += tree[index];return sum;}// 区间查询:[left, right]的和int range_query(int left, int right) {return query(right) - query(left - 1);}
};
1.3 树状数组的特性
操作 | 时间复杂度 | 空间复杂度 |
---|---|---|
构造 | O(n) | O(n) |
单点更新 | O(log n) | O(1) |
前缀查询 | O(log n) | O(1) |
区间查询 | O(log n) | O(1) |
二、树状数组优化DP的原理
2.1 优化场景分析
当动态规划的状态转移方程满足以下形式时:
dp[i] = f( dp[j] ) for j in [L(i), R(i)]
其中:
- L(i)和R(i)是关于i的函数
- f为可累加函数(求和、最大值、最小值等)
使用树状数组可将转移复杂度从O(n)降至O(log n)。
2.2 优化核心思想
- 状态重定义:将DP状态看作序列
- 坐标映射:根据转移条件建立映射关系
- 实时更新:按顺序处理状态并更新数据结构
- 高效查询:利用树状数组快速获取区间信息
2.3 适用问题特征
- 区间依赖:状态i依赖于区间[L(i), R(i)]
- 顺序处理:状态可按特定顺序计算(如升序、降序)
- 可离散化:索引范围较大但可压缩
- 可累加:转移函数支持增量计算
三、一维树状数组优化DP
3.1 最长上升子序列(LIS)优化
问题描述:求序列的最长上升子序列长度
状态转移:
dp[i] = max{ dp[j] | j < i && a[j] < a[i] } + 1
优化实现:
int lengthOfLIS(vector<int>& nums) {// 离散化处理vector<int> sorted = nums;sort(sorted.begin(), sorted.end());sorted.erase(unique(sorted.begin(), sorted.end()), sorted.end());int n = nums.size();FenwickTreeMax tree(n); // 最大值树状数组vector<int> dp(n, 1);int ans = 0;for (int i = 0; i < n; i++) {// 获取当前值在离散化后的位置int pos = lower_bound(sorted.begin(), sorted.end(), nums[i]) - sorted.begin();// 查询[1, pos-1]的最大dp值if (pos > 1) {dp[i] = tree.query(pos - 1) + 1;}ans = max(ans, dp[i]);// 更新当前位置的最大值tree.update(pos, dp[i]);}return ans;
}
3.2 最大值树状数组实现
class FenwickTreeMax {
private:vector<int> tree;int n;public:FenwickTreeMax(int size) : n(size), tree(size + 1, INT_MIN) {}void update(int index, int value) {for (; index <= n; index += index & -index)tree[index] = max(tree[index], value);}int query(int index) {int res = INT_MIN;for (; index > 0; index -= index & -index)res = max(res, tree[index]);return res;}
};
3.3 优化效果对比
方法 | 时间复杂度 | 空间复杂度 | n=10^5时的耗时 |
---|---|---|---|
朴素DP | O(n²) | O(n) | >10秒 |
树状数组优化 | O(n log n) | O(n) | <50毫秒 |
四、二维树状数组优化DP
4.1 二维树状数组实现
class FenwickTree2D {
private:vector<vector<int>> tree;int n, m;public:FenwickTree2D(int rows, int cols) : n(rows), m(cols), tree(rows + 1, vector<int>(cols + 1, 0)) {}void update(int x, int y, int delta) {for (int i = x; i <= n; i += i & -i)for (int j = y; j <= m; j += j & -j)tree[i][j] += delta;}int query(int x, int y) {int sum = 0;for (int i = x; i > 0; i -= i & -i)for (int j = y; j > 0; j -= j & -j)sum += tree[i][j];return sum;}int range_query(int x1, int y1, int x2, int y2) {return query(x2, y2) - query(x1-1, y2) - query(x2, y1-1) + query(x1-1, y1-1);}
};
4.2 矩阵最大和路径问题
问题描述:在n×m矩阵中找一条从左上到右下的路径,使得路径和最大
状态转移:
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + grid[i][j]
树状数组优化:
int maxPathSum(vector<vector<int>>& grid) {int n = grid.size(), m = grid[0].size();FenwickTree2DMax tree(n, m); // 二维最大值树状数组vector<vector<int>> dp(n, vector<int>(m, 0));for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {int left = (j > 0) ? tree.query(i+1, j) : INT_MIN;int up = (i > 0) ? tree.query(i, j+1) : INT_MIN;dp[i][j] = grid[i][j] + max(0, max(left, up));tree.update(i+1, j+1, dp[i][j]);}}return dp[n-1][m-1];
}
4.3 二维最大值树状数组
class FenwickTree2DMax {
private:vector<vector<int>> tree;int n, m;public:FenwickTree2DMax(int rows, int cols) : n(rows), m(cols), tree(rows + 1, vector<int>(cols + 1, INT_MIN)) {}void update(int x, int y, int value) {for (int i = x; i <= n; i += i & -i)for (int j = y; j <= m; j += j & -j)tree[i][j] = max(tree[i][j], value);}int query(int x, int y) {int res = INT_MIN;for (int i = x; i > 0; i -= i & -i)for (int j = y; j > 0; j -= j & -j)res = max(res, tree[i][j]);return res;}
};
五、树状数组优化区间DP
5.1 区间划分问题
问题描述:将序列划分为k段,使得每段和的最大值最小
状态转移:
dp[i][j] = min{ max(dp[k][j-1], sum[k+1..i]) } for k < i
优化实现:
int splitArray(vector<int>& nums, int k) {int n = nums.size();vector<int> prefix(n + 1, 0);for (int i = 1; i <= n; i++)prefix[i] = prefix[i - 1] + nums[i - 1];// dp[j] 表示前i个元素划分j段的最小最大和vector<int> dp(n + 1, INT_MAX);vector<int> new_dp(n + 1, INT_MAX);dp[0] = 0;for (int seg = 1; seg <= k; seg++) {FenwickTreeMin tree(prefix[n]); // 最小值树状数组fill(new_dp.begin(), new_dp.end(), INT_MAX);tree.update(1, 0); // 初始化for (int i = 1; i <= n; i++) {// 查询满足 sum <= prefix[i] 的最小dp值int best = tree.query(prefix[i]);if (best != INT_MAX) {new_dp[i] = max(best, prefix[i] - prefix[0]);}// 更新树状数组if (dp[i] != INT_MAX) {tree.update(prefix[i] + 1, dp[i]);}}swap(dp, new_dp);}return dp[n];
}
5.2 最小值树状数组实现
class FenwickTreeMin {
private:vector<int> tree;int n;const int INF = INT_MAX;public:FenwickTreeMin(int size) : n(size), tree(size + 1, INF) {}void update(int index, int value) {for (; index <= n; index += index & -index)tree[index] = min(tree[index], value);}int query(int index) {int res = INF;for (; index > 0; index -= index & -index)res = min(res, tree[index]);return res;}
};
六、树状数组在计数类DP中的应用
6.1 逆序对计数问题
问题描述:计算序列中逆序对的数量
状态转移:
count = sum{ 1 | j < i && a[j] > a[i] }
优化实现:
int countInversions(vector<int>& nums) {// 离散化处理vector<int> sorted = nums;sort(sorted.begin(), sorted.end());sorted.erase(unique(sorted.begin(), sorted.end()), sorted.end());int n = nums.size();FenwickTree tree(n);int count = 0;// 从后往前处理for (int i = n - 1; i >= 0; i--) {int pos = lower_bound(sorted.begin(), sorted.end(), nums[i]) - sorted.begin();count += tree.query(pos); // 查询小于当前值的数量tree.update(pos + 1, 1); // 更新当前位置}return count;
}
6.2 区间计数问题
问题描述:统计满足条件的区间数量
状态转移:
count = sum{ 1 | L ≤ i ≤ R, condition }
优化实现:
int countRangeSums(vector<int>& nums, int lower, int upper) {int n = nums.size();vector<long> prefix(n + 1, 0);for (int i = 0; i < n; i++)prefix[i + 1] = prefix[i] + nums[i];// 离散化所有可能的前缀和set<long> values;for (long p : prefix) {values.insert(p);values.insert(p - lower);values.insert(p - upper);}vector<long> sorted(values.begin(), values.end());FenwickTree tree(sorted.size());int count = 0;tree.update(lower_bound(sorted.begin(), sorted.end(), prefix[0]) - sorted.begin() + 1, 1);for (int i = 1; i <= n; i++) {long p = prefix[i];int left = lower_bound(sorted.begin(), sorted.end(), p - upper) - sorted.begin() + 1;int right = lower_bound(sorted.begin(), sorted.end(), p - lower) - sorted.begin() + 1;count += tree.range_query(left, right);tree.update(lower_bound(sorted.begin(), sorted.end(), p) - sorted.begin() + 1, 1);}return count;
}
七、树状数组优化背包问题
7.1 多重背包优化
问题描述:物品有限数量,求不超过容量的最大价值
优化思路:按模分组 + 树状数组优化
int knapsack(vector<int>& weights, vector<int>& values, vector<int>& counts, int capacity) {int n = weights.size();vector<int> dp(capacity + 1, 0);for (int i = 0; i < n; i++) {int w = weights[i], v = values[i], c = counts[i];vector<int> temp = dp;FenwickTreeMax tree(capacity); // 最大值树状数组// 按模w分组处理for (int j = 0; j < w; j++) {for (int k = j, cnt = 0; k <= capacity; k += w, cnt++) {tree.update(k + 1, temp[k]);if (cnt > c) {int remove = k - w * (c + 1);if (remove >= 0) tree.update(remove + 1, INT_MIN);}dp[k] = max(dp[k], tree.query(k + 1) + cnt * v);}}}return dp[capacity];
}
7.2 完全背包优化
问题描述:物品无限数量,求恰好达到容量的方案数
优化实现:
int coinChange(vector<int>& coins, int amount) {FenwickTree tree(amount);vector<int> dp(amount + 1, 0);dp[0] = 1;tree.update(1, 1); // dp[0] = 1for (int coin : coins) {for (int j = coin; j <= amount; j++) {dp[j] += tree.query(j - coin + 1);if (dp[j] != 0) {tree.update(j + 1, dp[j]);}}}return dp[amount];
}
八、树状数组在树形DP中的应用
8.1 树上路径统计
问题描述:统计树上满足条件的路径数量
优化思路:DFS序 + 树状数组
vector<vector<int>> tree;
vector<int> in, out;
int timer = 0;void dfs(int u, int parent) {in[u] = ++timer;for (int v : tree[u]) {if (v == parent) continue;dfs(v, u);}out[u] = timer;
}int countPaths(int n, vector<pair<int, int>>& edges, int maxDist) {tree.resize(n + 1);in.resize(n + 1);out.resize(n + 1);for (auto& e : edges) {tree[e.first].push_back(e.second);tree[e.second].push_back(e.first);}dfs(1, 0);FenwickTree fenw(n);vector<int> nodes(n);iota(nodes.begin(), nodes.end(), 1);sort(nodes.begin(), nodes.end(), [&](int a, int b) {return in[a] < in[b];});int ans = 0;for (int i = 0, j = 0; i < n; i++) {// 移除距离超过maxDist的节点while (j < i && dist(nodes[i], nodes[j]) > maxDist) {fenw.update(in[nodes[j]], -1);j++;}// 查询当前节点子树内的节点数ans += fenw.range_query(in[nodes[i]], out[nodes[i]]);fenw.update(in[nodes[i]], 1);}return ans;
}
8.2 子树查询优化
问题描述:维护子树信息并支持更新
优化实现:
class TreeDP {
private:vector<vector<int>> tree;vector<int> in, out;FenwickTree fenw;int timer = 0;void dfs(int u, int parent) {in[u] = ++timer;for (int v : tree[u]) {if (v == parent) continue;dfs(v, u);}out[u] = timer;}public:TreeDP(int n, vector<pair<int, int>>& edges) : fenw(n), tree(n+1), in(n+1), out(n+1) {for (auto& e : edges) {tree[e.first].push_back(e.second);tree[e.second].push_back(e.first);}dfs(1, 0);}void update(int node, int delta) {fenw.update(in[node], delta);}int query_subtree(int node) {return fenw.range_query(in[node], out[node]);}int query_path(int u, int v) {// 路径查询需要LCA支持// 简化版:假设查询到根的路径return fenw.range_query(1, in[u]);}
};
九、树状数组优化DP的进阶技巧
9.1 滚动数组优化空间
int optimizedLIS(vector<int>& nums) {// 离散化处理vector<int> sorted = nums;sort(sorted.begin(), sorted.end());sorted.erase(unique(sorted.begin(), sorted.end()), sorted.end());int n = nums.size();FenwickTreeMax tree(n);int ans = 0;for (int i = 0; i < n; i++) {int pos = lower_bound(sorted.begin(), sorted.end(), nums[i]) - sorted.begin();int cur = 1;if (pos > 0) cur = tree.query(pos) + 1;ans = max(ans, cur);tree.update(pos + 1, cur);}return ans;
}
9.2 延迟更新技巧
class LazyFenwickTree {
private:vector<int> tree;vector<int> lazy;int n;public:LazyFenwickTree(int size) : n(size), tree(size+1, 0), lazy(size+1, 0) {}void range_update(int l, int r, int delta) {update(l, delta);update(r+1, -delta);}void update(int index, int delta) {for (; index <= n; index += index & -index)lazy[index] += delta;}int query(int index) {int sum = 0;for (int i = index; i > 0; i -= i & -i)sum += lazy[i];return sum;}int range_query(int l, int r) {return query(r) - query(l-1);}
};
9.3 高维树状数组优化
class FenwickTree3D {
private:vector<vector<vector<int>>> tree;int x, y, z;public:FenwickTree3D(int x, int y, int z) : x(x), y(y), z(z), tree(x+1, vector<vector<int>>(y+1, vector<int>(z+1, 0))) {}void update(int i, int j, int k, int delta) {for (int a = i; a <= x; a += a & -a)for (int b = j; b <= y; b += b & -b)for (int c = k; c <= z; c += c & -c)tree[a][b][c] += delta;}int query(int i, int j, int k) {int sum = 0;for (int a = i; a > 0; a -= a & -a)for (int b = j; b > 0; b -= b & -b)for (int c = k; c > 0; c -= c & -c)sum += tree[a][b][c];return sum;}int range_query(int x1, int y1, int z1, int x2, int y2, int z2) {return query(x2, y2, z2) - query(x1-1, y2, z2) - query(x2, y1-1, z2) - query(x2, y2, z1-1) +query(x1-1, y1-1, z2) + query(x1-1, y2, z1-1) + query(x2, y1-1, z1-1) - query(x1-1, y1-1, z1-1);}
};
十、性能分析与对比
10.1 树状数组 vs 线段树
特性 | 树状数组 | 线段树 |
---|---|---|
代码复杂度 | 简单 | 较复杂 |
时间复杂度 | O(log n) | O(log n) |
空间复杂度 | O(n) | O(4n) |
支持区间更新 | 有限支持 | 完全支持 |
支持区间查询 | 前缀区间 | 任意区间 |
高维扩展 | 简单 | 复杂 |
10.2 优化效果对比
问题类型 | 朴素DP复杂度 | 树状数组优化后 | 加速比 |
---|---|---|---|
最长上升子序列 | O(n²) | O(n log n) | 1000倍 |
逆序对计数 | O(n²) | O(n log n) | 1000倍 |
区间计数 | O(n²) | O(n log n) | 1000倍 |
背包问题 | O(nW) | O(n√W) | 100倍 |
树形DP查询 | O(n) | O(log n) | 100倍 |
十一、实战训练(附C++代码)
11.1 CodeForces 597C:子序列计数
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;class FenwickTree {
private:vector<ll> tree;int n;
public:FenwickTree(int size) : n(size), tree(size + 1, 0) {}void update(int index, ll delta) {for (; index <= n; index += index & -index)tree[index] += delta;}ll query(int index) {ll sum = 0;for (; index > 0; index -= index & -index)sum += tree[index];return sum;}
};int main() {int n, k;cin >> n >> k; k++;vector<int> a(n);for (int i = 0; i < n; i++) cin >> a[i];vector<FenwickTree> trees(k + 1, FenwickTree(n));vector<ll> dp(n, 1);for (int i = 0; i < n; i++) {trees[1].update(a[i], 1);for (int j = 2; j <= k; j++) {ll cnt = trees[j-1].query(a[i]-1);dp[i] = cnt;trees[j].update(a[i], cnt);}}ll ans = 0;for (int i = 0; i < n; i++) ans += dp[i];cout << ans << endl;return 0;
}
11.2 LeetCode 315:计算右侧小于当前元素的个数
class Solution {
public:vector<int> countSmaller(vector<int>& nums) {// 离散化处理vector<int> sorted = nums;sort(sorted.begin(), sorted.end());sorted.erase(unique(sorted.begin(), sorted.end()), sorted.end());int n = nums.size();FenwickTree tree(n);vector<int> counts(n, 0);// 从右向左处理for (int i = n - 1; i >= 0; i--) {int pos = lower_bound(sorted.begin(), sorted.end(), nums[i]) - sorted.begin();counts[i] = tree.query(pos); // 查询小于当前值的数量tree.update(pos + 1, 1); // 更新当前位置}return counts;}private:class FenwickTree {vector<int> tree;int n;public:FenwickTree(int size) : n(size), tree(size + 1, 0) {}void update(int index, int delta) {for (; index <= n; index += index & -index)tree[index] += delta;}int query(int index) {int sum = 0;for (; index > 0; index -= index & -index)sum += tree[index];return sum;}};
};
11.3 LeetCode 327:区间和的个数
class Solution {
public:int countRangeSums(vector<int>& nums, int lower, int upper) {long sum = 0;vector<long> prefix = {0};for (int num : nums) {sum += num;prefix.push_back(sum);}// 离散化所有可能的前缀和set<long> values;for (long p : prefix) {values.insert(p);values.insert(p - lower);values.insert(p - upper);}vector<long> sorted(values.begin(), values.end());FenwickTree tree(sorted.size());int count = 0;tree.update(getIndex(sorted, prefix[0]), 1);for (int i = 1; i < prefix.size(); i++) {long p = prefix[i];int left = getIndex(sorted, p - upper);int right = getIndex(sorted, p - lower);count += tree.range_query(left, right);tree.update(getIndex(sorted, p), 1);}return count;}private:int getIndex(vector<long>& sorted, long value) {return lower_bound(sorted.begin(), sorted.end(), value) - sorted.begin() + 1;}class FenwickTree {vector<int> tree;int n;public:FenwickTree(int size) : n(size), tree(size + 1, 0) {}void update(int index, int delta) {for (; index <= n; index += index & -index)tree[index] += delta;}int query(int index) {int sum = 0;for (; index > 0; index -= index & -index)sum += tree[index];return sum;}int range_query(int left, int right) {if (left > right) return 0;return query(right) - query(left - 1);}};
};
十二、总结与扩展学习
12.1 树状数组优化DP要点
- 识别优化点:状态转移是否依赖区间信息
- 设计状态映射:将DP状态映射到树状数组索引
- 选择树状数组类型:标准、最大值、最小值
- 处理边界条件:离散化、索引偏移
- 更新顺序:按状态依赖顺序处理
12.2 扩展学习资源
-
书籍推荐:
- 《算法竞赛进阶指南》- 李煜东
- 《挑战程序设计竞赛》- 秋叶拓哉
-
在线题库:
- LeetCode:315, 327, 493, 673
- CodeForces:61E, 597C, 1311F
- POJ:2352, 2481, 3067
-
进阶技巧:
- 树状数组上二分搜索
- 可持久化树状数组
- 树状数组维护差分数组
- 树状数组结合分块
“树状数组虽小,能量巨大。掌握它,你将在算法竞赛中拥有无往不利的利器。” —— 匿名算法竞赛选手
通过系统学习树状数组优化DP的技巧,能够解决许多原本复杂度过高的问题。关键在于不断练习,将理论转化为实践!