「REMAKE系列」区间dp篇

总结

不明显的区间DP题目

  • 结合二叉搜索树背景 CF1025D

经典模型

关路灯

习题

dls动态规划中级课

石子合并变形---ICPC Beijing 2017 J, Pangu and Stones

2017-ICPC-Beijing-J

石子合并模型,限制每次合并只能合并连续的 \([L, R]\) 堆石子,询问最小的合并代价,如果没法操作,输出 0

思路

  • 普通石子合并状态无法完成,考虑多加一维状态: \(f[l][r][k]\) 将 [l, r] 合并成 k 堆石子最小代价。
  • 对于上述状态很朴素的想法是枚举分界点,然后枚举分界点左右的石子堆个数,由于还要枚举 k 这一维,所以复杂度是 \(O(n^5)\) 无法通过。
  • 考虑优化一维枚举,其实发现,枚举左右连续合并石子堆的个数,其实就是枚举左边合并成 1 堆,右边合并成 k - 1 堆,这样的枚举可以不重不漏。
  • 最后注意边界, f[i][i][1] = 0,其他赋值正无穷
const int N = 110;
int f[N][N][N], a[N], s[N];

int main() {
    // freopen("input.txt", "r", stdin);
    // freopen("output.txt", "w", stdout);
    IOS;
    int n, L, R;
    while (cin >> n >> L >> R) {
        for (int i = 1; i <= n; i++) cin >> a[i], s[i] = s[i - 1] + a[i];
        for (int l = 1; l <= n; l++) {
            for (int r = l; r <= n; r++)
                for (int k = 0; k <= n; k ++)
                    f[l][r][k] = 1 << 29;
        }
        for (int len = 1; len <= n; len++) {
            for (int l = 1; l + len - 1 <= n; l ++) {
                int r = l + len - 1;
                if (l == r) {
                    f[l][r][1] = 0;
                    continue;
                }
                for (int k = 2; k <= len; k ++) {
                    for (int mid = l; mid < r; mid ++) {
                        f[l][r][k] = min(f[l][r][k], f[l][mid][1] + f[mid + 1][r][k - 1]);
                    }
                    if (k >= L && k <= R)       // 可以被合并为一堆
                        f[l][r][1] = min(f[l][r][1], f[l][r][k]);
                }
                f[l][r][1] += s[r] - s[l - 1];
            }
        }
        if (f[1][n][1] >= 1 << 29) f[1][n][1] = 0;
        cout << f[1][n][1] << endl;
    }
    // fclose(stdin);
    // fclose(stdout);
    return 0;
}

2020-ICPC-KunMing C, Citys

给定序列 a,每次选取连续的相同一段染成其他颜色,询问将全序列染成相同颜色的最小代价。

思路

  • 贪心的发现,最小代价与,每次染色左右两端是否相同有关,每有一堆就会让代价 - 1,原总的代价是 n - 1 次。
  • 可以设 \(dp[l][r]\) 为 [l, r] 内减小代价的最大值,也可以设 \(dp[l][r]\) 为染色 [l, r] 相同的最小代价。
  • 对于前者就是代码中的转移,\(dp[l][r] = max(dp[l + 1][r], max\{dp[l + 1][x - 1] + dp[x][r] + 1\})\)
  • 对于后者,感觉更容易考虑:
    • 朴素转移 \(dp[l][r] = min(dp[l + 1][r] + 1, dp[l][r - 1] + 1)\)
    • 如果 \(a[l] = a[r]\),则 \(dp[l][r] = min(dp[l][r], dp[l + 1][r - 1] + 1)\)
    • 如果在 (l, r) 内存在 k,满足 a[k] = a[r] ,那么 \(dp[l][r] = min(dp[l][r], dp[k + 1][r - 1] + dp[l][k] + 1)\)
const int N = 5010;
int a[N], f[N][N], pos[N], nxt[N];

int main() {
    // freopen("input.txt", "r", stdin);
    // freopen("output.txt", "w", stdout);
    int T;
    re(T);
    while (T--) {
        int n;
        re(n);
        for (int i = 0; i <= n; i ++)
            pos[i] = n + 1;
        for (int i = 1; i <= n; i ++) {
            re(a[i]);
            if (i != 1 && a[i] == a[i - 1]) {
                i--, n--;
                continue;
            }
        }
        for (int i = n; i >= 1; i--) {
            nxt[i] = pos[a[i]];
            pos[a[i]] = i;
        }
        for (int l = 1; l <= n; l++)
            for (int r = l; r <= n; r++)
                f[l][r] = 0;
        for (int len = 1; len <= n; len++) {
            for (int l = 1; l + len - 1 <= n; l++) {
                int r = l + len - 1;
                int x = nxt[l];
                f[l][r] = f[l + 1][r];
                while (x <= r) {
                    f[l][r] = max(f[l][r], f[l + 1][x - 1] + f[x][r] + 1);
                    x = nxt[x];
                }
            }
        }
        printf("%d\n", n - 1 - f[1][n]);
    }
    // fclose(stdin);
    // fclose(stdout);
    return 0;
}

