Loading

区间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);
    }
}
posted @ 2020-08-19 16:48  御坂20001  阅读(99)  评论(0编辑  收藏  举报