1. 程式人生 > 實用技巧 >區間DP專題

區間DP專題

區間DP

寫在前面

由於動態規劃可以出的題目過於靈活,區別於一般的處理技巧,往往沒有可以統一的正規化(模版),需要不斷刷題總結經驗和培養直覺。


F&Q

  1. 怎麼看出這是一道區間DP題?(總結特徵)

    問題具有最優子結構:如果問題的最優解所包含的子問題的解也是最優的,就稱該問題具有最優子結構性質。

  2. 如何推出狀態轉移方程?

    從題面的描述的中找到最基礎的一步,將問題分解為子問題。

    設計出合理的狀態,需要什麼,維護什麼。

    列舉拆分問題,討論所有情況。


POJ 2955

題意:給你一串由小括號、中括號組成的字串,問最長可以相互匹配的子序列。

總結:

  1. 狀態轉移方程:\(dp[l][r]=min([match(s_l,s_r)]+dp[l+1][r-1],dp[l][m]+dp[m+1][r])\)

  2. 從狀態轉移方程中可以看出,問題轉移成子問題有兩種方式:頭尾括號匹配;列舉拆分問題。

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
const int N = 105;
char s[N];
int dp[N][N];
int solve(int l, int r) {
    if(l >= r) return 0;
    if(~dp[l][r]) return dp[l][r];
    int res = 0;
    if(s[l] == '(' && s[r] == ')')
        res = max(res, 1 + solve(l+1, r-1));
    if(s[l] == '[' && s[r] == ']')
        res = max(res, 1 + solve(l+1, r-1));
    for (int m = l; m < r; m++) {
        res = max(res, solve(l, m) + solve(m+1, r));
    }
    return dp[l][r] = res;
}
int main() {
    while (scanf("%s", s+1) && s[1] != 'e') {
        memset(dp, -1, sizeof(dp));
        int n = strlen(s+1);
        printf("%d\n", 2*solve(1, n));
    }
}

LightOJ 1422

題意:給你一個數組,從左到右遍歷整個陣列,要求在棧加入數字或取出數字 ,使得棧頂數字等於陣列數字,問棧加入數字的最小次數。

總結:

  1. 狀態轉移方程:\(dp[l][r]=min([a_l\neq a_r]+dp[l][r-1],dp[l][m]+dp[m+1][r])\)
  2. 考慮最大化利用,在首位壓入數字後,末位是一定能利用到的;而中間位置的情況,由於列舉分解了問題,不會被忽略。
#include <bits/stdc++.h>
using namespace std;
const int N = 105;
int c[N], n, dp[N][N];
int solve(int l, int r) {
    if(~dp[l][r]) return dp[l][r];
    if(l == r) return 1;
    int res = n;
    if(c[l] != c[r]) res = min(res, solve(l, r-1) + 1);
    else return res = min(res, solve(l, r-1));
    for (int m = l; m < r; m++) {
        res = min(res, solve(l, m) + solve(m+1, r));
    }
    return dp[l][r] = res;
}
int main() {
    int t, cas = 0;
    scanf("%d", &t);
    while (t--) {
        memset(dp, -1, sizeof(dp));
        scanf("%d", &n);
        for (int i = 1; i <= n; i++) scanf("%d", c+i);
        printf("Case %d: %d\n", ++cas, solve(1, n));
    }
}

POJ 1651

題意:矩陣鏈乘法

總結:

  1. 狀態轉移方程:\(dp[l][r]=min(dp[l][m]+dp[m][r]+a[l]*a[m]*a[r])\)
  2. 以m為最後一個取出列舉所有情況。
#include <cstring>
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
const int N = 105;
int a[N], dp[N][N];
int solve(int l, int r) {
    if(~dp[l][r]) return dp[l][r];
    if(l+1 == r) return 0;
    int res = 0x3f3f3f3f;
    for (int m = l+1; m < r; m++) {
        res = min(res, solve(l, m) + solve(m, r) + a[l] * a[m] * a[r]);
    }
    return dp[l][r] = res;
}
int main() {
    memset(dp, -1, sizeof(dp));
    int n; scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d", a+i);
    }
    printf("%d\n", solve(1, n));
}

HDU 2476

題意:給你字串A、B,現可以將A中的子串變成一種字元,要求用上述操作將A變成B,問最小次數。

總結

  1. 問題被分解成兩部分,算出操作的最小代價(區間dp算出空串變為B的代價)和算出操作的最小次數(貪心)。
  2. 狀態轉移方程:\(dp[l][r]=min(dp[l][r-1]+[p_l \neq p_m], dp[l][m]+dp[m+1][r])\)
#include <bits/stdc++.h>
using namespace std;
const int N = 105;
char s[N], p[N];
int dp[N][N], f[N];
int solve(int l, int r) {
    if(~dp[l][r]) return dp[l][r];
    if(l == r) return 1;
    if(l > r) return 0;
    int res = r-l+1;
    if(p[l] == p[r]) res = min(res, solve(l, r-1));
    for (int m = l; m < r; m++) {
        res = min(res, solve(l, m) + solve(m+1, r));
    }
    return dp[l][r] = res;
}
int main() {
    while(~scanf("%s%s", s+1, p+1)) {
        int n = strlen(s+1);
        memset(dp, -1, sizeof(dp));
        f[0] = 0;
        for (int i = 1; i <= n; i++) {
            f[i] = solve(1, i);
            if(s[i] == p[i]) {
                f[i] = f[i-1];
            }
            else {
                for (int j = 1; j < i; j++) {
                    f[i] = min(f[i], f[j] + solve(j+1, i));
                }
            }
        }
        printf("%d\n", f[n]);
    }
}

