动态规划
动态规划需要满足的条件:
1.重叠子问题(最优子结构)
2.小问题->大问题
设计状态+状态转移
设计状态:
1.状态表示:对当前状态的充分描述,即什么因素影响答案,并且要尽量简洁
2.最优化值:则是对应的状态集合下的最优化信息(方案值),我们最终能通过其直接或间接得到答案。
对于状态的表示,要满足三条性质
1:具有最优化子结构:即问题的最优解能有效地从问题的子问题的最优解构造而来。
2:能够全面的描述一个局面。一个局面有一个答案,而这个局面是需要一些参数来描述的。
3:同时具有简洁性:尽可能的简化状态的表示,以获得更优的时间复杂度。
设计状态的关键就是 充分描述,尽量简洁
转移:从小到大
考虑到所有的可能性
计算时间复杂度:
一般来说都是 状态数*状态转移复杂度
加速状态转移:数据结构或者分析性质
例题:最长上升子序列
构造状态:设dp[i]为以a[i]为末尾的最长上升子序列的长度。
最后的答案就是枚举一下最长上升子序列的结束位置,然后取一个dp[i]最大值即可。
时间复杂度:O(n^2)
例:乘积最大
设有一个长度为N的数字串,要求选手使用K个乘号将它分成K+1个部分,找出一种分法,使得这K+1个部分的乘积能够为最大。
考虑最后一个乘号在哪个位置
用 f[i][a] 表示前 i 位数包含 a 个乘号所能达到的最大乘积,我们只需要枚举上一个乘号所在的位置即可。
将 j 从 a 到 i - 1 进行一次枚举,表示前 j 位中含有 a-1 个乘号,且最后一个乘号的位置在 j 处。那么当最后一个乘号在 j 处时最大值为前 j 位中含有 a - 1 个乘号的最大值乘上 j 处之后到i的数字。
因此得出了状态转移方程 f[i][a] = max(f[i][a] , f[j][a-1] * cut(j + 1,i))
——(cut(b + 1,i) 表示 b + 1 到 i 位数字)
然后再写个高精度即可。
bzoj4247: 挂饰
N个装在手机上的挂饰。挂饰附有可以挂其他挂件的挂钩。每个挂件要么直接挂在手机上,要么挂在其他挂件的挂钩上。直接挂在手机上的挂件最多有1个。此外,每个挂件有一个安装时会获得的喜悦值,用一个整数来表示,可能为负。
想要选出一些挂饰挂在一起,最大化所有挂饰的喜悦值之和。
1<=N<=2000
0<=Ai<=N(1<=i<=N)表示挂勾的数量
-10^6<=Bi<=10^6(1<=i<=N)表示喜悦值。
考虑排序
首先贪心的想,如果最终选出的一组挂饰,肯定是从上到下先挂所含挂钩多的。所以先按照挂钩数量从大到小排序。
设计状态:
首先肯定要有一维1~n表示到第i个物品
还要有一维表示挂钩数
然后设dp[i][j]前i个挂饰,剩余j个挂钩的最大喜悦值是多少即可。
转移枚举下一个挂饰是否挂。
注意dp[i][0]不能转移。
时间复杂度O(n^2)
洛谷P1233 木棍加工
一堆木头棍子共有n根,每根棍子的长度和宽度都是已知的。棍子可以被一台机器一个接一个地加工。机器处理一根棍子之前需要准备时间。准备时间是这样定义的:
第一根棍子的准备时间为1分钟;
如果刚处理完长度为L,宽度为W的棍子,那么如果下一个棍子长度为Li,宽度为Wi,并且满足L>=Li,W>=Wi,这个棍子就不需要准备时间,否则需要1分钟的准备时间;
计算处理完n根棍子所需要的最短准备时间。比如,你有5根棍子,长度和宽度分别为(4, 9),(5, 2),(2, 1),(3, 5),(1, 4),最短准备时间为2(按(4, 9)、(3, 5)、(1, 4)、(5, 2)、(2, 1)的次序进行加工)。
N<=5000
把L从大到小排序,求W最少的不上升子序列覆盖数
dilworth定理:最小覆盖数=反链长度
在这里就是最小覆盖数=最长上升子序列的长度
然后就求一个最长上升子序列就行了
LIS加强版
求最长上升子序列。
N<=10^5
用另两种方法。
分析性质:
考虑找到最大的k,满足dp[j]==k&&a[j]<a[i]
设h[k]表示dp[j]==k的所有j当中的最小的a[j],就是说长度为k的最长上升序列,最后一个元素的最小值是多少,因为最后一个元素越小,说明他的潜力更大,也就是说后面可以更容易的加上元素
而h[k],肯定是单调不下降的,就是说“长度为k的最长上升序列最后一个元素的最小值”一定是小于“长度为k+1的最长上升序列最后一个元素的最小值”,如果不是的话,我们可以用后者所在上升子序列构造出一个更小的前者。
然后这个样子我们对于一个a[i]就可以找到,最大的k,满足h[k]是小于a[i]的,然后f[i]=k+1。 找的过程是可以二分加速的。
然后同时在维护出h数组即可。
数据结构:
状态转移:dp[i]=max{ dp[j] | a[j]<a[i] && j<i } +1 ;
我们把a[j]看成坐标,dp[j]看成权值,这就是每次求坐标小于等于某个值的权值最大值,然后每算完一个单点修改即可。
线段树能做,但是大材小用了。
其实前缀求最大值树状数组就可以解决。
最长公共子序列
我们设dp[i][j]表示,S串的第i个前缀和T串的第j个前缀的最长公共子序列。
分情况:
如果S[i]==T[j],dp[i][j]=dp[i-1][j-1]+1;
如果S[i]!=T[j],dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
最后答案就是dp[n][m]
DP思路
大师:根据经验和直觉设计dp状态然后转移。
一般1:考虑结尾,分几种情况,发现可以转化为形式类似的子问题,根据这个类似的形式,设计状态。
(一般是只有一个结尾的问题我们这么考虑,对于多个结尾的(LIS),直接考虑一个分部即可。)
一般2:考虑搜索,然后转成记忆化再到递推:搜索->记忆化搜索->递归变递推。
最长公共上升子序列LCIS——分析性质优化状态转移
给两个序列A长度为n和B长度为m,求最长的公共子序列,还要保证这个序列是上升的
四次方复杂度:设dp[i][j]表示A串前i个和B串前j个的最长公共上升子序列,其中强制A[i]==B[j],也就是最后这个位置是匹配的。若是A[i]!=B[j]则对应函数值为0。枚举这之前的所有dp值的最大值然后更新
三次方复杂度:我们发现每次对所有的取max有很多重复算过的,而实际上dp[i][j]转移到dp[i][j+1]只多出了i个值(dp[1][j]~dp[i][j]),所以我们只需要对原来的max和之后的值取max就好了·
二次方复杂度:设f[j]=max dp[1~i-1][j],那么每次添加进去就是O(1)的了
对于同一个i,计算B串的dp值的时候,f不用更改,转移次数是O(n),当i变成i+1的时候会多增加一个值,有O(n)次增加,修改的复杂度也是O(n)的,总的修改的复杂度是O(2^n)
for(int i=1;i<=n;i++) { int tmp=0;//tmp表示前j个的合法的dp值的最大值 for(int j=1;j<=n;j++) { if(a[i]==b[j])//如果两个相等才有dp值 { dp[i][j]=tmp+1;//转移 f[j]=max(f[j],tmp+1); //其实不用取max,肯定是后者更大 } else if(a[i]>b[j]) tmp=max(tmp,f[j]);//更新tmp } }
之所以是a[i]>b[j],是因为如果j要对后面a[i]=b[k]的k的dp值产生贡献,就必然有b[j]<b[k],也就是b[j]<a[i]
Bzoj5124波浪序列
和LCIS完全一样的解法。
设f[i][j][0/1]表示第一个序列前i和第二个序列前j个位置,最后一个位置是上升还是下降,转移和之前一样,记录一个辅助数组即可。
注意这里是记方案数。
容斥原理
最基本的容斥模型:
给定一些条件, 问全部满足的对象的个数。
答案 = 所有对象 - 至少不满足其中一个的 + 至少不满足其中两个的 - 至少不满足其中三个的 +……
证明:考虑对于一个恰好不满足k个的的对象,被计算了几次。
显然只有当k=0时,这个对象才会被算进答案,即上式=1,否则上式=0(二项式定理),所以我们就证明了上面这个容斥方法的正确性。
Bzoj3782 简化版(网格路径计数 强化版)
从n*m网格图的左下角走到右上角(n,m<=10^6),有t个坐标不能经过(t<=200),只能向上向右走,问有多少种不同的走法,对10^9+7取模。
这道题可以用容斥来做。随意填-至少遇到一个障碍的方案数+至少遇到两个障碍的方案数-至少遇见三个障碍的方案数………………
给障碍点从左到右从下到上排个序,记f[i][j]表示走到了第i个障碍点且包括第i个点在内强制经过了j个障碍点的路径条数(除此之外也可能有经过的),枚举上一个经过的障碍点即可。
转移的时候乘上一个组合数表示从k到i的走法数目
另一种形式的容斥dp,枚举第一个遇到的障碍是哪一个来容斥。
实际上这是由下面这个推出来的。
记忆化搜索
还是网格
我们从另一个角度来思考这个问题。
我们用搜索算法来计算答案,先看看没有障碍的情况,有障碍只改一点。
int dfs(int x,int y) { if(x==n&&y==m) return 1; int ans=0; if(x<n) ans+=dfs(x+1,y); if(y<m) ans+=dfs(x,y+1); return ans; }
然而搜索的时间复杂度是指数级的。
观察一下:这是有些重复计算的。
我们发现在这个dfs的过程中,dfs出来的值只与带入参数,也就是(x,y)有关,而不同的(x,y)有N*M个,而我们之前搜索的问题在于有大量的重复计算,多次调用同一个(x,y),每次都重新计算。
有一个很直观的想法就是,第一次调用的时候就把答案记下来,之后调用不重新算,直接返回之前已经计算出的答案即可。——这就是记忆化搜索。
int dfs(int x,int y) { if(mp[x][y]==-1) return 0; if(x==n&&y==m) return 1; if(dp[x][y]!=-1) return dp[x][y]; int ans=0; if(x<n) ans+=dfs(x+1,y); if(y<m) ans+=dfs(x,y+1); dp[x][y]=ans; return ans; }
这是有障碍的情况,mp[x][y]==-1表示有障碍
种类不多,有可能多次调用同一个参数,就可以把它记下来,减少重复计算
滑雪
给定一个区域,由一个二维数组给出。数组的(i,j)代表点(i,j)的高度。我们要找一个最长的滑雪路径,注意滑雪只能从高往低处滑。下面是一个例子。
dp[x][y]存储在当前位置下山的最大长度,它等于 它旁边的(上下左右)比它矮的山的dp值加1 的最大值,即
dp[x][y]=max(dp[x-1][y] , dp[x][y-1] , dp[x][y+1] , dp[x+1][y])+1。
要保证对应的高度小于H[x][y]才能取max。
1:一般递推式动态规划还要注意枚举状态的顺序,要保证算当前状态时子状态都已经算完了。
2:但是记忆化搜索不需要,因为记忆化搜索就是个搜索,只不过把重复的部分记下来了而已。我们不用像递推一样过于关注顺序,像搜索一样直接要求什么,调用什么就好。
在有一些dp问题中,状态之间的转移顺序不是那么确定,并不能像一些简单问题一样写几个for循环就解决了。
我们可以直接计算最终要求的状态,然后在求这个状态的过程中,要调用哪个子状态就直接调用即可,但是每一个状态调用一遍之后就存下来答案,下次计算的时候就直接取答案即可,就不需要从新再计算一遍。
虽然看上去每一次都计算不少,但是因为每一个状态都计算一次,所以均摊下来,复杂度还是状态数*状态转移。
bzoj3810
考虑分割成两个矩形,对于任意一种分割方案都一定存在一条贯穿横向或者纵向的线,那么枚举这条线即可。
然后设f[x][y][t]表示长为x宽为y,面向大海的边状态是t,最小的不满意度。转移就枚举从那个地方断开即可。
拓扑图的dp
拓扑图dp通常是在拓扑图上求关于所有路径的某种信息之和。当然这里的“和”的运算法则可以是加法或是取max和min。或者其他定义的运算。
按拓扑序沿着有向边转移就可以了。
BZOJ4562 食物链
给定n个点m条边的有向无环食物网,求其中有多少条极长食物链(从入度为0的点到出度为0的点的方案数)。
n<=10^5,m<=2*10^5
dp[i]表示节点i为终点的食物链数量
dp[i]=Σ(dp[j])(j->i)
算到达的点的时候前面的点一定算完了
按照拓扑序的顺序转移即可
拓扑图dp
其实我们对于一般非有关期望和概率的dp,如果题目中每一个转移关系是双边的,那么如果我们把dp的每一个状态记为一个点, dp状态之间关系构成的图就是一个拓扑图。
拓扑图dp实际上就是已经给了我们这个拓扑关系了,也就不需要我们自己找了,其实是更简单。
例:
给一个n个点m条边的无向图,每一条边(u,v)有两个参数(len,cnt)表示边的长度以及边上不同的礼物数量,我们在每一个走过的边(u,v,len,cnt)只能选1个礼物,选择的方案数是cnt。
我们现在想从S走到T,我们想要求出在只走最短路径的情况下有多少种选择的礼物的方案数。
一条路径选择礼物的方案数就是每条边的cnt的乘积。答案对一个大质数取模。
n<=100000,m<=300000
先建最短路图(把所有最短路的边加进去)
对于一条边(u,v,len),如果dis[u]+len=dis[v],就说明这条边在最短路上
然后就是求DAG上从S到T,路径的方案数。
设f[u]为从u到T路径的方案数,
答案就是f[S]。
也可以记忆化
最大子矩阵
在n*n的整数矩阵中找一个最大的子矩阵,最大化元素和。
枚举上界和下界,然后求最大子段和
dp[i]=a[i]+max(dp[i-1],0)