这周学的例题挺多的,怎么说呢,感觉这种做题思路有时候挺难想的,有时候感觉模模糊糊,大体上懂方向,但是不太会列动态转移方程,看答案后需要想好久,或者画图才能理解,可能我训练的还是少。再说说学例题的感受吧,一般动态规划题抽象出的一维或二维数组的i和j我们的给其特定的含义,这个含义就相当于我们找的不同方式。而我们对其处理,既要考虑当前又要考虑以前,在以前的基础上进行操作得到最后的结果,这个过程有点像累加的过程。一般来说,题目给出一组的变量我们只需构造一个一维数组,如果题干给出两组变量,比如时间和路径,我们需要开一个二维数组i表示时间j表示路径。当然这些构造方式是最基本的。
总结几个最近见到的题型:
•计数问题:最长上升子序列个数,删数序号位置最大等
•求和问题:最大子段和,最大子矩阵和,最大子段绝对值和等
•时段问题:最大空闲时间,最大段价值等
•求方案数问题:毛毛虫走果树,雇佣工人等
•贪心+DP:肥猫速度
回顾一下本周的例题,加深印象
A串B串生成C串
A每个字符的顺序在C串中的顺序不能变,B也一样。设A串m个字符,B串n个字符。首先我们考虑极端情况,要么C串前m个字符全是A,要么C串前n个字符全是B,这些分别按一种情况处理。构造dp[i][j],i表示已经匹配了s1前i个字符,j表示s2前j个字符的方案数
dp[0][0]=1;
for(i=1; i<=m,i++){
if(s1[i]==s3[i])
dp[i][0]=dp[i-1][0];}
for(i=1; i<=n; i++){
if(s2[i]==s3[i])
dp[0][i]=dp[0][i-1];}
然后考虑一般情况,拿s1的第i个字符与s3的第i+j个字符比较(i+j表示s2的前j个字符与s1的前i-1个字符方案数已经匹配好了,我们需要考虑s3的i-1+j的下一位的情况),相等的话就在现在的方案数基础上加上s1前i-1的方案数,两个方案的j相同(因为相等说明该方案与s1的i-1的方案为同一个方案),s2同理。同时考虑完s1和s2后才能确定一个dp[i][j]的值。状态转移方程为
if(s1[i] == s3[i+j]) {
dp[i][j] = (dp[i][j] + dp[i-1][j]) %mod;}
if(s2[j] == s3[i+j]) {
dp[i][j] = (dp[i][j] + dp[i][j-1]) %mod;}
线段覆盖价值问题
给你n条线段的左右端点ai,bi和价值ci,线段两两不覆盖(端点可以重合)求最大价值和。
联系贪心,需要先对线段的右端点按照从小到大排序,然后外循环表示排序后第几条线段截止,内循环找截止线段前与其不覆盖的线段的最大价值,加上截止线段的价值表示以当前线段为止的最大价值,不断更新最大价值找最终答案
for (int i = 1; i <= n; i++)
{
int maxx = 0;
for (int j = 1; j < i; j++)
if (a[i].l >= a[j].r)
maxx = max(maxx, dp[j]);
dp[i] = maxx + a[i].jz;
ans = max(ans, dp[i]);
}
尼克的任务
知道尼克的工作时间,任务数以及每个任务开始和持续时间,求最大闲暇时间。
先对所有任务的初始时间按从大到小排序(后开始的排前面,方便倒着推),然后倒着推,定一个一维数组dp[i],i有两种情况,无任务(或者有任务但不是任务的开始点,因为我们变空闲时间时只考虑的任务的开始点)dp[i]=d[i+1]+1
,就是在后一个时间的基础上加1个空闲时间
有任务的开始处dp[i]=max(dp[i+任务时长],dp[i])
,就是比较当前点开始的所有任务中,做完这个任务后面空闲时间最大的作为当前空闲时间。
心得:挺绕的,思路有几个点需要注意,排序一定要降序排,这样后面求任务时长时间就可以直接用N[now++].b表示了,N是结构体,b是任务持续时间,now++一开始写后就不用每次都变了。几个关键步骤如下:
N[i].a=a;
N[i].b=b;
mp[a]++
记录该点开始的任务个数
for(int i=k;i>=1;i--)
{
if(mp[i]==0) dp[i]=dp[i+1]+1;
else
{
for(int ii=0;ii<mp[i];ii++)//循环开始点的任务数
{
dp[i]=max(dp[i+N[now++].b],dp[i]);
}
}
}
cout<<dp[1]<<endl;
数列
给你一组数,擦掉某些数后,最后剩下的数列中最多能有多少个数在自己的位置上,即Ai=i最多能有多少。
我们需要记录数组的数和擦掉的数,所以需要二维数组,用f[i][j]表示前i个数擦掉j个后的最大数。每次循环时我们要给f[i][j]一个初值,因为这个数可以直接擦掉,所以f[i][j]=f[i-1][j-1];(初值不能是0,否则可能会漏掉一些情况),然后
if(a[i]=i-j)//表示第i个数a[i]在擦掉j个数之后符合条件
f[i][j]=max(f[i][j],f[i-1][j]+1)//表示第i-1个数擦掉j个数的最大值加上第i个数符合条件的1个数
else f[i][j]=max(f[i][j],f[i-1][j])//不需要加,直接延续前一个数即可
最后答案在第n个数擦掉0到n个数中的最大答案。
整数划分
将N分为若干个不同整数的和,有多少种不同的划分方式,例如:n = 6,{6} {1,5} {2,4} {1,2,3},共4种。
想到整数i可以划分为j个数,所以构建一个二维数组,把dp[i][j]看作整数i的划分为j个不同的数字的方案数。分两种情况,一种是划分不带1的,可以先把划分的j个不同数字每一个都提一个1,即把i-j的数分成j个数,划分的每一个都加1就变成原数,如:7划分成两个数可以是3和4,相当于7-2也就是5个数划分成两个数2和3,2和3分别加1就构成了7。同样的方法7划分成1个数找到了7,两个数找到了3和4,2和5。另一种情况是每一个划分都带1,也就是把i-j个数划分成j-1个数,每一个数加1,然后最后再加1就构成了原数,如:7相当于5划分成1个数为5,每个数加1为6,最后加1就是7。相当于7划分1和6,划分成1和2和4等。
列动态转移方程:
dp[i][j] = dp[i-j][j] + dp[i-j][j-1]
前面表示从i-j的数字划分为j个不同的数字,到达i只需每一个数字加1;
后面是数字i-j划分为j-1个数字,我们把每一个加1之后再加上最后的1就是dp[i][j]。
dp[0][0] = 1;
for(int i = 1;i <= n; i++) {
for(int j = 1;j*j <= i*2; j++) {//1+2+…+j<=n->(1+j)*j<=2*i->j*j<=2*i,省时间做法
dp[i][j] = (dp[i-j][j]+dp[i-j][j-1])%mod;
}
最大M段子段和
基本题型最大子段和
本题挺难的,不好想,先说一下老师讲的方法,我们先构建一个二维数组,dp[i][j] 表示在前i个数选j段且第i个数在最后一组中的最大子段和。讨论第i个数有两种决策:第i个数和第i-1个数构成一段 dp[i][j] = dp[i-1][j] + a[i]
相当于在第i-1的值上加本身的值
第i个数自己单独做一段,前面需要 j-1段
dp[i][j] = max{dp[k][j-1]|j-1<=k<i}+a[i]
这里k的初始范围为前j-1个数为一段,在范围内寻找最大值在加上本身即可。
状态转移方程 :
dp[i][j]=max(dp[i-1][j],max{dp[k][j-1]|j - 1<= k < i})+a[i];
这样写空间复杂度高,可对其优化,发现发现更新 dp[i][j]的时候只用到了dp[.][j]和 dp[.][j-1]只需两列j,所以用滚动数组轮换表示,学习这种方式
j1=1;
{......
dp[i][j1]=max(dp[i][j1],dp[k][j1^1]);
dp[i][j1] += a[i];
......
}
j1^=1; //滚动数组交换0,1。
其他人的方法:https://blog.csdn.net/mirocky/article/details/104163844?utm_source=app&app_version=4.5.8