大工軟件學院DP入門練習題解

傳送門:戳這裏

第一題:get the ball

狀態很容易能看出來是一個二維的狀態,定義dp(i,j)表示第i次傳球球在第j個人手裏的方案數。因爲只能從相鄰的人手裏拿到,所以狀態轉移方程也很好確定,dp[i,j] = dp[i-1,j-1] + dp[i-1,j+1]。只需要做好初始化就可以了,唯一需要注意的就是這個題不可以用遞歸的方法來計算,即使做了記憶化依然會超時,因爲過程中遞歸次數過多。

第二題:旅行花費

這個題也是很簡單的一個記憶化搜索的題目,定義狀態dp(i)表示行駛i公里的最小化費,轉移方程就是dp[i] = min(dp[i-j] + f[j]),其中j是小於等於10的正整數。沒什麼難度,隨便過。

第三題:收拾垃圾

這個題目是之前講過的0/1揹包的弱化問題,只需要考慮體積,不用考慮價值。思路類似0/1揹包,定義狀態方程dp(i)表示i這個體積能否達成,轉移方程就是枚舉垃圾的體積v[j],dp[i] = dp[i - v[j]]。

注:本次練習的目的是讓大家完成前三題,而後三題是讓大家見識更多各種各樣的動態規劃問題,希望看完後三題題解之後,大家能有這樣的感受:“我去,這也是DP?”和“我去,這也能DP?”

第四題:贏下游戲

一個區間動規的基礎題,沒有接觸過區間DP的可能會有點困難。取數的過程有一個典型的特點,就是取走一個數之後這個大區間就被分成了兩個小區間,也就是說可以定義狀態dp(i,j)表示從第i個數到第j個數能得到的最小分數,所以狀態轉移方程就是枚舉這個區間間斷點k,dp[i,j] = min(dp[i,k] + dp[k,j] + a[i] * a[k] * a[j])。這樣根據這個遞推式子遞推一下就可以得到正確答案了。問題的分割、整合轉移是區間動態規劃的典型特點,而如何看出一個問題是區間動規則需要大家更多的接觸區間動規的題目,掌握區間的真正含義,而不是僅僅停留在數學上的區間。

代碼:

#include <iostream>
#include <cstring>
#include <cstdio>
 
using namespace std;
 
const int maxn = 0x3f3f3f3f;
 
int n,a[101];
int f[101][101];
 
int main() {
    //freopen("in.txt","r",stdin);
    while (cin>>n) {
        for (int i = 1; i<=n; i++) cin>>a[i];
        memset(f,maxn,sizeof(f));
        for (int i = 1; i<=n; i++)
            f[i][i-1] = f[i][i] = f[i][i+1] = 0;
        for (int i = n-2; i>0; i--)
            for (int j = i+2; j<=n; j++)
                for (int k = i+1; k<j; k++)
                    if (f[i][j]>f[i][k]+a[i]*a[k]*a[j]+f[k][j])
                       f[i][j]=f[i][k]+a[i]*a[k]*a[j]+f[k][j];
        cout<<f[1][n]<<endl;
    }
}

第五題:休息時間

看問題,不難看出來是一個一維DP,但是問題就出在瞭如何轉移的問題,不妨各種想法都試試,比如就定義dp(i)表示1-i分鐘能有多少休息時間,而不難發現這樣的方程是存在後效性的,因爲第i分鐘的工作狀態是影響這個狀態往後轉移的。所以就換一種思想,定義dp(i)表示i-n分鐘能休息的時間,可見這個狀態是完全可行的,只需要判斷是否有在第i分鐘開始的工作即可,對於沒有在第i分鐘開始的情況,dp[i] = dp[i+1] + 1,而對於有開始的就要枚舉在第i分鐘開始的工作j,計算dp[i+t[j]]的最大值。當然我還是很仁慈的,數據裏邊的工作開始時間給出的順序是按照遞增順序給出的,所以一個完美的O(n+k)的複雜度。

代碼:

#include <cstdio>
#include <iostream>
#include <cstring>
 
using namespace std;
 
const int maxn = 10000 + 5;
int n,k;
int p[maxn],t[maxn],f[maxn];
 
int main() {
    //freopen("in.txt","r",stdin);
    while (cin>>n>>k) {
        for (int i = 1; i <= k; i++) scanf("%d%d",&p[i],&t[i]);
        int x = k;
        memset(f,0,sizeof(0));
        for (int i = n; i > 0; i--) {
            if (i > p[x]) f[i] = f[i+1] + 1;
            else {
                f[i] = f[i+t[x]];
                x--;
                while (i == p[x]) {
                    f[i] = max(f[i],f[i+t[x]]);
                    x--;
                }
            }
        }
        cout<<f[1]<<endl;
    }
}
第六題:測試

問題很簡潔,就是LCIS,LCS和LIS的完美結合。確實需要在動態規劃上下了很多功夫之後才能做這個題目。也是希望大家不要說我喪心病狂就好。這個問題確實有很多的重疊子問題,目前需要解決的問題就是如何利用這些重疊子問題來記憶化搜索,所以可以用類似LIS和LCS的思想定義一個狀態dp(i,j)表示a串前i個元素和b串前j個元素並且以b[j]爲結尾構成的LCIS的長度。(希望大家能好好品味一下狀態的定義)。之後就是考察這個狀態,還是一樣如果正想它能轉移到哪裏實在是天方夜譚,所以反着想,哪些狀態能轉移過來,這樣來看好像只有兩種情況了,第一種就是a[i] != b[j],這個狀態轉移很簡單就是dp[i,j] = dp[i-1,j](想一想,爲什麼?),而第二種a[i] == b[j]的情況,想想轉移的話好像有點複雜,沒關係,慢慢看,首先第一維i,被拿去和b[j]匹配了,所以肯定不用想了,而i-2這個狀態必然沒有i-1這個狀態好(想一想,爲什麼?),所以只需要枚舉b[1] - b[j-1]中小於b[j]並且LCIS最長的長度,拿過來+1就好。於是得到了狀態轉移方程:dp[i,j] = (a[i] != b[j]) ? dp[i-1,j] : max(dp[i-1,k]) + 1,其中k是1~j-1的整數並且b[k] < b[j]。當然這是個O(n^3)的算法,對於3000這種數據量來說是無法承受的,所以就需要點小優化,就是在計算過程做到不用枚舉k,具體看程序就好了。

代碼:

#include <cstdio>
#include <iostream>
#include <cstring>
 
using namespace std;
 
const int maxn = 3000 + 10;
int n;
int a[maxn*2],b[maxn*2];
int f[maxn*2];
 
int main() {
    //freopen("in.in","r",stdin);
    //freopen("out.txt","w",stdout);
    while (cin>>n) {
        for (int i = 1; i <= n; i++) {
            scanf("%d",&a[i]);
        }
        for (int i = 1; i <= n; i++) {
            scanf("%d",&b[i]);
        }
        memset(f,0,sizeof(f));
        for (int i = 1; i <= n; i++) {
            int tmp = 0;
            for (int j = 1; j <= n; j++) {
                if (a[i] > b[j]) tmp = max(tmp,f[j]);
                if (a[i] == b[j]) f[j] = tmp + 1;
            }
        }
        int ans = 0;
        for (int i = 1; i <= n; i++) ans = max(ans,f[i]);
        cout<<ans<<endl;
    }
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章