Atcoder ARC-104

D

可以发现,从整体上来看这个式子是不好计数的,可以考虑反过来将贡献拆到单个的每个数上:

\[\sum\limits_{i = 1} ^ n a_i \times (i - x) = 0 \]

于是每个数对该式子的贡献都将会是 \(a_i \times (i - x)\),此时枚举每个 \(x\) 我们可以轻松地得到一个 \(\mathcal{O(n ^ 4k)}\) 的暴力 \(dp\)

可以发现,在枚举每个 \(x\) 后我们都重新 \(dp\) 了一遍,可以从此处下手思考一下不同的 \(x\) 会对原式有何影响。

不难发现,实际上 \(x\) 规定了有 \(n - x\) 个数对答案的贡献系数分别为 \(1, 2, \cdots n - x\),有 \(x - 1\) 个数分别对答案的贡献系数为 \(-1, -2, \cdots -(x - 1)\) 还有一个数随便选任何数均可。

基于此可以发现,本质上我们就是要求:

\[\sum\limits_{i = 1} ^ {n - x} a_{i + x} \times i = \sum\limits_{i = 1} ^ {x - 1} a_{x - i} \times i \]

的方案数,注意到每个数 \(a\) 都是等价的,于是可以看作是 \(\sum\limits_{i = 1} ^ {n - x} a_{i} \times i = \sum\limits_{i = 1} ^ {x - 1} a_{i} \times i\) 的方案数。

那么此时每个数的贡献就与 \(x\) 无关了,首先可以 \(dp\) 预处理出 \(\sum\limits_{i = 1} ^ n a_i \times i = x\) 的方案数,然后合并即可。

最简单的 \(dp\) 可以考虑令 \(f_{i, j}\) 表示考虑完前 \(i\) 个数,当前贡献和为 \(j\) 的方案数,于是有转移:

\[f_{i, j} = \sum\limits_{x = 1} ^ k f_{i - 1, j - ix} \]

这个转移是一个经典的可以使用模 \(i\) 的同余性用前缀和优化的东西,可以做到 \(\mathcal{O(1)}\) 转移,于是总复杂度为 \(\mathcal{O(n ^ 3k)}\)

需要注意的是,我们忽略了 \(i = x\) 时贡献系数为 \(0\) 的情况,随便选择任何数均可,同时,因为 \(\sum a_i \ne 0\) 因此总方案还要减 \(1\)

#include <bits/stdc++.h>
using namespace std;
#define rep(i, l, r) for (int i = l; i <= r; ++i)
const int N = 1e2 + 5;
int n, k, P, ans[N], S[N][N * N], dp[N][N * N * N];
int Inc(int a, int b) { return (a += b) >= P ? a - P : a;}
int Dec(int a, int b) { return (a -= b) < 0 ? a + P : a;}
int Mul(int a, int b) { return 1ll * a * b % P;}  
int main() {
    cin >> n >> k >> P;
    dp[0][0] = 1;
    rep(i, 1, n) {
        rep(j, 0, i * i * k) {
            S[j % i][j / i] = dp[i - 1][j];
            if(j / i > 0) S[j % i][j / i] = Inc(S[j % i][j / i], S[j % i][j / i - 1]); 
            dp[i][j] = S[j % i][j / i];
            if(j / i > k) dp[i][j] = Dec(dp[i][j], S[j % i][j / i - k - 1]);
        }
    }
    rep(i, 1, n) rep(j, 0, n * n * k) ans[i] = Inc(ans[i], Mul(dp[n - i][j], dp[i - 1][j]));
    rep(i, 1, n) ans[i] = Dec(Mul(ans[i], k + 1), 1);
    rep(i, 1, n) printf("%d\n", ans[i]);
    return 0;
}

E

可以发现,本题每种情况是等概率的,因此可以将原问题转化为求所有情况的 \(\rm LIS\) 之和。

一个直接的想法就是暴力枚举每个数选择什么,但这样是没有前途的只能换一个方向试试。

注意到每种情况的 \(\rm LIS\) 很小,可以考虑通过一种方式固定 \(\rm LIS\) 然后统计这种情况下的方案数即可。

因为值域巨大,因此只能通过确定这些数的相对关系才能知道 \(\rm LIS\)

但麻烦的是会出现权值相等的情况,这样一来题目要求的 \(\rm LIS\) 就不好直接求了。

可以考虑调整一下相对关系以做到能求出 \(\rm LIS\)

如果不会出现权值相等的情况,那么排名序列的 \(\rm LIS\) 就是原序列的 \(\rm LIS\)

