• 清北学堂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]的最大值即可。

  • 相关阅读:
    SEH(Structured Exception Handling)详细解释
    Command Query Responsibility Segregation
    C#中Func和Expression的区别
    C#的yield return是怎么被调用到的?
    C#的static constructor抛了异常会怎么处理?
    developer应该知道的image知识(JPG和PNG)
    网站前台与后台的连接
    短消息类新旧服务代码对应表
    无线广告巨头渠道火拼
    中国移动下一代移动技术将选择LTE
  • 原文地址:https://www.cnblogs.com/gongcheng456/p/11311353.html
Copyright © 2020-2023  润新知