清北学堂2019.8.6
Day 1 赵和旭
前面他讲了一堆,啥也不知道(他没开广播硬讲。。。)
bzoj4247: 挂饰(洛谷 P4138)
按a排序
dp:dp[i][j]表示前i个挂饰,剩余j个挂钩的最大喜悦值
枚举下一个挂钩是否挂
dp[i][0]无法转移
dp[i][j]=max(dp[i-1][j],dp[i-1][max(j-a[i],0)+1]+v[i])
洛谷P1233 木棍加工(洛谷 P1233)
按w从大到小排序,w相同的l从大到小排序,我们对于1分钟能处理的一串棍子实际上就是一个l的不上升子序列,我们这里是求一个不上升子序列覆盖数。
不上升子序列覆盖数=最长上升子序列长度。
(严格证明参考:dilworth定理)
所以其实就是求一个最长上升子序列即可。
LIS 加强版
方法一:
h[k] 表示长度为k的子序列末尾值的最小值
那么h序列是单调递增的
二分求h数组
LIS 分析性质
状态转移:dp[i]=max{ dp[j] | a[j]<a[i] && j<i } +1 ;
我们观察一下这个dp式子的转移,他到底是在做一个什么操作。
我们是找比a[i]小的a[j]里面,dp[j]的最大值。
从这个角度不是很好优化,我们考虑另外一个思路,我们找最大的k,满足存在dp[j]==k&&a[j]<a[i]。
我们设h[k]表示dp[j]==k的所有j当中的最小的a[j],就是说长度为k的最长上升序列,最后一个元素的最小值是多少,因为最后一个元素越小,肯定后面更容易再加上一个元素了。
然后我们发现了个奇妙的性质。
而h[k],肯定是单调不下降的,就是说“长度为k的最长上升序列最后一个元素的最小值”一定是小于“长度为k+1的最长上升序列最后一个元素的最小值”,如果不是的话,我们可以用后者所在上升子序列构造出一个更小的前者。
然后这个样子我们对于一个a[i]就可以找到,最大的k,满足h[k]是小于a[i]的,然后f[i]=k+1。 找的过程是可以二分加速的。
然后同时在维护出h数组即可。
方法二:
数据结构不需要什么灵巧的闪光就是套路。
状态转移:dp[i]=max{ dp[j] | a[j]<a[i] && j<i } +1 ;
我们把a[j]看成坐标,dp[j]看成权值,这就是每次求坐标小于等于某个值的权值最大值,然后每算完一个单点修改即可。
树状数组即可。
最长公共子序列
我们设dp[i][j]表示,S串的第i个前缀和T串的第j个前缀的最长公共子序列。
分情况:
如果S[i]==T[j],dp[i][j]=dp[i-1][j-1]+1;
如果S[i]!=T[j],dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
最后答案就是dp[n][m]
最长公共上升子序列LCIS——分析性质优化状态转移
我们设dp[i][j]表示A前i个位置和B前j个位置所能产生的最长公共上升子序列的长度。其中强制A[i]==B[j],也就是最后这个位置是匹配的。若是A[i]!=B[j]则对应函数值为0。
我们从1到n枚举i计算dp值,在枚举i的过程中维护
f[k]=max{dp[1…(i-1)][k]}
然后dp[i][j]=max{f[k] | k<j && B[k]<A[i]},如果我们再从小到大枚举j的话只要边枚举j边记录满足条件的f[k]最大值即可。
总复杂度O(n*m)
Code:
for (int i=1;i<=n;i++) { int tmp=0; for (int j=1;j<=m;j++) if (a[i]==b[j]) { dp[i][j]=tmp+1; f[j]=max(f[j],tmp+1); } else if (a[i]>b[i]) tmp=max(tmp,f[j]); }
Bzoj5124波浪序列
和LCIS完全一样的解法啊。
设f[i][j][0/1]表示第一个序列前i和第二个序列前j个位置,最后一个位置是上升还是下降,转移和之前一样,记录一个辅助数组即可。
注意这里是记方案数。
容斥原理(小学奥数( ′◔ ‸◔`)):
Bzoj3782 简化版(网格路径计数 强化版)
这道题就可以用容斥来做。
第一种容斥
随意填-至少遇到一个障碍的方案数+至少遇到两个障碍的方案数-至少遇见三个障碍的方案数……
给障碍点从左到右从下到上排个序,记f[i][j]表示走到了第i个障碍点且包括第i个点在内强制经过了j个障碍点的路径条数(除此之外也可能有经过的),枚举上一个经过的障碍点即可。
转移的时候乘上一个组合数表示从k到i的走法数目
第二种容斥
枚举第一个遇到的障碍是哪一个来容斥。
实际上这是由下面这个推出来的。
记忆化搜索
1:一般递推式动态规划还要注意枚举状态的顺序,要保证算当前状态时子状态都已经算完了。
2:但是记忆化搜索不需要,因为记忆化搜索就是个搜索,只不过把重复的部分记下来了而已。我们不用像递推一样过于关注顺序,像搜索一样直接要求什么,调用什么就好。
在有一些dp问题中,状态之间的转移顺序不是那么确定,并不能像一些简单问题一样写几个for循环就解决了。
我们可以直接计算最终要求的状态,然后在求这个状态的过程中,要调用哪个子状态就直接调用即可,但是每一个状态调用一遍之后就存下来答案,下次计算的时候就直接取答案即可,就不需要从新再计算一遍。
虽然看上去每一次都计算不少,但是因为每一个状态都计算一次,所以均摊下来,复杂度还是状态数*状态转移。
上题中
我们从另一个角度来思考这个问题。
我们用搜索算法来计算答案,先看看没有障碍的情况,有障碍只改一点。
int dfs(int x,int y) { if (x==n&&y==m) return 1; int ans=0; if (x<n) ans+=dfs(x+1,y); if (y<m) ans+=dfs(x,y+1); return ans; }
然而搜索的时间复杂度是指数级的。
我们发现在这个dfs的过程中,dfs出来的值只与带入参数,也就是(x,y)有关,而不同的(x,y)有N*M个,而我们之前搜索的问题在于有大量的重复计算,多次调用同一个(x,y),每次都从新计算。
有一个很直观的想法就是,第一次调用的时候就把答案记下来,之后调用不重新算,直接返回之前已经计算出的答案即可。——这就是记忆化搜索。
这是有障碍的情况,mp[x][y]==-1表示有障碍。
int dfs(int x,int y) { if (mp[x][y]==-1) return 0; if (x==n&&y==m) return 1; if (dp[x][y]!=-1) return dp[x][y]; int ans=0; if (x<n) ans+=dfs(x+1,y); if (y<m) ans+=dfs(x,y+1); return ans; }
Bzoj 3810: [Coci2015]Stanovi
考虑分割成两个矩形,对于任意一种分割方案都一定存在一条贯穿横向或者纵向的线,那么枚举这条线即可。
然后设f[x][y][t]表示长为x宽为y,面向大海的边状态是t,最小的不满意度。转移就枚举从那个地方断开即可。
主程序如下:
拓扑图的DP
BZOJ4562 食物链 (洛谷 P3183)
设f[u]为以节点u为终点的食物链数量。
按照拓扑序的顺序转移即可。
上面的式子是求一个点时,枚举其所有入边转移,具体写代码的时候,我们一般就只记出边再记录一个入边太麻烦了。所以我们一般不枚举入边,而是枚举出边从每个点往后更新,也就是在上面的式子中,我们对于每个v向后进行更新,枚举v出边指向的所有u点,然后f[u]+=f[v]。
for(int i=1;i<=n;i++) if(!rd[i]) { if(cd[i]) f[i]=1; q.push(i); } while(!q.empty()) { int x=q.front(); q.pop(); for(int i=head[x];i;i=next[i]) { f[to[i]]+=f[x];
ed[to[i]]--; if(!rd[to[i]]) q.push(to[i]); } } for(int i=1;i<=n;i++) if(!cd[i]) ans+=f[i];
经典题
int dfs(u) { if(u==T) return 1; if(f[u]!=-1) return f[u]; f[u]=0; for(int i=hd[u];i;i=pr[i]) { dfs(to[i]); f[u]=((long long)f[to[i]]*cnt+f[u])%mod; } return f[u]; }
(记忆化搜索的代码相对好写,但会很大的增加时间)
序列上的dp
bzoj1003
其实就是分成很多段,每一段选同一个运输路线,然后得到一个最优的划分方案,使得成本最小。
f[i]表示前i天的运输最小成本。
f[i]=min{ f[j]+k+w(j+1,i)*(i-j) | j<i }
其中w(x,y)表示最短的在第x天到第y天都能用的路线长度,把能在则几天一直走的点加进图中,跑最短路径即可。
bzoj1296 粉刷匠
如果只有一条木板,那么设g[i][j]表示前i个格子刷j次的最多正确格子
g[i][j]=max{ g[k][j-1]+w(k+1,i) | k<i }
w(x,y)为第x到第y个格子的最多同色格子数,哪个颜色出现的多刷哪个,直接记一个前缀和即可。
有多条木板,设f[i][j]表示前i个木板刷j次的最大答案。
f[i][j]=Max{ f[i-1][k]+gi[m][j-k] | k<=j }
括号序列模型及解法
Codeforces314E
括号序列问题,往往就是把左括号看成+1,右括号看成-1,我们只需要保证任意一个前缀大于等于0,且总和为0,就代表是个合法括号序列了。
令dp[i][j]表示当前到第i个字符,现在还有j个左括号。
那么分三种情况考虑:
若第i+1个字符是左括号,则能转移到dp[i+1][j+1]。
若第i+1个字符是右括号,则能转移到dp[i+1][j-1]。
若第i+1个字符是问号,则能转移到dp[i+1][j-1]与dp[i+1][j+1]。
最终dp[n][0]就是方案总数
时间复杂度为O(n^2)
BZOJ3709
1:如果a[i]-d[i]>0,说明打掉这个怪兽有血可恢复,那么血量会变多,明显我们按照伤害d[i]从小到大排序即可,然后一个个杀下来。
2:如果a[i]-d[i]<0,说明会亏血。一个精妙的想法就是,最后剩余的血量值,假设是x,那么x是固定的。然后可以看作初始血量为x,怪兽的属性a,d交换,这样就和上一种情况一样了。
bzoj4922
我们还是把左括号看成+1,右括号看成-1,同样是保证任意一个前缀大于等于0,且总和为0。
那就是每一个给定的序列都是 先-Li再+Ri,Li是对消后左端右括号的数量,Li是对消后右端左括号的数量。然后依次拼起来之后任何一个前缀都大于等于0,这个其实和刚刚所讲的题目完全一样。
我们按照上一题的做法排序即可,排序后我们从左往右做dp。
设f[i][j]为 前i个括号序列-1与+1的和j个时选出括号序列最长的长度和。
也就是 前i个括号序列左括号比右括号多j个时的最长的长度和。
转移时考虑下一个括号序列选不选即可。
Len[i]为排完序后第i个括号序列的长度。
f[i+1][j-L[i+1]+R[i+1]]←f[i][j] + len[i+1] (j>=L[i+1])
f[i+1][j]←f[i][j]
最后答案就是f[n][0]
一套有趣的题目
1:1,2,3…n 以此进栈,求有多少种可能的出栈序列。
2:由n对括号形成的合法的括号序列由多少个。
这个问题还有很多其他的表达形式。(小声bb
3:n个节点共能构成多少种二叉树,左右子树是认为不同。
4:凸多边形的三角划分的方案数:把一个凸多边形用n-3条直线连接n-3对顶点,共形成n-2个三角形,求方案数。
5:一个n*n的格子,从(0,0)走到(n,n),求不跨过(0,0)->(n,n)这条直线的路径方案数。
我们设f[n]表示n个数依次进栈所能形成的出栈序列数。
似乎和之前不一样,好像不是划分成一段一段那样的简单形式。
我们可以考虑另一种形式的状态转移方式,以转移到子问题。
注意一段一段划分我们可以枚举最后一段的起点,但是这里不是一段一段的,我们要考虑另外的转移方式。
实际上我们发现我们可以枚举1这个数是什么时候出栈的。
那么我们可以得到
Vocabulary简化版
一般的dp
令dp[i][0/1][0/1]表示前i个字符,第一个字符串是否等于第二个字符串,第二字符串是否等于第三个字符串。
枚举第i+1位所有问号是什么字母,直接转移。
复杂度O(n*26^3)。
预处理转移的系数,是个很经典的优化技巧。
预处理出f[i][j][k][0/1][0/1][0/1][0/1]表示下一个位置,第一个串字符是i,第二个串字符是j,第三个串字符是k,由于可能出现”?”的情况,我们用0表示”?”。前两个[0/1]表示之前的位置1和2串是否相等,2和3串是否相等,后两个[0/1]表示将下一个位置所有”?”用字母代替后,第1个串的与第二个串的是否相等,2和3串是否相等,在转移时直接拿f数组转移。
利用这个转移系数的数组做dp的时间复杂度就是O(n)的了。
区间DP
合并石子
dp[i][j]表示将区间[i,j]这段区间内的石子合并为1堆的最小体力值。
答案就是dp[1][n]。
转移,考虑对于区间[i,j]它一定是由两段区间[i,k],[k+1,j]合并成的,所以转移就考虑枚举[i,j]区间内的一个分割点k转移即可。dp[i][j]=min(dp[i][k]+dp[k+1][j] | i<=k<j)+sum[i,j]。
for(int l=n;l>=1;l--) for(int r=l;r<=n;r++)//notice the enumeration order if(l==r) dp[l][r]=0; else { dp[l][r]=inf; for(int k=l;k<r;k++) dp[l][r]=min(dp[l][r],dp[l][k]+dp[k+1][r]+(sum[r]-sum[l-1])); }
poj3280
dp[i][j]代表区间i到区间j成为回文串的最小代价,那么对于dp[i][j]有三种
情况:
1、dp[i+1][j]表示区间i到区间j已经是回文串了的最小代价,那么对于s[i]这个字母,我们有两种操作,删除与添加,对应有两种代价,dp[i+1][j]+add[s[i]]或dp[i+1][j]+del[s[i]],取这两种代价的最小值。
2、dp[i][j-1]表示区间i到区间j-1已经是回文串了的最小代价,那么对于s[j]这个字母,同样有两种操作,dp[i][j-1]+add[s[j]]或dp[i][j-1]+del[s[j]],取最小值。
3、若是s[i]==s[j],dp[i+1][j-1]表示区间i+1到区间j-1已经是回文串的最小代价,那么对于这种情况,我们考虑dp[i][j]与dp[i+1][j-1]的大小
然后dp[i][j]取上面这些情况的最小值即可。
括号最大匹配
dp[i][j]代表从区间i到区间j所匹配的括号的最大个数,首先,假设不匹配,那么dp[i][j]=dp[i+1][j];然后查找i+1~~j有木有与第i个括号匹配的,
有的话,dp[i][j]=max(dp[i][j],dp[i+1][k-1]+dp[k][j]+2)//其中c【i】与c【k】匹配。
bzoj1900
f[l][r]表示,把l~r这个区间折叠的最短长度,然后我们想,对于一个区间来说,我们有两种选择,一种是把这个区间它自己来折叠,另一种是两块已经折叠的区间接起来。
对于第二种情况,直接枚举断点(区间dp中很常见),找最小的一种方案,第一种则是,找出它所有的折叠方案,在折叠方案中取一个最优的。
能量项链
在读入的时候现将珠子们复制一遍放到后面,断环成链
设f[j][i]表示左端点为j号珠子,右端点为i号珠子的区间所能得到的最大能量,转移就枚举最后一步聚合的位置即可。
ANS=
某经典题
首先也是断环成链,同时倍长一下。
然后设dp[i][j]表示区间[i+1,j-1]全部消掉的最小代价是多少,剩下a[i]和a[j],枚举最后一次消掉的数转移即可。
dp[i][i+1]=0.
最后只需要枚举剩下的两个珠子x、y即可,取dp[x][y]+dp[y][x+n]的最大值即可。