单调队列优化DP


在多重背包问题3中,我们用到了单调队列优化DP,实际上,不止这个问题,很多问题我们仔细观察,就能发现它有类似的性质,我们就能用单调队列优化,我们通过几道题的练习来观察观察其中的规律。

最大子序和

题目链接:最大子序和
分析:看到这题,我们很容易想到比较暴力的方法:我们预处理出来前缀和数组,然后枚举右端点,那么f[i]=max(s[i]-s[j])(1<=i-j<=m),那我们很容易想出来用两重循环来解决这个问题,但是看这个数据范围毫无疑问是会超时的,因此我们可以考虑优化。我们发现,对于每一个i,有f[i]=s[i]-min(s[j])(1<=i-j<=m),因此我们可以用一个单调队列(不懂的可以看看滑动窗口)来动态维护s[j]的最小值。也就是对每一个i,我们的队头就是满足i-m<=j<=i-1的最小的s[j]对应的下标j。
代码实现:

#include<iostream>
#define x first
#define y second
using namespace std;
const int N=3e5+10;
int n,m,s[N],hh,tt=-1,q[N],res=-1e9;
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>s[i],s[i]+=s[i-1];//处理前缀和数组
    for(int i=1;i<=n;i++){
        while(hh<=tt&&q[hh]<i-m)hh++;//j超过滑动窗口范围的就出去
        while(hh<=tt&&s[q[tt]]>=s[i-1]) tt--;//队尾大于等于新加入值的出队
        q[++tt]=i-1;//新元素入队
        res=max(res,s[i]-s[q[hh]]);//res取max
    } 
    cout<<res;
    return 0;
}

修剪草坪

题目链接:修剪草坪
分析:我们用f[i]表示考虑1~i的所有奶牛的所有选择方案中贡献的最大值,那么对于状态转移,我们可以枚举最后一次没有选择的元素,对集合进行划分,设这个没有选择的数是j,贡献值的前缀和数组为s,则f[i]=max(f[j-1]+s[i]-s[j])=max(f[j-1]-s[j])+s[i](i-m<=j<=i),这里我们就能很明显的看出这题要用滑动窗口了,因为这题比较特殊,j的初始值可以为0,表示都不选0,即都选,因此我们需要最初把j=0的情况加入队列中。(另一种思路:题目要求我们最多连续选k个使效率最大,我们可以转换为每k+1个里面必须不选一个,使不选的总效率最小,然后用总效率减去不选的效率就是答案,剩下的就可以参考下面烽火传递那道题的思路了。)
代码实现:

#include<iostream>
using namespace std;
typedef long long LL;
const int N=1e5+10;
int q[N],tt,hh,n,k;
LL s[N],f[N];
LL get(int x){//由于我们多次需要用到求f[i-1]-s[i],我们就单独写个函数
    return f[max(0,x-1)]-s[x];//这里当x=0时返回f[0]-s[0]即可,也就是0,这种情况返回什么应该结合具体问题具体分析
}
int main(){
    cin>>n>>k;
    for(int i=1;i<=n;i++) cin>>s[i],s[i]+=s[i-1];
    hh=0,tt=-1;
    q[++tt]=0;
    for(int i=1;i<=n;i++){
        while(hh<=tt&&q[hh]<i-k) hh++;
        while(hh<=tt&&get(q[tt])<=get(i)) tt--;
        q[++tt]=i;
        f[i]=s[i]+get(q[hh]);
    }
    cout<<f[n]<<endl;
    return 0;
}

旅行问题

题目链接:旅行问题
分析:对于这道题,每个节点有两种属性:油量和到下一节点的距离,我们可以简化为一个属性,油量减去到下一节点的距离,如果这一属性大于等于0,就说明能够到达下一节点,否则就不能,每经过一个点我们就累加此属性,如果我们从1节点出发,按顺时针走到n节点的途中经过每个点时的值都大于等于0,就说明我们能够转一圈,我们破环为链,把原数组再复制一遍,然后先考虑顺时针的情况(逆时针的就再算一遍,差不多的)。我们用s[i]1表示从1到i点所有的油量减去到下一节点的距离之差之和,对顺时针的情况,从某个点i出发,对任意i<=j<=i+n-1,都有s[j]-s[i-1]大于等于0,即固定i之后,我们要找出s[j](i<=j<=i+n-1)的最小值。遍历的时候从大到小遍历,用单调队列维护s[j]的最小值,逆时针的时候相同的思路就能写出来了。
代码实现:

