这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战.
概念
单调队列,常用来维护连续区间的最大值:
- 将元素入队
- 将元素出队
- 求当前队列中的最大值
实现
实现可以做到单个操作平均O(1),实际存储时队列中元素满足如下性质:
- 越先入队越靠前
- 越大越靠前
- 不符合以上两个条件的元素会被扔掉。
此时三个操作转化为:
- x入队:从队尾开始,将所有小于等于x的元素出队,然后将x入队。
- x出队:如果x已经出队,则忽略,否则将其出队。
- 最大值:队首元素即为最大值。
因为队列中实际存储的元素为单调递减,所以又称单调队列。
多重背包的单调优化
很多动态规划问题的状态转移都有类似于这样的情况:每个状态可以由上一行中的连续一段状态转移得到。 最经典的例子是多重背包:,其中分别为本种物品的体积,价值,件数。
举例来说,如果vol是1,val是2,num是3,那么可由以下状态转移得到:
dp[i-1][3]+6
dp[i-1][4]+4
dp[i-1][5]+2
dp[i-1][6]
而可由以下状态转移得到:
dp[i-1][4]+6
dp[i-1][5]+4
dp[i-1][6]+2
dp[i-1][7]
而可由以下状态转移得到:
dp[i-1][5]+6
dp[i-1][6]+4
dp[i-1][7]+2
dp[i-1][8]
可以看到,三个状态的前驱状态有很大的重合部分,且满足单调队列的性质,此时可使用单调优化。
首先,为了排除后面所加的数字不同造成的影响,我们将上一列所有的状态值进行统一化处理:,此时的前驱状态是
dp[i-1][3]+12
dp[i-1][4]+12
dp[i-1][5]+12
dp[i-1][6]+12
同理,的每个前驱状态之后都为+14,此时把这些常数扔掉,在求出的最大值后加上即可。 此时的前驱状态是:
dp[i-1][3]
dp[i-1][4]
dp[i-1][5]
dp[i-1][6]
而的前驱状态是:
dp[i-1][4]
dp[i-1][5]
dp[i-1][6]
dp[i-1][7]
直接使用单调队列优化即可。
价值和件数取任意值时得到的结论均相同,而当物品体积不为1时,转移就变成了当前体积除以物品体积的余数相同时,进行上述优化。
代码
因为出队操作的不确定性,单调队列需要维护两个数组:下标和值,以此判断何时应当出队。
/* 多重背包_使用单调优化 */
int main(void)
{
int T = read();
while(T--)
{
int v=read(), n=read();
int dp[M]={};
for(int i=1; i<=n; ++i)
{
int vol=read(), val=read(), num=read(); //体积,价值,数量
for(int k=0; k<vol; ++k) //枚举体积的余数
{
int a[M], b[M], l=0, r=0; //下标, 值, 队头, 队尾, 左闭右开
for(int j=k; j<=v; j+=vol)
{
int y = dp[j] - j/vol*val; //当前体积的贡献值
while(l<r && y>=b[r-1]) r--; //入队
a[r] = j; b[r++] = y;
while(a[l]<j-num*vol) ++l; //出队
dp[j] = b[l] + j/vol*val; //选择最大值
}
}
}
printf("%d\n",dp[v] );
}
return 0;
}
习题
- hdu2191 多重背包裸题,代码如上。
- hdu3401 Trade
本文也发表于我的 csdn 博客中。