本文已参与「新人创作礼」活动,一起开启掘金创作之路。DP优化(状压DP & 倍增优化DP & 环形DP的单调队列优化DP)

111 阅读15分钟

优化DP专题

这是c++提高的第一讲

大纲

1.状压DP概念 2.例题 3.倍增 & RMQ 4.倍增优化DP 5.环形DP 6.环形DP的单调队列优化

1.状压DP概念

动态规划是解决“多阶段决策最优化问题”的一种算法思想。阶段的划分决定了状态的定义,状态定义的一个重要特性就是要确保**“无后效性”。** 很多DP问题在定义状态的时候,为了确保无后效性,需要在状态中加入多个维度,如果每个维度都用一维数组来表示的话,当维度较多时会导致占用的空间太大。 很多时候状态的维度虽然很多,但是决策非常少,特别的很多时候只有两种决策。例如背包问题中每个物品只有0和1两种决策。对于这种情况,没有必要为每个维度都分配一维空间,而是用一个二进制数来存储所有维度,每个二进制位记录一个维度的决策。这种使用二进制对状态进行压缩的DP,称为状态压缩DP。

2.例题

POJ 2411

思路:

按照行进行阶段划分,对于每一行中的每个格子做决策,某一行的任何一个格子,如果其是一个 竖着的1 * 2长方形的上半部分,那么它会对下一行的决策产生影响,否则其对下一行没有影响。 分别用1和0来表示格子的两种状态,1表示格子是1 * 2长方形的上半部分,0表示其他情况。 可以用一个M位的二进制数来表示每一行的某一行的格子状态。定义状态d(i, j)表示第i行的状态为j时前i行的分隔方案的总数,j是用十进制记录的M位二进制数。

在这里插入图片描述第i-1行的形态k能转移到第i行的形态j,当且仅当: k和j执行与位运算(&)的结果是0(两行的同一列不能同为1(都是12长方形的上半部分))。 k和j执行或位运算(|)的结果中,连续0的数量都是偶数个(代表横着的12的方块)。 为了提升性能,可以预处理出[0, 2^M - 1]中所有满足连续0的数量都是偶数的整数集合S。 状态转移方程:d(i, j) = sum(d(i-1, k)); j & k == 0 && j | k ∈ S 初始化:d(i, j) = 0, d(0, 0) = 1;最终答案为:d(N, 0); 时间复杂度:O(N * 2^M * 2^M) = O(N * 4^M); 代码:

#include <bits/stdc++.h>

using namespace std ;

const int N = 12 ;
long long n , m , dp[N][1 << N] ;
bool ok[1 << N] ;

int main ()
{


	while (cin >> n >> m && n)
	{
		memset (ok , 0 , sizeof (ok)) ;
		for (int i = 0; i < (1 << m); i++)
		{
			bool now = true , odd = true ;
			for (int j = 0; j < m; j++)
			{
				if (i & (1 << j)) odd &= now , now = true ;
				else now = ! now ;
			}
			ok[i] = odd & now ;
		}
		memset (dp , 0 , sizeof (dp)) ;
		dp[0][0] = 1 ;
		for (int i = 1; i <= n; i++)
		{
			for (int j = 0; j < (1 << m); j++)
			{
				for (int k = 0; k < (1 << m); k++)
				{
					if ((j & k) == 0 && ok[j | k])
					{
						dp[i][j] += dp[i - 1][k] ;
					}
				}
			}
		}
		cout << dp[n][0] << endl ;
	}


	return 0 ;
}

题目:

 宝藏 [treasure]
题目描述

参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了n个深埋在地下的宝藏屋,也给出了这 n个宝藏屋之间可供开发的 m 条道路和它们的长度。 小明决心亲自前往挖掘所有宝藏屋中的宝藏。但是,每个宝藏屋距离地面都很远,

