寒假训练补题记录


XCPC 2021 Happy Winter Vacation #1

E. 护花行动(差分、单调队列)

https://acm.ecnu.edu.cn/contest/361/problem/E/

补的时候感觉比想象中巧妙很多,特别是差分的部分,花了很长时间才理解(甚至补完了觉得也没完全理解)。

从朴素的角度看,如果我想知道能放下几个$x\times y$的矩阵,那我就枚举每个格子,把当前格子作为右下角,看看能不能放的下这个$x\times y$的矩阵。换言之,我只要知道“对于每个格子,我以这个格子为右下角,再枚举宽$a$,如果放一个宽为$a$的矩阵,它的高$b$最多为多少”,如果高为$b$,那我就令$ans[a][b]++$。求出$ans$数组之后,输入为$x\times y$时的答案就是$\sum\limits_{z>=y} ans[x][z]$,这个求和可以直接做一次前缀和来预处理出来,所以不算在复杂度里。

问题在于怎么求解$ans$数组,直接暴力是$O(n^3)$,但是可以发现,随着$a$的增大,$b$是单调减少的,也就是说很可能会存在很多段连续的$a$,它们对应的$b$大小是相同的,那么我就用差分数组+前缀和的方法,$O(1)$实现$ans[L][b]$到$ans[R][b]$这段区间的修改。把每个$b$记录在单调递增队列里,那么整体复杂度就变成了$O(n^2k)$,其中$k$是每个状态下单调队列的平均长度。

#include <bits/stdc++.h>

using namespace std;

const int N = 2e3 + 5;

struct S {
    int l, r, h;
    S(){}
    S(int L, int R, int H):l(L), r(R), h(H){}
}st[N];

int top;

int n, m, k, a[N][N], ans[N][N];

int main() {
    cin >> n >> m >> k;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            a[i][j] = 1;
        }
    }
    for (int i = 1; i <= k; i++) {
        int x, y; scanf("%d %d", &x, &y);
        a[x][y] = 0;
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (a[i][j]) a[i][j] += a[i - 1][j];
        }
    }
    for (int i = 1; i <= n; i++) {
        int tp = 0;
        for (int j = 1; j <= m; j++) {
            if (!tp || a[i][j] > st[tp].h) {
                st[++tp] = S(j, j, a[i][j]);
            } else {
                while(tp && a[i][j] <= st[tp].h) tp--;
                tp++;
                st[tp] = S(st[tp].l, j, a[i][j]);
            }
            for (int k1 = 1; k1 <= tp; k1++) {
                int R = j - st[k1].l + 1;
                int L = j - st[k1].r + 1;
                ans[L][st[k1].h]++;
                ans[R + 1][st[k1].h]--;
            }
        }
    }
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            ans[i][j] += ans[i - 1][j];
        }
    }
    for (int i = 1; i <= m; i++) {
        for (int j = n - 1; j >= 1; j--) {
            ans[i][j] += ans[i][j + 1];
        }
    }
    int T; cin >> T;
    for (int i = 1; i <= T; i++) {
        int x, y; scanf("%d %d", &x, &y);
        printf("%d\n", ans[y][x]);
    }
    return 0;
}

虽然直觉上这个复杂度已经比较优秀而且可以通过这道题了,但是好像还是有退化成$O(n^3)$的可能性。所以其实是有办法优化到$O(n^2)$的,但是计算方法会相对来说更抽象一点。

我们不能在每一列都对$ans$数组进行更新,而是只在某一段区间在单调队列中弹出时更新$ans$。但是更新的时候要注意不能出现重复的情况,比如说下面这个情况,第二列高度大于第三列,此时第二列出列时,只能计算黄色部分高度,因为低于黄色高度的部分会在之后绿色部分出列时计算,如果都计算的话就会出现重复,同样用差分方法计算的话就是在出列时

ans[j - st[tp - 1].r - 1][max(a[i][j], st[tp - 1].h) + 1]++;
ans[j - st[tp - 1].r - 1][st[tp].h + 1]--;

image-20210213164214409

同时这里$ans$又和第一种做法的$ans$不太一样,因为这里只记录了宽度最大的那个矩阵,所以要做两次前缀和。为啥是两次呢,比如有一个$6\times 6$的矩形,那就可以有两个$5\times 6$的,三个$4\times 6$的,四个$3\times 6$,五个$2\times 6$,六个$1\times 6$的。做两次前缀和恰好可以得到这个结果。

#include <bits/stdc++.h>

using namespace std;

const int N = 2e3 + 5;

struct S {
    int l, r, h;
    S(){}
    S(int L, int R, int H):l(L), r(R), h(H){}
}st[N];

int top;

int n, m, k, a[N][N], ans[N][N];