CSP2021-S-括号序列

CSP2021-提高组-括号序列

题意略;

思路

  • 细节很多的一道题,只要思考清楚还是可以。
  • 状态定义:
    • \(f[l][r]\) 将 [l,r] 合法括号序列方案数
    • \(g[l][r]\) 一定是 l 与 r 匹配的合法括号序列方案数,用于判重
    • \(h[l][r]\) 形如 SA 这样的方案数(不是合法的,用于优化)
  • Case1: (), (S),很容易考虑
  • Case2: ABASB
    • \(s[l]\) 必须是 '(' 因为合法括号序列左右一定是括号
    • 枚举前面的 [l,mid] 为 A。
      • AB\(f[mid + 1][r]\) 转移
      • ASA\(h[mid + 1][r]\) 转移
      • 以上转移需要乘 \(g[l][mid]\)
  • Case3: (A),(SA),(AS)
    • (A) 直接由 \(f[l+1][r-1]\) 转移
    • (SA)\(h[l+1][r-1]\) 转移
    • (AS) 枚举后缀 [i, r - 1] 为 '*',由 \(f[l + 1][i - 1]\) 转移
const int N = 510, mod = 1e9 + 7;
int f[N][N], g[N][N], h[N][N], n, k;
char s[N];

void add(int &a, int b) {
    a += b;
    if (a >= mod) a %= mod;
}

bool match(int i, char c) {
    return s[i] == '?' || s[i] == c;
}

int main() {
    // freopen("input.txt", "r", stdin);
    // freopen("output.txt", "w", stdout);
    re(n), re(k);
    scanf("%s", s + 1);
    for (int len = 2; len <= n; len++) {
        for (int l = 1; l + len - 1 <= n; l++) {
            int r = l + len - 1;
            if (match(l, '(') && match(r, ')')) {
                if (len == 2) {
                    g[l][r] = 1;
                }
                else {
                    // (A)
                    add(g[l][r], f[l + 1][r - 1]);
                    // (S)
                    if (r - l - 1 <= k) {
                        bool ok = true;
                        for (int i = l + 1; i < r && i <= l + k; i++) {
                            if (!match(i, '*')) {
                                ok = false;
                                break;
                            }
                        }
                        if (ok) add(g[l][r], 1);
                    }
                    // (AS)
                    for (int i = r - 1; i >= r - k && i > l + 1; i--) {
                        if (!match(i, '*')) break;
                        add(g[l][r], f[l + 1][i - 1]);
                    }
                    // (SA)
                    add(g[l][r], h[l + 1][r - 1]);
                }
            }
            f[l][r] = g[l][r];
            if (match(l, '(')) {
                for (int mid = l; mid < r; mid ++) {
                    int tmp = 0;
                    // AB
                    add(tmp, f[mid + 1][r]);
                    // ASB
                    add(tmp, h[mid + 1][r]);
                    add(f[l][r], 1ll * g[l][mid] * tmp % mod);
                }
            }
            for (int i = l; i < r - 1 && i <= l + k - 1; i++) {
                if (!match(i, '*')) break;
                add(h[l][r], f[i + 1][r]);
            }
            // printf("%d %d %d %d\n", l, r, f[l][r], g[l][r]);
        }
    }
    printf("%d\n", f[1][n]);
    // fclose(stdin);
    // fclose(stdout);
    return 0;
}

ICPC CERC 2014 L, Outer space invaders

ICPC CERC 2014 L, Outer space invaders

思路

  • 经典变形区间DP,直接从题上莽区间DP不好做状态,问题明显可以数形结合
  • 从极端情况入手,就是对于某一时刻的,要消灭完,必然要消灭最大的,然后呢,你消灭最大的,同一时刻小于他的都会被消灭。之后问题变成了 \([1,x-1]\), \([x+1, n]\) 的子问题。
  • 上面的子问题显然是有最优子结构,然后就区间dp,设计状态 \(dp[l][r]\) 消灭 [l,r] 区间内完整出现的怪物最小代价,
  • 注意是完整出现,因为我们递归子问题之后,子问题需要解决的是在两个子区间内完整出现的怪物,如果有在 x 点出现就被消灭了。
  • 然后就很好写,这里写的记忆化搜索。复杂度 \(O(n^3)\)
vector<int> alls;
const int N = 610;
int dp[N][N], a[N], b[N], n, m, d[N]; 

int find(int x) {
    return lower_bound(ALL(alls), x) - alls.begin() + 1;
}

int dfs(int l, int r) {
    if (dp[l][r] != -1) return dp[l][r];
    int &ans = dp[l][r];
    if (l > r) return ans = 0;
    ans = 2e9;
    int mxd = -1, pos = 0;
    for (int i = 0; i < n; i ++) {
        if (d[i] > mxd && a[i] >= l && b[i] <= r) {
            mxd = d[i];
            pos = i;
        }
    }
    if (mxd == -1) return ans = 0;
    for (int mid = a[pos]; mid <= b[pos]; mid ++) {
        ans = min(ans, dfs(l, mid - 1) + dfs(mid + 1, r));
    }
    ans += mxd;
    return ans;
}

