1. 程式人生 > >2016級算法第三次上機-G.Winter is coming

2016級算法第三次上機-G.Winter is coming

mem tdi 代碼 參考 pri span 循環 重疊 continue

904 Winter is coming

思路

難題。首先簡化問題, \(n\) 個0與 \(m\) 個1排成一列,連續的0不能超過x個,連續的1不能超過y個,求排列方法數。

顯然會想到這是動態規劃。最快想到的方法是 \(dp[i][j][x][y]\) 表示已經有i個北境兵j個野人參與排列,且末尾有x個連續北境士兵或y個連續野人士兵的方案數。這方法顯然是正確的,但是光是 \(dp[200][200][10][10]\) 數組已經十分接近本題內存限制了,保證MLE。狀態轉移方法是大模擬,四層for循環,每次增加一個人放在最後,討論各種情況。具體代碼可見MLE參考代碼,比較好理解。

不過這個方法已經和答案很接近了,只需要稍微優化一下。可以發現第三維和第四維很多空閑的空間被浪費了,我們沒有必要用兩個維度來分別記錄有幾個0或1,可以把第四維變成一個標誌位,0代表北境軍,1代表野人軍,而第三維記錄最後一段連續的人數,這樣空間變成原來的1/6,算是簡單的優化了一下,思想並沒有變。具體可見優化代碼,在此感謝孟堯提供。

本題還可以繼續優化,換種方式,dp[i][j][k]:已經有i個北境兵j個野人參與排列,第三維k是標誌位(0代表北境軍,1代表野人軍)的排列方法數。狀態轉移方程變為:: \(dp[i][j][0]=∑(dp[i-k][j][1])%MOD\) ;其中 \(k∈[1,min(i,x)]\) 。同理, \(dp[i][j][1]=∑(dp[i][j-k][0])%MOD\) ;其中 \(k∈[1,min(j,y)]\) 。相信你很快就能看懂,這裏用 \(dp[i-k][j][1]\) 來代表最後有k個0,相當於同時把三四維合並了,巧妙至極。具體可見最優參考代碼。

分析

本題不卡時間,卡的是內存。目的是讓大家在解決問題的時候有優化的思想(實際的目的是把它從中等題變成難題)。

DP只能意會,不可言傳。大家在做DP題的時候一定要理清思路,一般是先不管空間,畢竟以空間換時間,大多數題都是先卡時間再卡空間的。

以本題為例粗略講解一下DP,以後不會再講。記住DP具備的兩個要素:最優子結構和子問題重疊,見《算法導論》225頁。本問題明顯備最優子結構,最少的排列數是由多個較短一點的最少排列數組成。DP的多層循環也是有規律的,因為子問題的重疊,你得先把子問題算出來,才能計算更深層的。這裏i和j從小到大地計算,保證所加上的都是已經計算過的,才不會出現問題,如果這題加一個dp[i+1][j][1],那明顯不對了,因為這個還沒有計算過。狀態轉移方程有時候很微妙,需要一番數學推理。

最優參考代碼

//
// Created by AlvinZH on 2017/10/24.
// Copyright (c) AlvinZH. All rights reserved.
//

#include <cstdio>
#include <cstring>
#include <iostream>
#define MOD 1000007
using namespace std;

int n, m, x, y;
int dp[205][205][2];

int main()
{
    while(~scanf("%d %d %d %d", &n, &m, &x, &y))
    {
        memset(dp, 0, sizeof(dp));
        for (int i = 0; i <= x; ++i)
            dp[i][0][0] = 1;
        for (int i = 0; i <= y; ++i)
            dp[0][i][1] = 1;

        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= m; ++j) {
                for (int k = 1; k <= min(i,x); ++k)
                    dp[i][j][0] = (dp[i][j][0] + dp[i-k][j][1]) % MOD;

                for (int k = 1; k <= min(j,y); ++k)
                    dp[i][j][1] = (dp[i][j][1] + dp[i][j-k][0]) % MOD;
            }
        }

        printf("%d\n", (dp[n][m][0] + dp[n][m][1]) % MOD);
    }
}

/* 把第三四維合並,因為我們只要在狀態轉移的時候保證最後連續一段不超過x或y就好了,第三維用來記錄最後連續的是0還是1就好了。
 * dp[i][j][k]:已經有i個北境兵j個野人參與排列,k為標誌位(0代表北境軍,1代表野人軍)的排列方法數。
 */

優化參考代碼

/*
 Author: 孟爻(12593)
 Result: AC Submission_id: 403884
 Created at: Sun Nov 12 2017 23:05:10 GMT+0800 (CST)
 Problem: 904   Time: 73    Memory: 11232
*/

#include <cstdio>
#include <cstring>

long f[205][205][15][2];
long M = 1000007;
long n,m,x,y;

int main() {
    while(~scanf("%ld%ld%ld%ld",&n,&m,&x,&y))
    {
        memset(f,0,sizeof(f));
        f[0][0][0][0]=1;
        for(long i=0; i<=n; i++) {
            for(long j=0; j<=m; j++) {
                if(i) {
                    for(long k=1; k<=x; k++)
                        f[i][j][k][0]=(f[i][j][k][0]+f[i-1][j][k-1][0])%M;
                    for(long k=0; k<=y; k++)
                        f[i][j][1][0]=(f[i][j][1][0]+f[i-1][j][k][1])%M;
                }
                if(j) {
                    for(long k=1; k<=y; k++)
                        f[i][j][k][1]=(f[i][j][k][1]+f[i][j-1][k-1][1])%M;
                    for(long k=0; k<=x; k++)
                        f[i][j][1][1]=(f[i][j][1][1]+f[i][j-1][k][0])%M;
                }
            }
        }
        long ans=0;
        for(long k=1; k<=x; k++)
            ans=(ans+f[n][m][k][0])%M;
        for(long k=1; k<=y; k++)
            ans=(ans+f[n][m][k][1])%M;

        printf("%ld\n",ans);
    }
}

MLE代碼

//
// Created by AlvinZH on 2017/10/24.
// Copyright (c) AlvinZH. All rights reserved.
//

#include <cstdio>
#include <cstring>
#define MOD 1000007

int n, m, x, y;
int dp[205][205][12][12];

int main()
{
    while(~scanf("%d %d %d %d", &n, &m, &x, &y))
    {
        memset(dp, 0, sizeof(dp));
        dp[0][0][0][0] = 1;
        for (int i = 0; i <= n; ++i) {
            for (int j = 0; j <= m; ++j) {
                for (int k = 0; k <= x; ++k) {
                    for (int l = 0; l <= y; ++l) {
                        if(dp[i][j][k][l] == 0) continue;
                        if(i != n && k != x)//末尾是北境兵
                        {
                            dp[i+1][j][k+1][0] += dp[i][j][k][l];
                            dp[i+1][j][k+1][0] %= MOD;
                        }
                        if(j != m && l != y)//末尾是野人兵
                        {
                            dp[i][j+1][0][l+1] += dp[i][j][k][l];
                            dp[i][j+1][0][l+1] %= MOD;
                        }
                    }
                }
            }
        }
        int ans = 0;
        for (int i = 1; i <= x; ++i)
            ans = (ans + dp[n][m][i][0]) % MOD;
        for (int i = 1; i <= y; ++i)
            ans = (ans + dp[n][m][0][i]) % MOD;

        printf("%d\n", ans);
    }
}

/*
 * dp[i][j][x][y]表示已經有i個北境兵j個野人參與排列,且末尾有x個連續北境士兵或y個連續野人士兵的方案數。
 */

2016級算法第三次上機-G.Winter is coming