dms去储备营了嘤嘤嘤
不过今天是zhx讲课qwq
DP
先看个例子
斐波那契数列对不对?
下面这个式子就是在告诉我们应该怎么算第n项
这个式子也就是其他算好的结果算自己的结果,是第一种写法
第二种写法:
用自己的结果算其他的结果
在这里,由fn可以推出fn+1,fn+2
就像这样
注意有的题用一种方法难写,但用另一种方法会好写
最后一种写法
记忆化
先来个直白的搜索
复杂度:O(f(n))
因为f(n)是几,就需要几次return 1(或是return 0)
f(n)和2n是一个级别的
那为什么它出奇的慢?
因为它会重复计算很多次同一个项
那怎么办?
我们可以把搜索到的值记录下来
就像这个样子(传说中的记忆化搜索)
有些名词:
无后效性
阶段性
转移方程
状态
状态就是你要算什么,转移方程就是你怎么算
无后效性:状态视作点,转移视作边,这就是有向无环图
据说有乱序转移的题。这时转移关系是有向无环图,具有拓扑序。拓扑排序后for一遍就好了辣
特殊类型动态规划
数位dp
树形dp
状压dp
博弈论dp(明天讲)
背包
背包
f[i][j]表示前i个物品占用j的体积,最大价值是多少
i代表前i个物品已经考虑完了
当前物品只有放与不放两种情况
不放:f[i][j]=f[i-1][j]
放:f[i][j]=f[i-1][j-v[i]]+w[i]
为了最大化,所以我们取max
上面是别人更新自己的写法
自己更新别人的写法
答案:max{f[n][j]}(0<=j<=m)
唉唉一维优化呢?
大概被吃了叭
(第i个物品放0个的转移)
放n个:f[i-1][j-n*v[i]]->f[i][j]
所以我们枚举第i个物品放了多少个
最内层循环
终止条件:k*v[i]>j
唉唉一维优化呢?都说了被吃了辣
我们发现它出奇的慢,因为这是三重循环
我们让他跑的快一点,变到n2级别
for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++) { f[i][j]=f[i-1][j]; if(v[i]<=j) f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]); } }
为什么这样改就是对的?
我们之前说过dp是一个DAG
我们不妨来画个图
因为物品可以选无限个,所以递推的时候就可以向上走,也可以横着走,这样横着走就是f[i][j-v[i]]
1.枚举第i个物品用多少次O(nm每个物品个数)
2.考虑优化
我们可以把几个物品捆绑一下
比如这个物品有13个,我们就把它捆绑成酱紫
我们这么捆绑,那么无论用这个物品选几个,都可以用这些捆绑后包的组合来表示
每个捆绑包只能用一次,所以就变成了01背包
复杂度O(n2k) k是捆绑包的个数
不难猜出捆绑包是按二进制拆的
但最后一个为什么是6不是8?
13-1-2-4=6,6<8,所以是6
就是当最后的数(原数-20-21-22....)不够2k时,最后一个捆绑包的大小是最后的数
造捆绑包:
完整版代码
int main() { cin >> n >> m; int cnt = 0; for (int a=1;a<=n;a++) { int v_,w_,z; cin >> v_>> w_ >> z; int x = 1; while (x <= z)//制造捆绑包 { cnt ++; v[cnt] = v_*x;//注意捆绑包的权值和体积都跟着变 w[cnt] = w_*x; z-=x; x*=2; } if (z>0) { cnt ++; v[cnt] = v_*z; w[cnt] = w_*z; } } n=cnt; for (int i=1;i<=n;i++) for (int j=0;j<=m;j++)//对捆绑包进行01背包的操作 { f[i][j] = f[i-1][j]; if (j >= v[i]) f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]); } int ans=0; for (int a=0;a<=m;a++) ans = max(ans,f[n][a]); cout << ans << endl; return 0; }
基础类dp
ioi真题之-----------------------
数字三角形
咋做?
当然是用dp辽
状态:f[i][j]表示在a[i][j]时的最大值
转移方程
数字三角形2
求mod100之后的和最大
记录f[i][j]mod 100的最大值?
错
why?
如果选f[i-1][j-1],f[i-1][j]中的最大值来进行转移
那对于a[i][j]=1的这个点来说,f[i][j]=0,因为我们选择了点权为99的那个点来更新它
但其实选择98的那个点更优
经典套路:目前做不出来,加维度。加一维不够,再来一维。
定义f[i][j][k]是走到(i,j)时,当前的和mod 100==k是否可行
有两种情况
f[i][j][k]=true:更新
f[i][j][k]=false:什么也不做
更新:
向下走:
f[i+1][j][(a[i+1][j]+k])%100]=true
向右走:
f[i+1][j+1][(a[i+1][j+1]+k)%100]=true
最后答案:最后一行f值为1的最大的k
最长上升子序列
f[i]表示以i结尾的最长上升子序列的长度
复杂度O(n2),显然太慢了,所以我们要进行一番优化
我们要的是j<i的最大值,所以我们可以搞个线段树什么的
所以我们有些时候可以用数据结构优化dp求值
常用技巧:加维度,数据结构(多一个条件,多一个维度)
区间dp
有好多堆石子排成一排,每次可以选择合并相邻两堆,代价就是合并的这两堆的石子个数,求最终合并成一堆最小代价
满足合并相邻的两个东西,就是区间dp。
我们发现如果我们合并了a1,a4,则a2,a3都在这一堆石子里面
f[l][r]:第l堆石子到第r堆石子合并起来的最小代价
初始化:f[i][i]=0
我们要合并l到r,首先要合并l到k,k到r(l<=k<r),这个k就相当于分界线一般的存在。
所以我们枚举分界线
f[l][r]=min{f[l][k]+f[k+1][r]}+a[l]+a[l+1]+a[l+2]....+a[r]
后面这些a[i]是[l,r]的区间和,区间和用前缀和来求
也就是f[l][r]=min{f[l][k]+f[k+1][r]}+sum[r]-sum[l-1]
典型错误示范:
for(int l=1;l<=n;l++) for(int r=1;r<=n;r++) for(int k=l;k<r;k++) f[l][r]=max(f[l][r],f[l][k]+f[k+1][r]+sum[r]-sum[l-1]);
为什么是错的?
这样我们是先算左端点为1的区间,再算左端点为2的区间....
但是当我们算到f[1][n]的时候,假如p枚举到3,但是f[4][n]没有算出来
正确示范:
for(int len=2;len<=n;len++) for(int l=1,r=len;r<=n;l++,r++) for(int k=l;k<r;k++) f[l][r]=max(f[l][r],f[l][k]+f[k+1][r]+sum[r]-sum[l-1]);
这里规定了区间长度,是按照区间长度从小到大的,也就保证了上述例子中的f[4][n]会在f[1][n]之前算出来
我也不造这到题叫啥
我们有N个矩阵,告诉你每个矩阵的大小,数据保证它们可以乘起来
就像酱紫
问把这些矩阵乘起来的最小次数
举个栗子
f[l][r]表示把第l个矩阵到第r个矩阵乘到一起,所花费的最小次数
则f[l][r]=min{f[l][k]+f[k+1][r]+al*ak*ar}(l<=k<l)
为什么?
我们结合上面给出的例子,发现多增加的步数就是第一个矩阵的行*第一个矩阵的列*第二个矩阵的列
状压dp
在二维平面上有n个点,坐标为(xi,yi),问求从1号点出发,把所有的点都走至少一遍之后再回到1号点,最短的距离是多少(想怎么走怎么走)
就比如这两个点之间可以这么走
当然我们知道两点之间线段最短
每个点没有必要走两次
dp设计要考虑变化量有哪些
这里的变化量就是我们当前在哪个点,已经走过哪些点
但是走过了哪些点不能直接用一个整数表示,而是要用数组表示
这时候就是状态压缩了(把这个数组压成一个数)
每个点只有走过和没走过两种状态,所以用0表示走过,用1代表走过
假设我们现在到过1,2,4折三个点
这就是一个二进制数
所以f[s][i]中的s就是状态压缩后的数
在转移的时候要考虑哪个位置没有到达过,也就是s的二进制表示中哪一位是0
判断哪一位是0:(1<<i)&s==0,则第i位是0(注意这里是按照2k的第k位,即存在第0位)
最终答案:min{f[(1<<n)-1][i]+juli(i,1)}
juli就是计算两点间的距离
复杂度O(2n*n2)
数据范围:n≤22或n≤20
f[i][s]表示前i行草种完,第i行的草长的样子
(s的二进制表示每个位置有没有种草)
如果两行之间有相邻的草,则s&s'不为0
判断草不相邻:(j&(j<<1))=0&&(j&(j>>1))==0
判断贫瘠的地方不种草:将每行能种草的地方压成一个二进制数no[i],然后j&no[i]=j,就说明贫瘠的地方没有种草
先枚举这一行的种草情况j,再枚举上一行合法的种草情况k,k&j==0即为合法情况
代码:
#include<bits/stdc++.h> #define ll long long using namespace std; inline int read() { char ch=getchar(); int x=0;bool f=0; while(ch<'0'||ch>'9') { if(ch=='-')f=1; ch=getchar(); } while(ch>='0'&&ch<='9') { x=(x<<3)+(x<<1)+(ch^48); ch=getchar(); } return f?-x:x; } int n,m,f[20][1<<12+9],ans; const int mod=100000000; int no[20],mmax; int main() { m=read();n=read(); for(int i=1;i<=m;i++) { for(int j=1;j<=n;j++) { int x=read(); no[i]+=x<<j-1;//no[i]代表第i行能种草的情况 } } mmax=(1<<n)-1;//所有格子都种草的情况(也就是最大的情况) no[0]=mmax; f[0][0]=1; for(int i=1;i<=m;i++) { for(int j=0;j<=mmax;j++) { if((j&no[i])!=j)continue; if(((j<<1)&j)!=0||((j>>1)&j)!=0)continue; for(int k=0;k<=mmax;k++) { if((k&no[i-1])!=k)continue;//对枚举的k进行相同的操作 if(((k<<1)&k)!=0||((k>>1)&k)!=0)continue; if((k&j)==0) { f[i][j]+=f[i-1][k]; f[i][j]=(f[i][j]+mod)%mod; } } } } for(int i=0;i<=mmax;i++)//把最后一行加起来 ans=(ans+f[m][i])%mod,ans=(ans+mod)%mod; printf("%d",ans); }
luogu P1879
这里要求恰好k个国王,所以我们再加一个维度
f[i][s][j]表示前i行已经摆好,第i行摆了j个国王,长成s的样子
再注意一下对角线的判断
luogu P1896
数位dp
顾名思义,这是按照数的高位推导数的低位的dp
T1:给出两个数l,r,问l到r中有几个数
当然是r-l+1了
不过我们用数位dp做
我们还是要联系上面的r-l+1这个式子
要求r-l+1,我们一般用前缀和来实现,即sum[r]-sum[l-1],所以我们就把问题转化成了求0~x中的数的形式
先算0~r中所有的数,再算0~l-1中所有的数
这样就都是形如0~x的形式了
即求y,0≤y≤x
假设x是3245,那么y最多有4位
这样我们就转换成了填数问题
我们考虑到底是从高位开始填还是从低位开始填
如果我们从低位开始填
我们在个位填个9,能说明什么?
什么都说明不了
但我们从最高位开始填,如果填了9,就能说明当前数一定比x大,如果填了1,就说明当前数一定比x小
用f[i][0/1]表示填到第i位,当前的数已经填上的那几位是否与x的对应位相等
所以这里我们要先预处理x的每一位上的数
while (x>0) { l++; z[l] = x%10; x/=10; }
数位dp转移:
枚举下一位填0~9当中的哪个数
肯定会有填某些数不行的情况,那么哪些情况不行呢?
当j已经=1,且枚举的K大于x对应的位的时候,就不行了
当j=1且枚举的k与x对应位相等时,f[i-1][j]=f[i][j]
当j=0时:枚举的每一个k对答案的贡献是f[i][j],所以是f[i-1][j]+=f[i][j]
自己按照记忆胡的代码
while(x) { l++; z[l]=x%10; x/=10; } memset(f,0,sizeof(f)); f[l+1][1]=1; for(int i=l+1;i>1;i--)//这里是自己更新别的写法 { for (int j=0;j<=1;j++) { for (int k=0;k<=9;k++)//枚举这一位填什么 { if (j==1 && k>z[i-1]) continue;//如果不行,就continue int j_;//判断要更新的j if (j==0) j_=0; else if (k==z[i-1]) j_=1;//如果这一位填到了和x的对应为相等的数,且j=1,则j_也是1 else j_=0; f[i-1][j_] += f[i][j];//更新 } } }
什么意思呢?
假设[l,r]区间里的数是 19,20,21,那ans=1+9+2+0+2+1=15
设g[i][j]表示填到第i位,与x的关系是j的数位之和
枚举填的数k,计算对答案的贡献。
j=0:对答案的贡献是k*(f[i][j]) 这里的f[i][j]就是上一个题中的f[i][j](也就是数的数量)
递推式:
为什么要加g[i][j]?因为前面的f[i][j]*k是当前枚举的K对答案的贡献,是增加量,还要再加上原有的g[i][j]
加一维海阔天空
f[i][j][k]:i,j定义不变,k表示第i位填的k
枚举是只要避开两位之差小于2的情况
多一个条件,多一个维度
f[i][j][r],r代表已经填了的数字的乘积
枚举第i-1位填什么,r就直接乘
但是我们发现r炸了,r太大了,数组开不下啊
再来一维?
错,是再来好几维
我们发现r的因数里面不能有超过10的质数,所以数组有大部分是空的(就像我的脑子一样qwq?)
于是我们多来几个维度
然后我们还是过不了qwq
继续优化ρωρ
下面是abcd的大概取值
a:60 b:30 c:20 d:10
我们发现abcd不可能同时取到上界,因此还有很大的浪费
对此强(sang)大(xin)无(bing)比(kuang)的官方是这么干的:
预处理所有可能取到的数,f[i][j][k]表示当前这个数是这些数中的第几个
树形dp
长者的简单题
给你一棵n个点的树,求这棵树有多少个点
n个点,此题完毕
并不
树形dp:dp子树的信息,从下往上来
放在这个题里面,f[i]表示以i为根的子树有多少个点
f[i]=∑f[j]+1(j为i的儿子)
神马是树的直径?在树上找到两个点,使得这两个点的距离最远
路径与子树有什么关系?
我们求两个点的路径,就要找lca
我们一般都是这么走的
(最上面的点是lca)
现在我们这样走
这样我们找最大值,就变成了找这个lca向下的最长的路径
最长的路径当然是向下最长路+向下次长路咯
现在所有的pj的f都求出来了
我们要求最大值,那肯定是最大的那个f[p][0]+1咯
次长:
所有儿子的最长路和次长路混在一起求?
错
如果有一个pk,它很神仙,f[pk][0]是pk和它的所有兄弟中最长的那条边,f[pk][1]是次长的那条边
那么这样最长选了pk,这么找次长也可能是pk,这就不合法了
所以我们应该是在所有儿子的最长路中找最长。因为次长路不可能比所有的f[p][0]中次大的那个还长
伪代码(雾)
求树上所有路径的总长度之和
用f[i]表示以i为根的子树有多少个点
思想:看有多少条路劲经过当前枚举的这条边
一条边被算进路径:在子树里找一个点,在子树外面找一个点,这样一条路径肯定会经过这条边
因为可以正着走,也可以反着走,所以要*2
f[i][0]代表不选I的最大价值
f[i][1]代表选i的最大价值
选的时候,所有儿子都不能选
不选的时候,儿子可以选,也可以不选,取最大值
和上个题的思路差不多
f[i][1]=∑min(f[j][1],f[j][0]) (后面没有+1)
拓展
每个士兵可以守护与自己距离不超过2的所有节点,求守护所有节点最少的士兵数量
加维警告
f[i][0/1/2]以i为根的子树被全部覆盖,i向下走,到达最近的士兵的距离
f[i][0]:在i上放士兵
这时候它的儿子放的士兵就与它无瓜了
f[i][1]就有点让人 疼了
这时候我们另外跑一个dp来算它
g[j][0/1]表示是否有一个儿子距离下面的士兵的距离是0(也就是第j个儿子有没有士兵)
f[i][2]
还是要跑个dp
dp套dp真的是涨姿势了