前情提要,参考资料:单调队列优化DP(超详细!!!) - endl\n - 博客园
【动态规划】选择数字(单调队列优化dp)_哔哩哔哩_bilibili
背景:最近作者快被DP逼疯了,写篇博客做记录。
以下是对各DP的原理阐释:
单调队列通过队列元素的吸入与弹出,形成单调性的结构,使算法能够进行线性处理,大大优化了时间复杂度。接下来讲解单调队列在区间DP、背包DP、树形DP还有数位DP中的应用:
1.单调队列优化区间DP:
(1)原理:区间DP通常用于求解与区间相关的最值问题。单调队列可以优化区间DP中的状态转移,通过维护一个单调递增或递减的队列,快速找到当前区间内的最优解。
(2)案例:以“滑动窗口最大值”问题为例,要求在数组中找到每个长度为k的滑动窗口的最大值。使用单调队列时,队列中存储的是数组元素的索引,且队列中的元素对应的值是单调递减的。每次移动窗口时,只需要将队列中超出窗口范围的元素移除,并将新元素加入队列,同时保持队列的单调性。队列的队首元素对应的值即为当前窗口的最大值。这种方法的时间复杂度为O(n),相比暴力解法的O(nk)有显著提升。
2.单调队列优化背包DP:
(1)原理:在背包DP中,单调队列优化主要应用于分组背包问题和多重背包问题。通过维护一个单调队列,可以快速找到在当前容量下能够获得的最大价值。
(2)案例:在分组背包问题中,每组物品只能选择一个,且每组物品的价值和重量不同。使用单调队列优化时,对于每个容量,维护一个单调递增的队列,队列中的元素表示在当前容量下能够获得的最大价值。在状态转移时,通过队列快速找到最优解,从而避免了暴力枚举的高时间复杂度。
3.单调队列优化树形DP:
(1)原理:树形DP通常用于求解树上的最值问题。但单调队列优化在树形DP中的应用相对较少,至少在某些特定问题中可以起到优化作用。通过维护一个单调队列,可以快速找到在当前子树中能够获得的最优解。
(2)案例:在“树上最长路径”问题中,要求找到树上最长的路径。使用单调队列优化时,对于每个节点,维护一个单调递增的队列,队列中的元素表示从该节点到其子节点的路径长度。在状态转移时,通过队列快速找到最长路径,从而避免了暴力枚举的高时间复杂度。
4.单调队列优化数位DP:
(1)原理:数位DP通常用于求解与数字相关的最值问题。单调队列优化在数位DP中的应用也相对较少,在某些特定问题中还是可以起到优化作用的。通过维护一个单调队列,可以快速找到在当前数位上能够获得的最优解。
(2)案例:在“数位和最大”问题中,要求找到一个数字,使其数位和最大。使用单调队列优化时,对于每个数位,维护一个单调递增的队列,队列中的元素表示在当前数位上能够获得的最大数位和。在状态转移时,通过队列快速找到最优解,从而避免了暴力枚举的高时间复杂度。
需要注意的是,单调队列优化并非万能,它仅适用于具有特定性质的动态规划问题。在实际应用中,需要根据具体问题的特点选择合适的优化方法。(别说看了这篇文章后见DP就单调队列优化,优异的算法多的是,视境而择)
二、证明及示例代码
1)单调队列优化区间DP:
1.证明:
° 四边形不等式:对于函数 w(i, j),若满足 i ≤ i′ < j ≤ j′,有 w(i, j) + w(i′, j′) ≤ w(i′, j)+w(i, j′),则称 w 满足四边形不等式。
°区间单调性:若满足i ≤ i' ≤ j ≤ j',有 w(i', j) ≤ w(i, j'),则称 w 具有区间单调性。
°决策单调性:对于动归的状态转移方程 dp(i, j) = min(dp(i, k-1), dp(k, j)) + cost(i, j),若 cost 满足四边形不等式和区间单调性,则 dp 也满足四边形不等式,且其最优决策点 s(i, j)满足 s(i, j) ≤ s(i, j+1) ≤ s(i+1, j+1)。
2.代码ED:(以合并石子为例)
#include
#include
#include
using namespace std;
int n;//记录石子堆数
vector cost;//记录由1到i的花费
vector> s;//记录将石子i到j全部合并的最佳分割点
void fun_dp(){
vector> dp(n + 5, vector(n + 5, 0));
for(int len = 2; len <= n; len++){
for(int i = 1; i <= n - len + 1; i++){
int j = i + len - 1;
dp[i][j] = numeric_limits::max();//初始化值
for(int k = s[i][j - 1]; k <= s[i + 1][j]; k++){
if(dp[i][k] + dp[k + 1][j] + cost[j] - cost[i - 1] < dp[i][j]){
dp[i][j] = dp[i][k] + dp[k + 1][j] + cost[j] - cost[i - 1];
s[i][j] = k;
}
}
}
}
cout << dp[1][n] << endl;
}
int main(void){
while(cin >> n){
cost.resize(n + 5);//记录由1到i的花费
s.resize(n + 5, vector(n + 5, 0));
int x = 0;
for(int i = 1; i <= n; i++){
cin >> x;
cost[i] = cost[i - 1] + x;
s[i][i] = i;//初始化最佳分割点
}
fun_dp();
}
return 0;
}
2)单调队列优化背包DP:
1.证明:
(1)在多重背包问题中,状态转移方程为 dp[j] = max(dp[j], dp[j−k⋅v] + k*w),其中 v 是物品的体积,w 是物品的价值,k 是物品的数量。
(2)通过将状态按体积 v 分类,每个同余类的状态转移则可以用单调队列进行优化。单调队列用来存储状态的下标,且队列中的状态值 dp[j] - k*w 需严格单调递增。
(3)由于 dp[j] 仅依赖于同余类内的状态,因此可以在每个同余类中维护一个单调队列来寻找最优解。
2.代码ED:
#include
#include
using namespace std;
const int N = 1e5+9;
int n, m;
int v, w, s;
int dp[N], g[N];
int q[N];
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr),cout.tie(nullptr);
cin >> n >> m;
for(int i = 1; i <= n; ++i){
cin >> v >> w >> s;
memcpy(g, dp, sizeof dp);
for(int j = 0; j < v; ++j){
int hh = 0, tt = -1;
for(int k = j; k <= m; k += v){
if(hh <= tt && k - q[hh] > s * v) hh++;
if(hh <= tt) dp[k] = max(dp[k], g[q[hh]] + (k - q[hh]) / v * w);
while(hh <= tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j) / v * w) tt--;
q[++tt] = k;
}
}
}
cout << dp[m] << '\n';
return 0;
}
3)单调队列优化树形DP:
1.证明:
(1)树形DP中能用单调队列解决的,有求解树上最长路径问题。
(2)对于每个节点,去维护一个单调递增队列,则以队列中的元素表示从该节点到其子节点的路径长度,从而快速找到最长路径。
2.代码ED:
#include
#include
#include
using namespace std;
const int N = 1e5+9;
int n;
int u, v;
vector g[N];
int dp[N], f[N];
void dfs(int u, int fa){
priority_queue pq;
for(int v : g[u]){
if(v == fa)continue;
dfs(v, u);
pq.push(dp[v]);
}
while(!pq.empty() && pq.size() > 2){
pq.pop();
}
dp[u] = 0;
while(!pq.empty()){
dp[u] += pq.top();
pq.pop();
}
f[u] = dp[u];
if(!pq.empty()){
f[u] += pq.top();
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr),cout.tie(nullptr);
cin >> n;
for(int i = 1; i < n; ++i){
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
cout << f[1] << '\n';
return 0;
}
4)单调队列优化数位DP:
1.证明:
(1)对于数位DP的单调队列求解问题,有求解数位和最大问题。
(2)对于每个数位,维护一个单调递增队列,队列中的元素表示在当前数位上能够获得的最大数位和,从而快速找到最优解。
2.代码ED:
#include
#include
#include
#include
#include
using namespace std;
const int N = 1e5+9;
int n;
int dp[N];
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr),cout.tie(nullptr);
cin >> n;
vector digits;
while(n){
digits.push_back(n % 10);
n /= 10;
}
reverse(digits.begin(), digits.end());
int len = digits.size();
dp[0] = 0;
for(int i = 1; i <= len; ++i){
dp[i] = dp[i - 1] + digits[i - 1];
}
cout << dp[len] << '\n';
return 0;
}
总结:似了......