1. 程式人生 > 其它 >動態規劃-狀態機模型專題(題目彙總)

動態規劃-狀態機模型專題(題目彙總)

狀態機模型

狀態機題面的顯著特點:描述過程而非結果,重點是描述狀態。
狀態機的入口全部初始化為0,非入口全初始化為無窮。

AcWing 1049. 大盜阿福

題意

給定長度為N的陣列,不能選擇相鄰的數,問可以求得的最大和?

題解

狀態機DP

狀態表示

f[i] [0]:前i個數且不選當前數的最大和

f[i] [1]:前i個數且選當前數的最大和

狀態計算

狀態劃分:最後一步

f[i] [0]:

  • 0->0
  • 1->0

f[i] [1]:

  • 0 -> 1

狀態機的入口全部初始化為0,非入口全初始化為無窮。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;
int w[N], f[N][2];

int main()
{
    int T;
    scanf("%d", &T);
    while (T -- )
    {
        scanf("%d", &n);
        for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);
        
        for (int i = 1; i <= n; i ++ ){
            f[i][0] = max(f[i - 1][1], f[i - 1][0]);
            f[i][1] = f[i - 1][0] + w[i];
        }
        printf("%d\n", max(f[n][0], f[n][1]));
    }

    return 0;
}

滾動陣列優化

可以看出狀態只依賴於上一個狀態:

#include <iostream>
#include <cstring>

using namespace std;

const int N = 1e5 + 10;

int n;
int w[N];
int f[2][2];

void solve()
{
    memset(f, -0x3f, sizeof f);
    f[0][0] = 0;

    cin >> n;
    for (int i = 1; i <= n; ++ i) cin >> w[i];
    for (int i = 1; i <= n; ++ i)
    {
        f[i & 1][0] = max(f[(i - 1) & 1][1], f[(i - 1) & 1][0]);
        f[i & 1][1] = f[(i - 1) & 1][0] + w[i];
    }
    cout << max(f[n & 1][0], f[n & 1][1]) << endl;
}
int main()
{
    int T = 1;
    cin >> T;
    while (T -- ) solve();
    return 0;
}

第二種思路

f[i]表示前 i 個店鋪的最大收益

狀態劃分:

  • 不搶第 i個店鋪時的最大收益為f[i−1]

  • 搶第 i 個店鋪時的最大收益位f[i−2]+w[i]

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;
int w[N], f[N];

int main()
{
    int T;
    scanf("%d", &T);
    while (T -- )
    {
        scanf("%d", &n);
        for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);
        int res = 0;
        f[1] = w[1];
        for (int i = 2; i <= n; i ++ ){
            f[i] = max(f[i - 2] + w[i], f[i - 1]);
            res = max(f[i], res);
        }
        printf("%d\n", res);
    }

    return 0;
}

AcWing 1057. 股票買賣 IV

題意

給定一個長度為 N 的陣列w,陣列中的第 i個數字表示一個給定股票在第i天的價格。你可以在第i天買進,在第j天賣出,獲取利潤為w[j] - w[i](i <= j)。最多可以完成 k 筆交易所能獲取的最大利潤。

題解

狀態機DP

狀態表示

f(i, j, 0)前i天完成了j次交易且手中沒有股票的最大利潤。

f(i, j, 1)前i天完成了j次交易且手中有股票的最大利潤。

狀態劃分:

線性DP

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010, M = 110, INF = 0x3f3f3f3f;

int n, m;
int w[N];
int f[N][M][2];

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);

    memset(f, -0x3f, sizeof f);
    for (int i = 0; i <= n; i ++ ) f[i][0][0] = 0;

    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            f[i][j][0] = max(f[i - 1][j][0], f[i - 1][j][1] + w[i]);
            f[i][j][1] = max(f[i - 1][j][1], f[i - 1][j - 1][0] - w[i]);
        }
    }
    int res = 0;
    for (int i = 0; i <= m; i ++ ) res = max(res, f[n][i][0]);

    printf("%d\n", res);

    return 0;
}