也就是说,从地面打通一条到某个宝藏屋的道路是很困难的,而开发宝藏屋之间的道路 则相对容易很多。

小明的决心感动了考古挖掘的赞助商,赞助商决定免费赞助他打通一条从地面到某个宝藏屋的通道,通往哪个宝藏屋则由小明来决定。

在此基础上,小明还需要考虑如何开凿宝藏屋之间的道路。已经开凿出的道路可以 任意通行不消耗代价。每开凿出一条新道路,小明就会与考古队一起挖掘出由该条道路 所能到达的宝藏屋的宝藏。另外,小明不想开发无用道路,即两个已经被挖掘过的宝藏 屋之间的道路无需再开发。

新开发一条道路的代价是:

这条道路的长度 × 从赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的 宝藏屋的数量(包括赞助商帮你打通的宝藏屋和这条道路起点的宝藏屋)。

请你编写程序为小明选定由赞助商打通的宝藏屋和之后开凿的道路,使得工程总代价最小,并输出这个最小值。
输入格式

输入文件名为 treasure.in。

第一行两个用空格分离的正整数 n 和 m,代表宝藏屋的个数和道路数。

接下来 m 行,每行三个用空格分离的正整数,分别是由一条道路连接的两个宝藏屋的编号(编号为 1~n),和这条道路的长度 v。
输出格式

输出文件名为 treasure.out。 输出共一行,一个正整数,表示最小的总代价。
输入输出样列
输入样例14 5
1 2 1
1 3 3
1 4 1
2 3 4
3 4 1

输出样例14

输入样例24 5
1 2 1
1 3 3
1 4 1
2 3 4
3 4 2

输出样例25

说明

【输入输出样例1说明】

【输入输出样例2说明】


【数据规模与约定】

对于20%的数据:
保证输入是一棵树,1≤n≤8,v≤5000 且所有的v都相等。

对于40%的数据:
1≤n≤8,0≤m≤1000,v≤5000且所有的v都相等。

对于70%的数据:
1≤n≤8,0≤m≤1000, v≤ 5000

对于100%的数据:
1≤n≤12,0≤m≤1000, v≤ 500000

