DP专场。。
动态规划是运筹学的一个分支, 求解决策过程最优化的数学方法。
我们一般把动态规划简称为DP(Dynamic Programming)
1.动态规划的背包问题
有一个容量为m的背包,有n个物品,每一个物品i的重量为w[i],价值为v[i]。
要求选择一些物品放入背包中,每种物品只能最多使用一次,使得在不超重的情况下让背包中所有物品价值总和最大。
正常向解法:设状态数组f[i][j]为把前i个物品放入一个容量为j的背包中所能获得的最大价值(以下同设),则状态转移方程为:
f[i][j] = max(f[i-1][j],f[i-1][j-w[i]]+v[i])
可优化至一维数组,令j从大到小枚举。
f[j] = max(f[j],f[j-w[i]]+v[i])
1.1完全背包
仍然是容量为m的背包,n个物品,每一个物品i的重量为w[i],价值为v[i]。
要求选择一些物品放入背包中,并且每一件物品可以选无限多次,使得在不超重的情况下让背包中所有物品价值总和最大。
f[i][j] = max(f[i-1][j],f[i-1][j-k*w[i]]+k*v[i])
第一维也可以优化。
f[j] = max(f[j],f[j-k*w[i]]+k*v[i])
1.2多重背包
依旧是容量为m的背包,n个物品,每一个物品i的重量为w[i],价值为v[i]。
要求选择一些物品放入背包中,并且每一件物品的数量为s[i],使得在不超重的情况下让背包中所有物品价值总和最大。
它相对于完全背包的不同之处是每种物品可选的数量有一个上限。
f[i][j] = max(f[i-1][j],f[i-1][j-k*w[i]]+k*v[i]),k*v[i] ≤ j, k ≤ s[i]
它的时间复杂度是O(m*Sigma(s[i])),而且看起来并不能像之前那样优化了。。
实际上它是可以优化的。
用「二进制拆分」即可。
对每种物品,我们将它转化成若干个物品,其中每个物品有一个系数,这个物品的费用和价值均是原费用和原价值乘这个系数。令这些系数分别为1,2,4,...,2^k-1,s[i]-(2^k)+1,且k是满足s[i]-(2^k)+1>0的最大整数,例如,如果s[i] = 11,就将这种物品分成系数分别为1,2,4,4这四个物品。
这些物品的系数和能拼出[0,s[i]]中的任意一个整数。这样一来我们就把一个n个物品的多重背包问题转化成了一个Sigma(log s[i])个物品的01背包问题了。
时间复杂度为O(m*Sigma(log s[i]))
实际上,这还能再优化。。。
用单调队列。
单调队列形似一个普通队列,但其中的点按顺序存在某种单调性(我记得我Day1的整理上提到过)。
不同于优先队列,他俩内部结构不太一样。
注意转移式:f[i][j] = max(f[i-1][j],f[i-1][j-k*w[i]]+k*v[i]),这时候k的范围已经变成了k≤min(s[i],ceil(j/w[i])).
对于某一个i而言,因为j = k*w[i]+s的点只能转移到j = (k+1)*w[i]+s , (k+2)*w[i]+s...,所以我们可以按对w[i]取模的值对j进行分类。
把模w[i]相等的点放在一起进行转移,转移后的点会放入单调队列中。我们发现由于队列中的点j是从小到大的,所以会存在转移某一点k,队首点head已经不能作为转移点,即(k-head)/w[i]>s[i]。
显然,该点在以后的转移中也不会成为转移点,所以直接弹出队首即可,直到队首可以作为转移点。转移时选择队首的点作为转移点, 因为这个单调队列是保证转移式是单调递减的。
将当前点k压入队列时,需要判断队尾点tail与k的优劣。如果tail作为转移点不比k更优,显然tail在以后的转移也不会作为转移点了,弹出tail,直至tail比k更优。
对于每个i来说,单次转移是O(m)的,所以时间复杂度是O(nm).
2.动态规划的线性(序列)类(一维)问题
经典例题:一个长度为n的数列,第i个数为a[i],要求选择连续且非空的一段,使这一段中所有数的和加起来最大,输出这个最大和。
正在看这篇随笔的读者朋友,您应该会显然想到O(n^2)的做法。
我们考虑DP。令f[i]表示以i为结尾的最大和子段,那么转移只有:和前面的最大子段和连起来,或者自己单独成为一段。
f[i] = a[i] +max(f[i-1],0)
答案是max(f[i]),i∈[1,n]。
时间复杂度O(n)
2.1 LIS
有一个长度为n的序列,第i个数为a[i],求最长上升子序列的长度。
最长上升子序列的英文名叫Longest Increasing Subsequence,简称LIS。
正在看这篇随笔的读者朋友,您应该会显然想到O(n^2)的做法。
我们重点讨论O(nlogn)的做法。
我们设一个数组g[i],它表示长度为i的LIS中,作为结尾最小的数。
如果原数组a[i] = {6,7,1,5,4,3,4,2,8},那么g数组的变化应该是:
1:g [1] = 6
2:g [1] = 6; g [2] = 7
3:g [1] = 1; g [2] = 7
4:g [1] = 1; g [2] = 5
5:g [1] = 1; g [2] = 4
6:g [1] = 1; g [2] = 3
7:g [1] = 1; g [2] = 3; g [3] = 4
8:g [1] = 1; g [2] = 2; g [3] = 4
9:g [1] = 1; g [2] = 2; g [3] = 3; g [4] = 8
容易看出,每次g数组只会修改一个值或者加入一个值。而且无论如何,g数组总是保持单调递增。
显然,当LIS的长度相等时,结尾的数肯定是越小越好,这样才有更大的机会与后面的数相接,才能生成更长的LIS。这样一来,g数组一定是单调递增的,因为g数组不会存一个较长的LIS,使得它的结尾数比较短的LIS还小。每次考虑以i结尾的上升子序列时,我们只要在g数组中找到最大的的小于a[i]的位置j,令g[j+1] = len即可。
由于g单调,所以可以用二分查找来找到位置j,单次转移复杂度为O(logn)。
最终复杂度为O(nlogn).
例:有两个序列a,b,长度分别为n,m,求它们的LCS。
LCS指最长公共子序列。表示从两个序列中各自选出长度相等的子序列,这两个子序列的数对应相等。
设f[i][j]表示两个序列分别dp到第i,j个数,公共子序列的最大长度。
则有
f[i][j] = max(f[i-1][j],f[i][j-1]) (a[i]!=b[j])
f[i][j] = f[i-1][j-1] + 1(a[i] == b[j])
时间复杂度O(n^2)。
例2:在上例条件不变的情况下,求LCIS
LCIS指最长公共上升子序列。表示从两个序列中各自选出长度相等的上升子序列,这两个子序列的数对应相等。
设f[i][j]表示两序列分别以i,j结尾的LCIS。
一个简单的想法是
f[i][j] = max(f[k][l]+1) (k < i , l < j, a[k] < a[i], b[l] < b[j], a[i]==b[j])
我们先枚举i,再枚举j,当a[i] != b[j] 时f[i][j] = 0。但当a[i] >b[j] 时,在同一个i下,所有f[k][j] (k<i)是可以作为转移点转移后面的状态f[i][j‘],因为存在方案的f[k][j]一定保证a[k]<a[i],b[j]<b[j‘]。
我们可以开一个数组来记录第二维为j且第一维小于i的f的最大值,不过第一维是可以被优化掉的。
设计状态f[i]表示b序列以b[i]结尾的LCIS,具体实现:
1 for (int i=1;i<=n;i++){ 2 int k= 0; 3 for (int j=1;j<=m;j++) 4 if (a[i] == b[j]) 5 f[j] = max(f[j],k+1); 6 else 7 if (a[i] > b[j]) 8 k = max(k,f[j]); 9 }
空间复杂度降为O(m)。
3.树形(树上)DP
顾名思义,是在树上所做的动态规划,其基础是树具有严格的层数关系而不会重复的特性。
一般来说树形DP是处理子树的信息以及其相互关系来进行转移。其状态一般会表示成以i为根的子树的DP值。
3.1邻接表存树
在之前的知识点整理上提前说了,这里只提供代码。
1 struct Edge_tree{ 2 int u,v,w; 3 int next; 4 5 }; 6 Edge_tree edge[maxn]; 7 int cnt = 0; 8 int first[maxn]; 9 void add_edge(int from,int to,int dis){ 10 edge[++cnt].u = from; 11 edge[cnt].v = to; 12 edge[cnt].w = dis; 13 edge[cnt].next = fisrt[from]; 14 first[from] =cnt; 15 16 edge[++cnt].v = from; 17 edge[cnt].u = to; 18 edge[cnt].w = dis; 19 edge[cnt].next = first[to]; 20 first[to] = cnt; 21 22 } 23 24 25 void dfs_tree(int x,int fa){ 26 //cout << x << " "; 27 for (int i = first[x];i!=0;i = edge[i].next) 28 if (edge[i].v != fa) 29 dfs_tree(edge[i].v,x); 30 }
3.2最大连通子树
有一个n个节点的树,每个点有点权a[i],求一棵连通子树使点权之和最大。
(n ≤ 10^5,|a[i]| ≤ 10^9)
f[u] = a[u] + Sigma(max(f[v],0))
最终答案是max(f[i]), i∈[1,n]
3.3树的直径
有一个n个节点的树,每条边有边权w[i],求一条路径使得它的所有边权和最大。
这条路径就叫做这棵树的直径。
(n ≤ 10^5,|a[i]| ≤ 10^9)
如果边权保证非负,我们可以用两遍bfs求得直径。具体做法是第一次随便选一个点,bfs求得与它距离最远的点x,再从x出发bfs求得与x距离最远的点y,x与y之间的距离就是树的直径。
若边权存在负数,则不能使用这个方法。
设f[u]表示在u为根的子树中,存在点u的最大路径边权和(u可以为端点也可以为中间的点)
设g[u]表示在u为根的子树中,存在点u的最大路径边权和(u只能为端点,即由u发出的一条链)
f[u] = max( firstmax{g[v] + w(u,v)}, 0) + max(secondmax{g[v] + w(u,v)},0)
g[u] = max{g[v] + w (u,v)}
4.区间DP以及其他DP
4.1区间DP
顾名思义,是在一个区间上进行的一系列动态规划,一般考虑对于每段区间,它们的最优值都是由两段或者更多段的小区间点的最优值得到,是分治思想的一种应用。
一般定义状态f[i][j]表示从区间i到j的DP最优值,转移时枚举中间点k,从f[i][k],f[k+1][j]来进行合并
例题:合并石子
n堆石子排成一列,每堆石子有一个重量w[i],每次可以合并相邻的两堆石子,一次合并的代价为二者重量之和。问怎样安排合并顺序使得代价最小。
n,w ≤100
解:用s[i]表示石子的前缀和,有
f[i][j] = min(f[i][k]+f[k+1][j]+s[j]-s[i-1])
时间复杂度O(n^3)。
变式:n堆石子排成一个圆,其他条件不变。
解:在后面加上一条排列相同的石子堆,扩展到2n-1个石子。依然是用s[i]表示w[i]的前缀和。有
f[i][j] = min(f[i][k]+f[k+1][j]+s[j]-s[i-1])
时间复杂度O(n^3),最终答案是min(f[i][i+n-1]),其中1 ≤ i ≤ n。
变式:使最终代价最大, 其他条件不变
解:贪心的来想,我们肯定是让每个石子重量都尽可能多的被计算,也就是说每次只合并一个石子进来应该是一个最优的策略。
f[i][j] = max(f[i+1][j],f[i][j-1])+s[j]-s[i-1]
时间复杂度O(n^2)。
4.2棋盘DP
非常好想的一类DP,在一个二维网格(地图)上做DP。
一般设f[i][j]表示走到(i,j)位置上的最优值。
4.3DAG上的DP
给定一个DAG(有向无环图),要求统计一些信息。
DAG是一个比较规则的结构,我们可以对这些点进行拓扑排序后再DP。
4.4状压DP
(GTMDNOIP2016D2T3
状态压缩DP,通过二进制位上的0/1表示状态,一般用于要记录一段较小规模的状态且该状态包含信息较多的问题。
就目前来说,在NOIP史上只有去年考到了。
谁知道今年还会考什么奇怪的东西呢……
4.5概率期望DP
(GTMDNOIP2016D1T3
这种题目会丧心病狂的让你求某一事件的期望或者概率。。
并不想写这类丧心病狂的东西。
4.6数位DP
(这已经超纲了吧。
这种题目一般会让你统计某一区间内的与数位或与数相关的信息,但由于区间较大而没法暴力求解,所以要在数位上进行DP。
就目前来说,没考过,谁知道今年考不考。。
5.相关优化
5.1前缀和优化
一般用于转移点的取值是一段连续的区间,做一下前缀和可以把转移从O(n)降至O(1)。
5.2滚动数组优化
这个还挺常见的。一般用于二维及以上的DP。如果某一维i的dp值只与i-1的dp值有关那么我们不用存这一维全部的情况,用0/1状态来存储当前状态和转移点状态就可以了,这样会降低空间复杂度。
5.3单调性优化
利用对某一属性的单调性来加速转移,体现在减少转移点数量和快速查询转移点的情况,常见的是单调队列, 二分查找等。
5.4数据结构优化
即利用一些数据结构来进行优化。NOIP阶段常用堆。
5.5其他优化
减少冗余状态
利用数据结构的特殊性质
女装