AcWing 1058. 股票買賣 V

題意

給定一個長度為 N 的陣列w,陣列中的第 i個數字表示一個給定股票在第i天的價格。你可以在第i天買進,在第j天賣出,獲取利潤為w[j] - w[i](i <= j),不能同時參與多筆交易。賣出股票後,你無法在第二天買入股票 (即冷凍期為1天),求所能獲取的最大利潤?

題解

f(i, 0)表示前i天,手裡有股票的最大利潤。

f(i, 1)表示前i天,手裡沒有股票第一天的最大利潤。

f(i, 2)表示前i天,手裡沒有股票第>= 2天的最大利潤。

狀態劃分:

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010, M = 110, INF = 0x3f3f3f3f;

int n, m;
int w[N];
int f[N][M][2];

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);

    memset(f, -0x3f, sizeof f);
    for (int i = 0; i <= n; i ++ ) f[i][0][0] = 0;

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
        {
            f[i][j][0] = max(f[i - 1][j][0], f[i - 1][j][1] + w[i]);
            f[i][j][1] = max(f[i - 1][j][1], f[i - 1][j - 1][0] - w[i]);
        }

    int res = 0;
    for (int i = 0; i <= m; i ++ ) res = max(res, f[n][i][0]);

    printf("%d\n", res);

    return 0;
}

Acwing 1059. 股票買賣 VI

題意

給定一個長度為 N 的陣列,陣列中的第 i 個數字表示一個給定股票在第 i 天的價格,再給定一個非負整數 f,表示交易股票的手續費用。

題解

f(i, 0)表示前i天,手裡有股票的最大利潤。

f(i, 1)表示前i天,手裡沒有股票的最大利潤。

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010, M = 110, INF = 0x3f3f3f3f;

int n, m;
int w[N];
int f[N][2];

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);

    memset(f, -0x3f, sizeof f);
    for (int i = 0; i <= n; i ++ ) f[i][0] = 0;

    for(int i = 1; i <= n; i++){
            f[i][0] = max(f[i - 1][0], f[i - 1][1] + w[i] - m);
            f[i][1] = max(f[i - 1][1], f[i - 1][0] - w[i]);
    }

    printf("%d\n", f[n][0]);

    return 0;
}

Acwing 1583. PAT 計數

題意

現在給定一個字串,請你求出字串中包含的 PAT 子序列的數量。

題解

f(i, j)表示前i個字元走到狀態j的所有路線的數量。

狀態j如下圖:0 1 2 3

f(i, j)來源:

  1. 當前字母不用:f(i - 1, j)
  2. 匹配當前字母:f(i - 1, j - 1),當s[i] == p[j]
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int MOD=1000000007;
const int N=100010,M=110;
int f[N][M],n;
char p[]=" PAT";
char s[N];

int main()
{
   
    cin>>s+1;
    f[0][0]=1;
    n=strlen(s+1);
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=3;j++)
        {
            f[i][j]=f[i-1][j];
            if(s[i]==p[j])
                f[i][j]=(f[i][j]+f[i-1][j-1])%MOD;
        }
    }
    cout<<f[n][3];
    return 0;
}

Acwing 3195. 有趣的數

題意

我們把一個數稱為有趣的,當且僅當:

  1. 它的數字只包含 0,1,2,3,且這四個數字都出現過至少一次。
  2. 所有的 0 都出現在所有的 1 之前,而所有的 2 都出現在所有的 3之前。
  3. 最高位數字不為 0。

請計算恰好有 n 位的有趣的數的個數。

4 位:2013、2031 和 2301

題解

f(i, j)表示填充了i位密碼對應狀態j的方案數