int main() {
    cin >> n >> m >> k;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            a[i][j] = 1;
        }
    }
    for (int i = 1; i <= k; i++) {
        int x, y; scanf("%d %d", &x, &y);
        a[x][y] = 0;
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (a[i][j]) a[i][j] += a[i - 1][j];
        }
    }
    for (int i = 1; i <= n; i++) {
        int tp = 0;
        for (int j = 1; j <= m + 1; j++) {
            if (!tp || a[i][j] > st[tp].h) {
                st[++tp] = S(j, j, a[i][j]);
            } else {
                while(tp && a[i][j] < st[tp].h) {
                    ans[j - st[tp - 1].r - 1][max(a[i][j], st[tp - 1].h) + 1]++;
                    ans[j - st[tp - 1].r - 1][st[tp].h + 1]--;
                    tp--;
                }
                while(tp && a[i][j] == st[tp].h) tp--;
                tp++;
                st[tp] = S(st[tp].l, j, a[i][j]);
            }
        }
    }
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            ans[i][j] += ans[i][j - 1];
        }
    }
    for (int j = 1; j <= n; j++) {
        for (int i = m - 1; i >= 1; i--) {
            ans[i][j] += ans[i + 1][j];
        }
        for (int i = m - 1; i >= 1; i--) {
            ans[i][j] += ans[i + 1][j];
        }
    }

    int T; cin >> T;
    for (int i = 1; i <= T; i++) {
        int x, y; scanf("%d %d", &x, &y);
        printf("%d\n", ans[y][x]);
    }
    return 0;
}

XCPC 2021 Happy Winter Vacation #3

F. 树上游戏

https://acm.ecnu.edu.cn/contest/363/problem/F/

第一步也是最重要的一步转化是

第二步把所有大于$x$的数视作1,所有小于$x$的数视作0,或者说用0表示较小的那部分数,用1表示较大的那部分数。

第三步用$F[i]$表示走到$i$这个点时选的数是0还是1,根据博弈论知识可以知道

接着开始往叶子结点里填0/1,用$f[x][y][0/1]$表示$x$这棵子树内,有$y$个0,且$F[x]=0/1$的方案数(这里的方案数指的是0/1填法的方案数)。这个dp是一个理论上非常基础但是确实做的比较少的树上背包问题。。类似南京的M题,可以$O(n^2)$解决。

https://ac.nowcoder.com/acm/contest/view-submission?submissionId=46492826&returnHomeType=1&uid=425400227

最后$f[1][i][1]$表示填了$i$个0且最后能选到一个大于等于$i$的数的方案数,0和1内部顺序无所谓所以直接乘上排列数$fac[i]$和$fac[m-i]$就有$P(ans >= i) = f[1][i][1]\times fac[i] \times fac[m-i]$

所以其实是用填0/1的方案数一一对应到了某一种填1~m的方案数。而转化成0/1则保证了状态数的有限。

实在是太妙了。

#include <bits/stdc++.h>
#define debug(x) cerr << #x << " = " << x << ' '
#define debugl(x) cerr << #x << " = " << x << '\n'

using namespace std;
typedef long long ll;

const int N = 5e3 + 5, MOD = 1e9 + 7;
inline int add(int a, int b){return a + b >= MOD ? a + b - MOD: a + b;}
inline int sub(int a, int b){return a - b < 0 ? a - b + MOD : a - b;}
inline int mul(int a, int b){return 1LL * a * b % MOD;}

vector<int> G[N];

int n, dp[N][N][2], tmp[N][2], ans, fac[N];

int dfs(int x, int ff, int turn) {
    int o = 0, p;
    for (auto y : G[x]) {
        if (y == ff) continue;
        p = dfs(y, x, turn ^ 1);
        if (!o) {
            o = p;
            memcpy(dp[x], dp[y], sizeof(dp[x]));
            continue;
        }
        memset(tmp, 0, sizeof(tmp));
        for (int i = 0; i <= o; i++) {
            for (int j = 0 ; j <= p; j++) {
                int tot = mul(add(dp[x][i][0], dp[x][i][1]), add(dp[y][j][0], dp[y][j][1]));
                int t = mul(dp[x][i][turn ^ 1], dp[y][j][turn ^ 1]);
                tmp[i + j][turn ^ 1] = add(tmp[i + j][turn ^ 1], t);
                tmp[i + j][turn] = add(tmp[i + j][turn], sub(tot, t));
            }
        }
        memcpy(dp[x], tmp, sizeof(tmp));
        o += p;
    }
    if (!o) {
        dp[x][0][0] = dp[x][1][1] = 1;
        o = 1;
    }
    return o;
}

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n - 1; i++) {
        int u, v; scanf("%d %d", &u, &v);
        G[u].push_back(v);
        G[v].push_back(u);
    }
    int m = dfs(1, -1, 1);
    fac[0] = 1;
    for (int i = 1; i <= n; i++) fac[i] = mul(fac[i - 1], i);
    for (int i = 1; i <= m; i++) {
        ans = add(ans, mul(dp[1][i][1], mul(fac[i], fac[m - i])));
    }
    cout << ans;
    return 0;
}