清北学堂2019.8.6

Day 1 赵和旭

前面他讲了一堆,啥也不知道(他没开广播硬讲。。。)

bzoj4247: 挂饰洛谷 P4138

a排序

dpdp[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个障碍点的路径条数(除此之外也可能有经过的),枚举上一个经过的障碍点即可。

转移的时候乘上一个组合数表示从ki的走法数目

第二种容斥

枚举第一个遇到的障碍是哪一个来容斥。

实际上这是由下面这个推出来的。

 

 

记忆化搜索

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+RiLi是对消后左端右括号的数量,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]

一套有趣的题目

11,2,3n 以此进栈,求有多少种可能的出栈序列。

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]表示之前的位置12串是否相等,23串是否相等,后两个[0/1]表示将下一个位置所有”?”用字母代替后,第1个串的与第二个串的是否相等,23串是否相等,在转移时直接拿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]有三种

情况:

1dp[i+1][j]表示区间i到区间j已经是回文串了的最小代价,那么对于s[i]这个字母,我们有两种操作,删除与添加,对应有两种代价,dp[i+1][j]+add[s[i]]dp[i+1][j]+del[s[i]],取这两种代价的最小值。

2dp[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)//其中ci】与ck】匹配。

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.

最后只需要枚举剩下的两个珠子xy即可,取dp[x][y]+dp[y][x+n]的最大值即可。

posted @ 2019-08-06 19:42  卍GC卐  阅读(218)  评论(0编辑  收藏  举报