数据结构优化DP
概述
数据结构优化动态规划是一种通过高级数据结构将低效DP解决方案转换为最优解决方案的强大技术。这种技术包括线段树DP优化、凸包优化(CHT)、分治优化和李超线段树。这些方法可以将时间复杂度从O(n²)或O(n³)降低到O(n log n)甚至O(n),使得在竞赛编程时间限制内解决以前难以处理的问题成为可能。
线段树DP优化
核心概念
当DP转移涉及区间查询(最小值、最大值、求和)时,线段树可以将这些操作从每次查询O(n)优化到O(log n)。典型的模式是:dp[i] = optimal(dp[j] + cost(j, i)),其中j在某个区间内变化。
带懒标记的线段树DP
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
class SegmentTreeDP {
private:
vector<long long> tree, lazy;
int n;
void push(int node, int start, int end) {
if (lazy[node] != 0) {
tree[node] += lazy[node];
if (start != end) {
lazy[2 * node] += lazy[node];
lazy[2 * node + 1] += lazy[node];
}
lazy[node] = 0;
}
}
void updateRange(int node, int start, int end, int l, int r, long long val) {
push(node, start, end);
if (start > r || end < l) return;
if (start >= l && end <= r) {
lazy[node] += val;
push(node, start, end);
return;
}
int mid = (start + end) / 2;
updateRange(2 * node, start, mid, l, r, val);
updateRange(2 * node + 1, mid + 1, end, l, r, val);
push(2 * node, start, mid);
push(2 * node + 1, mid + 1, end);
tree[node] = min(tree[2 * node], tree[2 * node + 1]);
}
long long queryRange(int node, int start, int end, int l, int r) {
if (start > r || end < l) return LLONG_MAX;
push(node, start, end);
if (start >= l && end <= r) return tree[node];
int mid = (start + end) / 2;
return min(queryRange(2 * node, start, mid, l, r),
queryRange(2 * node + 1, mid + 1, end, l, r));
}
public:
SegmentTreeDP(int size) : n(size) {
tree.assign(4 * n, LLONG_MAX);
lazy.assign(4 * n, 0);
}
void update(int l, int r, long long val) {
updateRange(1, 0, n - 1, l, r, val);
}
long long query(int l, int r) {
if (l > r) return LLONG_MAX;
return queryRange(1, 0, n - 1, l, r);
}
void pointUpdate(int idx, long long val) {
updateRange(1, 0, n - 1, idx, idx, val);
}
};
// 示例:将数组分成k段的最小代价
vector<long long> minCostSplit(vector<int>& arr, int k) {
int n = arr.size();
vector<long long> dp(n + 1, LLONG_MAX);
dp[0] = 0;
SegmentTreeDP segTree(n + 1);
segTree.pointUpdate(0, 0);
for (int i = 1; i <= n; i++) {
// 从有效的前面位置查询最小值
long long minPrev = segTree.query(max(0, i - k), i - 1);
if (minPrev != LLONG_MAX) {
dp[i] = minPrev + arr[i - 1];
segTree.pointUpdate(i, dp[i]);
}
}
return dp;
}
int main() {
vector<int> arr = {3, 1, 4, 1, 5, 9, 2, 6};
int k = 3; // 最大段长度
cout << "数组: ";
for (int x : arr) cout << x << " ";
cout << "\n最大段长度: " << k << endl;
vector<long long> dp = minCostSplit(arr, k);
cout << "最小代价: " << dp.back() << endl;
return 0;
}带懒标记的线段树支持高效的区间更新和查询,对于需要同时更新多个位置的复杂DP优化是必不可少的。
凸包优化(CHT)
何时使用CHT
CHT优化形如dp[i] = min(dp[j] + b[j] * a[i] + c[i])的DP转移,其中斜率b[j]单调且查询点a[i]单调。这将复杂度从O(n²)降低到O(n)。
双端队列优化的凸包技巧
#include <iostream>
#include <vector>
#include <deque>
#include <climits>
using namespace std;
struct Line {
long long slope, intercept;
int id;
Line(long long slope, long long intercept, int id = -1)
: slope(slope), intercept(intercept), id(id) {}
long long eval(long long x) const {
return slope * x + intercept;
}
// 检查在给定两条其他直线的情况下,这条直线是否冗余
bool isBad(const Line& l1, const Line& l2) const {
// 叉积比较以避免浮点数
return (intercept - l1.intercept) * (l1.slope - l2.slope)
<= (l2.intercept - l1.intercept) * (l1.slope - slope);
}
};
class ConvexHullTrick {
private:
deque<Line> lines;
public:
void addLine(long long slope, long long intercept, int id = -1) {
Line newLine(slope, intercept, id);
// 移除变得冗余的直线
while (lines.size() >= 2 &&
newLine.isBad(lines[lines.size()-2], lines[lines.size()-1])) {
lines.pop_back();
}
lines.push_back(newLine);
}
pair<long long, int> query(long long x) {
if (lines.empty()) return {LLONG_MAX, -1};
// 移除不再最优的直线
while (lines.size() >= 2 &&
lines[0].eval(x) >= lines[1].eval(x)) {
lines.pop_front();
}
return {lines[0].eval(x), lines[0].id};
}
long long queryValue(long long x) {
return query(x).first;
}
};
// 示例:工厂生产优化
// dp[i] = min(dp[j] + setup_cost[j] * demand[i] + production_cost[i])
vector<long long> optimizeProduction(vector<long long>& setup_cost,
vector<long long>& demand,
vector<long long>& production_cost) {
int n = setup_cost.size();
vector<long long> dp(n + 1, LLONG_MAX);
dp[0] = 0;
ConvexHullTrick cht;
cht.addLine(0, 0, 0); // 初始状态
for (int i = 1; i <= n; i++) {
auto [cost, from] = cht.query(demand[i - 1]);
dp[i] = cost + production_cost[i - 1];
// 为未来转移添加新直线
if (i < n) {
cht.addLine(-setup_cost[i - 1], dp[i], i);
}
}
return dp;
}
int main() {
vector<long long> setup_cost = {2, 3, 1, 4, 2};
vector<long long> demand = {1, 2, 3, 2, 1};
vector<long long> production_cost = {5, 3, 4, 2, 6};
cout << "设置成本: ";
for (long long x : setup_cost) cout << x << " ";
cout << "\n需求: ";
for (long long x : demand) cout << x << " ";
cout << "\n生产成本: ";
for (long long x : production_cost) cout << x << " ";
cout << endl;
vector<long long> dp = optimizeProduction(setup_cost, demand, production_cost);
cout << "最小总成本: " << dp.back() << endl;
return 0;
}CHT维护线性函数的凸包,当查询和斜率都是单调的时候,允许在摊销O(1)时间内进行最优DP转移。
李超线段树
高级直线管理
李超线段树在CHT条件不满足时处理动态直线插入和点查询。它为每个段维护最优直线,支持任意直线插入顺序。
李超线段树实现
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
struct Line {
long long a, b; // y = ax + b
Line(long long a = 0, long long b = LLONG_MAX) : a(a), b(b) {}
long long eval(long long x) const {
if (b == LLONG_MAX) return LLONG_MAX;
return a * x + b;
}
};
class LiChaoTree {
private:
vector<Line> tree;
int n;
void update(int node, int start, int end, Line line) {
if (start == end) {
if (line.eval(start) < tree[node].eval(start)) {
tree[node] = line;
}
return;
}
int mid = (start + end) / 2;
bool left_better = line.eval(start) < tree[node].eval(start);
bool mid_better = line.eval(mid) < tree[node].eval(mid);
if (mid_better) {
swap(tree[node], line);
}
if (left_better != mid_better) {
update(2 * node, start, mid, line);
} else {
update(2 * node + 1, mid + 1, end, line);
}
}
long long query(int node, int start, int end, int pos) {
if (start == end) {
return tree[node].eval(pos);
}
int mid = (start + end) / 2;
long long result = tree[node].eval(pos);
if (pos <= mid) {
result = min(result, query(2 * node, start, mid, pos));
} else {
result = min(result, query(2 * node + 1, mid + 1, end, pos));
}
return result;
}
public:
LiChaoTree(int size) : n(size) {
tree.assign(4 * n, Line());
}
void addLine(long long a, long long b) {
update(1, 0, n - 1, Line(a, b));
}
long long queryMin(int x) {
return query(1, 0, n - 1, x);
}
};
// 示例:任意直线插入的动态规划
vector<long long> solveWithLiChao(vector<int>& costs, vector<int>& multipliers) {
int n = costs.size();
vector<long long> dp(n + 1, LLONG_MAX);
dp[0] = 0;
LiChaoTree lct(n + 1);
lct.addLine(0, 0); // 初始直线
for (int i = 1; i <= n; i++) {
dp[i] = lct.queryMin(multipliers[i - 1]) + costs[i - 1];
// 为未来状态添加新直线
lct.addLine(-i, dp[i]);
}
return dp;
}
int main() {
vector<int> costs = {3, 1, 4, 1, 5};
vector<int> multipliers = {2, 1, 3, 2, 1};
cout << "成本: ";
for (int x : costs) cout << x << " ";
cout << "\n乘数: ";
for (int x : multipliers) cout << x << " ";
cout << endl;
vector<long long> dp = solveWithLiChao(costs, multipliers);
cout << "最优结果: " << dp.back() << endl;
return 0;
}李超线段树为每个段维护最优直线,处理CHT无法管理的任意直线插入模式。
分治DP
四边形不等式优化
当DP递推满足四边形不等式(Monge性质)时,分治优化将复杂度从O(kn²)降低到O(kn log n)。关键洞察是最优分割点是单调的。
分治DP优化
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
class DivideConquerDP {
private:
vector<vector<long long>> dp;
vector<long long> cost;
int n, k;
// 代价函数 - 必须满足四边形不等式
long long C(int i, int j) {
if (i > j) return LLONG_MAX;
return (cost[j] - cost[i]) * (cost[j] - cost[i]);
}
void compute(int level, int left, int right, int opt_left, int opt_right) {
if (left > right) return;
int mid = (left + right) / 2;
int best_k = -1;
long long best_cost = LLONG_MAX;
// 为位置mid找到最优分割点
for (int split = opt_left; split <= min(mid - 1, opt_right); split++) {
long long current_cost = dp[level - 1][split] + C(split + 1, mid);
if (current_cost < best_cost) {
best_cost = current_cost;
best_k = split;
}
}
dp[level][mid] = best_cost;
// 递归解决左右两部分
compute(level, left, mid - 1, opt_left, best_k);
compute(level, mid + 1, right, best_k, opt_right);
}
public:
vector<long long> solve(vector<long long>& arr, int groups) {
n = arr.size();
k = groups;
cost = arr;
// 计算前缀和用于代价函数
for (int i = 1; i < n; i++) {
cost[i] += cost[i - 1];
}
dp.assign(k + 1, vector<long long>(n, LLONG_MAX));
// 基础情况:一组
for (int i = 0; i < n; i++) {
dp[1][i] = C(0, i);
}
// 使用分治填充DP表
for (int level = 2; level <= k; level++) {
compute(level, level - 1, n - 1, level - 2, n - 2);
}
return dp[k];
}
};
int main() {
vector<long long> arr = {1, 2, 3, 4, 5, 6, 7, 8};
int k = 3; // 组数
cout << "数组: ";
for (long long x : arr) cout << x << " ";
cout << "\n组数: " << k << endl;
DivideConquerDP solver;
vector<long long> result = solver.solve(arr, k);
cout << "最小代价: " << result[arr.size() - 1] << endl;
return 0;
}分治DP利用代价函数满足四边形不等式时最优分割点的单调性,显著降低时间复杂度。
优化策略指南
🎯 何时使用每种技术
- 线段树: DP转移中的区间查询
- CHT: 斜率单调的线性函数
- 李超: 任意直线插入模式
- 分治: 满足四边形不等式
⚡ 性能考虑
- 内存: 线段树需要4n空间
- 常数: CHT比李超有更好的常数
- 精度: 尽可能使用整数运算
- 缓存: 顺序访问模式更快
🔧 实现技巧
- 溢出: 中间计算使用long long
- 边界情况: 处理空区间和单元素
- 调试: 验证单调性条件
- 测试: 小输入与暴力比较
📊 复杂度分析
- 线段树DP: 每次转移O(n log n)
- CHT: 单调查询总计O(n)
- 李超: n条直线和查询O(n log n)
- 分治DP: k组O(kn log n)