AlvinZH掉坑系列讲解

Posted AlvinZH

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AlvinZH掉坑系列讲解相关的知识,希望对你有一定的参考价值。

本文由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\\n", 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\\n",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\\n", 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\\n",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\\n", 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\\n", 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\\n", 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\\n", 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\\n", cnt);
            else printf("Case %d:%d \\n", cnt, Dis[des]);
            cnt++;
        }
        printf("\\n");
    }
}

以上是关于AlvinZH掉坑系列讲解的主要内容,如果未能解决你的问题,请参考以下文章

A1-2017级算法第一次上机练习赛 C AlvinZH去图书馆

掉坑里出不来系列之JS的Event Loop

851 AlvinZH的鬼畜密码(N)

Docker从入门到掉坑:容器太多,操作好麻烦

906 AlvinZH的奇幻猜想----整数乘积(O)

Kotlin基础从入门到进阶系列讲解(基础篇)Fragment的基本使用