线性DP例题(洛谷P5858 Golden Sword)及其单调队列优化

题目描述

制造一把金宝剑需要 n 种原料,编号为 1 到 n,编号为 i 的原料的坚固值ai。
炼金是很讲究放入原料的顺序的,因此小 E 必须按照 1 到 n 的顺序依次将这些原料放 入炼金锅。
但是,炼金锅的容量非常有限,它最多只能容纳 w 个原料。
所幸的是,每放入一个原料之前,小 E 可以从中取出一些原料,数量不能超过 s 个。
我们定义第 i 种原料的耐久度为:放入第 i 种原料时锅内的原料总数(包括正在放入的原料)× ai,则宝剑的耐久度为所有原料的耐久度之和。
小 E 当然想让他的宝剑的耐久度尽可能得大,这样他就可以带着它进行更多的战斗,请求出耐久度的最大值。

注:这里的“放入第 i 种原料时锅内的原料总数包括正在放入锅中的原料,详细信息请见样例。

输入格式

第一行,三个整数 n,w,s。
第二行,n 个整数 a1,a2,…,an。

输出格式

一行一个整数,表示耐久度的最大值。

样例

输入#1

5 3 3
1 3 2 4 5

输出#1

40

题解

根据题意可知,放入锅中的原料只能顺序放置,且必须放入。
因此,可以设一个dp[i][j]表示放第i件原料时,且锅中的原料总数为j,能够得到的最大耐久度。
可知,状态转移方程可以写做:
dp[i][j] = max{dp[i-1][k]} + a[i] * j(j-1 <= k <= j+s-1)
详细代码如下:

#include
#include
#include
#define INF 0x3f3f3f3f
using namespace std;
const int N = 5000 + 10;
typedef long long ll;
 
ll n, w, s;
ll a[N];
ll dp[N][N];
int main() {
	scanf("%lld%lld%lld", &n, &w, &s);
	memset(dp, -INF, sizeof(dp));
	for (int i = 1; i <= n; i++) {
		scanf("%lld", &a[i]);
	}
	dp[0][0] = 0;
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= w; j++) {
			for (int k = j - 1; k <= min(j + s - 1, w); k++)
				dp[i][j] = max(dp[i][j], dp[i - 1][k] + a[i] * j);
		}
	}
	ll ans = -INF;
	for (int i = 1; i <= w; i++)
		ans = max(dp[n][i], ans);
	cout << ans;
	return 0;
}

这个代码的时间复杂度可以达到O(nw^2)
可以考虑用单调队列优化掉max
因为 j-1 <= k <= j+s-1,所以要将 j 的循环改为从大到小。
用数组q[]模拟的队列来保存前面已经得到的dp的最大值。
维护队头,超出范围q[head] > j + s - 1,则踢出队头,head++。
维护队尾,dp[i-1][j-1]是最晚可能被踢出队头的,如果当前队尾的dp[i - 1][q[tail]],比dp[i - 1][j - 1]还要小,肯定就不如dp[i - 1][j - 1]更优了。就tail- -。

详细代码如下


#include
#include
#include
#define INF 0x3f3f3f3f
using namespace std;
const int N = 5000 + 10;
typedef long long ll;

ll n, w, s;
ll a[N];
ll dp[N][N], q[N];
int main() {
	scanf("%lld%lld%lld", &n, &w, &s);
	memset(dp, -INF, sizeof(dp));
	for (int i = 1; i <= n; i++) {
		scanf("%lld", &a[i]);
	}
	dp[0][0] = 0;
	for (int i = 1; i <= n; i++) {
		int l = 1, r = 1;
		q[l] = w;
		for (int j = w; j >= 1; j--) {
			//维护队尾
			while (q[l] > j + s - 1 && l <= r) l++;
			//维护队头
			while (dp[i - 1][q[r]] < dp[i - 1][j - 1] && l <= r) r--;
			//将最近的一个(最晚可能被踢出队的)入队尾
			q[++r] = j - 1;
			//队头一定是可行范围内的最大的(最优)
			dp[i][j] = dp[i - 1][q[l]] +  a[i] * j;
		}
	}
	ll ans = -INF;
	for (int i = 1; i <= w; i++)
		ans = max(dp[n][i], ans);
	cout << ans;
	return 0;
}

你可能感兴趣的:(笔记,算法,c++,c语言)