论单调队列优化DP

前情提要,参考资料:单调队列优化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;
}

总结:似了......

你可能感兴趣的:(c++,动态规划,推荐算法)