#include<iostream>
using namespace std;
typedef long long LL;
const int N=2e6+10;
LL s[N];
bool flag[N];
int n,q[N],hh,tt,oil[N],dist[N];
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>oil[i]>>dist[i];
        s[i]=s[i+n]=oil[i]-dist[i];//破环为链
    } 
    for(int i=1;i<=2*n;i++) s[i]+=s[i-1];//处理前缀和
    hh=0,tt=-1;
    for(int i=2*n;i;i--){
        while(hh<=tt&&q[hh]>i+n-1) hh++;
        while(hh<=tt&&s[q[tt]]>=s[i]) tt--;
        q[++tt]=i;
        if(i<=n&&s[q[hh]]-s[i-1]>=0) flag[i]=true;
    }
    dist[0]=dist[n];//逆时针时每个点的s为oid[i]-dist[i-1],而第一个点应该减去dist[n],因此把dist[0]赋值为dist[n]
    hh=0,tt=-1;//清空队列
    for(int i=n;i;i--) s[i]=s[i+n]=oil[i]-dist[i-1];//处理s的值
    for(int i=2*n;i;i--) s[i]+=s[i+1];//处理后缀和
    for(int i=1;i<=2*n;i++){//对每个s[i],需要s[j]-s[i+1]>=0(i-n+1<=j<=i)
        while(hh<=tt&&q[hh]<i-n+1) hh++;
        while(hh<=tt&&s[q[tt]]>=s[i]) tt--;
        q[++tt]=i;
        if(i>n&&s[q[hh]]-s[i+1]>=0) flag[i-n]=true;//这里不要忘记了i-n让其回到在1~n中的原位置
    }
    for(int i=1;i<=n;i++)
        if(flag[i]) puts("TAK");
        else puts("NIE");
    return 0;
}

烽火传递

题目链接:烽火传递
分析:这是一道很典型的DP问题,我们先按照暴力的分析方法来求解这个DP问题,f[i]表示考虑1~i的烽火台,点燃第i个烽火台的所有方案中的最小花费,那么最终答案就是f[n-m+1 ~n]中的最小值,对于每一个f[i],它可以由f[j] (i-m<=j<i)转移而来,看到这里我们就知道如何用单调队列优化了。
代码实现:
代码很easy

#include<iostream>
using namespace std;
const int N=2e5+10;
int n,m,w[N],dp[N],q[N],hh,tt=-1;//dp[i]表示考虑1~i的烽火台、点燃i的合法方案中的最小花费
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>w[i];
    for(int i=1;i<=n;i++){
        while(hh<=tt&&q[hh]<i-m) hh++;
        while(hh<=tt&&dp[q[tt]]>=dp[i-1]) tt--;
        q[++tt]=i-1;
        dp[i]=dp[q[hh]]+w[i];
    }
    int res=1e9;
    for(int i=n-m+1;i<=n;i++) res=min(res,dp[i]);
    cout<<res<<endl;
    return 0;
}

绿色通道

题目链接:绿色通道
分析:看到这个题,我们很容易发现答案具有单调性,因此我们可以二分这个最长空题段,然后求出这个最长空题段对应的最短时间(这不就是上一题吗?),因此本题还是比较简单的。不过要注意,上一题是连续m个位置必须有一个,这一题是可以有连续m个空着的,因此连续m+1个必须有一个。
代码实现:

#include<iostream>
using namespace std;
const int N=5e4+10;
int n,t,w[N],q[N],hh,tt,dp[N];
bool check(int m){
    hh=0,tt=-1;//和上题一毛一样
    for(int i=1;i<=n;i++){
        while(hh<=tt&&q[hh]<i-m) hh++;
        while(hh<=tt&&dp[q[tt]]>=dp[i-1]) tt--;
        q[++tt]=i-1;
        dp[i]=dp[q[hh]]+w[i];
    }
    for(int i=n-m+1;i<=n;i++)
        if(dp[i]<=t)//有一种方案满足时间小于等于t就返回true
            return true;
    return false;//返回false
}
int main(){
    cin>>n>>t;
    for(int i=1;i<=n;i++) cin>>w[i];
    int l=0,r=n;
    while(l<r){//二分
        int mid=l+r>>1;
        if(check(mid+1)) r=mid;
        else l=mid+1;
    }
    cout<<l<<endl;
    return 0;
}

理想的正方形

题目链接:理想的正方形
分析:二维RMQ问题?没有这么麻烦,因为正方形的边长是固定的。这一题如果列数为1的话不就是滑动窗口吗?受此启发,假设用w数组存储所有值,k表示边长,我们先处理出来一个子矩阵中每一行的最值,然后把这些值排为一列,处理出来它的最值,就求出了子矩阵的最值。
代码实现:

#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010, INF = 1e9;
int n, m, k;
int w[N][N];
int row_min[N][N], row_max[N][N];//row_min[i][j]存储以i,j为右下角的子矩阵的行最小值,row_max同理
int q[N];
void get_min(int* a, int* b, int tot){
    int hh = 0, tt = -1;
    for (int i = 1; i <= tot; i ++ ){
        if (hh <= tt && q[hh] <= i - k) hh ++ ;//边长为k
        while (hh <= tt && a[q[tt]] >= a[i]) tt -- ;
        q[ ++ tt] = i;
        b[i] = a[q[hh]];
    }
}
void get_max(int* a, int* b, int tot){
    int hh = 0, tt = -1;
    for (int i = 1; i <= tot; i ++ ){
        if (hh <= tt && q[hh] <= i - k) hh ++ ;
        while (hh <= tt && a[q[tt]] <= a[i]) tt -- ;
        q[ ++ tt] = i;
        b[i] = a[q[hh]];
    }
}
int main(){
    scanf("%d%d%d", &n, &m, &k);
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            scanf("%d", &w[i][j]);
    for (int i = 1; i <= n; i ++ ){
        get_min(w[i], row_min[i], m);//求子矩阵的行最小值
        get_max(w[i], row_max[i], m);//求子矩阵的行最大值
    }
    int res = INF;
    int a[N], b[N], c[N];//a,b,c是三个临时数组
    for (int i = k; i <= m; i ++ ){//枚举列,从k开始就行了
        for (int j = 1; j <= n; j ++ ) a[j] = row_min[j][i];//a数组存储下来第j列的行最小值
        get_min(a, b, n);//再取一遍滑动窗口,找出最小值放在b数组中
        for (int j = 1; j <= n; j ++ ) a[j] = row_max[j][i];//同理
        get_max(a, c, n);
        for (int j = k; j <= n; j ++ ) res = min(res, c[j] - b[j]);//更新答案
    }
    printf("%d\n", res);
    return 0;
}

所以,这和DP有毛的关系?我也不知道。

