一起加油吧~|

shiranui

园龄:2年7个月粉丝:17关注:12

2022-10-24 20:17阅读: 171评论: 0推荐: 2

dp优化 | 各种dp优化方式例题精选

前言

本文选题都较为基础,仅用于展示优化方式,如果是要找题单而不是看基础概念,请忽略本文。

本文包含一些常见的dp优化(“√”表示下文会进行展示,没“√”表示暂时还咕着):前缀和优化(√)、单调队列优化(√)、斜率优化(√)、四边形不等式优化、数据结构优化……

由于写本文主要是记录蒟蒻的dp优化学习过程,所以可能很不完善,也会有很多错误 (?) 。推荐看巨佬的:【学习笔记】动态规划—各种 DP 优化 - 辰星凌

1. 前缀和优化dp

进行状态转移时,如果发现需加上前面的一类状态,就可以选择使用数组进行累计操作,以达到降维度的效果。

1.1 P1521 求逆序对

1.1.1 题目大意

给出 nk,问 1..n 的排列中正好有 k 个逆序对的排列数。

1.1.2 数据范围

1n1001kn(n1)/2

1.1.3 做法

fi,j 表示 1..i 的全排列中有 j 个逆序对的排列数。答案即为 fn,k

考虑在 1..(i1) 的排列中加入一个 i 所能贡献的逆序对数量。由于 i 是最大的,故当它被排在第 j 个时,相应的逆序对数量会增加 ij 个。

不难列出转移式:fi,j=k=0min(j,i1)fi1,jk

其中的 k 表示新增的逆序对数。

同时初始化 f1,0=1

由于此题比较水,所以不优化也能过。

const int N = 110, mod = 10000;
int n, k, f[N][N * N >> 1];
int main() {
	n = read(), k = read();
	f[1][0] = 1;
	for (int i = 2; i <= n; i++)
		for (int j = 0; j <= (i * (i - 1)) >> 1; j++)
			for (int k = 0; k <= min(j, i - 1); k++)
				f[i][j] = (f[i][j] + f[i - 1][j - k]) % mod;
	printf("%d\n", f[n][k]);
	return 0;
}

接下来开始优化

现在把上面转移的式子改一下,方便优化:

fi,j=k=max(0,j(i1))jfi1,k,相应的,代码可以改成这样:

	for (int i = 2; i <= n; i++)
		for (int j = 0; j <= (i * (i - 1)) >> 1; j++)
			for (int k = max(0, j - (i - 1)); k <= j; k++)
				f[i][j] = (f[i][j] + f[i - 1][k]) % mod;

开数组 si,j=k=0jfi,k,那么 si,j=si,j1+fi,j

相应的,转移式变为 fi,j=si1,jsi1,j(i1)1注意边界问题

for (int i = 1; i <= n; i++) f[i][0] = s[i][0] = 1;
	for (int i = 2; i <= n; i++) {
		for (int j = 1; j <= (i * (i - 1)) >> 1; j++) 
			s[i - 1][j] = (s[i - 1][j - 1] + f[i - 1][j]) % mod;
		for (int j = 1; j <= (i * (i - 1)) >> 1; j++) 
			f[i][j] = (s[i - 1][j] + mod - ((j - (i - 1) - 1) < 0 ? 0 : s[i - 1][j - (i - 1) - 1])) % mod;
	}

注意到 s 数组的前一维似乎没有什么用处,考虑使用滚动数组继续优化。

for (int i = 1; i <= n; i++) f[i][0] = 1;
	s[0] = 1;
	for (int i = 2; i <= n; i++) {
		for (int j = 1; j <= (i * (i - 1)) >> 1; j++) 
			s[j] = (s[j - 1] + f[i - 1][j]) % mod;
		for (int j = 1; j <= (i * (i - 1)) >> 1; j++) 
			f[i][j] = (s[j] + mod - ((j - (i - 1) - 1) < 0 ? 0 : s[j - (i - 1) - 1])) % mod;
	}

1.2 P2513 [HAOI2009]逆序对数列

1.2.1 题目大意

给出 nk,问 1..n 的排列中正好有 k 个逆序对的排列数。

1.2.2 数据范围

1n,k1000

1.2.3 做法

乍一眼看是不是和上题一模一样。

如果直接提交上题的代码(改了数据范围),就会得到30分的好成绩。(最后几个点全部MLE)

