当前位置: 首页 > news >正文

树状数组优化动态规划

树状数组优化动态规划:从入门到精通(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 优化核心思想

  1. 状态重定义:将DP状态看作序列
  2. 坐标映射:根据转移条件建立映射关系
  3. 实时更新:按顺序处理状态并更新数据结构
  4. 高效查询:利用树状数组快速获取区间信息

2.3 适用问题特征

  1. 区间依赖:状态i依赖于区间[L(i), R(i)]
  2. 顺序处理:状态可按特定顺序计算(如升序、降序)
  3. 可离散化:索引范围较大但可压缩
  4. 可累加:转移函数支持增量计算

三、一维树状数组优化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时的耗时
朴素DPO(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要点

  1. 识别优化点:状态转移是否依赖区间信息
  2. 设计状态映射:将DP状态映射到树状数组索引
  3. 选择树状数组类型:标准、最大值、最小值
  4. 处理边界条件:离散化、索引偏移
  5. 更新顺序:按状态依赖顺序处理

12.2 扩展学习资源

  1. 书籍推荐

    • 《算法竞赛进阶指南》- 李煜东
    • 《挑战程序设计竞赛》- 秋叶拓哉
  2. 在线题库

    • LeetCode:315, 327, 493, 673
    • CodeForces:61E, 597C, 1311F
    • POJ:2352, 2481, 3067
  3. 进阶技巧

    • 树状数组上二分搜索
    • 可持久化树状数组
    • 树状数组维护差分数组
    • 树状数组结合分块

“树状数组虽小,能量巨大。掌握它,你将在算法竞赛中拥有无往不利的利器。” —— 匿名算法竞赛选手

通过系统学习树状数组优化DP的技巧,能够解决许多原本复杂度过高的问题。关键在于不断练习,将理论转化为实践!

http://www.dtcms.com/a/276036.html

相关文章:

  • 【技术面试提+HR面试题】Python中循环与循环嵌套的基础知识以及Python中循环的基础编程题
  • 【设计模式】适配器模式(包装器模式),缺省适配器模式,双向适配器模式
  • OneCode 3.0架构升级:注解驱动与开放接口生态详解
  • 1068万预算!中国足协大模型项目招标,用AI技术驱动足球革命
  • [es自动化更新] 策略体系 | 策略源(容器镜像)
  • Java_Springboot技术框架讲解部分(一)
  • 使用Java完成下面程序
  • Vue3 学习教程,从入门到精通,Vue3指令知识点及使用方法详细介绍(6)
  • 组合数学学习笔记
  • Stance Classification with Target-Specific Neural Attention Networks
  • Linux解决vim中文乱码问题
  • SE机制深度解析:从原理到实现
  • tiktok 弹幕 逆向分析
  • 缺陷特征粘贴增强流程
  • 李宏毅(Deep Learning)--(三)
  • python内置函数 —— zip
  • MyBatis实现分页查询-苍穹外卖笔记
  • 在 Android 库模块(AAR)中,BuildConfig 默认不会自动生成 VERSION_CODE 和 VERSION_NAME 字段
  • docker基础与常用命令
  • 如何让AI更高效
  • 留学真相:凌晨两点被海关拦下时,我才明白人生没有退路
  • 如何用Python编程实现一个简单的Web爬虫?
  • Echarts学习方法分享:跳过新手期,光速成为图表仙人!
  • 【Lucene/Elasticsearch】 数据类型(ES 字段类型) | 底层索引结构
  • 易混淆英语单词对比解析与记忆表
  • 股票的k线
  • BKD 树(Block KD-Tree)Lucene
  • 以太坊重放攻击
  • 特辑:Ubuntu,前世今生
  • 关于学习docker中遇到的问题