1 条题解

  • 0
    @ 2025-8-24 22:30:46

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar _zhangcx
    支持互关 || Bocchi the Rock 第二季快来啊

    搬运于2025-08-24 22:30:46,当前版本为作者最后更新于2025-02-06 13:10:52,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    [USACO21OPEN] Balanced Subsets P

    如果有蒟蒻想出状态但不会转移的请看这篇题解,就比如我

    首先解析一下题目中“平衡”条件,就是实心凸多边形的充要条件,因为如果是凹的,一定会有一行/一列不连续。

    求平衡连通块个数,显然是计数 DP。

    那么从上到下扫一遍这个四边形,那么左、右端点一定是先扩张后收缩的,于是得到状态设计:

    fl,r,p,qf_{l,r,p,q}(其中 p,q{0,1}p,q \in \{0,1\}):扫到第 ii 行,第 ii 行左右端点分别为 llrr,且到此时左端点是扩张/收缩趋势(pp),右端点是扩张/收缩趋势(qq)的方案数。

    方便起见,我们把 ppqq 这两维称为“变化趋势”。

    SubTask

    n50n \le 50 的情况直接转移状态即可。

    可以看看我的代码和图理解一下。

    图1

    时间复杂度 O(n5)O(n^5)

    #define int long long int
    namespace SubTask {
    /* O(n^5): 照状态直接转移即可,能拿到50pts高分
     */
    void solve() {
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++)
                ss[j] = ss[j - 1] + (s[i][j] == 'G');
            for (int l = 1; l <= n; l++) {
                for (int r = l; r <= n; r++) { // 枚举这一个区间 [l, r]
                    if (ss[r] - ss[l - 1] != r - l + 1)
                        continue;  // 没有填满色
                    f[i][l][r][0][0] = 1; // 这个代表现在第 i 行的 [l, r] 是整个图形的顶部
                    for (int x = 1; x <= n; x++)
                        for (int y = x; y <= n; y++) {  // 枚举上一个区间 [x, y]
                            // 注意到变化趋势:0可以转到1,但是1不可以转到0,如果是l/r不变那么认为l/r的变化趋势也不变
    
                            // l, r都扩张,变化趋势不变
                            if (l <= x && y <= r)
                                add(f[i][l][r][0][0], f[i - 1][x][y][0][0]);
                            // l一定保证扩张,分讨r是收缩/不变的两种情况
                            if (l <= x && x <= r && r < y)
                                add(f[i][l][r][0][1], f[i - 1][x][y][0][0]);
                            if (l <= x && x <= r && r <= y)
                                add(f[i][l][r][0][1], f[i - 1][x][y][0][1]);
    
                            // r一定保证扩张,分讨l是收缩/不变的两种情况
                            if (x < l && l <= y && y <= r)
                                add(f[i][l][r][1][0], f[i - 1][x][y][0][0]);
                            if (x <= l && l <= y && y <= r)
                                add(f[i][l][r][1][0], f[i - 1][x][y][1][0]);
    
                            // 分讨l是收缩/不变及r是收缩/不变的四种情况
                            if (x <= l && y >= r)
                                add(f[i][l][r][1][1], f[i - 1][x][y][1][1]);
                            if (x < l && y >= r)
                                add(f[i][l][r][1][1], f[i - 1][x][y][0][1]);
                            if (x <= l && y > r)
                                add(f[i][l][r][1][1], f[i - 1][x][y][1][0]);
                            if (x < l && y > r)
                                add(f[i][l][r][1][1], f[i - 1][x][y][0][0]);
                        }
                    for (int c = 0; c < 4; c++)
                        add(ans, f[i][l][r][c >> 1][c & 1]);
                }
            }
        }
        printf("%lld\n", ans);
    }
    };
    

    FullSolution

    n150n \le 150 的情况要考虑优化。

    可以看出我们转移状态的时候用了 xxyy 帮助转移,但这样也大大增加了我们的时间复杂度。

    考虑去掉 xxyy 这两层循环,把情况列出来试一试:

    图2

    可以发现对于每种情况的 xxyy ,它们分别都在一个连续区间内

    于是把每一种情况用前缀和优化一下即可

    时间复杂度 O(n3)O(n^3)

    #include <bits/stdc++.h>
    
    #define int long long int
    
    using namespace std;
    
    const int N = 1.5e2 + 10, mod = 1e9 + 7;
    int n;
    char s[N][N];
    int f[N][N][N][2][2]; // 见状态设计
    int ss[N];
    
    void add(int &x, int ad) { x = (x + (ad % mod + mod)) % mod; }
    
    namespace SubTask {
    /** O(n^5): 照状态直接转移即可,能拿到50pts高分
     */
    void solve() {
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++)
                ss[j] = ss[j - 1] + (s[i][j] == 'G');
            for (int l = 1; l <= n; l++) {
                for (int r = l; r <= n; r++) { // 枚举这一个区间 [l, r]
                    if (ss[r] - ss[l - 1] != r - l + 1)
                        continue;  // 没有填满色
                    f[i][l][r][0][0] = 1; // 只有一层[l,r]的情况也要算在内
                    for (int x = 1; x <= n; x++)
                        for (int y = x; y <= n; y++) {  // 枚举上一个区间[x, y]
                            // 注意到变化趋势:0可以转到1,但是1不可以转到0,如果是l/r不变那么认为l/r的变化趋势也不变
    
                            // l, r都扩张,变化趋势不变
                            if (l <= x && y <= r)
                                add(f[i][l][r][0][0], f[i - 1][x][y][0][0]);
                            // l一定保证扩张,分讨r是收缩/不变的两种情况
                            if (l <= x && x <= r && r < y)
                                add(f[i][l][r][0][1], f[i - 1][x][y][0][0]);
                            if (l <= x && x <= r && r <= y)
                                add(f[i][l][r][0][1], f[i - 1][x][y][0][1]);
    
                            // r一定保证扩张,分讨l是收缩/不变的两种情况
                            if (x < l && l <= y && y <= r)
                                add(f[i][l][r][1][0], f[i - 1][x][y][0][0]);
                            if (x <= l && l <= y && y <= r)
                                add(f[i][l][r][1][0], f[i - 1][x][y][1][0]);
    
                            // 分讨l是收缩/不变及r是收缩/不变的四种情况
                            if (x <= l && y >= r)
                                add(f[i][l][r][1][1], f[i - 1][x][y][1][1]);
                            if (x < l && y >= r)
                                add(f[i][l][r][1][1], f[i - 1][x][y][0][1]);
                            if (x <= l && y > r)
                                add(f[i][l][r][1][1], f[i - 1][x][y][1][0]);
                            if (x < l && y > r)
                                add(f[i][l][r][1][1], f[i - 1][x][y][0][0]);
                        }
                    for (int c = 0; c < 4; c++)
                        add(ans, f[i][l][r][c >> 1][c & 1]);
                }
            }
        }
        printf("%lld\n", ans);
    }
    };
    
    namespace FullSolution {
    /** O(n^3): 对于O(n^5)的算法,考虑优化
     *  发现每次f[i][...]转移加上的就是一个f[i-1][...]的子矩阵和
     *  所以去掉枚举x和y的两层,改为加上上一维对应f的子矩阵和
     */
    int sf[N][N][2][2]; // f的前缀和 (滚掉[i]项)
    void init_sf(int i) { // 处理f[i][...]的前缀和数组sf
        memset(sf, 0, sizeof(sf));
        for (int c = 0; c < 4; c++)
            for (int l = 1; l <= n; l++)
                for (int r = 1; r <= n; r++) {
                    int p = c >> 1, q = c & 1;
                    add(sf[l][r][p][q], sf[l - 1][r][p][q] + sf[l][r - 1][p][q] - sf[l - 1][r - 1][p][q] + f[i][l][r][p][q]);
                }
    }
    int getsum(int l1, int r1, int l2, int r2, int p, int q) { // 求子矩阵和
        int res = 0;
        l1--, l2--;
        add(res, sf[r1][r2][p][q] - sf[l1][r2][p][q] - sf[r1][l2][p][q] + sf[l1][l2][p][q]);
        return res;
    }
    void solve() {
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++)
                ss[j] = ss[j - 1] + (s[i][j] == 'G');
            for (int l = 1; l <= n; l++) {
                for (int r = l; r <= n; r++) { // 枚举这一个区间[l, r]
                    if (ss[r] - ss[l - 1] != r - l + 1)
                        continue;  // 没有填满色
                    add(f[i][l][r][0][0], 1 + getsum(l, r, l, r, 0, 0));
                    add(f[i][l][r][0][1], getsum(l, r, r + 1, n, 0, 0)
                                        + getsum(l, r, r, n, 0, 1));
                    add(f[i][l][r][1][0], getsum(1, l - 1, l, r, 0, 0)
                                        + getsum(1, l, l, r, 1, 0));
                    add(f[i][l][r][1][1], getsum(1, l, r, n, 1, 1)
                                        + getsum(1, l - 1, r + 1, n, 0, 0)
                                        + getsum(1, l - 1, r, n, 0, 1)
                                        + getsum(1, l, r + 1, n, 1, 0));
                    // 每一个getsum()都对应之前的一个分讨
                    for (int c = 0; c < 4; c++)
                        add(ans, f[i][l][r][c >> 1][c & 1]);
                }
            }
            init_sf(i); // 更新一下f的前缀和
        }
        printf("%lld\n", ans);
    }
    };
    
    main() {
        scanf("%lld", &n);
        for (int i = 1; i <= n; i++)
            scanf("%s", s[i] + 1);
        // SubTask::solve(); // 这里是O(n^5)的做法
        FullSolution::solve(); // 这是正解
        return 0;
    }
    
    
    • 1

    信息

    ID
    6714
    时间
    1000ms
    内存
    256MiB
    难度
    6
    标签
    递交数
    0
    已通过
    0
    上传者