该博客会一直更新优秀dp的状态
1.求两个字符串相同子序列的个数:
设dp[i][k] 表示表示字符串s前i个字符与字符串t前k个字符相同的子序列个数
那么
dp[i][k]=dp[i-1][k]+dp[i][k-1]-dp[i-1][k-1]//类似于容斥
如果s[i]==t[k] dp[i][k]+=dp[i-1][k-1]+1 //新增子序列+自己
2.二进制给出L,求满足a+b<=L&&a^b=a+b的(a,b)有多少个
首先,a^b=a+b 可以推出 a与b的二进制任何一位都不可以同时为1
对于每一位来言,ab在这一位上的组合有3种 01 10 00
我们设dp[i][1]代表到第i位,a+b=L且a+b=a^b的对数
设dp[i][0]代表到第i位,a+b<L且a+b=a^b的对数
那么如果L的第i位为1 那么此时这一位可以取三种情况00 01 10
并且由于权值从大到小枚举,所以加上1,也不会使得之前小于L的数量改变(因为小于L 绝对是小于了一个2^k)
所以dp[i][0]=dp[i-1][0]*3
如果当前 L第i位 为1 :
dp[i][1]=dp[i-1][1]*2 //取(01 10)使得之前等于L 现在也等于L
dp[i][0]+=dp[i-1][1] // 这一位如果不取可以使之前等于L的变的小于L
如果当前 L第i位 为0 不做修改
3.将一段递增的区间划分为若干段,要求每段长度必须大于k,求如何划分使得 每段的极差和最小
划分dp的题目,比赛中由没有推出来,赛后自己推出来了,记录一下。
设f[i]表示前i个元素已经满足要求的最小花费
首先,区间长度必须要大于k,所以起始取值为k,又发现在k到2*k-1之间,只能划分为一段,否则前面的一段将长度<k
并且 k~2*k-1之间,f[i]的值为 num[i]-num[1]
对于大于>=2*k的位置,f[i]=min(f[j-1]+num[i]-num[j],f[i])//确保j-1段满足要求,使得[j,i]段取值成为新的一段序列,可得出j的取值范围 (k<=j<=i-k+1)
对转移方程 :f[i]=min(f[j-1]+num[i]-num[j],f[i])进行转换:f[i]=min((f[j-1]-num[j])+num[i],f[i])
所以我们只需要维护一个f[j-1]-num[j]的前缀最小值即可,O(1)转移
4.树形dp:在树上的第i个节点放置一个守卫需要花费val[i],放置完成后,相邻的所有点都被监视到,问如何在树上放置点,使得花费最小并且监视到所有点?
很不错的树形dp,状态比较难想 ,搞清楚状态之后其实这题不难
对于dp[i][3],令:
dp[i][0]为该位置不放节点,该节点由父节点监视。
dp[i][1]为该位置不放节点,该节点由子节点监视。
dp[i][2]为该位置放节点
那么很容易得到:
dp[i][0]:因为该位置不放节点,由父节点监视,所以子节点有两种状态:第一种放置守卫,第二种不放守卫继续由子节点的子节点监视。
dp[i][1]:因为该位置不放节点,由子节点监视,所以需要有一个子节点进行监视,然后其余的子节点两种状态,不放-由子节点监视(这时不可以由父节点监视)或者放。进行状态转移时,我们首先求和所有子节点的 sum+=min(dp[e][2],dp[e][1]),所以我们枚举一下放哪一个子节点就好了,枚举第k个节点监视该节点产生的最小花费为 sum-min(dp[k][1],dp[k][2])+d[k][2]
dp[i][2]:该位置放,那么对于子节点的状态而言,没有什么限制,可以加三种状态的最小值。
5.有n个数字,问从n个数字中取出若干个数字,他们的和modp为m,问有多少种取法?
这题是基础计数dp,很容易想到对于每一个数字来讨论,dp[k]代表mod p为k的取法有多少种,剩下的无法文字解释,然后这篇文章的第一个代码就出现了:
dp[num[1]%p]=1;
for(int i=2;i<=n;i++){
for(int k=0;k<p;k++) cop[k]=dp[k];
for(int k=0;k<p;k++)
cop[k]=dp[k]+dp[(k-num[i]%p+p)%p];
for(int k=0;k<p;k++)
dp[k]=cop[k];
dp[num[i]%p]++;
}
printf("%lld\n",m==0?dp[m]+1:dp[m]);
转移显然成立,每一位的贡献都计算在内了。
6.给定一个集合A,集合A的真子集共有个,定义每个集合的权值是集合内所有数之和,问真子集权值排序之后中位数是多少?
题目要求 :N<=2000,a[i]<=2000
首先考虑如何与dp靠边,假设我选择一个集合权值为S,那么他的补集就是 SUM-S(SUM为全集的权值)
所以说我们将空集考虑进去,那么 所有的权值都是关于S/2对称的,也就是说中位数必须是(SUM+1)/2到SUM的其中一个。
那么dp的思路也就来,设dp[i][j]代表前i个数是否可以组成数j:
状态转移方程即为 :
考虑到第二维的状态过于庞大所以采用bitset优化为一维设f[i]代表数字i可以表示那么,对于新输入的数字x,则有f|=f<<x
最后统计(SUM+1)/2~SUM间第一个可以表示的数即可
bitset转义复杂度是普通复杂度的64倍,即该复杂度为:~=7e7
7.天平秤重:给定一些物品既可以放在左边也可以放在右边,问是否可以称出重为w的物品?
揹包问题:
假设当前k可以称出,那么abs(k-x)与(k+x)都可以称出,那么状态转移方程很显然了。
不过此时只能用二维了,因为与k-x与k+x都有关。
所以二维转移
空间复杂度:O(N*M) M为所有物品最大总重量
时间复杂度:O(N*M) M为所有物品最大总重量
8.数位dp:1~n中有多少个数可以被自己数位的和整除?
显然成立问题:
首先考虑,模数一共有9*12种,分开枚举即可
dp[pos][sum][remain]表示到达第pos位时,sum为数位和,remain为前面所表示的数对当前模数取余剩余多少与当前sum加和为多少,确定两个状态即可加上满子树的值
9.一个长度为n的字符串,有多少个本质不同的长度为k的子序列?
好题!
考虑对于第i位而言,他可以作为长度为k的子序列中的第[1,k]位
所以由此可以推出dp[i][k]表示以str[i]结尾的长度为k的字符串的数量
很容易得到方程:
但是这样计算的子序列中显然存在重复,并且时间复杂度也不允许。
所以考虑什么情况下不重复:
考虑当前str[i]作为第j位,那么第j-1位置上的字母一共有26种选择,所以此时状态方案数只需要加上这26种即可,但是前面有很多相同字母,该加哪个字母的方案数呢?
例如:abbd 以d结尾的方案数应该加第二个b还是第一个b,答案显然第二个b。
所以状态转移就很清楚了:
所以可以维护一个数组f[j]{j>=1&&j<=26},表示在i之前离i最近的j+'a'的值是多少。
之后,dp[i][k]+=f[j]——{1<=j<=26}
以上是第一种解法:
考虑第二种解法:
原理不变,第i个位置长度为k的方案数只与离他最近的有关系——由此可以用序列自动机优化一下:
因为只要出现两个相同的字母,前一个字母已经把后一个字母的方案数给隔断了,所以显然没有重复
///bacc -> ba bc ac cc
总结:子序列问题掺杂dp可以与序列自动机结合一下
10.摆花问题:有n种花编号从1到n,有m个花坛,每个花坛一朵花,每种花不得超过a_i个,且必须按照编号从小到大顺序排列,问有多少种组合方式?n<=100,a_i<=100
样例:
2 4
3 2
ans:2
解释 : (1,1,1,2) ,(1,1,2,2)
思路转换一下,刚开始想麻烦了,可以由题目限制可以得到,这些画必须从编号从小到大放,而且如果放了k个,那么这k个一定是连续的。
所以可以考虑三重for:
现在开始放编号为i的花
从第j个位置开始往前放k个
这样放就保证了,绝对从编号有小到大开始放
所以设dp[i][k]为第i个位置,放完标号为k的花之后一共会有多少种方案:
Code
for(int i=1;i<=n;i++){///放第几个标号
for(int k=1;k<=a[i];k++){///一次放多少个
for(int j=k;j<=m;j++){
dp[i][j]=(dp[i][j]+dp[i-1][j-k])%mod;
}
}
for(int j=1;j<=m;j++) dp[i][j]=(dp[i][j]+dp[i-1][j])%mod;///更新 因为覆盖之前的第j个位置可以由任意编号结束
}
解决,需要注意的是,这里的dp只能二维 ,不然会影响转移。
11.树形dp:给出一个棵树,求对于每条边而言,必须经过这条边的最长链多长?
最长链 首先考虑树的直径求法,通常转换一下:边数 + 1 = 点数 所以记录点数即可
这里用到了换根dp:
对于每个边而言,两端点为u、e,分为向上(从u出发)的和向下(从e出发)的两条路径,必须经过这条边的最长链的长度即为 向上的最大+向下的最大
首先处理向下的:
dp[u] = max(dp[u],dp[e]+1)
并且显然从e出发的最大值即为dp[e]
再处理向上的:
首先记录u的父节点fa,那么父节点向上的最大肯定也是u节点向上最大的一种选择,所以首先:
dp1[u]=dp1[fa]+1(加上当前节点u)
其次我们遍历所有u的所有兄弟节点,此时u的向上节点长度: dp1[u] = dp1[bro]+2 (fa节点与u节点)
但是u不一定要从fa过来,还有可能从u的其他孩子节点过来,所以为了保证更新是最大值,记录u向下的一个最大值,一个次大值。
当枚举这条边的终点e满足dp[e] +1 ==dp[u]时(i代表当前边):
ans[i] = max(ans[i],次大值+dp[e])
ans[i]=max(ans[i],dp[e]+dp1[u])
否则:
ans[i] = max(ans[i],最大值+dp[e])
ans[i]=max(ans[i],dp[e]+dp1[u])
12.给出一个矩阵,每次操作可以矩阵内的某个数-1,每个数可以可以向右向下移动,问操作几次可以使得从(1,1)到(n,m)存在一个单调自增1的路径
经典套路:枚举不动点,不动点确定,起点就确定,终点也确定,进行dp检验即可
复杂度O(n^2*m^2)
13.给出一段只含有小写字母的字符串,询问最少修改多少个字母使得字符串中不含有aabb
令:
dp[1] 代表 字符串中不含有a的最小修改次数
dp[2] 代表 字符串中不含有aa的最小修改次数
dp[3] 代表 字符串中不含有aab的最小修改次数
dp[4] 代表 字符串中不含有aabb的最小修改次数
所以如果当前字符为'a',那么dp[2] = min(dp[1],dp[2]+1) :
可以修改前i-1个a:dp[1]
修改当前字符a:dp[2]+1
这里dp[2]+1可能会存在一些疑惑,这里解释一下:
dp[2]来源于两种情况:
1.之前有aa存在,修改掉了a,还剩一个a
2.之前有a存在,不做任何修改。
此时再碰到一个a时 ,必定会出现aa,所以此时dp[2]需要修改一下才可以不会出现aa。
所以这样写保证dp[2]代表了再遇到一个a时 序列中百分百含有aa 所以此时需要修改当前的a ,或者 使得序列没有第一个a
至于dp[3] dp[4]同理
14.给定一棵树,每个点都有权值,删除某个点需要花费其权值,求出最少花费使得树上没有长度大于等于l的链。
设dp[i][k]表示以i为端点的最长链小于等于k 并且子树都满足题意
所以:
1.dp[i][0] 代表的即为 删除该节点 ,但又要保证子树满足题意,所以说对于任意一颗子树,都要加上最小值:
2.dp[i][k] ,k>0 时代表保留该节点。
保留该节点,对于新来的子树,枚举新来子树的最长链与当前确定的最长链,只要i+j<m即可转移:
for(int i=1;i<m;i++){
for(int k=0;k<m;k++){
if(i+k<m) dp[u][max(i,k+1)]=min(dp[u][max(i,k+1)],temp[i]+dp[e][k]);
else break;
}
}
此时记得max(i,k+1)
temp[i] 保留上一层的值
此时复杂度为O(n^2)->这么考虑:每两个点都只会被两点的lca计算一次!