HDU 4283

題意:給你一個數組,要求用一個棧重新排列陣列,問最小的\(\sum{i*D_i}\)

總結:

  1. 狀態轉移方程:\(dp[l][r]=min(d[l]*(k-l)+(sum[r]-sum[k])*(k-l+1)+dp[l+1][k]+dp[k+1][r])\)
  2. 首個元素壓入棧中後,列舉其出來的位置。
#include <bits/stdc++.h>
using namespace std;
const int N = 105;
const int inf = 0x3f3f3f3f;
int d[N], sum[N], dp[N][N];
int solve(int l, int r) {
    if(~dp[l][r]) return dp[l][r];
    if(l >= r) return 0;
    int res = inf;
    for (int k = l; k <= r; k++) {
        int tmp = d[l] * (k - l) + (sum[r] - sum[k]) * (k - l + 1) + solve(l + 1, k) + solve(k + 1, r);
        res = min(res, tmp);
    }
    return dp[l][r] = res;
}
int main() {
    int t, cas = 0;
    scanf("%d", &t);
    while (t--) {
        memset(dp, -1, sizeof(dp));
        int n; scanf("%d", &n);
        for (int i = 1; i <= n; i++) {
            scanf("%d", d+i);
            sum[i] = sum[i-1] + d[i];
        }
        printf("Case #%d: %d\n", ++cas, solve(1, n));
    }
}

CodeForces 149D

題意:給你一串由相互匹配的括號組成的字串,現將括號染色,滿足

  1. 匹配的括號有且只有一個被染色

  2. 括號可以是無色、藍色、紅色

  3. 相鄰括號不能是同種顏色(無色除外)

    問方案數。

總結

  1. 分情況討論。
  2. “相鄰括號不能是同種顏色”需要在狀態中維護,否則會違反dp的無後效性(當前的若干個狀態值一旦確定,則此後過程的演變就只和這若干個狀態的值有關,和之前是採取哪種手段或經過哪條路徑演變到當前的這若干個狀態沒有關係。)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod = 1e9 + 7;
const int N = 705;
char s[N];
int match[N];
ll f[N][N][3][3];
ll dfs(int l, int r, int i, int j) {
    if(~f[l][r][i][j]) return f[l][r][i][j];
    ll res = 0;
    int m = match[l];
    if(m == r) {
        if(l+1 == r) {
            if(!i && j > 0) return 1;
            if(i > 0 && !j) return 1;
            return 0;
        }
        else {
            if(!i && !j) return 0;
            if(i && j) return 0;
            for (int q = 0; q < 3; q++)
                for (int p = 0; p < 3; p++) {
                    if((i > 0 && p == i) || (j > 0 && q == j)) continue;
                    (res += dfs(l+1, r-1, p, q)) %= mod;
                }
        }
    }
    else {
        for (int p = 0; p < 3; p++)
            for (int q = 0; q < 3; q++) {
                if(p > 0 && q > 0 && p == q) continue;
                (res += dfs(l, m, i, p) * dfs(m+1, r, q, j) % mod) %= mod;
            }
    }
    return f[l][r][i][j] = res;
}
int main() {
    memset(f, -1, sizeof f);
    scanf("%s", s+1);
    int n = strlen(s+1);
    stack<int> stk;
    for (int i = 1; i <= n; i++) {
        if(s[i] == '(') {
            stk.push(i);
        }
        else {
            match[stk.top()] = i;
            stk.pop();
        }
    }
    ll ans = 0;
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            ans = (ans + dfs(1, n, i, j)) % mod;
    printf("%lld\n", ans);
}

ZOJ 3469

題意:x軸上有n個節點,告訴你每個節點的座標\(x_i\)和權重\(b_i\),你從座標p出發,速度為\(v^{-1}\),問 \(\min(\sum{t_i*b_i})\)

總結:

  1. 計算貢獻。從一個點出發到達另外一個點,途徑時間對剩餘所有點的貢獻,可用前綴合優化。
  2. 狀態轉移方程:詳見程式碼。
#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
const int inf = 0x3f3f3f3f;
struct Node {
    int x, b;
    bool operator < (const Node& rhs) const {
        return x < rhs.x;
    }
}a[N];
void Min(int &x, int y) {
    if(x > y) x = y;
}
int f[N][N][2], sum[N];
int n, v, x;
int dfs(int l, int r, bool p) {
    if(~f[l][r][p]) return f[l][r][p];
    if(l == r) return a[l].x == x ? 0 : inf;
    int res = inf;
    if(!p) {
        Min(res, dfs(l+1, r, 0) + (a[l+1].x - a[l].x) * (sum[l] + sum[n] - sum[r]));
        Min(res, dfs(l+1, r, 1) + (a[r].x - a[l].x) * (sum[l] + sum[n] - sum[r]));
    }
    else {
        Min(res, dfs(l, r-1, 1) + (a[r].x - a[r-1].x) * (sum[n] - sum[r-1] + sum[l-1]));
        Min(res, dfs(l, r-1, 0) + (a[r].x - a[l].x) * (sum[n] - sum[r-1] + sum[l-1]));
    }
    return f[l][r][p] = res;
}
int main() {
    while (~scanf("%d%d%d", &n, &v, &x)) {
        memset(f, -1, sizeof(f));
        for (int i = 1; i <= n; i++) {
            scanf("%d%d", &a[i].x, &a[i].b);
        }
        a[++n] = {x, 0};
        sort(a+1, a+n+1);
        for (int i = 1; i <= n; i++)
            sum[i] = sum[i-1] + a[i].b;
        printf("%d\n", min(dfs(1, n, 0), dfs(1, n, 1)) * v);
    }
}