【笔记】 单调队列及单调dp

概念

单调队列,又称滑动窗口,常用来维护连续区间的最大值:

  1. 将元素入队
  2. 将元素出队
  3. 求当前队列中的最大值

实现

实现可以做到单个操作平均O(1),实际存储时队列中元素满足如下性质:

  1. 越先入队越靠前
  2. 越大越靠前
  3. 不符合以上两个条件的元素会被扔掉。

此时三个操作转化为:

  1. x入队:从队尾开始,将所有小于等于x的元素出队,然后将x入队。
  2. x出队:如果x已经出队,则忽略,否则将其出队。
  3. 最大值:队首元素即为最大值。

因为队列中实际存储的元素为单调递减,所以又称单调队列。

多重背包的单调优化

很多动态规划问题的状态转移都有类似于这样的情况:每个状态可以由上一行中的连续一段状态转移得到。
最经典的例子是多重背包: d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j − k ∗ v o l ] + k ∗ v a l ) , 0 < = k < = n u m dp[i][j] = max(dp[i-1][j-k*vol]+k*val), 0<=k<=num dp[i][j]=max(dp[i1][jkvol]+kval),0<=k<=num,其中 v o l , v a l , n u m vol,val,num vol,val,num分别为本种物品的体积,价值,件数。

举例来说,如果vol是1,val是2,num是3,那么 d p [ i ] [ 6 ] dp[i][6] dp[i][6]可由以下状态转移得到:

dp[i-1][3]+6
dp[i-1][4]+4
dp[i-1][5]+2
dp[i-1][6]

d p [ i ] [ 7 ] dp[i][7] dp[i][7]可由以下状态转移得到:

dp[i-1][4]+6
dp[i-1][5]+4
dp[i-1][6]+2
dp[i-1][7]

d p [ i ] [ 8 ] dp[i][8] dp[i][8]可由以下状态转移得到:

dp[i-1][5]+6
dp[i-1][6]+4
dp[i-1][7]+2
dp[i-1][8]

可以看到,三个状态的前驱状态有很大的重合部分,且满足单调队列的性质,此时可使用单调优化。

首先,为了排除后面所加的数字不同造成的影响,我们将上一列所有的状态值进行统一化处理 d p [ i − 1 ] [ j ] − = 2 ∗ j dp[i-1][j]-=2*j dp[i1][j]=2j,此时 d p [ i ] [ 6 ] dp[i][6] dp[i][6]的前驱状态是

dp[i-1][3]+12
dp[i-1][4]+12
dp[i-1][5]+12
dp[i-1][6]+12

同理, d p [ i ] [ 7 ] dp[i][7] dp[i][7]的每个前驱状态之后都为+14,此时把这些常数扔掉,在求出 d p [ i ] [ j ] dp[i][j] dp[i][j]的最大值后加上 2 ∗ j 2*j 2j即可。
此时 d p [ i ] [ 6 ] dp[i][6] dp[i][6]的前驱状态是:

dp[i-1][3]
dp[i-1][4]
dp[i-1][5]
dp[i-1][6]

d p [ i ] [ 7 ] dp[i][7] dp[i][7]的前驱状态是:

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;
}

习题

  1. hdu2191 多重背包裸题,代码如上。
  2. hdu3401 Trade
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值