• AlvinZH掉坑系列讲解(背包DP大作战H~M)


    本文由AlvinZH所写,欢迎学习引用,如有错误或更优化方法,欢迎讨论,联系方式QQ:1329284394。

    前言

    动态规划(Dynamic Programming),是一个神奇的东西。DP只能意会,不可言传。大家在做DP题的时候一定要理清思路,一般是先不管空间,毕竟以空间换时间,大多数题都是先卡时间再卡空间的。

    DP具备的两个要素:最优子结构和子问题重叠,见《算法导论》225页。简单来讲就是问题是一个由多决策产生最优值的最优化问题。

    • 最优化原理:其子问题的最优会导致全局最优,具有最优子结构的性质。这是运用DP的"前提",是否符合最优化原理是一个问题的本质特征。如果不满足最优化原理,那最开始所做的决策都是徒劳的。
    • 无后效性:当前状态如果确定,以后过程的演变将不再受当前状态以前的各状态和以前的决策影响。这是运用DP的"条件",DP按次序去求每阶段的解,如果一个问题有后效性,那么这样的次序便是不合理的。一个问题的某个DP决策方法可能具有后效性,通过重新划分阶段,重新选定状态,或者增加状态变量的个数等手段,是可以把问题转化为满足无后效性的。所以决策的"顺序"也是问题的关键。

    接下来通过几道经典的题目,简单练习一下DP,比赛题目连接:BUAAOJ-DP大作战 H~M题。

    899 AlvinZH掉坑里了(H)

    思路

    简单DP。简单判断符合运用DP要求,求得到达某个点的最大金币数,至多只要比较两点(左点&上点)的最大金币数,即满足最优子结构。

    (dp[i][j]) :表示走到点(i,j)时取得的最大金币数。

    状态转移方程: (dp[i][j] = max(dp[i - 1][j],dp[i][j - 1]) + M[i][j])

    小技巧:①初始化为-INF;②真实数据存于[1n][1m]中,边缘统一。

    参考代码

    //
    // Created by AlvinZH on 2017/10/17.
    // Copyright (c) AlvinZH. All rights reserved.
    //
    
    #include <cstdio>
    #include <cstring>
    #define INF 0x3f3f3f3f
    
    int M[505][505];//矩阵数据
    int dp[505][505];//到达点(i,j)时最大金币个数
    
    inline int MAX(int i, int j) {
        if(dp[i - 1][j] > dp[i][j - 1]) return dp[i - 1][j];
        else return dp[i][j - 1];
    }
    
    int main()
    {
        int n, m;
        while(~scanf("%d %d", &n, &m))
        {
            memset(dp, -INF, sizeof(dp));
            for (int i = 1; i <= n; ++i)
                for (int j = 1; j <= m; ++j)
                    scanf("%d", &M[i][j]);
    
            dp[1][1] = M[1][1];
            for (int i = 1; i <= n; ++i)
                for (int j = 1; j <= m; ++j)
                    if(dp[i][j] < 0) dp[i][j] = MAX(i, j) + M[i][j];
    
            printf("%d
    ", dp[n][m]);
        }
    }
    
    /*
     * 简单DP
     * dp[i][j]表示走到点(i,j)时取得的最大金币数。
     * 状态转移方程:dp[i][j] = max(dp[i - 1][j],dp[i][j - 1]) + M[i][j]。
     */
    

    900 AlvinZH又掉坑里了(I)

    思路

    难题。

    错误思路:贪心。运用上一题的写法,先走一次,路径置零,再来一次,两次最大值相加。你会发现你样例都过不了(要是放个恰好满足的样例不知道要WA多少次)。仔细一想,两次最优加起来还会是最优吗?真不一定,看这题就知道了。

    既然不能分两次处理,那就同步处理吧。如何同步呢?多路DP,即想象两个人同时从左上走到右下,保证在同一点只取一次,求两人最大金币数和。用四维数组dp[205][205][205][205]?看着就挺吓人的,不过简单易懂,状态转移方程也可以很快得出:dp[i][j][x][y]=max{dp[i-1][j][x-1][y],dp[i-1][j][x][y-1],dp[i][j-1][x-1][y],dp[i][j-1][x][y-1]},代表两人到达(i,j)和(x,y)时的最大金币数。虽然明知会MLE,这一步的思考是有必要的,因为这是优化的基础。

    发现惊喜:上述状态转移方程四个决策中有 (i+j=x+y) ,故可以轻易的把四维降成三维。这里有两种方法优化:

    • 第一种方法稍微作优化,需要dp[405][205][205]。其中dp[step][x][y]:表示第step步时(两人一起走),第一个人在第x行,第二个人在第y行的最大收益,答案为dp[m + n][n][n]。两人坐标为(x,step-x)、(y,step-y),两个人在同一行时,一定在同一列,需要注意走到同一点时的处理方法。状态转移如下,四种决策(下下,下右,右下,右右)去最优,具体见参考代码一。
      //下下,下右,右下,右右四者取最大值
      dp[i][j][k] = MAX(dp[i-1][j-1][k-1], dp[i-1][j][k-1], dp[i-1][j-1][k], dp[i-1][j][k]);
      if (j == k)//走到同一行,必定在同一列,所以确定到达A[j][i - j]同一点
        dp[i][j][k] += M[j][i-j];
      else//走到不同行,所以确定到达A[j][i-j]、A[k][i-k]两点。
        dp[i][j][k] += (M[j][i-j] + M[k][i-k]);
    
    • 第二种方法优化更佳,也易懂,需要dp[205][205][205]。其中dp[i][j][k]表示第一个人走到(i,j),第二个人走到横坐标为k,由于两人一起走,可以算出第二人坐标为(k,i+j-k)。这里可以直接避免走到同一点,k!=i即可。状态转移方程如下,同样是取四种决策最优,具体见参考代码二。
      for(int i = 1; i <= n; ++i)
          for(int j = 1; j <= m; ++j)
              for(int k = 1; k <= n && k <= (i+j); ++k)
              {
                  int t = (i+j)-k;
                  if ( k != i )//保证不重复
                      dp[i][j][k] = M[i][j]+M[k][t]+MAX(dp[i-1][j][k],dp[i][j-1][k],dp[i-1][j][k-1],dp[i][j-1][k-1]);
              }
    

    这两种优化很相似,而第二种比第一种空间整整小了一倍,有人问为什么还要放在这里讨论,因为,第一种方法还可以继续优化,我们发现,在状态转移方程中,dp[i][][]只与dp[i-1][][]有关,这意味着什么?这意味着可以把第一维继续优化,即数组变为dp[2][205][205],采用滚动数组,把第一维循环利用。状态转移方程如下,具体可见参考代码三。

      int cur = 0;
      for (int i = 2; i <= n + m; i++) {
          cur ^= 1;
          for (int j = 1; j <= n&&i - j >= 0; j++) {
              for (int k = 1; k <= n&&i - k >= 0; k++) {
                  //下下,下右,右下,右右四者取最大值
                  dp[cur][j][k] = MAX(dp[cur^1][j-1][k-1], dp[cur^1][j][k-1], dp[cur^1][j-1][k], dp[cur^1][j][k]);
                  if (j == k)//走到同一行,必定在同一列,所以确定到A[j][i - j]一点
                      dp[cur][j][k] += M[j][i-j];
                  else//走到不同行,所以确定到A[j][i-j]、A[k][i-k]两点。
                      dp[cur][j][k] += (M[j][i-j] + M[k][i-k]);//右右
              }
          }
      }
    

    三种方法评测记录对比如下:

    参考代码一

    //
    // Created by AlvinZH on 2017/10/17.
    // Copyright (c) AlvinZH. All rights reserved.
    //
    
    #include <cstdio>
    #include <cmath>
    #include <cstring>
    #include <iostream>
    using namespace std;
    
    int m, n;
    int M[201][201];
    int dp[402][201][201];
    
    inline int MAX(int a, int b, int c, int d) {
        int minAns = a;
        if(minAns < b) minAns = b;
        if(minAns < c) minAns = c;
        if(minAns < d) minAns = d;
        return minAns;
    }
    
    int main()
    {
        while(~scanf("%d%d", &n, &m))
        {
            memset(dp, 0, sizeof(dp));
            for (int i = 1; i <= n; i++)
                for (int j = 1; j <= m; j++)
                    scanf("%d", &M[i][j]);
    
            for (int i = 2; i <= n + m; i++) {
                for (int j = 1; j <= n && i - j >= 0; j++) {
                    for (int k = 1; k <= n && i - k >= 0; k++) {
                        //下下,下右,右下,右右四者取最大值
                        dp[i][j][k] = MAX(dp[i-1][j-1][k-1], dp[i-1][j][k-1], dp[i-1][j-1][k], dp[i-1][j][k]);
                        if (j == k)//走到同一行,必定在同一列,所以确定到达A[j][i - j]同一点
                            dp[i][j][k] += M[j][i-j];
                        else//走到不同行,所以确定到达A[j][i-j]、A[k][i-k]两点。
                            dp[i][j][k] += (M[j][i-j] + M[k][i-k]);
                    }
                }
            }
            printf("%d
    ",dp[n + m][n][n]);
        }
        return 0;
    }
    

    参考代码二

    //
    // Created by AlvinZH on 2017/10/17.
    // Copyright (c) AlvinZH. All rights reserved.
    //
    
    #include <cstdio>
    #include <iostream>
    #include <cstring>
    using namespace std;
    
    int m, n;
    int M[201][201];
    int dp[201][201][201];
    
    inline int MAX(int a, int b, int c, int d) {
        int minAns = a;
        if(minAns < b) minAns = b;
        if(minAns < c) minAns = c;
        if(minAns < d) minAns = d;
        return minAns;
    }
    
    int main()
    {
        while(~scanf("%d%d", &n, &m))
        {
            memset(dp, 0, sizeof(dp));
            for(int i = 1; i <= n; ++i)
                for(int j = 1; j <= m; ++j)
                    scanf("%d", &M[i][j]);
    
            dp[1][1][1] = M[1][1];
    
            for(int i = 1; i <= n; ++i)
                for(int j = 1; j <= m; ++j)
                    for(int k = 1; k <= n && k <= (i+j); ++k)
                    {
                        int t = (i+j)-k;
                        if ( k != i )//保证不重复
                            dp[i][j][k] = M[i][j]+M[k][t]+MAX(dp[i-1][j][k],dp[i][j-1][k],dp[i-1][j][k-1],dp[i][j-1][k-1]);
                    }
            printf("%d
    ", dp[n][m-1][n-1] + M[n][m]);
        }
    }
    

    参考代码三(最优)

    //
    // Created by AlvinZH on 2017/10/17.
    // Copyright (c) AlvinZH. All rights reserved.
    //
    
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    using namespace std;
    
    int m, n;
    int M[201][201];
    int dp[2][201][201];
    
    inline int MAX(int a, int b, int c, int d) {
        int minAns = a;
        if(minAns < b) minAns = b;
        if(minAns < c) minAns = c;
        if(minAns < d) minAns = d;
        return minAns;
    }
    
    int main()
    {
        while(~scanf("%d%d", &n, &m))
        {
            memset(dp, 0, sizeof(dp));
            for (int i = 1; i <= n; i++)
                for (int j = 1; j <= m; j++)
                    scanf("%d", &M[i][j]);
    
            //发现每一步只与前一步有关,可以滚动数组,把一维滚动掉。
            int cur = 0;
            for (int i = 2; i <= n + m; i++) {
                cur ^= 1;
                for (int j = 1; j <= n&&i - j >= 0; j++) {
                    for (int k = 1; k <= n&&i - k >= 0; k++) {
                        //下下,下右,右下,右右四者取最大值
                        dp[cur][j][k] = MAX(dp[cur^1][j - 1][k - 1], dp[cur^1][j][k - 1], dp[cur^1][j - 1][k], dp[cur^1][j][k]);
                        if (j == k)//走到同一行,必定在同一列,所以确定到A[j][i - j]一点
                            dp[cur][j][k] += M[j][i - j];
                        else//走到不同行,所以确定到A[j][i - j]、A[k][i - k]两点。
                            dp[cur][j][k] += (M[j][i - j] + M[k][i - k]);//右右
                    }
                }
            }
            printf("%d
    ",dp[cur][n][n]);
        }
        return 0;
    }
    

    901 AlvinZH双掉坑里了(J)

    思路

    简单DP。简化问题:将n个金币放入m个盒子,无空盒。

    直接上dp吧,dp[i][j]:将i个金币放入j个盒子的方法数。此题的关键在于如何找到状态转移方程,很有可能会计算重复的方法。我们把答案分成两部分:

    ①放完之后所有盒子金币数量大于1;
    ②放完之后至少有一个盒子金币数量为1。
    

    这样分可以保证不会有重复计算。状态转移方程: (dp[i][j] = dp[i-j][j] + dp[i-1][j-1])

    (dp[i-j][j]) :将(i-j)个金币放到j个盒子,然后这j个盒子每个再放1个金币。表示的是将i个金币分成所有盒子金币数量大于1的方案总数。例如,求9分解成3份,6(9-3)分成3份可以分为{1,1,4}{1,2,3}{2,2,2},则9可以分为{2,2,5}{2,3,4}{3,3,3},共3种。

    (dp[i-1][j-1]) :将(i-1)个金币放到(j-1)个盒子,再来一个盒子放1个。表示的是将i个金币分成至少有一个盒子金币数量为1的方案总数。例如,求9分解成3份,8(9-1)分成2份可以分为{1,7}{2,6}{3,5}{4,4},则9可以分为{1,1,7}{1,2,6}{1,3,5}{1,4,4},共4种。

    难点在于如何避免重复,这里处理得十分巧妙,请细细体会。

    参考代码

    //
    // Created by AlvinZH on 2017/10/23.
    // Copyright (c) AlvinZH. All rights reserved.
    //
    
    #include <cstdio>
    #include <cstring>
    #define MOD 1000007
    
    int n, m;
    int dp[10005][1005];
    
    int main()
    {
        while(~scanf("%d %d", &n, &m))
        {
            memset(dp, 0, sizeof(dp));
            dp[0][0] = 1;
            for (int i = 1; i <= n; ++i) {
                for (int j = 1; j <= m; ++j) {
                    if(i - j >= 0)
                        dp[i][j] = (dp[i-j][j] + dp[i-1][j-1]) % MOD;
                }
            }
    
            printf("%d
    ", dp[n][m]);
        }
    }
    

    902 AlvinZH叒掉坑里了(K)

    思路

    简单DP。与上一题十分相似,问题简化为:将n个金币放入至多m个盒子,不存在相等数量金币的盒子。

    dp[i][j]:将i个金币放入j个盒子的方法数。本题同样可以沿用上一题思想,把答案分成两部分。但是有一个问题是不能有相同数量金币的盒子,如果像上一题一样处理,我们会出现多个1的情况,需要避免这些情况。

    ①放完之后所有盒子金币数量大于1;
    ②放完之后只有一个盒子金币数量为1。
    

    这样分可以保证不会有重复计算,而且不会有相同。状态转移方程: (dp[i][j] = dp[i-j][j] + dp[i-j][j-1])

    (dp[i-j][j]) :将(i-j)个金币放到j个盒子,然后这j个盒子每个再放1个金币。表示的是将i个金币分成所有盒子金币数量大于1的方案总数。

    (dp[i-j][j-1]) :将(i-j)个金币放到(j-1)个盒子,然后这(j-1)个盒子每个再放1个金币,最后再来一个盒子放1个金币。表示的是将i个金币分成至少有一个盒子金币数量为1的方案总数。

    对比上一题,状态转移方程仅仅差了一个字符

    难点在于如何避免重复以及相同数目,这里处理得十分巧妙,请细细体会。

    优化问题

    本题需要注意内存限制,dp[50005][50005]是会MLE的。由于本题要求分成不同的数目,1+2+3+...+m=n,可以得到 (m<sqrt(2_n)) ,于是dp数组变成dp[50005][350]。时间复杂度为 (O(n_sqrt(2n))) 。具体见参考代码一。

    与第二题相似,我们发现,dp[i][j]只与dp[][j]和dp[][j-1]有关,那么这里可以对空间再次优化,dp数组变为dp[50005][2],具体操作见参考代码二。真tm神奇啊~

    参考代码一

    //
    // Created by AlvinZH on 2017/10/23.
    // Copyright (c) AlvinZH. All rights reserved.
    //
    
    //正常写法
    #include <cstdio>
    #include <cstring>
    #define MOD 1000007
    
    int n;
    int dp[50005][350];
    
    int main()
    {
        while(~scanf("%d", &n))
        {
            memset(dp, 0, sizeof(dp));
            dp[0][0] = 1;
            int ans = 0;
    
            for (int i = 1; i < 350; ++i) {
                for (int j = 0; j <= n; ++j) {
                    if(j - i >= 0)
                        dp[j][i] = (dp[j-i][i] + dp[j-i][i-1]) % MOD;
                }
                ans = (ans + dp[n][i]) % MOD;
            }
    
            printf("%d
    ", ans);
        }
    }
    

    参考代码二(最优)

    #include <cstdio>
    #include <cstring>
    #define MOD 1000007
    
    int n;
    int dp[50005][2];
    
    int main()
    {
        while(~scanf("%d", &n))
        {
            memset(dp, 0, sizeof(dp));
            dp[0][0] = 1;
            int ans = 0;
    
            for (int i = 1; i < 350; ++i) {
                for (int j = 0; j < 350; ++j)//每次操作初始化
                    dp[j][i&1] = 0;
    
                for (int j = 0; j <= n; ++j) {
                    if (j - i >= 0)
                        dp[j][i&1] = (dp[j - i][i&1] + dp[j - i][(i - 1)&1]) % MOD;
                }
                ans = (ans + dp[n][i&1]) % MOD;
            }
    
            printf("%d
    ", ans);
        }
    }
    

    903 AlvinZH叕掉坑里了(L)

    思路

    难题。本题已经超越了dp,但其本质还是dp。简化题目:将一个数拆成一个或多个数的和,即无序整数拆分问题。

    无序整数拆分问题是欧拉五边形数定理的一个应用。详情请查看:分拆数 && hdu 4651 && hdu 4658

    证明五边形数定理以及证明无序拆分整数是五边形数定理的应用,这。。。就超出我的知识范围了。

    参考代码

    //
    // Created by AlvinZH on 2017/10/23.
    // Copyright (c) AlvinZH. All rights reserved.
    //
    
    #include <cstdio>
    #include <cstring>
    #define MaxSize 50005
    #define MOD 1000007
    #define f(x) (((x) * (3 * (x) - 1)) >> 1)
    #define g(x) (((x) * (3 * (x) + 1)) >> 1)
    
    using namespace std;
    
    int n, ans[MaxSize];
    
    void init()
    {
        memset(ans, 0, sizeof(ans));
        ans[0] = 1;
        for (int i = 1; i <= 50000; ++i) {
            for (int j = 1; f(j) <= i; ++j) {
                if (j & 1)
                    ans[i] = (ans[i] + ans[i - f(j)]) % MOD;
                else
                    ans[i] = (ans[i] - ans[i - f(j)] + MOD) % MOD;
            }
            for (int j = 1; g(j) <= i; ++j) {
                if (j & 1)
                    ans[i] = (ans[i] + ans[i - g(j)]) % MOD;
                else
                    ans[i] = (ans[i] - ans[i - g(j)] + MOD) % MOD;
            }
        }
    }
    
    int main()
    {
        init();
        while (~scanf("%d", &n))
        {
            printf("%d
    ", ans[n]);
        }
    }
    
    /*
     * 欧拉五边形定理:P(n)表示n的划分种数。
     * P(n) = ∑{P(n - k(3k - 1) / 2 + P(n - k(3k + 1) / 2 | k ≥ 1}
     * n < 0时,P(n) = 0;n = 0时, P(n) = 1即可。
     */
    

    916 AlvinZH不想掉坑里了(M)

    分析

    中等题。单源最短路径。最短路径是一个经典算法问题,所以我为其特地单独写了一篇随笔,仅供参考。

    AlvinZH又来骗访客量啦:四大算法解决最短路径问题

    参考代码

    //
    // Created by AlvinZH on 2017/11/3.
    // Copyright (c) AlvinZH. All rights reserved.
    //
    
    #include<iostream>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<queue>
    #include<vector>
    #include<algorithm>
    using namespace std;
    const int N=100010;
    const int INF = 0x3f3f3f3f;
    
    bool Vis[N];//是否被访问过
    int Dis[N];//距离
    
    struct DisAndStart
    {
        int dis;//距离
        int start;//起点
        bool operator < (const DisAndStart& p)const {
            return p.dis<dis;
        }
        DisAndStart(int d, int s):dis(d),start(s){}
    };
    
    vector<pair<int, int> > V[N];//二维的vector数组
    
    void dijkstra(int s)
    {
        priority_queue<DisAndStart> Q;
        memset(Dis,INF,sizeof(Dis));
        memset(Vis,0,sizeof(Vis));
    
        Dis[s]=0;
        Q.push(DisAndStart(0,s));
        while(!Q.empty())
        {
            DisAndStart p=Q.top();
            Q.pop();
            if(Vis[p.start]) continue;//已经访问过该点
            Vis[p.start]=1;
            for(int t=0;t<V[p.start].size();t++)
            {
                int end=V[p.start][t].first;
                int Time=V[p.start][t].second;
                if(Dis[p.start]+Time<Dis[end])
                {
                    Dis[end]=Dis[p.start]+Time;
                    Q.push(DisAndStart(Dis[end],end));
                }
            }
        }
    }
    int main()
    {
        //freopen("in2.txt", "r", stdin);
        //freopen("out2.txt", "w", stdout);
        int n, m, k, des;
        int x, y, Time;
        while(~scanf("%d%d%d", &n, &m, &k))
        {
            for(int i = 1; i <= n; i++)//清空数据
                V[i].clear();
            while(m--)
            {
                scanf("%d%d%d", &x, &y, &Time);
                V[x].push_back(make_pair(y, Time));
                V[y].push_back(make_pair(x, Time));
            }
            dijkstra(1);
            int cnt = 1;
            for(int i = 0; i < k; ++i)
            {
                scanf("%d", &des);
    
                if(Dis[des] == INF) printf("Case %d:-1
    ", cnt);
                else printf("Case %d:%d 
    ", cnt, Dis[des]);
                cnt++;
            }
            printf("
    ");
        }
    }
    
  • 相关阅读:
    bash:express:command not found
    Jquery的window.onload实现
    元素的class和id问题
    安装angular-cli
    [(ngModel)]的实现原理
    bodyparser
    git push不用重复输入用户名和密码(解决方案)
    git提交项目到已存在的远程分支
    angular的$filter服务
    网站上如何添加显示favicon
  • 原文地址:https://www.cnblogs.com/AlvinZH/p/7840604.html
Copyright © 2020-2023  润新知