思路: 已经开凿的道路可以任意通行而不消耗代价,故只需要花费最小的代价让所有宝藏屋连通即可, 因此最终修建的道路和宝藏屋一定构成了一棵树,否则一定存在冗余道路,并且删除冗余道路不 影响宝藏屋的连通性。赞助商打通的宝藏屋是整棵树的根。 搜索: ① 任选一个点为根(由赞助商帮忙打通的宝藏屋)。 ② 搜索的每一层,任选一个已经打通的宝藏屋,从该宝藏屋出发打通一条道路,到达一个还 未打通的宝藏屋y,代价为:道路(x, y)的长度 * 节点x的深度。 上述算法在每次递归搜索时都会遍历所有已经打通的宝藏屋尝试打通一条道路,显然整个搜索 过程中遍历了非常多的重复状态,时间复杂度太高,可以做出两点优化: ① 先打通较浅(到根节点较近)的宝藏屋,在打通较深的宝藏屋。因为在合法的前提下,对 于同一个树形结构,打通宝藏屋的代价与宝藏屋的打通顺序无关。 ② 设已经打通的宝藏屋集合为S。对于相同的集合S,只需要关注代价最小的那一个。因为打 通后续宝藏屋的代价与S的内部道路连接无关,只要集合S是连通的就是一样的。 优化方案中第1点限制由浅到深形成了动态规划的“阶段”,第2点只考虑最小代价满足了动态规划 的 “最优子结构”性质。考虑使用动态规划进行求解。 定义状态:d(i, j)表示已经打通的宝藏屋的深度为i,宝藏屋的打通状态为j时,花费的最小代价 (j是一个n位二进制数,表示n个宝藏屋的打通状态)。 状态转移方程:d(i, j) = min(d(i-1, k) + (i - 1) * cost(k, j)); (k需要能转移到j)。 初始化:d(1, 1 << (x - 1)] = 0, x∈[1, n],其余状态为无穷大。 最终答案:ans = min(d[i][(1<<n) - 1]; 1 <= i <= N; 状态k要只扩展一层的前提下,转移到状态j,需要满足以下条件: ① 状态k对应的节点集合是状态j对应的节点集合的子集,即:k & j = k。 ② 设ex(k)表示从k中所有节点出发,向下扩展一层以内的所有道路之后,所有被打通的宝藏 屋的集合。则:j对应的节点集合是ex(k)的子集,即:j & ex(k) = j; ex(k)可以先预处理获得:枚举k,遍历k中所有节点的边即可,同时可以求出从k中节点扩展 到新打通的节点x的最短道路,设为r(k, x) 如何知道k中哪些节点深度为i-1呢? 可以不考虑,把k中所有节点都按照深度为i-1进行处理和计算,这样计算并不会影响最终结 果。因为深度小于i-1的节点在更早阶段的时候已经计算过了,此时即使按照i-1的代价进行扩 展肯定也不是最优的,不影响最终结果。

3.倍增 & RMQ

(1)倍增

问题描述:小溪的兔子走丢了,现在她要去把兔子找回来,已知兔子位于一个长度为N的连续方格中的某个方格m内。小溪可以借助工具直接传送到她想到达的格子内,并且小溪在兔子身上装了追踪器,但是追踪器的精度有限,只能告诉小溪兔子在她的左边或者右边,小溪如何才能尽快的找到她的兔子呢?

方案1:一步一步的向右找,时间复杂度: O(M)。 方案2:使用二分策略进行查找,时间复杂度: O(log(N)) 方案3:倍增: 设小溪当前所在位置为x,接下来小溪将向右走d个格子。 ① 初始化: x = 0,d = 1; ② 决策: 如果x + d < m,则走到x + d位置,更新: x = x + d,d *= 2; 如果x + d > m,则保持不动,更新: d/= 2; ③ 循环: 重复②③两步,直到x + d == m

(2)RMQ问题

RMQ引入 在这里插入图片描述方案1:枚举:每次查询从A[1] 开始逐个累加判断,询问的花费时间与T有关,时间复杂度:O(N)。 方案2:前缀和 + 二分:预处理出A数组的前缀和数组sum,然后在sum中二分查找T的上界。 方案3:前缀和 + 倍增:在sum数组中使用倍增的方式查找T。 方案3实现: ① 初始化: k = 0,d = 1; ② 如果sum[k + d] ≤ T,则: k += d;d *= 2; ③ 如果sum[k + d] > T,则: d/= 2; ④ 重复②③两步,直d == 0,此时的k就是询问的答案。 RMQ问题 RMQ (Range Minimum/Maximum Query)问题是指:已知长度为N的数列A,回答若干询问Q(l,r),返回区间A [l , r] 的最小(大)值,即:RMQ问题是指求区间最值的问题。 ①枚举:单次询问时间复杂度:O(N)。 ②线段树 / 树状数组:单次询问时间复杂度:O(log(N))。 倍增 + DP:定义d(i,j)表示以A[i] 为起始的2 ^ j 个元素 (i, i + 2 ^ j - 1)的最小值。 状态转移方程: 在这里插入图片描述通过d推出答案: query(l, r) = min(d(l, k), d(r - 2 ^ k + 1, k));

4.倍增优化DP

题目:

开车旅行
题目描述

小 A 和小 B 决定利用假期外出旅行,他们将想去的城市从 1 到 N 编号,且编号较小的城市在编号较大的城市的西边,已知各个城市的海拔高度互不相同,记城市 i 的海拔高度为Hi,城市 i 和城市 j 之间的距离 d[i,j]恰好是这两个城市海拔高度之差的绝对值,即d[i,j] = |Hi− Hj|。 旅行过程中,小 A 和小 B 轮流开车,第一天小 A 开车,之后每天轮换一次。他们计划选择一个城市 S 作为起点,一直向东行驶,并且最多行驶 X 公里就结束旅行。小 A 和小 B的驾驶风格不同,小 B 总是沿着前进方向选择一个最近的城市作为目的地,而小 A 总是沿着前进方向选择第二近的城市作为目的地(注意:本题中如果当前城市到两个城市的距离相同,则认为离海拔低的那个城市更近)。如果其中任何一人无法按照自己的原则选择目的城市,或者到达目的地会使行驶的总距离超出 X 公里,他们就会结束旅行。

在启程之前,小 A 想知道两个问题:

1.对于一个给定的 X=X0,从哪一个城市出发,小 A 开车行驶的路程总数与小 B 行驶的路程总数的比值最小(如果小 B 的行驶路程为 0,此时的比值可视为无穷大,且两个无穷大视为相等)。如果从多个城市出发,小 A 开车行驶的路程总数与小 B 行驶的路程总数的比值都最小,则输出海拔最高的那个城市。

2.对任意给定的 X=Xi和出发城市 Si,小 A 开车行驶的路程总数以及小 B 行驶的路程总数。

输入格式

第一行包含一个整数 N,表示城市的数目。

第二行有 N 个整数,每两个整数之间用一个空格隔开,依次表示城市 1 到城市 N 的海拔高度,即 H1,H2,……,Hn,且每个 Hi都是不同的。

第三行包含一个整数 X0。

第四行为一个整数 M,表示给定 M 组 Si和 Xi。

接下来的 M 行,每行包含 2 个整数 Si和 Xi,表示从城市 Si出发,最多行驶 Xi公里。
输出格式

输出共 M+1 行。

第一行包含一个整数 S0,表示对于给定的 X0,从编号为 S0的城市出发,小 A 开车行驶的路程总数与小 B 行驶的路程总数的比值最小。

接下来的 M 行,每行包含 2 个整数,之间用一个空格隔开,依次表示在给定的 Si和Xi下小 A 行驶的里程总数和小 B 行驶的里程总数。
输入输出样列
输入样例14
2 3 1 4
3
4
1 3
2 3
3 3
4 3

输出样例11
1 1
2 0
0 0
0 0

输入样例210 
4 5 6 1 2 3 7 8 9 10 
7 
10 
1 7 
2 7 
3 7 
4 7 
5 7 
6 7 
7 7 
8 7 
9 7 
10 7

输出样例22 
3 2 
2 4 
2 1 
2 4 
5 1 
5 1 
2 1 
2 0 
0 0 
0 0

说明

【输入输出样例 1 说明】

各个城市的海拔高度以及两个城市间的距离如上图所示。

如果从城市 1 出发,可以到达的城市为 2,3,4,这几个城市与城市 1 的距离分别为 1,1,2,但是由于城市 3 的海拔高度低于城市 2,所以我们认为城市 3 离城市 1 最近,城市 2 离城市1 第二近,所以小 A 会走到城市 2。到达城市 2 后,前面可以到达的城市为 3,4,这两个城市与城市 2 的距离分别为 2,1,所以城市 4 离城市 2 最近,因此小 B 会走到城市 4。到达城市 4 后,前面已没有可到达的城市,所以旅行结束。

如果从城市 2 出发,可以到达的城市为 3,4,这两个城市与城市 2 的距离分别为 2,1,由于城市 3 离城市 2 第二近,所以小 A 会走到城市 3。到达城市 3 后,前面尚未旅行的城市为4,所以城市 4 离城市 3 最近,但是如果要到达城市 4,则总路程为 2+3=5>3,所以小 B 会直接在城市 3 结束旅行。

如果从城市 3 出发,可以到达的城市为 4,由于没有离城市 3 第二近的城市,因此旅行

还未开始就结束了。

如果从城市 4 出发,没有可以到达的城市,因此旅行还未开始就结束了。


【输入输出样例 2 说明】

当 X=7 时, 如果从城市 1 出发,则路线为 1 -> 2 -> 3 -> 8 -> 9,小 A 走的距离为 1+2=3,小 B 走的距离为 1+1=2。(在城市 1 时,距离小 A 最近的城市是 26,但是城市 2 的海拔更高,视为与城市 1 第二近的城市,所以小 A 最终选择城市 2;走到 9 后,小 A 只有城市 10 可以走,没有第 2 选择可以选,所以没法做出选择,结束旅行)

如果从城市 2 出发,则路线为 2 -> 6 -> 7 ,小 A 和小 B 走的距离分别为 24。

如果从城市 3 出发,则路线为 3 -> 8 -> 9,小 A 和小 B 走的距离分别为 21。

如果从城市 4 出发,则路线为 4 -> 6 -> 7,小 A 和小 B 走的距离分别为 24。

如果从城市 5 出发,则路线为 5 -> 7 -> 8 ,小 A 和小 B 走的距离分别为 51。

如果从城市 6 出发,则路线为 6 -> 8 -> 9,小 A 和小 B 走的距离分别为 51。

如果从城市 7 出发,则路线为 7 -> 9 -> 10,小 A 和小 B 走的距离分别为 21。

如果从城市 8 出发,则路线为 8 -> 10,小 A 和小 B 走的距离分别为 20。

如果从城市 9 出发,则路线为 9,小 A 和小 B 走的距离分别为 00(旅行一开始就结

束了)。

如果从城市10出发,则路线为 10,小A 和小B 走的距离分别为00。

从城市 2 或者城市 4 出发小 A 行驶的路程总数与小 B 行驶的路程总数的比值都最小,但是城市 2 的海拔更高,所以输出第一行为 2。


【数据范围】

对于30%的数据,有1≤N≤201≤M≤20;

对于40%的数据,有1≤N≤1001≤M≤100;

对于50%的数据,有1≤N≤1001≤M≤1,000;

对于70%的数据,有1≤N≤1,0001≤M≤10,000;

对于100%的数据,有1≤N≤100,0001≤M≤100,000-1,000,000,000≤Hi≤1,000,000,0000≤X0≤1,000,000,0001≤Si≤N,0≤Xi≤1,000,000,000,数据保证Hi 互不相同。

思路: 在这里插入图片描述在这里插入图片描述

题目:

题目描述

定义 conn(s,n) 为 n 个字符串 s 首尾相接形成的字符串,例如:
conn("abc",2)="abcabc"
称字符串 a 能由字符串 b 生成,当且仅当从字符串 b 中删除某些字符后可以得到字符串 a。例如“abdbec”可以生成“abc”,但是“acbbe”不能生成“abc”。
给定两个字符串 s1 和 s2,以及两个整数 n1 和 n2,求一个最大的整数 m,满足conn(conn(s2,n2 ),m) 能由 conn(s1,n1) 生成。
s1 和 s2 长度不超过100,n1 和 n2 不大于 10^6。
输入格式

包含多组数据。每组数据由2行组成,第一行是s2,n2,第二行是s1,n1。
输出格式

对于每组数据输出一行表示答案m。
输入输出样列
输入样例1:

ab 2
acb 4
acb 1
acb 1
aa 1
aaa 3
baab 1
baba 11
aaaaa 1
aaa 20

输出样例12
1
4
7
12

思路: 在这里插入图片描述

5.环形DP

在很多环形结构问题中,可以通过枚举法,选择一个位置把环断开,变成线性结构进行计算,最后根据每次枚举的结果求出最优解,把能通过上述枚举方式求解的环形问题称为:“可拆解的环形问题”。环形DP的核心是采取适当的策略避免枚举,从而降低时间复杂度。 常用策略: 两次DP:第1次在任意位置将环断开成链,按照线性问题求解;第2次通过适当的条件和初始化,保证计算出的状态等价于把断开的位置连接后的结果。 方法,复制成链:在任意位置把环断开成链,然后复制一倍接在尾部。 例题

 Naptime【USACO05JAN】
题目描述

Bessie是一只非常缺觉的奶牛.她的一天被平均分割成N段(3≤N≤3830),但是她要用其中的 B 段时间(2≤B<N)睡觉。每段时间都有一个体力恢复值 U_i(0≤U≤2×10^5),只有这段时间她在睡觉,才会获得恢复值。有了闹钟的帮助,贝茜可以选择任意的时间入睡,当然,她只能在时间划分的边界处入睡、醒来。贝茜想使所有睡觉效用的总和最大。不幸的是,每一段睡眠的第一个时间阶段都是“入睡”阶段,而旦不记入效用值。时间阶段是不断循环的圆(一天一天是循环的嘛),假如贝茜在时间段N和时间段1睡觉,那么她将得到时间段1的恢复值。
输入格式

第1行:两个用空格分隔的整数N和B,含义见题目描述。

第2到N+1行:每行一个整数,其中第i+1行的整数表示第i个时间段的恢复值。
输出格式

一行:一个整数,表示Bessie每天可以获得最大恢复值之和。
输入输出样列
输入样例15 3
2
0
3
1
4

输出样例16

说明

【样例说明】

Bessie每天从第4个时间段入睡,第1个时间段结束醒来,可以获得4 + 2 = 6点恢复值。

思路: 多阶段决策最优化问题,考虑动态规划,先不考虑环的问题。 定义状态:d[i][j]表示前i个小时休息j个小时可以获得的最大恢复值。 状态转移方程:对第i个小时做决策,休息或者不休息。 ① 第i个小时不休息:d[i][j] = d[i-1][j]; ② 第i个小时休息:此时需要知道第i-1小时是否休息: a) 如果第i-1小时休息,那么第i个小时的休息可以获得u[i]点的恢复值 b) 如果第i-1小时不休息,那么第i个小时的休息无法获得恢复值。 状态中不包含第i-1小时是否休息的信息,加入该信息。

