一些优秀的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可以与序列自动机结合一下

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] 代表的即为 删除该节点 ,但又要保证子树满足题意,所以说对于任意一颗子树,都要加上最小值:

dp[i][0] = num[i] + \sum_{e=son(i)} min(dp[e][k]) ,0<=k<=m-1

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计算一次!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章