DP问题
打卡第一天!
1.数字三角形模型(动态规划)
方法:从集合角度来考虑DP问题——y氏DP法
状态表示:集合(根据题目自己思考)and属性(数量/Max/Min)
状态计算:集合的划分
重要的划分依据:“最后”
重要的划分原则:1、不重复 2、不漏
摘花生问题
最低通行费问题
要注意边界的分类讨论和求最小值时,状态表示为正无穷。
方格取数问题
用三维的数组表示,特殊状态k为步数,步数相同时,如果两条路线的横坐标+纵坐标相等,则两条路径的重合。k = i + j ,横纵坐标之和。
传纸条问题
右下角往左上角传可以看作左上角往右下角传,转化为方格取数问题。
总结
题谱:
90%的dp问题都能转化为最短路问题,拓扑图可以转化为dp问题。记住模型,到相似题目就会有更清晰的思路,不会到无从下手。
2、最长上升子序列模型(LIS)
打卡第二天!
因为是一维的数组,所以状态表示只需要一维。
因为状态表示为以倒数第一个数结尾,所以考虑状态计算的时候,从倒数第二个数考虑(空的情况),是序列里只有a[i]的情况。
都有共同元素a[i],以a[k]结尾的最长上升子序列正好是f(k),所以等于f(k) + 1,且只有在a[k] < a[i]时成立。
最长上升子序列
基础的LIS模板。
怪盗基德滑翔翼
往左:最长上升子序列。往右:最长下降子序列(逆向最长上升)
登山
状态表示时从顶点考虑,a[k]被算了两次,所以重复了,-1。
合唱队形
登山问题的对偶问题。最少多少出列就是最多留下多少的意思,用总数减去结果即可。
友好城市
控制住自变量,去思考因变量,散列,转化为一维问题。把岸两边的数都拍好序,确保连接的航道是友好航道,有一个不是上升的就会相交,因此是最长上升子序列模型。思考过程很难(能想到LIS,基本上就解决了)
最大上升子序列和
比较简单,就是模型求个数和变成求值的和
拦截导弹
DP + 贪心(基于基础课的最长上升子序列(II))
也可以用贪心 + 贪心来做,Lower_bond和upper_bond。
lower_bond : (begin , end , a ) 返回第一个大于等于a的数组的下标
upper_bond : (begin , end , a) 返回第一个大于a的数组的下标
lower_bond : (begin , end , a , greater <int> () ) 返回第一个小于等于a的数组的下标
upper_bond : (begin , end , a , greater<int> () ) 返回第一个小于a的数组的下标
include <iostream>
#include <algorithm>
using namespace std;
const int N = 1100;
int f[N] , g[N];
int main()
{
int len = 0 , cnt = 0;
int a;
while(cin >> a)
{
//pos1 表示以a结尾的最长下降子序列长度
int pos1 = upper_bound(f , f + len , a , greater<int> ()) - f;
if (pos1 == len) f[len ++] = a; //开创一套新的导弹系统
else f[pos1] = a; //使a成为当前导弹系统的最后一位
int pos2 = lower_bound(g , g + cnt , a) - g;
if (pos2 == cnt) g[cnt ++ ] = a;
else g[pos2] = a;
}
cout << len << endl << cnt << endl;
return 0;
}
导弹的防御系统
在拦截导弹的基础上使用全局最小值DFS,实现两种不同的拦截导弹系统的部属。
最长公共上升子序列
最长公共子序列(4种)和最长上升子序列(从倒数第二个数考虑)结合考虑,用一个二维数组来表示集合。
根据状态计算可以用三重循环解决,复杂度是o(n^2)
最后一重循环也就是求1~j -1 的b序列的最长上升子序列。
但是最后一重循环在if的判断下,可以将b[j]替换成a[i],也就是跟k没有关系,因此可以优化成二重循环。
总结
题谱:
最长上升子序列可以转化变种为最长下降子序列,也可以结合贪心进行优化,还可以与最长公共子序列进行结合,最终都落实到用集合的划分。
3、背包模型
背包问题划分依据:物品作为一维,体积作为一维,互相约束,表示成集合。
所有问题都是01背包问题的拓展
01背包问题:每个物品只有选择或者不选
完全背包问题:每个物品可以选0、1、2....无数个
如果是这样的划分暴力搜索的话,需要遍历s个物品,算法复杂度会来到o(n^3),因此要考虑完全背包的优化。
另j = j - v,同时由于完全背包物体体积固定,所以s = j / v 是固定的(这点和多重背包不一样)。
可以惊奇的发现,上面的最大值就比下面的最大值多一个w。所以f[i , j]可以等于max(f[i - 1][j] , f[i][j - v] + w),省去一维的循环!
优化过的完全背包模型:f[i][j] = max(f[i - 1][j] , f[i][j - v] + w)
还可以把空间优化成一维。
注意:所有的背包问题都是先循环物品,然后体积,最后决策。
多重背包问题:每个物品可以选si个
如果是单纯的让j = j -v ,会发现会多出一项,不好直接表示。
注意:多重背包种的s是最多选s个,而完全背包的s是无上限,直到最大体积的。
所以最后一项为(j - v - sv) -> j - (s + 1)v,是这么来的。
多重背包的优化:用单调队列维护滑动串口
r可以看作是拿完所有i物品后,剩下的体积余数,即r = j % v。
用g来维护总价值,q单调队列来维护体积。期间涉及滑动窗口的弹出,以及单调队列的去除冗余操作,代码不难,思路难想。
#include <iostream>
#include <cstring>
using namespace std;
int n , m;
const int N = 20100;
int g[N]; //g的滚动数组维护下标
int f[N];
int q[N]; //q的单调队列维护体积
int main()
{
cin >> n >> m;
for(int i = 0 ; i < n ; i ++ )
{
int v , w , s;
cin >> v >> w >> s;
memcpy (g , f , sizeof f);
for(int j = 0 ; j < v ; j ++ )
{
int hh = 0 , tt = -1;
for(int k = j ; k <= m ; k += v)
{
while(hh <= tt && q[hh] < k - s * v) hh ++; //维护滑动窗口
while(hh <= tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j)/ v * w) tt --; // 去除冗余元素
q[++ tt] = k;
f[k] = g[q[hh]] + (k - q[hh]) / v * w; //增加的收益
}
}
}
cout << f[m] << endl;
return 0;
}
多重背包问题III(滑动窗口优化)
背包背景的问题主要从三个状态考虑:物品,体积,价值,要从实际问题提取成这三个状态。
庆功会
多重背包问题,可用滑动窗口+滚动数组来优化实现
分组背包问题:把物品分为若干组,每组物品选择一个决策。
状态表示:前i组物品中选,总体积不超过j的方案。
状态计算:0表示不选第i组物品,结果为f[i - 1][j]
1表示选第i组物品中的第1个物品,结果为f[i -1][j - v[i , 1]] + w[i , 1]
2表示选第i组物品中的第2个物品,结果为f[i - 1][j - v[i , 2]] + w[i , 2]
....
s[i]表示选第i组物品中的第s[i]个物品,结果为f[i - 1][j - v[i , s[i]] + w[i , s[i]]
分组背包和其它背包的最本质区别:每组物品以及组内的每个物品对应的价值不是定值!!
机器分配
把公司每个子公司的编号分成若干组,分配总数看作总体积,子公司分配数看作体积,每个体积对应一个价值。
要注意输入矩阵时,j不能从0开始,要和循环组的编号一致,从1开始
由于要输出具体方案,因此不能优化成一维。每组对应的体积用way数组来记录。
金明的预算法案
把每个主件和附件的组合看成一个组。每个组合由多个选择,每个选择作为一种决策。
第一个比较麻烦的点是怎么把每个物品组存下来----- 可以用pair 和 vector来存储每个物品组。
注意选择主键的时候,要加上主键的价值。
二进制循环容器比较方便,记住二进制循环的两重循环+判断。
采药
简单的01背包一维空间优化。
装箱问题
将体积看成价值,用01背包解决。
二维费用的背包问题
对于一个物品有两种对体积的限制,从状态表示的层面上修改状态计算。再用01背包的优化掉一维。
宠物小精灵之收服(二维费用)
注意:体力值的下限不能为0,所以从1开始,体积2的最大值要-1。
潜水员
由最大体积变为最小体积,最大值变为最小值,从状态表示的方面上修改状态计算。
第三种题型:体积至少是j
初始化:
f[0][0][0] = 1 表示一个物品都不选,氧气,氮气至少为0,是一种方案
f[0][j][k] = INF 表示一个物品都不选,氧气,氮气至少为j,k。这是不可能的,由于求的
是最小值,因此初始化为正无穷
注意:
j < vi 是,虽然j - v1 是负数,但是由于状态表示为至少,也就是方案下限存在,如果j-v1是负数的话,可以看作方案数为0,也就是另j = vi。跟其它题不同的就是不需要j >= vi,负数和不选气缸等价。
三种情况的体积限制
凡是从i - 1层转移来的, 都是从大到小循环;从i层转移的,都是从小到大循环。
数字组合
思考过程:
物品:每个数 , 体积:Ai , 价值:无,状态表示:所有从前i个物品中选择,总体积恰好是M的方案的总数count
不选物品的时候,体积为0,也是一种方案,因此在一维数组中,需要让f[0] = 1。
买书
完全背包模型,书的种类和价格是确定的,把n元钱看作体积,且体积恰好最大。
恰好的时候,注意初始状态的赋值:f[0] 也是一种方案,因此f[0] = 1
体积有四种,不需要输入,v[4]定义成全局变量
开心的金明
简单的01背包模型。
背包问题求具体方案
后面的方案由前面一层过来,因此记录状态的时候从后往前循环,注意状态计算中i - 1变成i + 1。
由于判断是否被选的时候,要求按照字典序最小输出,因此输出i的时候,从前往后循环。
货币系统
完全背包模型的求方案数。注意方案数很多,数组可能会越界,要用long long来存。
货币系统(NOIP2018)
b集合中的数都是从a集合中来的。可以看成完全背包求方案数。
把a集合中每个数据看成物品,金额看成体积,每个数据可以选无限次,因此可以看错是完全背包。
把ai看作是最大体积,a1~ai - 1 的体积恰好等于ai的方案总数。
后一个数一定是比前一个数大的,因此要先排序。
注意:排序如果是从1开始,sort(a , a + n + 1) 要多加个1
混合背包问题
取决于第i件物品的类型,因此只需要考虑第i件物品。如果只有一个,用01背包的状态方程;如果有限个,用多重背包的状态方程;如果是无限个,用完全背包的状态方程。
因为数据过大,所以使用多重背包模型II(二进制优化)
也可以使用多重背包模型III(滑动窗口优化),亲测更快。
有依赖的背包问题
有依赖的背包问题 = dfs + 邻接表存储树 + 完全背包模型
背包问题求方案数(最大价值)
与最大体积限制不同,限制的是最大价值。
收到最短路算法的启发,可以将状态计算成如下图所示。
需要统计出最大值,如果最大值与当前状态相等,总数加上方案数
能量石
神题,一定要吃透
先从贪心的角度来思考
如果从前往后来吃,得到的能量是Ei + Ei + 1 - Si*Li + 1
如果从后往前来吃,得到的能量是Ei + Ei + 1 - Si + 1 + Li
如果要让解最优,就让Si*Li + 1 < Si + 1 + Li 即可 ,得出Si/Li < Si + 1 / L i + 1
用背包考虑问题的时候,只需要从这个小集合考虑即可。小圈里一定包含最优解。
这里的体积表示时间
不超过和恰好是j都可以,恰好是j比较好做。
要排序的时候记得结构体下标从1开始,开头和结尾都要+1。
总结
要将题目转化为数量,体积,价值,体积可能有二维体积,价值可能没有;
不超过,恰好,至少,对应状态初始化注意区别
有些题目试着先用贪心的思想把集合范围缩小(复杂的问题精简化)
刷题图谱:
状态机模型
状态机的显著特点:不是表示一个点,而是表示一个过程。
大盗阿福
选了第i家店铺,那么第i - 1家就不能选。
这种思考方式有一个缺点:不知道i - 1是不是要选。如果第i - 1是最优解中的一组数据,那么选了i - 1之后,就不能选择i。这样只能从第i - 2层转移过来,效率不高,想要把状态从上一层转移过来。
因此,可以把每一层用状态表示出来。
每一种走法对应一种边的走法。
0表示不选择,1表示选择。
状态机中有入口的概念,要初始化边界。入口只能走到0。所以f[0][0] = 0 , f[0][1] = -INF。
股票买卖IV
f[i][j]表示第i天,正在进行第j次交易,从定义出发,一次买入卖出算作一次交易。(1 -> 0时,是卖出股票,表示第j次交易正在进行,是交易的一半,前面进行了j次交易,因此从j转移过来)(0 -> 1时,表示买入股票,表示第j次交易正在进行,前面进行了j - 1次交易,因此从j - 1转移过来)
f[i][j][0] = max(f[i - 1][j][0] ,f[i - 1][j][1] + w[i])
f[i][j][1] = max(f[i - 1][j][1] ,f[i - 1][j - 1][0] - w[i])
f[i][j][2] 表示第i天,正在进行第j次交易,有货为状态1,无货为状态0。一次买入卖出算做一次交易,最终一定是完整的交易,即停在无货状态0
股票买卖V
与股票IV的区别是,需要冷却一天,因此多了一个状态,无货的第一天是不能再买入的,只能转为无货的第>=2天的状态。
入口是最灵活的最后的状态,出口可能有两种情况。
由于没有限制交易的次数,因此不需要j这一维。
设计密码
KMP算法求出来的是所有包含的子串。不包含的子串,一定是到不了m这一跳的,即只需要求出j小于m中的kmp对应到不包含。
这本身是一个状态机模型,状态表示第一维是长度,第二维是在状态机上走到了哪个位置,这里是在枚举在位置j如果沿着k这条边走到u时,对f[i + 1, u]的方案数的贡献