一些优秀的dp状态总结

该博客会一直更新优秀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的真子集共有2^n-1个,定义每个集合的权值是集合内所有数之和,问真子集权值排序之后中位数是多少?

题目要求 :N<=2000,a[i]<=2000

首先考虑如何与dp靠边,假设我选择一个集合权值为S,那么他的补集就是 SUM-S(SUM为全集的权值)

所以说我们将空集考虑进去,那么 所有的权值都是关于S/2对称的,也就是说中位数必须是(SUM+1)/2到SUM的其中一个。

那么dp的思路也就来,设dp[i][j]代表前i个数是否可以组成数j:

状态转移方程即为 :dp[i][j]|=dp[i-1][j-num[i]]

考虑到第二维的状态过于庞大所以采用bitset优化为一维设f[i]代表数字i可以表示那么,对于新输入的数字x,则有f|=f<<x

最后统计(SUM+1)/2~SUM间第一个可以表示的数即可

bitset转义复杂度是普通复杂度的64倍,即该复杂度为:O(\frac{n*SUM}{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的字符串的数量

很容易得到方程:

dp[i][k]=\sum _{j=1}^{j=i-1}dp[j][k-1]

但是这样计算的子序列中显然存在重复,并且时间复杂度也不允许。

所以考虑什么情况下不重复:

考虑当前str[i]作为第j位,那么第j-1位置上的字母一共有26种选择,所以此时状态方案数只需要加上这26种即可,但是前面有很多相同字母,该加哪个字母的方案数呢?

例如:abbd  以d结尾的方案数应该加第二个b还是第一个b,答案显然第二个b。

所以状态转移就很清楚了:

dp[i][k]=\sum _{j=0}^{j=i-1}dp[j][k-1] \{j>=a&&j<=z}

所以可以维护一个数组f[j]{j>=1&&j<=26},表示在i之前离i最近的j+'a'的值是多少。

之后,dp[i][k]+=f[j]——{1<=j<=26}

以上是第一种解法:

考虑第二种解法:

原理不变,第i个位置长度为k的方案数只与离他最近的有关系——由此可以用序列自动机优化一下:

dp[nex[i][c]][k+1]+=dp[i][k]

因为只要出现两个相同的字母,前一个字母已经把后一个字母的方案数给隔断了,所以显然没有重复

///bacc -> ba bc ac cc

总结:子序列问题掺杂dp可以与序列自动机结合一下

发布了157 篇原创文章 · 获赞 146 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_43857314/article/details/104177095