稍稍计算一下,就会发现 499500000int 数组是不是有那么亿点点大?

那么如何优化代码呢?

注意到上题的代码中,逆序对数枚举的上限为 n×(n1)2,再瞅一眼本题数据范围,最大逆序对数只有 1000?!

不难想到改成以下代码:

const int N = 1010, mod = 10000;
int n, k, f[N][N], s[N ];
int main() {
	n = read(), k = read();
	for (int i = 1; i <= n; i++) f[i][0] = 1;
	s[0] = 1;
	for (int i = 2; i <= n; i++) {
		for (int j = 1; j <= min((i * (i - 1)) >> 1, k); j++) 
			s[j] = (s[j - 1] + f[i - 1][j]) % mod;
		for (int j = 1; j <= min((i * (i - 1)) >> 1, k); j++) 
			f[i][j] = (s[j] + mod - ((j - (i - 1) - 1) < 0 ? 0 : s[j - (i - 1) - 1])) % mod;
	}
	printf("%d\n", f[n][k]);
	return 0;
}

真好,既优化了空间又优化了时间。

2. 单调队列优化dp

OI-Wiki 传送门

借助单调队列的单调性,及时排除不可能的决策,保持候选集合的高度有效性和秩序性。

单调队列尤其适合优化决策取值范围的上、下界均单调变化,每个决策在候选集合中插入或删除至多一侧的问题。

2.1 P1440 求m区间内的最小值

2.1.1 题目大意

给定一个长度为 n 的数列 a,对于每个 i 输出 min{aim,aim+1,..,ai1}

2.1.2 数据范围

1mn2×1061ai3×107

2.1.3 做法

好像和单调队列优化dp没什么关系?

此题用于体验单调队列,就不多写了,直接用单调队列模拟操作即可。

const int N = 2000010;
int n, m, s[N], l = 1, r, a[N];
int main() {
	n = read(), m = read();
	printf("0\n");
	for (int i = 1; i <= n - 1; i++) {
		a[i] = read();
		while (r >= l && a[s[r]] > a[i]) r--;
		s[++r] = i;
		while (s[r] - s[l] + 1 > m && l <= r) l++;
		printf("%d\n", a[s[l]]);
	}
	return 0;
}

2.2 P5858 「SWTR-03」Golden Sword

2.2.1 题目大意

n 个物品,编号 1..n,每个物品有坚固值 ai

进行 n 次操作,对于每次操作,执行以下步骤:

  1. 取出不超过 s 个物品。
  2. 放入物品 i

其中容器最多容纳 w 个物品。

每次操作会产生 ai× 的贡献。

n 次操作后总贡献的最大值。

2.2.2 数据范围

1swn5×103|ai|109

2.2.3 做法

fi,j 表示正在执行第 i 次操作,容器内共有 j 个物品所能得到的最大贡献值。

那么 fi,j=max{fi1,k+ai×j}

其中 j1kmin{w,j1+s}

于是就得到了一个45分做法(long long没开全只有35)

const int N = 5010;
const ll INF = 1e18;
int n, w, s;
ll f[N][N], ans = -INF, a[N];
int main() {
	n = read(), w = read(), s = read();
	for (int i = 1; i <= n; i++) a[i] = read();
	for (int i = 0; i <= n; i++)
		for (int j = 0; j <= w; j++)
			f[i][j] = -INF;
	f[0][0] = 0;
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= w; j++)
			for (int k = j - 1; k <= min(w, j - 1 + s); k++)
				f[i][j] = max(f[i][j], f[i - 1][k] + a[i] * j);
	for (int i = 0; i <= w; i++) ans = max(ans, f[n][i]);
	printf("%lld\n", ans);
	return 0;
}

(不如先动手写个部分分做法?)

考虑优化。先把式子变一下:fi,j=max{fi1,k}+ai×j (j1kmin{w,j1+s})。很显然对吧,就是把原来max中重叠的部分提出来而已。虽然说这么一提好像不能优化什么,你会发现,max{fi1,k} 好像可以用单调队列优化?!

