1. 程式人生 > >AlvinZH掉坑系列講解

AlvinZH掉坑系列講解

路徑問題 通過 con dynamic mat 初始 原理 個人 分解

本文由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;②真實數據存於[1~n][1~m]中,邊緣統一。

參考代碼

//
// 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掉坑系列講解