核心矛盾在于选择排名序列的 \(\rm LIS\) 时可能出现选择了同一权值在排名序列中单调递增的数。

那么就只需要做到选择了同一个权值就不能选择这个权值之后的数即可,不难发现我们只需要将原序列的位置按照从大到小做为第二关键字排序即可。

因此,此时对于任意一个排名顺序,若 \(p_i < p_{i - 1}\)\(a_{i - 1} \le a_i\) 否则 \(a_{i - 1} < a_i\)

为了方便,我们将不等关系均转化为 \(\le\),那么只需要将有 \(<\) 的一段后缀的上界减 \(1\) 即可。

于是现在的问题转化为每个位置能选择 \([0, A_i]\) 权值范围内的数,要求满足 \(a_{i - 1} \le a_i\) 的序列数量。

不难发现这个问题实质上是 [APIO2016]划艇 的弱化版,直接套用即可做到 \(\mathcal{O(n ^ 3)}\)

但这个问题权值的范围不存在下界,因此还有一个更加优秀的做法。

首先可以将上界 \(A_i\) 后缀取 \(\min\) 将其转变为不降的序列。

因为 \(a_{i - 1} \le a_i\) 可以考虑 \(a\) 的差分数组 \(d\),那么本质上需要求的就是 \(x_1 + x_2 + \cdots x_n \ge 0, \forall i, x_i \ge 0, \sum\limits_{j = 1} ^ i x_j \le A_i\) 的方案数。

可以考虑使用 \(dp\) 统计答案,令 \(f_i\) 表示考虑完前 \(i\) 个元素满足条件的方案数。

转移时使用容斥转移,为了不重复计数,转移时考虑枚举第一个不合法的位置 \(j\),钦定到其至少前缀为 \(A_j + 1\) 即可:

\[f_i = \dbinom{i + A_i}{i} - \sum\limits_{j = 1} ^ {i - 1} f_{j - 1} \times \dbinom{A_i - A_j + i - j}{i - j - 1} \]

组合数使用下降幂的方式计算即可,若预处理组合数可以做到 \(\mathcal{O(n ^ 2)}\),因此复杂度 \(\mathcal{O(n!n ^ 3) / O(n!n ^ 2 + n ^ 4)}\)

#include <bits/stdc++.h>
using namespace std;
#define rep(i, l, r) for (int i = l; i <= r; ++i)
#define dep(i, l, r) for (int i = r; i >= l; --i)
const int N = 10 + 5;
const int Mod = 1e9 + 7;
bool book[N];
int n, S, ans, a[N], b[N], p[N], fac[N], inv[N];
int read() {
    char c; int x = 0, f = 1;
    c = getchar();
    while (c > '9' || c < '0') { if(c == '-') f = -1; c = getchar();}
    while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
    return x * f;
}
int Inc(int a, int b) { return (a += b) >= Mod ? a - Mod : a;}
int Dec(int a, int b) { return (a -= b) < 0 ? a + Mod : a;}
int Mul(int a, int b) { return 1ll * a * b % Mod;}
int fpow(int a, int b) { int ans = 1; for (; b; a = Mul(a, a), b >>= 1) if(b & 1) ans = Mul(ans, a); return ans;}
int C(int n, int m) {
    if(n < m) return 0;
    int ans = 1;
    rep(i, 1, m) ans = Mul(ans, n - i + 1);
    return Mul(ans, inv[m]);
}
int Lis() {
    int ans = 0, dp[N];
    rep(i, 1, n) {
        dp[i] = 1;
        rep(j, 1, i - 1) if(p[j] < p[i]) dp[i] = max(dp[i], dp[j] + 1);
        ans = max(ans, dp[i]);
    }
    return ans;
}
int solve() {
    rep(i, 1, n) a[i] = b[p[i]];
    rep(i, 1, n) if(p[i] > p[i - 1] && i != 1) rep(j, i, n) --a[j];
    dep(i, 1, n - 1) a[i] = min(a[i], a[i + 1]);
    if(a[1] < 0) return 0;
    int dp[N]; dp[0] = 1;
    rep(i, 1, n) {
        dp[i] = C(i + a[i], i);
        rep(j, 1, i - 1) dp[i] = Dec(dp[i], Mul(dp[j - 1], C(a[i] - a[j] + i - j, i - j + 1)));
    }
    return Mul(dp[n], Lis());
}
void dfs(int x) {
    if(x > n) { ans = Inc(ans, solve()); return ;}
    rep(i, 1, n) if(!book[i]) book[i] = 1, p[x] = i, dfs(x + 1), book[i] = 0;
}
int main() {
    n = read(), S = 1;
    rep(i, 1, n) b[i] = read() - 1, S = Mul(S, b[i] + 1);
    fac[0] = inv[0] = 1;
    rep(i, 1, n) fac[i] = Mul(fac[i - 1], i), inv[i] = fpow(fac[i], Mod - 2);
    dfs(1);
    printf("%d", Mul(ans, fpow(S, Mod - 2)));
    return 0;
}

