高级动态规划
掌握区间DP、树形DP和复杂优化技术
概述
高级动态规划是解决复杂优化问题的核心技术。本章节深入探讨区间动态规划、树形动态规划、背包问题的高级变种,以及各种DP优化技术,为解决世界级竞赛中的挑战性问题提供强大工具。
核心内容
1. 区间动态规划
区间DP是处理区间相关问题的经典技术,通过枚举分割点来求解最优子结构问题。
// 区间动态规划经典问题
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
// 问题1:矩阵链乘法
int matrixChainMultiplication(vector<int>& dimensions) {
int n = dimensions.size() - 1; // n个矩阵
vector<vector<int>> dp(n, vector<int>(n, 0));
// len是链的长度
for (int len = 2; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
dp[i][j] = INT_MAX;
// 尝试所有可能的分割点
for (int k = i; k < j; k++) {
int cost = dp[i][k] + dp[k + 1][j] +
dimensions[i] * dimensions[k + 1] * dimensions[j + 1];
dp[i][j] = min(dp[i][j], cost);
}
}
}
return dp[0][n - 1];
}
// 问题2:回文串分割(最少分割次数)
int minPalindromePartitions(string s) {
int n = s.length();
// 预处理:判断子串是否为回文
vector<vector<bool>> isPalindrome(n, vector<bool>(n, false));
// 奇数长度回文
for (int center = 0; center < n; center++) {
int left = center, right = center;
while (left >= 0 && right < n && s[left] == s[right]) {
isPalindrome[left][right] = true;
left--;
right++;
}
}
// 偶数长度回文
for (int center = 0; center < n - 1; center++) {
int left = center, right = center + 1;
while (left >= 0 && right < n && s[left] == s[right]) {
isPalindrome[left][right] = true;
left--;
right++;
}
}
// DP:计算最少分割次数
vector<int> dp(n, INT_MAX);
for (int i = 0; i < n; i++) {
if (isPalindrome[0][i]) {
dp[i] = 0; // 整个前缀是回文
} else {
for (int j = 0; j < i; j++) {
if (isPalindrome[j + 1][i]) {
dp[i] = min(dp[i], dp[j] + 1);
}
}
}
}
return dp[n - 1];
}
// 问题3:最优二叉搜索树
double optimalBST(vector<double>& keys, vector<double>& freq) {
int n = keys.size();
vector<vector<double>> dp(n + 1, vector<double>(n + 1, 0));
vector<double> sum(n + 1, 0);
// 计算频率前缀和
for (int i = 1; i <= n; i++) {
sum[i] = sum[i - 1] + freq[i - 1];
}
// 区间DP
for (int len = 1; len <= n; len++) {
for (int i = 1; i <= n - len + 1; i++) {
int j = i + len - 1;
dp[i][j] = DBL_MAX;
double freqSum = sum[j] - sum[i - 1];
// 尝试每个节点作为根
for (int root = i; root <= j; root++) {
double cost = freqSum;
if (root > i) cost += dp[i][root - 1];
if (root < j) cost += dp[root + 1][j];
dp[i][j] = min(dp[i][j], cost);
}
}
}
return dp[1][n];
}
int main() {
// 测试矩阵链乘法
vector<int> dimensions = {40, 20, 30, 10, 30}; // 4个矩阵的维度
cout << "矩阵链乘法最少标量乘法次数: "
<< matrixChainMultiplication(dimensions) << endl;
// 测试回文分割
string s = "aabcbaa";
cout << "字符串 "" << s << "" 的最少分割次数: "
<< minPalindromePartitions(s) << endl;
// 测试最优BST
vector<double> keys = {10, 12, 20};
vector<double> freq = {34, 8, 50};
cout << "最优BST的期望搜索成本: "
<< optimalBST(keys, freq) << endl;
return 0;
}2. 树形动态规划
树形DP是在树结构上进行动态规划,通过DFS遍历和状态转移解决树上的优化问题。
// 树形动态规划经典问题
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct TreeNode {
int val;
vector<TreeNode*> children;
TreeNode(int x) : val(x) {}
};
// 问题1:树的直径
class TreeDiameter {
private:
int maxDiameter;
int dfs(TreeNode* root) {
if (!root) return 0;
vector<int> depths;
for (TreeNode* child : root->children) {
depths.push_back(dfs(child));
}
// 找到最深的两条路径
sort(depths.rbegin(), depths.rend());
int diameter = 0;
if (depths.size() >= 2) {
diameter = depths[0] + depths[1];
} else if (depths.size() == 1) {
diameter = depths[0];
}
maxDiameter = max(maxDiameter, diameter);
return depths.empty() ? 1 : depths[0] + 1;
}
public:
int findDiameter(TreeNode* root) {
maxDiameter = 0;
dfs(root);
return maxDiameter;
}
};
// 问题2:树上最大独立集
class MaxIndependentSet {
private:
pair<int, int> dfs(TreeNode* root) {
if (!root) return {0, 0};
int include = root->val; // 包含当前节点
int exclude = 0; // 不包含当前节点
for (TreeNode* child : root->children) {
auto childResult = dfs(child);
include += childResult.second; // 子节点必须不选
exclude += max(childResult.first, childResult.second); // 子节点可选可不选
}
return {include, exclude};
}
public:
int maxIndependentSet(TreeNode* root) {
auto result = dfs(root);
return max(result.first, result.second);
}
};
// 问题3:树的重心
class TreeCentroid {
private:
int n;
vector<vector<int>> adj;
vector<int> subtreeSize;
int centroid;
int minMaxSubtree;
void dfs(int u, int parent) {
subtreeSize[u] = 1;
int maxSubtree = 0;
for (int v : adj[u]) {
if (v != parent) {
dfs(v, u);
subtreeSize[u] += subtreeSize[v];
maxSubtree = max(maxSubtree, subtreeSize[v]);
}
}
// 考虑父节点方向的子树
maxSubtree = max(maxSubtree, n - subtreeSize[u]);
if (maxSubtree < minMaxSubtree) {
minMaxSubtree = maxSubtree;
centroid = u;
}
}
public:
int findCentroid(vector<vector<int>>& edges) {
n = edges.size() + 1;
adj.resize(n);
subtreeSize.resize(n);
minMaxSubtree = n;
for (auto& edge : edges) {
adj[edge[0]].push_back(edge[1]);
adj[edge[1]].push_back(edge[0]);
}
dfs(0, -1);
return centroid;
}
};
// 问题4:树上背包
class TreeKnapsack {
private:
vector<vector<int>> adj;
vector<int> weight, value;
vector<vector<int>> dp;
void dfs(int u, int parent, int capacity) {
// 初始化:只考虑当前节点
for (int w = 0; w <= capacity; w++) {
if (w >= weight[u]) {
dp[u][w] = value[u];
}
}
// 处理每个子树
for (int v : adj[u]) {
if (v != parent) {
dfs(v, u, capacity);
// 合并子树的结果
for (int w = capacity; w >= 0; w--) {
for (int k = 0; k <= w; k++) {
dp[u][w] = max(dp[u][w], dp[u][k] + dp[v][w - k]);
}
}
}
}
}
public:
int solve(vector<vector<int>>& edges, vector<int>& weights, vector<int>& values, int capacity) {
int n = weights.size();
adj.resize(n);
weight = weights;
value = values;
dp.assign(n, vector<int>(capacity + 1, 0));
for (auto& edge : edges) {
adj[edge[0]].push_back(edge[1]);
adj[edge[1]].push_back(edge[0]);
}
dfs(0, -1, capacity);
return dp[0][capacity];
}
};
int main() {
// 构建测试树
TreeNode* root = new TreeNode(1);
root->children.push_back(new TreeNode(2));
root->children.push_back(new TreeNode(3));
root->children[0]->children.push_back(new TreeNode(4));
root->children[0]->children.push_back(new TreeNode(5));
root->children[1]->children.push_back(new TreeNode(6));
// 测试树的直径
TreeDiameter diameterSolver;
cout << "树的直径: " << diameterSolver.findDiameter(root) << endl;
// 测试最大独立集
MaxIndependentSet misSolver;
cout << "最大独立集大小: " << misSolver.maxIndependentSet(root) << endl;
// 测试树的重心
vector<vector<int>> edges = {{0, 1}, {0, 2}, {1, 3}, {1, 4}, {2, 5}};
TreeCentroid centroidSolver;
cout << "树的重心: " << centroidSolver.findCentroid(edges) << endl;
// 测试树上背包
vector<int> weights = {2, 1, 3, 2, 1, 4};
vector<int> values = {3, 2, 4, 4, 2, 5};
TreeKnapsack knapsackSolver;
cout << "树上背包最大价值: "
<< knapsackSolver.solve(edges, weights, values, 7) << endl;
return 0;
}3. 高级背包问题
// 高级背包问题变种
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 多重背包(二进制优化)
int multipleKnapsack(vector<int>& weights, vector<int>& values, vector<int>& counts, int capacity) {
vector<int> w, v;
// 二进制拆分
for (int i = 0; i < weights.size(); i++) {
int count = counts[i];
for (int k = 1; k <= count; k *= 2) {
w.push_back(k * weights[i]);
v.push_back(k * values[i]);
count -= k;
}
if (count > 0) {
w.push_back(count * weights[i]);
v.push_back(count * values[i]);
}
}
// 01背包
vector<int> dp(capacity + 1, 0);
for (int i = 0; i < w.size(); i++) {
for (int j = capacity; j >= w[i]; j--) {
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
return dp[capacity];
}
// 分组背包
int groupKnapsack(vector<vector<pair<int, int>>>& groups, int capacity) {
vector<int> dp(capacity + 1, 0);
for (auto& group : groups) {
for (int j = capacity; j >= 0; j--) {
// 尝试选择组内每个物品
for (auto& item : group) {
int weight = item.first, value = item.second;
if (j >= weight) {
dp[j] = max(dp[j], dp[j - weight] + value);
}
}
}
}
return dp[capacity];
}
// 有依赖的背包(树形背包)
class DependentKnapsack {
private:
struct Item {
int weight, value;
vector<int> children;
};
vector<Item> items;
vector<vector<int>> dp;
void dfs(int u, int capacity) {
// 初始化:选择当前物品
for (int w = items[u].weight; w <= capacity; w++) {
dp[u][w] = items[u].value;
}
// 处理依赖的子物品
for (int child : items[u].children) {
dfs(child, capacity);
// 合并子物品的结果
for (int w = capacity; w >= items[u].weight; w--) {
for (int k = 0; k <= w - items[u].weight; k++) {
dp[u][w] = max(dp[u][w], dp[u][w - k] + dp[child][k]);
}
}
}
}
public:
int solve(vector<pair<int, int>>& itemData, vector<vector<int>>& dependencies, int capacity) {
int n = itemData.size();
items.resize(n);
dp.assign(n, vector<int>(capacity + 1, 0));
for (int i = 0; i < n; i++) {
items[i].weight = itemData[i].first;
items[i].value = itemData[i].second;
items[i].children = dependencies[i];
}
int result = 0;
for (int i = 0; i < n; i++) {
// 检查是否为根物品(没有被其他物品依赖)
bool isRoot = true;
for (int j = 0; j < n; j++) {
for (int child : items[j].children) {
if (child == i) {
isRoot = false;
break;
}
}
if (!isRoot) break;
}
if (isRoot) {
dfs(i, capacity);
for (int w = 0; w <= capacity; w++) {
result = max(result, dp[i][w]);
}
}
}
return result;
}
};
// 二维背包
int twoDimensionalKnapsack(vector<int>& weights, vector<int>& volumes, vector<int>& values,
int maxWeight, int maxVolume) {
vector<vector<int>> dp(maxWeight + 1, vector<int>(maxVolume + 1, 0));
for (int i = 0; i < weights.size(); i++) {
for (int w = maxWeight; w >= weights[i]; w--) {
for (int v = maxVolume; v >= volumes[i]; v--) {
dp[w][v] = max(dp[w][v], dp[w - weights[i]][v - volumes[i]] + values[i]);
}
}
}
return dp[maxWeight][maxVolume];
}
int main() {
// 测试多重背包
vector<int> weights = {2, 3, 4};
vector<int> values = {3, 4, 5};
vector<int> counts = {3, 2, 1};
cout << "多重背包最大价值: "
<< multipleKnapsack(weights, values, counts, 10) << endl;
// 测试分组背包
vector<vector<pair<int, int>>> groups = {
{{1, 2}, {2, 3}, {3, 4}}, // 第一组
{{2, 3}, {3, 5}, {4, 6}}, // 第二组
{{1, 1}, {4, 7}} // 第三组
};
cout << "分组背包最大价值: "
<< groupKnapsack(groups, 8) << endl;
// 测试二维背包
vector<int> weights2d = {2, 3, 4};
vector<int> volumes2d = {1, 3, 2};
vector<int> values2d = {3, 5, 4};
cout << "二维背包最大价值: "
<< twoDimensionalKnapsack(weights2d, volumes2d, values2d, 6, 4) << endl;
return 0;
}解题技巧
🎯 状态设计
合理设计DP状态是关键。区间DP考虑区间端点,树形DP考虑子树信息,复杂问题可能需要多维状态。
⚡ 优化技巧
使用滚动数组优化空间,二进制拆分优化多重背包,记忆化搜索处理复杂状态转移。
🔄 转移方程
仔细分析状态转移方程,注意边界条件和特殊情况。树形DP要考虑父子关系,区间DP要枚举分割点。
🧮 复杂度分析
准确分析时间和空间复杂度。区间DP通常是O(n³),树形DP是O(n),背包问题要考虑容量限制。