定义状态:d[i][j][0/1]表示前i小时休息j小时,且第i小时不休息/休息可以获得的最大恢复值。 状态转移方程:第i个小时做决策,休息或者不休息。 ① 第i个小时不休息:d[i][j][0] = max(d[i-1][j][0], d[i-1][j][1]); ② 第i个小时休息:d[i][j][1] = max(d[i-1][j-1][0], d[i-1][j-1][1] + u[i]); 问题:上述DP还有一种情况未考虑,由于一定是从第1个小时开始,所以第1个小时一定无法 获得恢复值。实际问题中, 可以依赖前一天的第N个小时让我们的第1小时可以获得恢复值, 此时第N小时和第1小时都必须在休息。 初始化:d[1][0][0] = 0, d[1][1][1] = 0,其他全部为-INF。 最终答案:ans = max(d[n][b][0], d[n][b][1]);

针对时间段1休息并获得恢复值的情况:最终答案:ans = d[n][b][1];初始化:d[1][1][1] = u[1], 其他全部为-INF。 按照上述两种情况,进行两次DP,两者的最大值就是问题的答案。

6.环形DP的单调队列优化

单调队列优化DP的状态转移方程一般可表示为:d[i] = min/max(d[j] + cost(i, j)) (L[i] <= j <= R[i]); L[i]和R[i]是关于变量i的一次函数,限制了决策j的取值范围,并保证其上下界变化具有单调性。 cost(i, j)是关于变量i和j的多项式函数,如果可以将cost(i, j)分成只和i相关以及只和j相关的两部分, 将关于j的部分和d[j]组合在一起,形成只和j相关的多项式,通过单调队列优化多项式的最值获取。