void solve() {
    re(n);
    alls.clear();
    for (int i = 0; i < n; i ++) {
        re(a[i]), re(b[i]), re(d[i]);
        alls.pb(a[i]), alls.pb(b[i]);
    }
    sort(ALL(alls));
    alls.erase(unique(ALL(alls)), alls.end());
    for (int i = 0; i < n; i ++)
        a[i] = find(a[i]), b[i] = find(b[i]);
    m = alls.size();
    for (int i = 1; i <= m; i ++)
        for (int j = 1; j <= m; j++)
            dp[i][j] = -1;
    dfs(1, m);
    printf("%d\n", dp[1][m]);
}

int main() {
    int T;
    re(T);
    while (T--) {
        solve();
    }
    return 0;
}

洛谷——「能力综合提升题单-区间DP篇」

P1005 [NOIP2007 提高组] 矩阵取数游戏

  • 简单区间DP,需要__int128高精度,用快速输出输出。

P2466 [SDOI2008] Sue 的小球

省选+/NOI- 关路灯模型

题意略。

思路

  • 类似关路灯这道题。不过要先进行一些转化
  • 答案 = 初始坐标和 - 损耗,最大化答案等价于最小化损耗。
  • 设立状态 \(f[l][r][0/1]\) 表示接完区间 [l, r] 最后在 左/右 端点的最小损耗。
  • 状态转移需要预处理前缀和,转移时代价为其他未被接到的小球损耗总和。
const int N = 1010;
PLL a[N];
ll dp[N][N][2], n, mid, s[N];
int main() {
    ll ans = 0;
    re(n), re(mid);
    for (int i = 1; i <= n; i++) {
        re(a[i].first);
    }
    for (int i = 1; i <= n; i++) {
        int y;
        re(y);
        ans += y;
    }
    for (int i = 1; i <= n; i++) 
        re(a[i].second);
    a[n + 1] = {mid, 0};
    n++;
    sort(a + 1, a + 1 + n);
    int t = mid;
    for (int i = 1; i <= n; i++) {
        s[i] = s[i - 1] + a[i].second;
        if (a[i].first == t) mid = i;
    }
    memset(dp, 0x3f, sizeof dp);
    dp[mid][mid][0] = dp[mid][mid][1] = 0;
    for (int len = 1; len <= n; len++) {
        for (int l = 1; l + len - 1 <= n; l++) {
            int r = l + len - 1;
            dp[l][r][0] = min(dp[l][r][0], dp[l + 1][r][0] + (a[l + 1].first - a[l].first) * (s[n] - s[r] + s[l]));
            dp[l][r][0] = min(dp[l][r][0], dp[l + 1][r][1] + (a[r].first - a[l].first) * (s[n] - s[r] + s[l]));
            dp[l][r][1] = min(dp[l][r][1], dp[l][r - 1][0] + (a[r].first - a[l].first) * (s[n] - s[r - 1] + s[l - 1]));
            dp[l][r][1] = min(dp[l][r][1], dp[l][r - 1][1] + (a[r].first - a[r - 1].first) * (s[n] - s[r - 1] + s[l - 1]));
        }
    }
    printf("%.3lf\n", (ans - min(dp[1][n][0], dp[1][n][1])) * 1.0 / 1000);
    return 0;
}

Codeforces

CF1025D. Recovering BST

*2100二叉搜索树背景

给你一个不递减大小为n的一个序列,如果两个数的最大公约数大于1,那么这两个数可以建一条边,问最后是否可以构成一颗二叉搜索树。

  • 状态设计
    • \(dp[i][j]\) 表示是否可以构成合法的二叉搜索树
    • \(L[i][j]\) 表示是否可以构成合法的左子树
    • \(R[i][j]\) 表示是否可以构成合法的右子树
  • 状态转移
    • 两棵子树拼接
    • 一棵树和一个节点拼接
const int N = 710;
int a[N];
int dp[N][N];
int L[N][N], R[N][N];
ll gcd(ll a, ll b) {
    return b ? gcd(b, a % b) : a;
}
bool can[N][N];
int main() {
    int n;
    re(n);
    for (int i = 1; i <= n; i++) re(a[i]), dp[i][i] = L[i][i] = R[i][i] = 1;
    a[0] = a[n + 1] = 1;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++)
            if (gcd(a[i], a[j]) > 1) can[i][j] = can[j][i] = 1;
    }
    for (int len = 1; len <= n; len++) {
        for (int l = 1; l + len - 1 <= n; l++) {
            int r = l + len - 1;
            for (int k = l; k <= r; k++) {
                if (L[l][k] && R[k][r]) {
                    dp[l][r] = 1;
                    if (can[k][l - 1]) R[l - 1][r] = 1;
                    if (can[k][r + 1]) L[l][r + 1] = 1;
                }
            }
        }
    }
    dp[1][n] ? puts("Yes") : puts("No");
    return 0;
}
posted @ 2022-09-07 15:02  Roshin  阅读(27)  评论(0编辑  收藏  举报
-->