Vijos 1243 初探单调队列优化DP

这是第一次接触单调队列优化DP,心想借这道题熟悉一下单调队列优化DP的写法,并加深一下对此的理解。

理解是有了,但当被这道题卡了一天后,理解都要忘得差不多了。


有一些DP的状态转移是从之前的某些状态中取一个最值,那么它就有可能用单调队列来优化。比如这道题,我们可以轻易的想出状态表示:f[i][j]表示第i个步骤,在第j台机器上完成时,前i个步骤的最小花费。对于状态转移,有两种思路,其中一种是f[i][j] = min{f[i-1][j']+t[i][j]},当然我没有把k写进去,还要多开一个数组记录l,所以这种转移比较麻烦,而且也与单调队列无关。一开始是想到的这个思路,不过本着熟悉单调队列优化DP,我在看了神犇的文档后写的另一种:f[i][j] = min{f[i'][j']+sum[i][j]-sum[i'][j]}。sum[i][j]表示前i个步骤在第j台机器上完成时的总花费。

在这个状态转移中对单调队列优化DP的条件有了一些理解,原始的上面的转移时不可以用单调队列优化的,变形后:f[i][j] = min{f[i'][j']-sum[i'][j]}+sum[i][j],这个原本是不符合单调队列优化的条件的,但是因为j很小,最多只有5个,所以我们对于每个j搞一个单调队列就算是符合条件了。对于每一个单调队列q,用于判断的值是f[i][j]-sum[i][q],出队条件是当前i与队首元素差值大于l。

就我的理解,我们搞m个单调队列的本质是把j变成了常量,状态转移中只有一个变量,而min之外的元素对之内的元素没有影响,或者没有交叉的地方,就能把min之内的作为权值搞单调队列优化,比如说f[i][j] = min{f[i'][j']+sum[i][j]}-sum[i'][j],就不可以了,因为这个决策的最优性会受sum[i'][j]影响,而sum[i][j]是受当前i值影响的,不能放到单调队列中。引用一段比较科学的话:

//  → → 代码暂时未AC,不粘。 后5个点能过前5个点齐刷刷wa掉也是醉了。  9.09

表示刚刚AC,还有点迷茫,卡了2天的题终于AC。

在网上搜很多很多的题解,发现他们写的十分令蒟蒻不理解,各种宏,但是单调队列与DP部分基本无异,所以搜题解并没有实质性的改善。终于,在某神犇的博客上看到了一句话:每次转移要先更新本次的f数组,然后再去更新单调队列。否则会挂前5个。

于是就将原来的:

for(int i = 1; i <= n; i++)
	for(int j = 1; j <= m; j++){
		while(h[j] < t[j] && i-i2 > l) h[j]++;
		f[i][j] = f[i2][j2] - sum[i2][j] + sum[i][j] + k;
		for(int y = 1; y <= m; y++){
			if(y == j) continue;
			while(h[y] < t[y] && f[i][j]-sum[i][y] < f[i3][j3]-sum[i3][y]) t[y]--;
			q[y[t[y]++] = i*10+j;
		}
	}

改成了:

	for(int i = 1; i <= n; i++){
		for(int j = 1; j <= m; j++){
			while(h[j] < t[j] && i-i2 > l) h[j]++;
			f[i][j] = f[i2][j2] - sum[i2][j] + sum[i][j] + k;
		}	  
		for(int j = 1; j <= m; j++){
	   		for(int y = 1; y <= m; y++){
				if(y == j) continue;
				while(h[y] < t[y] && f[i][j]-sum[i][y] < f[i3][j3]-sum[i3][y]) t[y]--;
				q[t[y]++][y] = i*10+j;
			}
		}
	}
虽然我没有看出什么差异,但是就神奇的AC了。
完整代码:
#include <cstdio>
#include <algorithm>
#include <cstring>
#define M 100001
#define i2 q[h[j]][j]/10
#define j2 q[h[j]][j]%10
#define i3 q[t[y]-1][y]/10 
#define j3 q[t[y]-1][y]%10
using namespace std;

int n, m, k, l, ans = 1<<30;
int sum[M][6], f[M][6];
int q[M][6], h[6], t[6];

int main()
{
	scanf("%d %d %d %d", &n, &m, &k, &l);
	for(int i = 1; i <= m; i++)
	for(int j = 1; j <= n; j++){
		scanf("%d", &sum[j][i]);
		sum[j][i] += sum[j-1][i];
	}
	
	memset(f, 0x3f, sizeof f);
	f[0][0] = 0;
	for(int i = 1; i <= m; i++) t[i]++;
	
	for(int i = 1; i <= n; i++){
		for(int j = 1; j <= m; j++){
			while(h[j] < t[j] && i-i2 > l) h[j]++;
			f[i][j] = f[i2][j2] - sum[i2][j] + sum[i][j] + k;
		}	  
		for(int j = 1; j <= m; j++){
	   		for(int y = 1; y <= m; y++){
				if(y == j) continue;
				while(h[y] < t[y] && f[i][j]-sum[i][y] < f[i3][j3]-sum[i3][y]) t[y]--;
				q[t[y]++][y] = i*10+j;
			}
		}
	}	
	
	for(int i = 1; i <= m; i++)
		ans = min(ans, f[n][i]);
	printf("%d", ans-k);
	return 0;
}

注:我的做法略奇葩,最开始在m条队列里的都只是f[0][0] = 0,在第一次转移的时候加上了K,但是第一个步骤的执行时不需要K的,所以最后减去K;队列中我保存的是i*10+k,这样就少开了一个数组,感觉更清晰一点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值