F

可以发现,这个构成 \(P\) 序列的过程非常类似于单调栈。

即从左至右维护一个由底而上严格单调递减的单调栈,那么每个位置所填的数即为其在单调栈内下方的第一个元素的位置。

但是这个过程是有局限性的,我们很难直接看出 \(i\) 踢出的元素为哪些,因此可以考虑将抽象问题具体化。

可以发现 \((i, P_i)\) 构成了一个二元组,最简单的想法就是 \(i \rightarrow P_i\) 连一条边。

观察此时的图可以发现这恰好构成了一棵树,且以 \(i\) 为根的一颗子树必定是原序列上 \([i, i + sz_i)\) 的一个区间,\(i\) 一定为整颗子树的最大值;若将编号小的节点放在左侧,则 \(i\) 必定踢出其左侧兄弟自上而下最靠右的一条链,又因为父亲节点必然大于儿子节点,因此我们只需保证 \(i\) 大与其左侧兄弟节点即可。

于是问题可以转化为:为求有多少棵大小为 \(n + 1\) 的树,满足 \(0\) 为根节点,且以 \(i\) 为根的一颗子树是原序列上 \([i, i + sz_i)\) 的一个区间,\(i\) 严格大于子树内所有点且不小于其左侧兄弟节点,满足 \(i\) 的权值不大于 \(A_i\) 的方案数。

因此我们肯定需要贪心地让每个点权值尽可能小才能满足权值的限制。

基于此想法,我们呢可以考虑这样一个 \(dp\),令 \(f_{l, r, x}\) 为以 \(l\) 为根的子树内对应原序列 \([l, r]\) 这个区间,点 \(l\) 填的最小权值为 \(x\) 的方案数。

那么转移只需要考虑往 \(l\) 下方插入一颗子树即可:

\[f_{l, r, x} = \sum\limits_{i = l + 1} ^ r \sum\limits_{j = 1} ^ {x - 1} f_{l, i - 1, x} \times f_{i, r, j} + \sum\limits_{i = l + 1} ^ r \sum\limits_{j = 1} ^ {x - 1} f_{l, i - 1, j} \times f_{i, r, x - 1} \]

后面的和式枚举到 \(x - 1\) 是为了避免记重,转移使用前缀和优化即可做到 \(\mathcal{O(n ^ 4)}\)

#include <bits/stdc++.h>
using namespace std;
#define rep(i, l, r) for (int i = l; i <= r; ++i)
const int N = 1e2 + 5;
const int Mod = 1e9 + 7;
int n, a[N], S[N][N][N], dp[N][N][N];
int read() {
    char c; int x = 0, f = 1;
    c = getchar();
    while (c > '9' || c < '0') { if(c == '-') f = -1; c = getchar();}
    while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
    return x * f;
}
int Inc(int a, int b) { return (a += b) >= Mod ? a - Mod : a;}
int Mul(int a, int b) { return 1ll * a * b % Mod;}
int main() {
    n = read(), a[1] = ++n;
    rep(i, 2, n) a[i] = read();
    rep(i, 1, n) {
        dp[i][i][1] = 1;
        rep(j, 1, n) S[i][i][j] = 1;
    }
    rep(t, 2, n) rep(l, 1, n) {
        int r = l + t - 1; if(r > n) break;
        rep(x, 1, min(a[l], n)) {
            rep(i, l + 1, r) if(a[i] >= x - 1) dp[l][r][x] = Inc(dp[l][r][x], Mul(dp[l][i - 1][x], S[i][r][x - 1])); 
            rep(i, l + 1, r) dp[l][r][x] = Inc(dp[l][r][x], Mul(dp[i][r][x - 1], S[l][i - 1][x - 1]));
        }
        rep(x, 1, n) S[l][r][x] = Inc(S[l][r][x - 1], dp[l][r][x]);
    }
    printf("%d", S[1][n][n]);
    return 0;
}
posted @ 2020-11-07 09:26  Achtoria  阅读(126)  评论(0编辑  收藏  举报