const int N = 5010;
const ll INF = 1e18;
int n, w, s;
ll f[N][N], ans = -INF, a[N];
int ss[N];
int main() {
	n = read(), w = read(), s = read();
	for (int i = 1; i <= n; i++) a[i] = read();
	for (int i = 0; i <= n; i++)
		for (int j = 0; j <= w; j++)
			f[i][j] = -INF;
	f[0][0] = 0;
	for (int i = 1; i <= n; i++) {
		int l = 1, r = 0;
		ss[++r] = w;
		for (int j = w; j; j--) {
			while (f[i - 1][ss[r]] < f[i - 1][j - 1] && r >= l) r--;
			ss[++r] = j - 1;
			while ((ss[l] - ss[r] + 1) - 1 > s && l <= r) l++;
			f[i][j] = f[i - 1][ss[l]] + j * a[i];
		}
	}
	for (int i = 0; i <= w; i++) ans = max(ans, f[n][i]);
	printf("%lld\n", ans);
	return 0;
}

3. 斜率优化dp

OI-Wiki 传送门

3.1 P3195 [HNOI2008]玩具装箱

3.1.1 题目大意

n 件物品,第 i 件物品压缩后占用 Ci 的长度。

现需把这些物品压缩进一些容器里,制作一个容器的花费为 (xL)2,其中 x 表示容器长度。

每个容器中的物品编号需要是连续的,而将编号 ij 的所有物品放在一个容器中,占用的空间 x=ji+k=ijCk

求压缩完所有物品所需的总花费的最小值。

3.1.2 数据范围

1n5×1041L1071Ci107

3.1.3 做法

fi 表示压缩到第 i 件物品所需的最小花费,不难列出转移方程:

fi=min{fj+(ij1+k=j+1ickL)2}


sumi=k=1ick,原式可转化为:

fi=min{fj+(ij1+sumisumjL)2}

移项得:

fi=min{fj+((i+sumi)(j+sumj)(L+1))2}

prei=sumi+i,原式可转化为:

fi=min{fj+(preiprej(L+1))2}


把式子展开再合并:

fi=min{fj+prei2prei×prej(L+1)×preiprei×prej+prej2+(L+1)×prej(L+1)×prei+(L+1)×prej+(L+1)2}

fi=min{fj+prei2+prej22×prei×prej2×(L+1)×(preiprej)+(L+1)2}

fi=min{fj+(preiprej)22×(preiprej)×(L+1)+(L+1)2}

fi=min{fj+(preiprej(L+1))2}


fi=min{fj+((prei(L+1))prej)2}

fi=min{fj+(prei(L+1))22×(prei(L+1))×prej+prej2}

fi(prei(L+1))2=min{fj+prej22×(prei(L+1))×prej}


令:

(1){bi=fi(prei(L+1)2)xj=prejyj=fj+prej2ki=2×(prei(L+1))

发现原式转化为 bi=min{yjki×xj}

看上去有那么亿点点的像 y=kx+b 呢……

考虑这个求 bi 的最小值的过程,就是在最小化直线的截距。把 (xj,yj) 看作平面上的一个点,现在有一条斜率为 ki 的直线,从下往上找(最小化),找到的第一个点就是转移决策点。

实际上,只需维护下凸壳的那些点。

对于本题,kii 的增大而增大,所以可以用单调队列进行维护。

const int N = 50010;
int n, c[N], l = 1, r = 0;;
ll sum[N], s[N], f[N], L;
ll Get(int x) {
	return f[x] + (sum[x] + L) * (sum[x] + L);
}
long double slope(int x, int y) {
	return (Get(y) - Get(x)) * 1.0 / (sum[y] - sum[x]);
}
int main() {
	n = read(), L = read() + 1;
	for (int i = 1; i <= n; i++) c[i] = read();
	for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + c[i] + 1;
	s[++r] = 0;
	for (int i = 1; i <= n; i++) {
		while (l < r && slope(s[l], s[l + 1]) <= (sum[i] << 1)) l++;
		f[i] = f[s[l]] + (sum[i] - sum[s[l]] - L) * (sum[i] - sum[s[l]] - L);
		while (l <= r && slope(s[r - 1], s[r]) >= slope(s[r - 1], i)) r--;
		s[++r] = i;
    }
    printf("%lld\n", f[n]);
	return 0;
}

N. 参考内容

DP优化 - zuytong

单调队列优化DP - superPG

本文作者:shiranui

本文链接:https://www.cnblogs.com/shiranui/p/16787371.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   shiranui  阅读(171)  评论(0编辑  收藏  举报
*/
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起