<think> 首先,用户查询是关于多背包问题使用单调队列优化的信息。用户引用了几个来源,但指出这些引用仅供参考,我需要专注于真正的需求。用户上一次的问题是查找多背包问题的单调队列优化方法。 关键点是:多背包问题(Multiple Knapsack Problem)通常涉及多个物品,每个物品有重量、价值和数量限制(或多个实例)。单调队列优化是一种高效方法,用于处理多重背包问题(Multiple Knapsack Problem with Quantities),其中每个物品有多个副本。 从用户的引用中: - 引用[1]讨论了多重背包的单调队列优化,并提供了C++代码。 - 引用[2]强调了队列内部元素的单调性维护。 - 引用[3]提供了另一个C++代码实现。 我需要综合这些信息,提供一个清晰、结构化的回答。回答必须: - 使用$...$格式行内数学表达式。 - 使用$$...$$格式独立公式单独成段。 - 使用中文。 - 生成相关问题。 - 末尾添加引用标识。 回答结构: 1. **解释问题**:简要描述多重背包问题。 2. **介绍优化方法**:解释单调队列优化的核心思想,为什么有效。 3. **算法步骤**:详细说明优化算法的逻辑。 4. **代码示例**:提供或描述代码实现。 5. **时间复杂度和优势**:讨论性能。 6. **生成相关问题**:基于内容提出相关问题。 使用引用标识:在回答中引用关键点时,末尾添加[^n]。 现在,构建内容: - **多重背包问题定义**:有一个背包容量$V$,$N$种物品,每种物品有体积$v_i$、价值$w_i$和可用数量$s_i$。目标是选择物品放入背包,使得总价值最大,且总容量不超过$V$。 - **标准解法的问题**:朴素多重背包解法(如转化为0-1背包)时间复杂度为$O(N \times V \times S)$,当$S$大时效率低。 - **单调队列优化思想**:优化动态规划的状态转移。对于每种物品,转移方程是: $$ dp[j] = \max_{k=0}^{s_i} \{ dp[j - k \cdot v_i] + k \cdot w_i \} \quad \text{for } j = 0 \text{ to } V $$ 这可以改写为: $$ dp[j] = \max_{k=0}^{s_i} \{ dp[j - k \cdot v_i] + k \cdot w_i \} $$ 单调队列用于高效计算这个最大值。 - **为什么用单调队列**:队列维护一个滑动窗口的最大值,确保在$O(1)$均摊时间获取最大值。 - **算法步骤**: - 对于每种物品$i$,复制当前dp数组到辅助数组$g$。 - 对于每个余数$j$ (mod $v_i$),维护一个单调递减队列。 - 遍历容量$k$从$j$到$V$,步长为$v_i$。 - 更新队列:移除过期元素(超出$s_i$限制),添加新元素,保持队列单调。 - 用队列头更新$dp[k]$。 - **时间复杂度**:优化后为$O(N \times V)$,显著优于朴素方法[^1]。 - **代码参考**:基于引用,提供简化代码描述或伪代码。 生成相关问题: 1. 多重背包问题单调队列优化的时间复杂度分析? 2. 如何实现单调队列优化中的队列操作? 3. 与其他优化方法相比的优势? 确保语言中文。 </think> ### 多重背包问题的单调队列优化方法 多重背包问题是背包问题的一个变种:给定$N$种物品,每种物品有体积$v_i$、价值$w_i$和数量上限$s_i$,以及一个背包容量$V$。目标是在总容量不超过$V$的前提下,最大化所选物品的总价值。朴素解法(如转化为0-1背包)的时间复杂度为$O(N \times V \times \max s_i)$,效率较低;而单调队列优化能将复杂度降至$O(N \times V)$,显著提升性能[^1][^2][^3]。 #### 优化核心思想 单调队列优化的核心在于高效处理动态规划的状态转移。多重背包的状态转移方程可表述为: $$ dp[j] = \max_{k=0}^{\min(s_i, \lfloor j / v_i \rfloor)} \{ g[j - k \cdot v_i] + k \cdot w_i \} \quad \text{对所有 } j = 0, 1, \dots, V $$ 其中: - $dp[j]$ 表示背包容量为$j$时的最大价值。 - $g$ 是辅助数组,存储放入当前物品前的状态(即上一种物品的dp值)。 - $k$ 表示当前物品的选取数量。 单调队列维护一个滑动窗口的最大值: 1. **窗口定义**:对于每个余数$r$($r = j \mod v_i$),遍历容量$j$(从$r$到$V$,步长为$v_i$)。 2. **单调性维护**:队列存储容量索引,保证队列头(head)对应的价值最大,且队列单调递减(即$g[q[\text{head}]] - \text{offset}$ 递减)。移除过期元素(超出物品数量$s_i$限制)和尾部较小值。 3. **高效更新**:在$O(1)$均摊时间内获取窗口最大值,用于更新$dp[j]$[^1][^2]。 #### 算法步骤 以下是基于引用代码的简化步骤(以C++为例): 1. **初始化**:读入$N$(物品种数)和$V$(背包容量),初始化dp数组为0。 2. **遍历每种物品**:对于每种物品$i$: - 复制当前dp数组到辅助数组$g$(保留放入$i$前的状态)。 - 遍历余数$r$($r = 0$ 到 $v_i - 1$),处理容量$j$:$r, r+v_i, r+2v_i, \dots, V$。 3. **维护单调队列**: - 队列$q$存储容量索引,$hh$(队头)和$tt$(队尾)初始化为0和-1。 - 对于每个$k$(从$r$到$V$,步长为$v_i$): - **移除过期元素**:如果队列头$q[hh]$超出$s_i$限制(即$k - q[hh] > s_i \cdot v_i$),则$hh++$。 - **更新dp[k]**:如果队列非空,$dp[k] = \max(dp[k], g[q[hh]] + (k - q[hh]) / v_i \cdot w_i)$。 - **入队新元素**:移除尾部小于当前值的元素,保持单调递减: ```c while (hh <= tt && g[q[tt]] - (q[tt] - r) / v_i * w_i <= g[k] - (k - r) / v_i * w_i) tt--; q[++tt] = k; ``` 4. **输出结果**:遍历所有物品后,$dp[V]$即为最大价值。 #### 代码示例(简化C++实现) 参考引用[1]和[3],以下是关键部分伪代码: ```cpp #include <iostream> #include <cstring> using namespace std; const int maxv = 20005; int dp[maxv], g[maxv], q[maxv]; int main() { int N, V; cin >> N >> V; memset(dp, 0, sizeof dp); // 初始化dp for (int i = 0; i < N; i++) { int v, w, s; cin >> v >> w >> s; // 物品体积、价值、数量 memcpy(g, dp, sizeof dp); // 保存当前状态 for (int r = 0; r < v; r++) { // 遍历余数r int hh = 0, tt = -1; // 队列初始化 for (int k = r; k <= V; k += v) { // 步长为v // 移除过期元素 (k - q[hh] > s * v) if (hh <= tt && (k - q[hh]) > s * v) hh++; // 更新dp[k]使用队列头最大值 if (hh <= tt) dp[k] = max(dp[k], g[q[hh]] + (k - q[hh]) / v * w); // 维护单调递减队列 while (hh <= tt && g[q[tt]] - (q[tt] - r) / v * w <= g[k] - (k - r) / v * w) tt--; q[++tt] = k; // 入队 } } } cout << dp[V] << endl; return 0; } ``` #### 时间复杂度与优势 - **时间复杂度**:$O(N \times V)$,因为每个余数类独立处理,且每个容量$j$在队列操作中均摊$O(1)$时间[^1][^3]。 - **优势**:相比二进制优化($O(N \times V \times \log s_i)$),单调队列优化避免了额外拆分,适合大$s_i$场景(如AcWing等竞赛问题)[^1][^2]。 该优化利用了动态规划的状态转移特性,通过单调队列高效管理滑动窗口,确保性能最优[^3]。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_bxzzy_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值