狀態設計:

  1. 使用了2,剩下 0,1,3

  2. 使用了2,0,剩下 1,3

  3. 使用了2,3,剩下0,1

  4. 使用了2,0,1,剩下3

  5. 使用了2,0,3, 剩下1

  6. 使用了全部

狀態劃分:

劃分依據:最後一位填充什麼

f(i, 0) = 1, 第i位只有填2一種選擇

f(i, 1) = f(i - 1, 0) (第i位填2)+ f(i - 1, 1) * 2 (第i位填2/0)

f(i, 2) = f(i - 1, 0) (第i位填3) + f(i - 1, 2) (第i位只能填2)

f(i, 3) = f(i - 1, 1) (第i位填1) + f(i - 1, 3) * 2 (第i位填2/1)

f(i, 4) = f(i - 1, 1) (第i位填3) + f(i - 1, 2) (第i位填0) + f(i - 1, 4) * 2 (第i位填0/3)

f(i, 5) = f(i - 1, 3)(第i位填3) + f(i - 1, 4)( 第i位填1) + f(i - 1, 5) * 2(第i位填1/3)

#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1010, MOD = 1000000007;
LL f[N][7];

int main(){
   
    LL n;
    cin>>n;
    for(LL i=1;i<=n;i++)
    {
        LL j = i-1;
        f[i][0] = 1;
        f[i][1] = (f[j][0] + f[j][1] * 2) % MOD;
        f[i][2] = (f[j][0] + f[j][2]) % MOD;
        f[i][3] = (f[j][1] + f[j][3] * 2) % MOD;
        f[i][4] = (f[j][1] + f[j][2] + f[j][4] * 2) % MOD;
        f[i][5] = (f[j][3] + f[j][4] + f[j][5] * 2) % MOD;
    }
    cout<<f[n][5]<<endl;
    return 0;
}


AcWing 1052. 設計密碼

題意

你現在需要設計一個密碼 S,S 需要滿足:

  • S 的長度是 N;
  • S 只包含小寫英文字母;
  • S 不包含子串 T;

題解

參考:https://www.acwing.com/solution/content/28022/

#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;

const int N=55,MOD=1e9+7;

int f[N][N],ne[N];
char str[N];//子串

int main()
{
    int n,m;
    cin>>n>>str+1;
    m=strlen(str+1);

    for(int i=2,j=0;i<=m;i++)//求出ne陣列(kmp模板)
    {
        while(j&&str[j+1]!=str[i]) j=ne[j];
        if(str[j+1]==str[i]) j++;
        ne[i]=j;
    }

    f[0][0]=1;//已經匹配了0位,且匹配的子串的位置是0時的方案數為1;(初始化)
    for(int i=0;i<n;i++)//列舉密碼位
     for(int j=0;j<m;j++)//把第i位密碼匹配到的子串位置都列舉一遍
     //j表示第i位密碼匹配到的位置,因為不能包含子串,所以不能匹配到m這個位置
      for(char k='a';k<='z';k++)//把第i+1所有可能的字母都列舉一遍
       {
           //匹配過程:尋找當第i+1的位置是k時,並且密碼已經生成了第i位,匹配的子串的位置是j時,能跳到哪個位置
           int u=j;
           while(u&&str[u+1]!=k) u=ne[u];
           if(str[u+1]==k) u++;

           if(u<m) f[i+1][u]=(f[i+1][u]+f[i][j])%MOD;
           //因為是從f[i][j](i+1的位置為k)跳到f[i+1][u]這個位置,所以f[i+1][u]=f[i+1][u]+f[i][j];
           /*
           注:可能存在重邊,因為j不同但ne[j]是相同的,並且k是相同的,所以此時
           f[i][j1]和f[i][j2]跳到的位置是一樣的(k相同,ne[j1]=ne[j2])
           */
       }

    int res=0;
    for(int i=0;i<m;i++) res=(res+f[n][i])%MOD;
    //將所有的方案數加起來即為總方案數
    printf("%d",res);

    return 0;
}