计数中的统计方法
计数中的统计方法
拆分法
CF660E Different Subsets For All Tuples
给定 \(n, m\) ,对于所有长度为 \(n\) ,值域为 \([1, m] \cap \mathbb{Z}\) 的序列,求每个序列中本质不同子序列数量(包括空序列)的和 \(\bmod (10^9 + 7)\) 。
\(n, m \le 10^6\)
考虑统计每一个子序列的出现次数和,为了避免算重,只统计其第一次出现的贡献。
对于长度为 \(i\) 、在 \(j\) 结束的子序列,则前 \(j\) 个位置中除了 \(i\) 个位置外的位置都只有 \(m - 1\) 种填法(不能与第一个右边的子序列内的数相同),因此答案即为:
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e6 + 7;
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int mi(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % Mod)
if (b & 1)
res = 1ll * res * a % Mod;
return res;
}
signed main() {
scanf("%d%d", &n, &m);
int ans = mi(m, n);
for (int i = 0; i < n; ++i)
ans = add(ans, 1ll * mi(m, n - i) * mi(m * 2 - 1, i) % Mod);
printf("%d", ans);
return 0;
}
[AGC023E] Inversions
给定序列 \(a_{1 \sim n}\) ,对于所有 \(\forall i, p_i \le a_i\) 的排列 \(p_{1 \sim n}\) ,求逆序对数量的和 \(\bmod (10^9 + 7)\) 。
\(n \times 2 \times 10^5\)
设 \(b_{1 \sim n}\) 表示 \(a\) 升序排序后的结果,容易求出排列的数量为 \(S = \prod_{i = 1}^n (b_i - i + 1)\) 。
记 \(r_i\) 表示 \(a_i\) 的排名,考虑枚举一对逆序对 \((i, j)\) 计算出现次数,其中 \(i < j\) ,因此 \(p_i > p_j\) 。
先考虑 \(r_i < r_j\) 的情况,此时 \(r_i < r_k < r_j\) 的 \(k\) 的可选集合都会少 \(1\) ,答案即为:
再考虑 \(r_i > r_j\) 的情况,此时 \(p_j\) 不能任意取。考虑补集转化,用总方案数减去 \(p_i < p_j\) 的方案数,答案即为:
下面考虑维护这个式子,按排名从小到大加位置,用树状数组维护当前位置左右已被加入的位置数量,线段树维护贡献(需要支持全局乘、单点加、区间查询),时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 2e5 + 7;
int a[N], id[N], pre[N], suf[N];
int n;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int C2(int n) {
return (1ll * n * (n - 1) / 2) % Mod;
}
namespace BIT {
int c[N];
inline int lowbit(int x) {
return x & (~x + 1);
}
inline void update(int x, int k) {
for (; x; x -= lowbit(x))
c[x] += k;
}
inline int query(int x) {
int res = 0;
for (; x <= n; x += lowbit(x))
res += c[x];
return res;
}
} // namespace BIT
namespace SMT {
int s[N << 2], tag[N << 2];
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
inline void pushup(int x) {
s[x] = add(s[ls(x)], s[rs(x)]);
}
inline void spread(int x, int k) {
s[x] = 1ll * s[x] * k % Mod, tag[x] = 1ll * tag[x] * k % Mod;
}
inline void pushdown(int x) {
if (tag[x] != 1)
spread(ls(x), tag[x]), spread(rs(x), tag[x]), tag[x] = 1;
}
void build(int x, int l, int r) {
tag[x] = 1;
if (l == r)
return;
int mid = (l + r) >> 1;
build(ls(x), l, mid), build(rs(x), mid + 1, r);
}
void update(int x, int nl, int nr, int pos, int k) {
if (nl == nr) {
s[x] = add(s[x], k);
return;
}
pushdown(x);
int mid = (nl + nr) >> 1;
if (pos <= mid)
update(ls(x), nl, mid, pos, k);
else
update(rs(x), mid + 1, nr, pos, k);
pushup(x);
}
int query(int x, int nl, int nr, int l, int r) {
if (l <= nl && nr <= r)
return s[x];
pushdown(x);
int mid = (nl + nr) >> 1;
if (r <= mid)
return query(ls(x), nl, mid, l, r);
else if (l > mid)
return query(rs(x), mid + 1, nr, l, r);
else
return add(query(ls(x), nl, mid, l, r), query(rs(x), mid + 1, nr, l, r));
}
} // namespace SMT
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
iota(id + 1, id + 1 + n, 1);
sort(id + 1, id + 1 + n, [](const int &x, const int &y) {
return a[x] < a[y];
});
pre[0] = 1;
for (int i = 1; i <= n; ++i)
pre[i] = 1ll * pre[i - 1] * (a[id[i]] - i + 1) % Mod;
suf[n + 1] = 1;
for (int i = n; i; --i)
suf[i] = 1ll * suf[i + 1] * (a[id[i]] - i + 1) % Mod;
SMT::build(1, 1, n);
int ans = 0;
for (int i = 1; i <= n; ++i) {
ans = add(ans, 1ll * SMT::query(1, 1, n, 1, id[i]) * suf[i + 1] % Mod);
ans = add(ans, 1ll * BIT::query(id[i]) * pre[n] % Mod);
ans = dec(ans, 1ll * SMT::query(1, 1, n, id[i], n) * suf[i + 1] % Mod);
BIT::update(id[i], 1), SMT::spread(1, a[id[i]] - i);
SMT::update(1, 1, n, id[i], 1ll * pre[i - 1] * C2(a[id[i]] - i + 1) % Mod);
}
printf("%d", ans);
return 0;
}
CF2064F We Be Summing
称一个序列 \(b_{1 \sim m}\) 是好的当且仅当存在一个 \(i \in [1, m)\) 满足 \((\min_{j = 1}^i b_j) + (\max_{j = i + 1}^m b_j) = k\) ,其中 \(k\) 为常数。
给出序列 \(a_{1 \sim n}\) 和常数 \(k\) ,求有多少好的子区间。
\(n \le 2 \times 10^5\) ,\(n < k < 2n\) ,\(a_i \in [1, n]\)
考虑枚举 \(x = \min_{j = 1}^i b_j, y = \max_{j = i + 1}^m b_j\) ,其中 \(x + y = k\) ,然后单独拿出 \(a = x\) 和 \(a = y\) 的位置做统计。
如果直接枚举值会算重,这是因为前后缀最值可能在一段区间内都是相等的。考虑枚举最值的位置 \(a_i = x, a_j = y\) ,记 \([l_i, r_i]\) 和 \([l_j, r_j]\) 表示 \(i\) 和 \(j\) 产生贡献的区间,其中 \([l_i, r_i]\) 为最大的包含 \(i\) 的区间满足 \(a_i\) 为最小值,\([l_j, r_j]\) 为最大的包含 \(j\) 的区间满足 \(a_j\) 为最大值。则只要满足 \(i < j \and l_j - 1 \le r_i\) ,\(i, j\) 就能产生贡献(在 \((l_j - 1, l_j)\) 处断开),即 \(l \in [l_i, i], r \in [j, r_j]\) 的区间均合法。
设 \(L_{i, 0/1}, R_{i, 0/1}\) 分别表示极长包含 \(i\) 的开区间满足 \(a_i\) 为最小值/最大值的左右端点,则条件转化为 \(i < j \and L_{j, 1} < R_{i, 0}\) 。
- 对于 \(i < j\) 的限制,这可以用双指针动态维护。
- 对于 \(L_{j, 1} < R_{i, 0}\) 的限制,其形如二维偏序,不难用树状数组处理。
为了去重方便,在值相同时可以钦定一个大小顺序,显然这不影响答案。
时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7;
vector<int> p[N];
int a[N], sta[N], L[N][2], R[N][2];
int n, k;
namespace BIT {
int c[N];
inline void clear(int n) {
memset(c + 1, 0, sizeof(int) * n);
}
inline void update(int x, int k) {
for (; x > 0; x -= x & -x)
c[x] += k;
}
inline int query(int x) {
int res = 0;
for (x = max(x, 1); x <= n; x += x & -x)
res += c[x];
return res;
}
} // namespace BIT
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i)
p[i].clear();
for (int i = 1; i <= n; ++i)
scanf("%d", a + i), p[a[i]].emplace_back(i);
for (int i = 1, top = 0; i <= n; ++i) {
while (top && a[sta[top]] > a[i])
--top;
L[i][0] = top ? sta[top] + 1 : 1, sta[++top] = i;
}
for (int i = 1, top = 0; i <= n; ++i) {
while (top && a[sta[top]] <= a[i])
--top;
L[i][1] = top ? sta[top] + 1 : 1, sta[++top] = i;
}
for (int i = n, top = 0; i; --i) {
while (top && a[sta[top]] >= a[i])
--top;
R[i][0] = top ? sta[top] - 1 : n, sta[++top] = i;
}
for (int i = n, top = 0; i; --i) {
while (top && a[sta[top]] < a[i])
--top;
R[i][1] = top ? sta[top] - 1 : n, sta[++top] = i;
}
BIT::clear(n);
ll ans = 0;
for (int i = max(1, k - n); i <= min(n, k - 1); ++i) {
int j = k - i, k = 0;
if (p[i].empty() || p[j].empty())
continue;
for (int l = 0; l < p[i].size(); ++l) {
for (; k < p[j].size() && p[j][k] < p[i][l]; ++k)
BIT::update(R[p[j][k]][0], p[j][k] - L[p[j][k]][0] + 1);
ans += (R[p[i][l]][1] - p[i][l] + 1) * BIT::query(L[p[i][l]][1] - 1);
}
for (--k; ~k; --k)
BIT::update(R[p[j][k]][0], -(p[j][k] - L[p[j][k]][0] + 1));
}
printf("%lld\n", ans);
}
return 0;
}
[AGC008F] Black Radius
有一棵 \(n\) 个点的树,一开始每个节点均为白色,其中一些点是关键点。
需要选择一个关键点 \(x\) ,然后选择一个自然数 \(d\) ,将所有与 \(x\) 距离不超过 \(d\) 的点都染成黑色。
求染色一次后可能的状态数。
\(n \le 2 \times 10^5\)
先考虑所有点都是关键点的情况,设 \(f(x, d) = \{ y \mid \mathrm{dist}(x, y) \le d \}\) ,考虑每次在 \(d\) 最小的点上统计该点集的答案(不统计整棵树的情况,最后算整棵树的贡献),即 \(f(x, d)\) 需要满足:
- \(f(x, d)\) 不能覆盖整棵树。
- 设 \(mx(x)\) 表示距离 \(x\) 的最远点 \(y\) 距离 \(x\) 的距离,设 \(mx(x)\) 表示 \(x\) 离最远点的距离,此时限制条件即为 \(f(x, d) < mx(x)\) 。
- 对于 \(x\) 的邻居 \(y\) ,均不存在 \(f(x, d) = f(y, d - 1)\) 。
- 考虑以 \(x\) 为根,则 \(y\) 子树外与 \(x\) 距离 \(\in [d - 1, d]\) 的点不在 \(f(y, d - 1)\) 中而在 \(f(x, d)\) 中。设 \(se(x)\) 表示删去 \(mx(x)\) 点所在子树后距离 \(x\) 最远点的距离,则限制条件可以表示为 \(se(x) > d - 2\) 。
因此一个点 \(x\) 的贡献即为 \(\min(se(x) + 2, mx(x))\) 。
再考虑原问题。对于一个非关键点 \(u\) ,考虑将 \(f(u, d_u)\) 转到 \(f(v, d_v)\) 上,其中 \(v\) 是一个关键点,且 \(f(u, d_u) = f(v, d_v)\) 。则要求 \(v\) 对应子树全部属于 \(f(u, d)\) ,求出这些 \(v\) 对应子树中深度最小的子树记为 \(low_u\) ,则 \(d_u \ge low_u\) 。不难用换根 DP 做到线性。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int siz[N], len[N];
char str[N];
ll ans = 1;
int n;
void dfs1(int u, int f) {
siz[u] = str[u] & 15, len[u] = 0;
for (int v : G.e[u])
if (v != f)
dfs1(v, u), siz[u] += siz[v], len[u] = max(len[u], len[v] + 1);
}
void dfs2(int u, int f, int out) {
int fir = out, sec = 0, low = (str[u] & 15 ? 0 : (siz[u] < siz[1] ? out : inf));
for (int v : G.e[u]) {
if (v == f)
continue;
if (len[v] + 1 > fir)
sec = fir, fir = len[v] + 1;
else if (len[v] + 1 > sec)
sec = len[v] + 1;
if (siz[v])
low = min(low, len[v] + 1);
}
ans += max(0, min(sec + 2, fir) - low);
for (int v : G.e[u])
if (v != f)
dfs2(v, u, len[v] + 1 == fir ? sec + 1 : fir + 1);
}
signed main() {
scanf("%d", &n);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
scanf("%s", str + 1);
dfs1(1, 0), dfs2(1, 0, 0);
printf("%lld", ans);
return 0;
}
[ARC176D] Swap Permutation
给定排列 \(p_{1 \sim n}\) ,求随机执行 \(m\) 次交换操作后 \(\sum_{i = 1}^{n - 1} |p_i - p_{i + 1}|\) 的期望乘上 \((\frac{n (n - 1)}{2})^m\) 的值。
\(n, m \le 2 \times 10^5\)
考虑刻画权值的计算方式,把贡献 \(|p_i - p_{i + 1}|\) 的贡献拆到 \(|p_i - p_{i + 1}|\) 个 \(1\) 上。枚举 \(x = 1, 2, \cdots, n\) ,记 \(q_i = [p_i \ge x]\) ,则权值可以表示为 \(\sum_{x = 1}^n \sum_{i = 1}^{n - 1} [q_i \ne q_{i + 1}]\) 。
考虑对于一对 \((q_i, q_{i + 1})\) ,计算其在所有 01 序列中的贡献和。设 \(f_{i, s}\) 表示操作 \(i\) 次后当前状态为 \(s\) 的方案数,其中 \(s\) 记录的是 \(q_i\) 和 \(q_{i + 1}\) 的 01 值,\(s\) 只有 \((0, 0), (1, 1), (0, 1) / (1, 0)\) 三种情况。
发现这个过程可以矩阵快速幂,而对于一对 \((q_i, q_{i + 1})\) ,等价的 \(x\) 不超过三种,因此可以做到 \(O(n \log m)\) 。
另一种方法更加暴力,对于一对 \((p_i, p_{i + 1})\) ,由于其他的值最终到达这里的概率都是相等的,因此可以不做区分,记 \(A = p_i\) 、\(B = p_{i + 1}\)、\(C\) 为其他数,则一共只有 \((A, B), (B, A), (A, C), (C, A), (C, B), (B, C), (C, C)\) 七种情况,暴力矩阵快速幂转移即可。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 2e5 + 7;
int a[N], c[N][3];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
struct Matrix {
int a[3][3];
inline Matrix() {
memset(a, 0, sizeof(a));
}
inline friend Matrix operator * (Matrix a, Matrix b) {
Matrix c;
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
for (int k = 0; k < 3; ++k)
c.a[i][k] = add(c.a[i][k], 1ll * a.a[i][j] * b.a[j][k] % Mod);
return c;
}
inline friend Matrix operator ^ (Matrix a, int b) {
Matrix res;
for (int i = 0; i < 3; ++i)
res.a[i][i] = 1;
for (; b; b >>= 1, a = a * a)
if (b & 1)
res = res * a;
return res;
}
} base;
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
for (int i = 1; i < n; ++i) {
++c[max(a[i], a[i + 1]) + 1][0];
++c[min(a[i], a[i + 1]) + 1][1], --c[max(a[i], a[i + 1]) + 1][1];
++c[1][2], --c[min(a[i], a[i + 1]) + 1][2];
}
int ans = 0, all = 1ll * (n - 2) * (n - 3) / 2 % Mod;
for (int i = 1; i <= n; ++i) {
int c0 = i - 1, c1 = n - i + 1;
base.a[0][0] = add(all, 2 * (c0 - 2) + 1), base.a[0][1] = 2 * c1, base.a[0][2] = 0;
base.a[1][0] = c0 - 1, base.a[1][1] = add(all, n - 1), base.a[1][2] = c1 - 1;
base.a[2][0] = 0, base.a[2][1] = 2 * c0, base.a[2][2] = add(all, 2 * (c1 - 2) + 1);
base = (base ^ m);
for (int j = 0; j < 3; ++j)
ans = add(ans, 1ll * (c[i][j] += c[i - 1][j]) * base.a[j][1] % Mod);
}
printf("%d", ans);
return 0;
}
P12558 [UOI 2024] Heroes and Monsters
给定 \(a_{1 \sim n}, b_{1 \sim n}\) ,定义 \(f_k\) 表示满足如下条件的集合 \(S\) 的数量:
- \(S \subseteq \{ 1, 2, \cdots, k \}\) ,\(|S| = k\) 。
- 存在 \(1 \sim n\) 的排列 \(p\) ,满足 \(\forall i \in S, a_i > b_{p_i}\) ,\(\forall i \notin S, a_i < b_{p_i}\) 。
\(q\) 次询问 \(\sum_{i = l}^r f_i\) 。
\(n \le 5000\) ,\(a_{1 \sim n}, b_{1 \sim n}\) 两两不同
先考虑如何判断 \(S\) 的可行性,考虑贪心,记 \(T = \{ 1, 2, \cdots, k \} \setminus S\) ,将 \(a, b\) 升序排序后需要满足 \(a_{S_i} > b_i\) 且 \(a_{T_i} < b_{|S| + i}\) 。
枚举 \(|S|\) ,设 \(f_{i, j}\) 表示考虑前 \(i\) 个 \(a\) 、选了 \(j\) 个的方案数,时间复杂度 \(O(n^3)\) 。
考虑优化,按 \(b_{|S|}\) 将 \(a\) 分成两部分,则前一部分显然可以放入 \(T\) ,后一部分显然可以放入 \(S\) 。因此只需考虑前一部分是否放入 \(S\) ,后一部分是否放入 \(T\) 即可。
这样限制就拆分开来了,只要对前后缀分别 DP 一次即可,每次统计答案就做一次卷积,时间复杂度 \(O(n^2)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 5e3 + 7;
int a[N], b[N], f[N][N], g[N][N], ans[N];
int n, q;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
for (int i = 1; i <= n; ++i)
scanf("%d", b + i);
sort(a + 1, a + n + 1), sort(b + 1, b + n + 1);
f[0][0] = 1;
for (int i = 1; i <= n; ++i)
for (int j = 0; j <= i; ++j)
f[i][j] = add(f[i - 1][j], j && a[i] > b[j] ? f[i - 1][j - 1] : 0);
g[n + 1][0] = 1;
for (int i = n; i; --i)
for (int j = 0; j <= n - i + 1; ++j)
g[i][j] = add(j ? g[i + 1][j - 1] : 0, a[i] < b[i + j] ? g[i + 1][j] : 0);
for (int i = 0, j = 0; i <= n; ++i) {
while (j < n && a[j + 1] < b[i])
++j;
for (int k = 0; k <= i; ++k)
ans[i] = add(ans[i], 1ll * f[j][k] * g[j + 1][i - k] % Mod);
}
for (int i = 1; i <= n; ++i)
ans[i] = add(ans[i], ans[i - 1]);
scanf("%d", &q);
while (q--) {
int l, r;
scanf("%d%d", &l, &r);
printf("%d\n", dec(ans[r], l ? ans[l - 1] : 0));
}
return 0;
}
贡献延迟计算
CF1608F MEX counting
给定 \(n, k, b_{1 \sim n}\) ,求满足以下条件的序列 \(a\) 的数量:
- 长度为 \(n\) ,\(a_i \in [0, n]\) 。
- \(|\mathrm{mex}(a_1, a_2, \cdots, a_i) - b_i| \le k\) 。
\(n \le 2000\) ,\(k \le 50\)
朴素的 DP 会想到记录当前选了哪些数,但是这样完全无法存储。
考虑只记录选的数字种数,不记录选的具体数字,具体的数字后面再计算。
设 \(f_{i, j, k}\) 表示考虑了前 \(i\) 个数、\(\mathrm{mex} = j\) 、选了 \(k\) 种数的方案数,考虑转移:
- \(a_{i + 1} \ne j\) :
- \(f_{i, j, k} \gets f_{i - 1, j, k} \times k\) :选之前选过的。
- \(f_{i, j, k} \gets f_{i - 1, j, k - 1}\) :选之前没选过的。
- \(a_{i + 1} = j\) :\(f_{i, j, k} \gets \sum_{l < j} f_{i - 1, l, k - 1} \times \binom{k - 1 - l}{j - l - 1} \times (j - l - 1)!\) 。
拆开组合数即可前缀和优化做到 \(O(n^2 k)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 2e3 + 7;
int f[2][N][N], s[2][N][N];
int a[N], L[N], R[N], fac[N], inv[N], invfac[N];
int n, k;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline void prework(int n) {
fac[0] = fac[1] = 1;
inv[0] = inv[1] = 1;
invfac[0] = invfac[1] = 1;
for (int i = 2; i <= n; ++i) {
fac[i] = 1ll * fac[i - 1] * i % Mod;
inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
}
}
signed main() {
scanf("%d%d", &n, &k), prework(n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i), L[i] = max(0, a[i] - k), R[i] = min(i, a[i] + k);
f[0][0][0] = s[0][0][0] = 1;
for (int i = 1; i <= n; ++i) {
for (int j = L[i]; j <= R[i]; ++j)
for (int k = j; k <= i; ++k) {
f[i & 1][j][k] = add(1ll * f[~i & 1][j][k] * k % Mod, k ? f[~i & 1][j][k - 1] : 0);
if (k && j)
f[i & 1][j][k] = add(f[i & 1][j][k],
1ll * s[~i & 1][min(j - 1, R[i - 1])][k - 1] * invfac[k - j] % Mod);
s[i & 1][j][k] = add(j ? s[i & 1][j - 1][k] : 0, 1ll * f[i & 1][j][k] * fac[k - j] % Mod);
}
for (int j = L[i - 1]; j <= R[i - 1]; ++j) {
memset(f[~i & 1][j] + j, 0, sizeof(int) * (i - j));
memset(s[~i & 1][j] + j, 0, sizeof(int) * (i - j));
}
}
int ans = 0;
for (int i = L[n]; i <= R[n]; ++i)
for (int j = i; j <= n; ++j)
ans = add(ans, 1ll * f[n & 1][i][j] * fac[n - i] % Mod * invfac[n - j] % Mod);
printf("%d", ans);
return 0;
}
划分阶段
P5369 [PKUSC2018] 最大前缀和
给定序列 \(a_{1 \sim n}\) ,求打乱排列后 \(n!\) 种可能情况中最大前缀和的和。
\(n \le 20\)
考虑在最后一个最大前缀和处拆贡献,设:
- \(sum_S\) :集合 \(S\) 的元素和。
- \(f_S\) :集合 \(S\) 组成的排列中最大前缀和为 \(sum_s\) 的方案数。
- \(g_S\) :集合 \(S\) 组成的排列中最大前缀和 \(< 0\) 的方案数。
则答案为 \(\sum_S sum_S \times f_S \times g_{U \setminus S}\) 。
先考虑 \(g\) 的转移,考虑从前往后加数,有 \(g_S = \begin{cases} 0 & sum_S \ge 0 \\ \sum_{x \in S} g_{S \setminus \{ x \}} & sum_S < 0 \end{cases}\) 。
再考虑 \(f\) 的转移,考虑从后往前加数。若 \(sum_S > 0\) ,则 \(f_{S \cup \{ x \}} \gets f_S\) ,否则无法产生贡献。
时间复杂度 \(O(2^n n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 20;
int s[1 << N], f[1 << N], g[1 << N];
int n;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
signed main() {
scanf("%d", &n);
for (int i = 0; i < n; ++i)
scanf("%d", s + (1 << i));
for (int i = 1; i < (1 << n); ++i)
s[i] = s[i & -i] + s[(i - 1) & i];
g[0] = 1;
for (int i = 0; i < (1 << n); ++i)
if (s[i] < 0) {
for (int j = 0; j < n; ++j)
if (i >> j & 1)
g[i] = add(g[i], g[i ^ (1 << j)]);
}
for (int i = 0; i < n; ++i)
f[1 << i] = 1;
for (int i = 0; i < (1 << n); ++i)
if (s[i] >= 0) {
for (int j = 0; j < n; ++j)
if (~i >> j & 1)
f[i | (1 << j)] = add(f[i | (1 << j)], f[i]);
}
int ans = 0;
for (int i = 1; i < (1 << n); ++i)
ans = add(ans, 1ll * (s[i] + Mod) * f[i] % Mod * g[((1 << n) - 1) ^ i] % Mod);
printf("%d", ans);
return 0;
}
[ARC100F] Colorful Sequences
给定 \(n\) 与长度为 \(m\) 、元素为 \(1 \sim k\) 中的整数的序列 \(a_{1 \sim m}\) ,求所有满足:
- 长度为 \(n\) 。
- 元素为 \(1 \sim k\) 中的整数。
- 存在一个长度为 \(k\) 的子区间满足 \(1 \sim k\) 出现恰好一次。
的序列中 \(a_{1 \sim m}\) 的出现次数之和 \(\bmod (10^9 + 7)\) 。
\(n \le 25000\) ,\(k \le 400\)
先考虑 \(a_{1 \sim m}\) 满足第三个条件的情况,此时答案即为 \(k^{n - m} (n - m + 1)\) 。
否则满足第三个条件的子区间一定不会跨过 \(a_{1 \sim m}\) ,考虑继续分类讨论。
若 \(a_{1 \sim m}\) 中存在相同元素,考虑统计不满足第三个条件的序列中 \(a_{1 \sim m}\) 的出现次数和,不难发现两边是独立的,且这只与 \(a_{1 \sim m}\) 左右极长不同色连续段的长度 \(l, r\) 有关。
设 \(f_{i, j}\) 表示长度为 \(i\) 的序列、末尾极长不同色连续段的长度为 \(j\) 的贡献和,则:
- 插入一个同色元素:\(f_{i, j} \to f_{i + 1, 1 \sim j}\) 。
- 插入一个不同色元素:\(f_{i, j} \times (k - j + 1) \to f_{i + 1, j + 1}\) 。
从 \(f_{0, l}\) 扩展左边,\(f_{0, r}\) 扩展右边,最后合并即可,注意转移过程中第二维 \(< k\) 。
若 \(a_{1 \sim m}\) 中不存在相同元素,不难发现此时对于任意 \(a_{1 \sim m}\) 答案都是一样的,考虑对所有长度为 \(m\) 的序列 \(a\) 计数,最后将方案数除以 \(\binom{k}{m} \times m!\) 即可。
在前面 DP 的基础上再设 \(g_{i, j}\) 表示长度为 \(m\) 的不同色区间的数量,转移一样,只要在 \(j \ge m\) 时令 \(g_{i, j} \gets f_{i, j}\) 即可。
使用前缀和优化 DP,时间复杂度 \(O(nk)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 2.5e4 + 7, K = 4e2 + 7;
int a[N], f[N][K], g[N][K];
int n, k, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int mi(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % Mod)
if (b & 1)
res = 1ll * res * a % Mod;
return res;
}
inline int check() {
set<int> st;
for (int i = 1, j = 1; i <= m; ++i) {
for (; st.find(a[i]) != st.end(); ++j)
st.erase(a[j]);
st.emplace(a[i]);
if (st.size() == k)
return 2;
}
return st.size() == m;
}
inline int solve1() {
set<int> st;
int len = 0;
while (st.find(a[len + 1]) == st.end())
st.emplace(a[++len]);
fill(f[0], f[0] + len + 1, 1), st.clear(), len = 0;
while (st.find(a[m - len]) == st.end())
st.emplace(a[m - len]), ++len;
fill(g[0], g[0] + len + 1, 1);
for (int i = 1; i <= n; ++i) {
for (int j = 1; j < k; ++j) {
f[i][j] = add(1ll * dec(f[i - 1][j - 1], f[i - 1][j]) * (k - j + 1) % Mod, f[i - 1][j]);
g[i][j] = add(1ll * dec(g[i - 1][j - 1], g[i - 1][j]) * (k - j + 1) % Mod, g[i - 1][j]);
}
for (int j = k; ~j; --j)
f[i][j] = add(f[i][j], f[i][j + 1]), g[i][j] = add(g[i][j], g[i][j + 1]);
}
int ans = 0;
for (int i = 0; i <= n - m; ++i)
ans = add(ans, 1ll * f[i][0] * g[n - m - i][0] % Mod);
return ans;
}
inline int solve2() {
f[0][0] = 1;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j < k; ++j) {
f[i][j] = add(1ll * dec(f[i - 1][j - 1], f[i - 1][j]) * (k - j + 1) % Mod, f[i - 1][j]);
g[i][j] = add(1ll * dec(g[i - 1][j - 1], g[i - 1][j]) * (k - j + 1) % Mod, g[i - 1][j]);
if (j >= m)
g[i][j] = add(g[i][j], f[i][j]);
}
for (int j = k; ~j; --j)
f[i][j] = add(f[i][j], f[i][j + 1]), g[i][j] = add(g[i][j], g[i][j + 1]);
}
int ans = g[n][0];
for (int i = k; i >= k - m + 1; --i)
ans = 1ll * ans * mi(i, Mod - 2) % Mod;
return ans;
}
signed main() {
scanf("%d%d%d", &n, &k, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
int flag = check(), all = 1ll * mi(k, n - m) * (n - m + 1) % Mod;
printf("%d", flag == 2 ? all : dec(all, flag ? solve2() : solve1()));
return 0;
}
AT_joisc2012_kangaroo カンガルー
有 \(n\) 个点,每个点有两个权值 \(a_i, b_i\) ,保证 \(a_i > b_i\) 。\(i\) 可以接在 \(j\) 底下当且仅当 \(j\) 是叶子且 \(b_j > a_i\) 。
不难发现最终局面形如若干条链,求不存在两条链可以拼接时最终局面的方案数。
\(n \le 300\)
考虑将 \(a_i, b_i\) 分开来看,将 \(2n\) 个数降序排序,相等时 \(a_i\) 在前。\(a_i\) 视为右括号,\(b_i\) 视为左括号,则拼接就相当于匹配一对括号。
不难发现最终状态下未匹配的括号可以分为两段,左边为若干个右括号,右边为若干个左括号。设 \(f_{i, j, 0/1}\) 表示前 \(i\) 个括号中存在 \(j\) 个未匹配的左括号、当前最终态是否切换到右半段的方案数,转移不难做到 \(O(n^2)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 3e2 + 7;
pair<int, int> a[N << 1];
int f[2][N][2];
int n;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d%d", &a[i].first, &a[i + n].first), a[i].second = 1, a[i + n].second = 0;
sort(a + 1, a + n * 2 + 1, greater<pair<int, int> >());
f[0][0][0] = 1;
for (int i = 1, cnt = 0; i <= n * 2; cnt += !a[i++].second) {
memset(f[i & 1], 0, sizeof(f[i & 1]));
for (int j = 0; j <= cnt; ++j) {
if (a[i].second) {
if (j) {
f[i & 1][j - 1][0] = add(f[i & 1][j - 1][0], 1ll * f[~i & 1][j][0] * j % Mod);
f[i & 1][j - 1][1] = add(f[i & 1][j - 1][1], 1ll * f[~i & 1][j][1] * j % Mod);
}
f[i & 1][j][0] = add(f[i & 1][j][0], f[~i & 1][j][0]);
} else {
f[i & 1][j + 1][0] = add(f[i & 1][j + 1][0], f[~i & 1][j][0]);
f[i & 1][j + 1][1] = add(f[i & 1][j + 1][1], f[~i & 1][j][1]);
f[i & 1][j][1] = add(f[i & 1][j][1], add(f[~i & 1][j][0], f[~i & 1][j][1]));
}
}
}
printf("%d\n", f[0][0][1]);
return 0;
}
P9385 [THUPC 2023 决赛] 阴阳阵
给定 \(n, m, P\) ,求满足以下条件的图的数量 \(\bmod P\) :
- 有 \(n + m\) 个点,其中 \(1 \sim n\) 为白点,\(n + 1 \sim n + m\) 为黑点。
- 每个点恰有一条出边,其中黑点的出边必须指向白点,白点的出边没有限制。
- 不难发现图为有向基环树森林,要求每个环上黑点数量与白点数量的乘积为偶数。
\(n, m \le 2000\)
考虑每次从编号最小且没定出边的点开始,一路确定出边,直到到达一个已经确定出边的点。不难发现每次要么新加一条链到连通块上,要么新加一个 \(\rho\) 状连通块,而 \(\rho\) 又可以拆分为链部分和环部分。
生成合法图的过程是分阶段的,设:
- \(f_{i, j}\) 表示进行若干轮后,确定了 \(i\) 个白点和 \(j\) 个黑点的出边的方案数。
- \(g_{i, j, 0/1, 0/1}\) 表示正在生成一条链,确定了 \(i\) 个白点和 \(j\) 个黑点的出边(不一定这 \(i + j\) 个点都在链上),同时记录钦定的这条链要接到的点的颜色、当前点的颜色。
- \(h_{i, j, 0/1}\) 表示正在生成一个 \(\rho\) 的链部分,确定了 \(i\) 个白点和 \(j\) 个黑点的出边(不一定这 \(i + j\) 个点都在链上),同时记录当前点的颜色。
- \(l_{i, j, 0/1, 0/1, 0/1, 0/1}\) 表示正在生成一个 \(\rho\) 的环部分,确定了 \(i\) 个白点和 \(j\) 个黑点的出边(不一定这 \(i + j\) 个点都在环上),同时记录环末尾的颜色、当前点颜色、环上的白点个数奇偶性、环上的黑点个数奇偶性。
转移就是在 DP 数组时间互相转移,有一些细节需要推导,时间复杂度 \(O(nm)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 7;
int f[N][N], g[N][N][2][2], h[N][N][2], l[N][N][2][2][2][2];
int n, m, Mod;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
signed main() {
scanf("%d%d%d", &n, &m, &Mod);
f[0][0] = 1;
for (int i = 0; i <= n; ++i)
for (int j = 0; j <= m; ++j) {
for (int a = 0; a <= 1; ++a)
for (int b = 0; b <= 1; ++b) {
if (!(a && b))
f[i][j] = add(f[i][j], g[i][j][a][b]);
for (int x = 0; x <= 1; ++x)
if (!(b && x))
g[i + !x][j + x][a][x] = add(g[i + !x][j + x][a][x],
1ll * g[i][j][a][b] * (x ? m - j : n - i) % Mod);
}
for (int a = 0; a <= 1; ++a) {
if (!h[i][j][a])
continue;
for (int x = 0; x <= 1; ++x)
if (!(a && x))
h[i + !x][j + x][x] = add(h[i + !x][j + x][x],
1ll * h[i][j][a] * (x ? m - j : n - i) % Mod);
l[i][j][a][a][!a][a] = add(l[i][j][a][a][!a][a], h[i][j][a]);
}
for (int a = 0; a <= 1; ++a)
for (int b = 0; b <= 1; ++b)
for (int c = 0; c <= 1; ++c)
for (int d = 0; d <= 1; ++d) {
if (!l[i][j][a][b][c][d])
continue;
if (!(a && b) && !(c && d))
f[i][j] = add(f[i][j], l[i][j][a][b][c][d]);
for (int x = 0; x <= 1; ++x)
if (!(b && x))
l[i + !x][j + x][a][x][c ^ !x][d ^ x] = add(l[i + !x][j + x][a][x][c ^ !x][d ^ x],
1ll * l[i][j][a][b][c][d] * (x ? m - j : n - i) % Mod);
}
if (i != n) {
g[i + 1][j][0][0] = add(g[i + 1][j][0][0], 1ll * f[i][j] * i % Mod);
g[i + 1][j][1][0] = add(g[i + 1][j][1][0], 1ll * f[i][j] * j % Mod);
h[i + 1][j][0] = add(h[i + 1][j][0], f[i][j]);
} else {
g[i][j + 1][0][1] = add(g[i][j + 1][0][1], 1ll * f[i][j] * i % Mod);
g[i][j + 1][1][1] = add(g[i][j + 1][1][1], 1ll * f[i][j] * j % Mod);
h[i][j + 1][1] = add(h[i][j + 1][1], f[i][j]);
}
}
printf("%d", f[n][m]);
return 0;
}
转移路线
对于一类简单的 DP 方程,可以考虑一条转移路线的贡献,转化为组合数的计算。
P3266 [JLOI2015] 骗我呢
给定 \(n, m\) ,求满足以下条件的 \(n \times m\) 的矩阵 \(a\) 数量 \(\bmod (10^9 + 7)\) :
- 每个元素为 \(0 \sim m\) 之间的整数。
- 对于所有 \(1 \le i \le n\) 与 \(1 \le j < m\) ,满足 \(a_{i, j} < a_{i, j + 1}\) 。
- 对于所有 \(1 < i \le n\) 与 \(1 \le j < m\) ,满足 \(a_{i, j} < a_{i - 1, j + 1}\) 。
\(n, m \le 10^6\)
不难发现一行 \(0 \sim m\) 中只有一个数没出现,因此可以用没出现的数表示一行的状态。
设 \(f_{i, j}\) 表示第 \(i\) 行没有 \(j\) 的方案数,则 \(f_{i, j} = \sum_{k = 1}^{j + 1} f_{i - 1, k} = f_{i, j - 1} + f_{i - 1, j + 1}\) 。
考虑一条转移路线的组合意义,答案相当于从 \((0, 0)\) 开始,每步可以向右或左上走,求走到 \((n, m + 1)\) 的方案数,并要求 \(x \le n\) 且 \(y \le m + 1\) 。
这样形式还是不够清晰,考虑将第 \(i\) 行向右平移 \(i\) 个单位,则问题转化为每步可以向右或上走,求走到 \((n + m + 1, n)\) 的方案数,并要求不能触碰直线 \(y = x + 1\) 与 \(y = x - (m + 2)\) 。
不难用格路计数的技巧解决。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 3e6 + 7;
int fac[N], inv[N], invfac[N];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline void prework() {
fac[0] = fac[1] = 1;
inv[0] = inv[1] = 1;
invfac[0] = invfac[1] = 1;
for (int i = 2; i < N; ++i) {
fac[i] = 1ll * fac[i - 1] * i % Mod;
inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
}
}
inline int C(int n, int m) {
return n < 0 || m < 0 || m > n ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}
inline int calc(int n, int m, int l, int r) {
auto flip = [](int &x, int &y, int k) {
swap(x, y), x += k, y -= k;
};
int ans = C(n + m, m), x = n, y = m;
while (x >= 0 && y >= 0) {
flip(x, y, l), ans = dec(ans, C(x + y, y));
flip(x, y, r), ans = add(ans, C(x + y, y));
}
x = n, y = m;
while (x >= 0 && y >= 0) {
flip(x, y, r), ans = dec(ans, C(x + y, y));
flip(x, y, l), ans = add(ans, C(x + y, y));
}
return ans;
}
signed main() {
prework();
scanf("%d%d", &n, &m);
printf("%d", calc(n + m + 1, n, -1, m + 2));
return 0;
}
P6811 「MCOI-02」Build Battle 建筑大师
给定 \(n\) ,\(q\) 次询问,每次给定 \(m\) ,求序列 \(a_{1 \sim n}\) 本质不同子序列的数量 \(\bmod (10^9 + 7)\) ,其中 \(a_i = (i - 1) \bmod m + 1\) 。
\(n, q \le 10^6\)
先考虑一般序列本质不同子序列的数量的求法,记上一个 \(a_i\) 出现的位置为 \(lst_i\) ,则 \(f_i \gets s_{i - 1} - s_{lst_i - 1}\) ,其中 \(s\) 为 \(f\) 的前缀和。
考虑原问题,由于 \(lst_i = i - m\) ,因此得到 \(f_i = s_{i - 1} - s_{i - m - 1}\) 。又由于 \(s_i = s_{i - 1} + f_i\) ,因此得到 \(s_i = 2 s_{i - 1} - s_{i - m - 1}\) 。
考虑一条转移路线的组合意义,有一个初始为 \(1\) 的变量 \(v\) ,有两种行动方式:
- \(x \to x + 1\) :\(v \gets 2v\) 。
- \(x \to x + m + 1\) :\(v \gets -v\) 。
可以得到:
预处理每个 \(m\) 的答案,时间复杂度 \(O(n \ln n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e6 + 7;
int fac[N], inv[N], invfac[N], pw[N], ans[N];
int n, q;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline void prework(int n) {
fac[0] = fac[1] = 1;
inv[0] = inv[1] = 1;
invfac[0] = invfac[1] = 1;
for (int i = 2; i <= n; ++i) {
fac[i] = 1ll * fac[i - 1] * i % Mod;
inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
}
pw[0] = 1;
for (int i = 1; i <= n; ++i)
pw[i] = 2ll * pw[i - 1] % Mod;
}
inline int C(int n, int m) {
return m > n ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}
inline int sgn(int n) {
return n & 1 ? Mod - 1 : 1;
}
signed main() {
scanf("%d%d", &n, &q);
prework(n);
for (int i = 1; i <= n; ++i)
for (int j = 0; j <= n / (i + 1); ++j)
ans[i] = add(ans[i], 1ll * pw[n - (i + 1) * j] * sgn(j) % Mod * C(n - i * j, j) % Mod);
while (q--) {
int x;
scanf("%d", &x);
printf("%d\n", ans[x]);
}
return 0;
}
差分去重
CF2048G Kevin and Matrices
给定 \(n, m, v\) ,求元素均为 \(1 \sim v\) 之间的整数,且满足下面式子的 \(n \times m\) 的矩阵数量 \(\bmod 998244353\) :
\[\min_{i = 1}^n \left( \max_{j = 1}^m a_{i, j} \right) \le \max_{j = 1}^m \left( \min_{i = 1}^n a_{i, j} \right) \]\(n \times v \le 10^6\) ,\(m \le 10^9\)
考虑补集转化,统计左式大于右式的方案数。设 \(f(a, b)\) 表示左式 \(\ge a\) 、右式 \(\le b\) 的方案数,答案即为:
下面考虑计算 \(f\) ,考虑对行容斥,钦定 \(i\) 行均 \(< a\) ,则每一列独立,列的方案数即为总方案数减去 \(> b\) 的方案数,因此得到:
直接计算即可做到 \(O(nv \log (nm))\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 1e6 + 7;
int fac[N], inv[N], invfac[N];
int n, m, v;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int mi(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % Mod)
if (b & 1)
res = 1ll * res * a % Mod;
return res;
}
inline int sgn(int n) {
return n & 1 ? Mod - 1 : 1;
}
inline void prework() {
fac[0] = fac[1] = 1;
inv[0] = inv[1] = 1;
invfac[0] = invfac[1] = 1;
for (int i = 2; i < N; ++i) {
fac[i] = 1ll * fac[i - 1] * i % Mod;
inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
}
}
inline int C(int n, int m) {
return m > n ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}
inline int f(int x, int y) {
int res = 0;
for (int i = 0; i <= n; ++i)
res = add(res, 1ll * sgn(i) * C(n, i) % Mod * mi(dec(1ll * mi(v, n - i) * mi(x - 1, i) % Mod,
1ll * mi(v - y, n - i) * mi(max(x - y - 1, 0), i) % Mod), m) % Mod);
return res;
}
signed main() {
prework();
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d%d", &n, &m, &v);
int ans = 0;
for (int i = 1; i <= v; ++i)
ans = add(ans, dec(add(f(i, i), f(i + 1, i - 1)), add(f(i + 1, i), f(i, i - 1))));
printf("%d\n", ans);
}
return 0;
}
P10592 BZOJ4361 isn
给定序列 \(a_{1 \sim n}\) ,若序列 \(a\) 不是不降序列,则必须从中删去一个数。这一操作将被不断执行,直到序列非降为止。
求不同的操作方案数 \(\bmod (10^9 + 7)\) ,操作方案不同当且仅当删除的顺序或次数不同。
\(n \le 2 \times 10^3\)
若最后剩下 \(l\) 个数,则操作方式有 \((n - l)!\) 种,考虑对最后的不降序列计数。
统计恰好不降比较困难,考虑统计长度为 \(l\) 的不降序列,再减去长为 \(l + 1\) 的不降序列数量的 \(l\) 倍,最后乘上 \((n - l)!\) 种操作方式即可。
设 \(f_{i, j}\) 表示 \(a_i\) 结尾长为 \(j\) 的不降子序列数量,不难树状数组优化到 \(O(n^2 \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 2e3 + 7;
int a[N], fac[N], f[N];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
struct BIT {
int c[N];
inline void update(int x, int k) {
for (; x <= m; x += x & -x)
c[x] = add(c[x], k);
}
inline int query(int x) {
int res = 0;
for (; x; x -= x & -x)
res = add(res, c[x]);
return res;
}
} bit[N];
signed main() {
scanf("%d", &n);
vector<int> vec;
for (int i = 1; i <= n; ++i)
scanf("%d", a + i), vec.emplace_back(a[i]);
sort(vec.begin(), vec.end()), vec.erase(unique(vec.begin(), vec.end()), vec.end());
m = vec.size();
for (int i = 1; i <= n; ++i)
a[i] = lower_bound(vec.begin(), vec.end(), a[i]) - vec.begin() + 1;
fac[0] = 1;
for (int i = 1; i <= n; ++i)
fac[i] = 1ll * fac[i - 1] * i % Mod;
bit[0].update(1, 1);
for (int i = 1; i <= n; ++i)
for (int j = i; j; --j)
bit[j].update(a[i], bit[j - 1].query(a[i]));
for (int i = 0; i <= n; ++i)
f[i] = 1ll * bit[i].query(m) * fac[n - i] % Mod;
int ans = 0;
for (int i = 0; i <= n; ++i)
ans = add(ans, dec(f[i], 1ll * f[i + 1] * (i + 1) % Mod));
printf("%d", ans);
return 0;
}
[AGC013D] Piling Up
有 \(n\) 个颜色为黑或白的球,但是每种球的数量未知。
进行 \(m\) 次操作,每次先拿出一个球,再放入黑球、白球各一个,再拿出一个球。
将拿出的球按顺序构成一个序列,对于所有可能的初始状态,求不同的序列数量 \(\bmod (10^9 + 7)\) 。
\(n, m \le 3000\)
先简化操作,一共有四种:
- 拿出黑球,放入黑、白球,拿出黑球。
- 白球数量的变化量:\(0 \to 0 \to 1 \to 1\) 。
- 拿出黑球,放入黑、白球,拿出白球。
- 白球数量的变化量:\(0 \to 0 \to 1 \to 0\) 。
- 拿出白球,放入黑、白球,拿出黑球。
- 白球数量的变化量:\(0 \to -1 \to 0 \to 0\) 。
- 拿出白球,放入黑、白球,拿出白球。
- 白球数量的变化量:\(0 \to -1 \to 0 \to -1\) 。
不难发现四种操作对应插入序列的元素是不同的,并且白球变化量的变化也是不同的,因此可以考虑对白球变化量的变化序列计数。
设 \(f_{i, j}\) 表示操作 \(i\) 次后白球数量为 \(j\) 时可能的最终序列数量,但是初始状态并不好设计。若将每个初始状态的 DP 值设为 \(1\) ,则会算重。
发现白球的变化可以用一条折线表示,而折线会算重当且仅当两条折线上下平移后会重合。因此只要用 \(n\) 的答案减去 \(n - 1\) 的答案即可,时间复杂度 \(O(nm)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 3e3 + 7;
int f[N][N];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int solve(int n) {
memset(f, 0, sizeof(f));
fill(f[0], f[0] + n + 1, 1);
for (int i = 1; i <= m; ++i)
for (int j = 0; j <= n; ++j) {
if (j < n) {
f[i][j + 1] = add(f[i][j + 1], f[i - 1][j]);
f[i][j] = add(f[i][j], f[i - 1][j]);
}
if (j) {
f[i][j] = add(f[i][j], f[i - 1][j]);
f[i][j - 1] = add(f[i][j - 1], f[i - 1][j]);
}
}
int res = 0;
for (int i = 0; i <= n; ++i)
res = add(res, f[m][i]);
return res;
}
signed main() {
scanf("%d%d", &n, &m);
printf("%d", dec(solve(n), solve(n - 1)));
return 0;
}
还有一种方法是只统计触碰到 \(0\) 的折线,时间复杂度同样为 \(O(nm)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 3e3 + 7;
int f[N][N][2];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 0; i <= n; ++i)
f[0][i][!i] = 1;
for (int i = 1; i <= m; ++i)
for (int j = 0; j <= n; ++j)
for (int k = 0; k <= 1; ++k) {
if (j < n) {
f[i][j + 1][k] = add(f[i][j + 1][k], f[i - 1][j][k]);
f[i][j][k] = add(f[i][j][k], f[i - 1][j][k]);
}
if (j) {
if (j == 1) {
if (k) {
f[i][j][k] = add(f[i][j][k], add(f[i - 1][j][0], f[i - 1][j][1]));
f[i][j - 1][k] = add(f[i][j - 1][k], add(f[i - 1][j][0], f[i - 1][j][1]));
}
} else {
f[i][j][k] = add(f[i][j][k], f[i - 1][j][k]);
f[i][j - 1][k] = add(f[i][j - 1][k], f[i - 1][j][k]);
}
}
}
int ans = 0;
for (int i = 0; i <= n; ++i)
ans = add(ans, f[m][i][1]);
printf("%d", ans);
return 0;
}
调整适应
在保证合法的情况下,对于涉及到多个元素的限制,可以让某一些元素任意取,将一个点的决策适应与其它部分的决策。但这可能会算重,容斥掉其他限制即可。
P3214 [HNOI2011] 卡农
对于 \(1 \sim n\) 个数组成的 \(2^n - 1\) 个非空集合,需要从中选出 \(m\) 个集合满足:
- 所选集合互不相同。
- 每个数在集合中的出现次数和为偶数。
求方案数 \(\bmod (10^8 + 7)\) 。
\(n, m \le 10^6\)
先将无序转化为有序,最后除以 \(m!\) 即可。
设 \(f_i\) 表示选了 \(i\) 个集合的方案数,注意此时钦定集合是有顺序的。
先解决出现次数为偶数的限制,考虑让前 \(i - 1\) 个集合随便选,那么第 \(i\) 个集合就是确定的,方案数为 \(\binom{2^n - 1}{i - 1} \times (i - 1)!\) 。
再考虑集合非空的限制,减去 \(f_{i - 1}\) 即可。
最后考虑集合互异的性质,减去 \(f_{i - 2} \times (i - 1) \times (2^n - i + 1)\) 即可。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e8 + 7;
const int N = 1e6 + 7;
int f[N];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int mi(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % Mod)
if (b & 1)
res = 1ll * res * a % Mod;
return res;
}
signed main() {
scanf("%d%d", &n, &m);
int pw = mi(2, n), fac = 1;
f[0] = 1;
for (int i = 1, mul = 1; i <= m; ++i) {
f[i] = dec(mul, f[i - 1]);
if (i >= 2)
f[i] = dec(f[i], 1ll * f[i - 2] * (i - 1) % Mod * dec(pw, i - 1) % Mod);
mul = 1ll * mul * dec(pw, i) % Mod, fac = 1ll * fac * i % Mod;
}
printf("%d", 1ll * f[m] * mi(fac, Mod - 2) % Mod);
return 0;
}
化子问题
通常是考虑某处的填法,然后将问题拆分为独立的子问题。
P9493 「SFCOI-3」进行一个列的排
给出 \(a_{0 \sim n - 1}\) ,求满足以下条件的 \(0 \sim n - 1\) 的排列数量:对于所有 \(i = 0, 1, \cdots, n - 1\) ,存在一个长度为 \(a_i\) 的子区间满足区间 \(\mathrm{mex}\) 为 \(i\) 。
\(n \le 5 \times 10^3\)
从 \(n - 1\) 开始考虑,不难发现 \(\mathrm{mex} = n - 1\) 的区间一定包含 \(0 \sim n - 2\) ,从而 \(a_{n - 1} = n - 2\) ,此时 \(n - 1\) 必须排在序列的开头或末尾,从而可以化子问题。
设 \(f_{l, r}\) 表示考虑了 \(r - l + 1 \sim n - 1\) 的限制,此时剩余的区间为 \(l, r\) 的方案数,转移枚举当前数放在开头或是结尾即可。
注意特判无解的情况,时间复杂度 \(O(n^2)\) ,空间可以滚动到 \(O(n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 5e3 + 7;
int a[N], f[2][N];
int n;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d", &n);
bool flag = (n > 1);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i), flag &= (i - 1 <= a[i] && a[i] < n);
if (!flag) {
puts("0");
continue;
}
memset(f[n & 1] + 1, 0, sizeof(int) * n), f[n & 1][1] = 1;
for (int len = n; len >= 2; --len) {
memset(f[~len & 1] + 1, 0, sizeof(int) * n);
for (int l = 1, r = len; r <= n; ++l, ++r) {
if (r - 1 >= a[len])
f[~len & 1][l] = add(f[~len & 1][l], f[len & 1][l]);
if (n - l >= a[len])
f[~len & 1][l + 1] = add(f[~len & 1][l + 1], f[len & 1][l]);
}
}
int ans = 0;
for (int i = 1; i <= n; ++i)
ans = add(ans, f[1][i]);
printf("%d\n", ans);
}
return 0;
}
SP3734 PERIODNI - Periodni
给定一个 \(n\) 列的表格,第 \(i\) 列自底向上有 \(h_i\) 个格子,求在其中放置 \(k\) 个车的方案数 \(\bmod (10^9 + 7)\) 。
\(n, k \le 500\)
建立小根笛卡尔树,则每个点的管辖范围是子树内高为它的一个极长子矩形,为了防止不同矩形的决策互相影响,考虑删去这个极长子矩形再递归儿子。
设 \(f_{u, i}\) 表示 \(u\) 子树内放 \(i\) 个车的方案数,考虑转移:
- 首先求出 \(g_i = \sum_{j = 0}^i f_{lc_u, j} \times f_{rc_u, i - j}\) ,即递归到左右儿子选取的方案数。
- 接下来枚举 \(u\) 矩形内放的点的数量,设该矩形高、宽分别为 \(H, W\) ,有 \(f_{u, i} = \sum_{j = 0}^i g_{i - j} \times \binom{H}{j} \times \binom{W - (i - j)}{j} \times j!\) 。
时间复杂度 \(O(n k^2)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e6 + 7;
int a[N], fac[N], inv[N], invfac[N];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline void prework() {
fac[0] = fac[1] = 1;
inv[0] = inv[1] = 1;
invfac[0] = invfac[1] = 1;
for (int i = 2; i < N; ++i) {
fac[i] = 1ll * fac[i - 1] * i % Mod;
inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
}
}
inline int C(int n, int m) {
return m > n ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}
namespace CST {
int lc[N], rc[N], sta[N];
int root;
inline void build() {
for (int i = 1, top = 0; i <= n; ++i) {
int k = top;
while (k && a[sta[k]] > a[i])
--k;
if (k)
rc[sta[k]] = i;
else
root = i;
if (k < top)
lc[i] = sta[k + 1];
sta[top = ++k] = i;
}
}
vector<int> dfs(int x, int l, int r, int fa) {
if (!x)
return {1};
int H = a[x] - a[fa], W = r - l + 1;
vector<int> f(W + 1), g(W + 1), gl = dfs(lc[x], l, x - 1, x), gr = dfs(rc[x], x + 1, r, x);
for (int i = 0; i <= x - l; ++i)
for (int j = 0; j <= r - x; ++j)
g[i + j] = add(g[i + j], 1ll * gl[i] * gr[j] % Mod);
for (int i = 0; i <= W; ++i)
for (int j = 0; j <= i; ++j)
f[i] = add(f[i], 1ll * g[i - j] * C(H, j) % Mod * C(W - (i - j), j) % Mod * fac[j] % Mod);
return f;
}
} // namespace CST
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
CST::build(), prework();
printf("%d", CST::dfs(CST::root, 1, n, 0)[m]);
return 0;
}
[AGC026D] Histogram Coloring
给定 \(n\) 列的网格,第 \(i\) 列高为将每 \(h_i\) ,将每个格子染色成红色或蓝色,使得每个 \(2 \times 2\) 的区域都恰好有两个蓝格子和两个红格子,求方案数。
\(n \le 100\)
先考虑 \(h\) 全相等的情况,若最底行存在两个相邻的同色位置,则上面的填色方案是确定的,否则每一行都有两种方法(红蓝交替)。
考虑建立小根笛卡尔树,设 \(f_u\) 表示 \(u\) 子树红蓝交替的方案数,\(g_u\) 表示 \(u\) 子树不为红蓝交替的方案数。
为了防止不同矩形的决策互相影响,考虑删去这个极长子矩形再递归儿子,这样就可以化子问题了。
先考虑 \(f\) 的转移,则要求左右子树均为红蓝交替,即 \(f_u = f_{lc_u} \times f_{rc_u} \times 2^h\) ,其中 \(h\) 表示 \(u\) 管辖的子矩形的高。
再考虑 \(g\) 的转移,不难发现只要考虑最高的行的情况就确定了下面行的情况。若一侧为交替,则最高行有两种填色方案,否则只有一种方案。分类讨论贡献系数:
- 若左右子树均为红蓝交替,则对于 \((u - 1, u, u + 1)\) 三个位置,仅有红蓝红、蓝红蓝不合法(不满足同色),贡献系数为 \(6\) 。
- 若恰有一个子树为红蓝交替,则 \(u\) 位置和红蓝交替的一侧各有两种方案,贡献系数为 \(4\) 。
- 若没有子树红蓝交替,则仅 \(u\) 位置有两种方案,贡献系数为 \(2\) 。
\(g\) 的转移需要特殊处理一下区间最小值在端点的情况。
忽略快速幂的复杂度,时间复杂度 \(O(n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e2 + 7;
int h[N];
int n;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int mi(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % Mod)
if (b & 1)
res = 1ll * res * a % Mod;
return res;
}
namespace CST {
int lc[N], rc[N], sta[N], f[N], g[N];
int root;
inline void build() {
for (int i = 1, top = 0; i <= n; ++i) {
int k = top;
while (k && h[sta[k]] > h[i])
--k;
if (k)
rc[sta[k]] = i;
else
root = i;
if (k < top)
lc[i] = sta[k + 1];
sta[top = ++k] = i;
}
}
void solve(int x, int l, int r, int fa) {
if (!x)
return;
solve(lc[x], l, x - 1, x), solve(rc[x], x + 1, r, x);
f[x] = 1ll * (lc[x] ? f[lc[x]] : 1) * (rc[x] ? f[rc[x]] : 1) % Mod * mi(2, h[x] - h[fa]) % Mod;
if (l == r)
return;
else if (x == l)
g[x] = 2ll * add(f[rc[x]], g[rc[x]]) % Mod;
else if (x == r)
g[x] = 2ll * add(f[lc[x]], g[lc[x]]) % Mod;
else
g[x] = add(add(6ll * f[lc[x]] * f[rc[x]] % Mod, 4ll * f[lc[x]] * g[rc[x]] % Mod),
add(4ll * g[lc[x]] * f[rc[x]] % Mod, 2ll * g[lc[x]] * g[rc[x]] % Mod));
}
} // namespace CST
using namespace CST;
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", h + i);
build(), solve(root, 1, n, 0);
printf("%d", add(f[root], g[root]));
return 0;
}
减少状态数
CF924F Minimal Subset Difference
记 \(f(n)\) 表示在 \(n\) 的所有相邻数位之间插入加号或减号,最终得到的算式的值的绝对值的最小值。
\(T\) 组询问,每次给定 \(l, r, k\) ,求 \(n \in [l, r]\) 且 \(f(n) \le k\) 的 \(n\) 的数量。
\(T \le 5 \times 10^4\) ,\(1 \le l \le r \le 10^{18}\) ,\(0 \le k \le 9\)
先考虑如何求 \(f\) ,只要做背包即可。
考虑背包的值域,有结论:对于值域为 \([0, w]\) 的序列,最小化 \(f(n)\) 时最大前缀和的绝对值 \(\le w(w - 1)\) 。
由于正负是对称的,因此只要记录 \([0, 72]\) 即可,转移就是 \(f_j \to f_{j + n_i}\) 以及 \(f_j \to f_{|j - n_i|}\) 。
暴搜可以发现对于所有 \(n\) ,有效的背包状态只有 \(10^4\) 级别,因此可以压到一个 __int128
里用 map
存储。
预处理 \(g_{i, S, k}\) 表示无最高位限制,还要填 \(i\) 位,此时背包状态为 \(S\) 时 \(f(n) \le k\) 方案数,直接数位 DP 即可。
注意背包时 \(j + n_i\) 和 \(j - n_i\) 的部分用位运算,\(n_i - j\) 的部分暴力枚举 \(j \in [0, n_i]\) 可以优化复杂度。注意 __int128
的常数。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e4 + 7;
map<__int128, int> mp;
__int128 num[N], trans[N][10];
ll f[20][N][10];
int digit[20];
__int128 all = ((__int128)1 << 73) - 1;
int tot;
void dfs1(int d, int u) {
if (~f[d][u][0])
return;
else if (!d) {
for (int i = 0; i <= 9; ++i)
f[d][u][i] = num[u] & ((1 << (i + 1)) - 1) ? 1 : 0;
return;
}
memset(f[d][u], 0, sizeof(f[d][u]));
for (int i = 0; i <= 9; ++i) {
if (trans[u][i] == -1) {
__int128 v = ((num[u] << i) | (num[u] >> i)) & all;
for (int j = 0; j <= i; ++j)
v |= (num[u] >> j & 1) << (i - j);
if (mp.find(v) == mp.end())
num[mp[v] = ++tot] = v;
trans[u][i] = mp[v];
}
dfs1(d - 1, trans[u][i]);
for (int j = 0; j <= 9; ++j)
f[d][u][j] += f[d - 1][trans[u][i]][j];
}
}
ll dfs2(int x, bool lead, int s, int k) {
if (!lead)
return f[x][s][k];
else if (!x)
return num[s] & ((1 << (k + 1)) - 1) ? 1 : 0;
ll res = 0;
for (int i = 0, high = lead ? digit[x] : 9; i <= high; ++i)
res += dfs2(x - 1, lead && i == high, trans[s][i], k);
return res;
}
inline ll solve(ll n, int k) {
int len = 0;
do
digit[++len] = n % 10, n /= 10;
while (n);
return dfs2(len, true, 1, k);
}
signed main() {
memset(f, -1, sizeof(f)), memset(trans, -1, sizeof(trans));
num[mp[1] = ++tot] = 1;
for (int i = 0; i < 20; ++i)
dfs1(i, 1);
int T;
scanf("%d", &T);
while (T--) {
ll l, r;
int k;
scanf("%lld%lld%d", &l, &r, &k);
printf("%lld\n", solve(r, k) - solve(l - 1, k));
}
return 0;
}
优化状态
[ARC119F] AtCoder Express 3
有 \(n + 1\) 个点,标号为 \(0 \sim n\) ,其中 \(i\) 和 \(i + 1\) 之间有一条边。
点 \(1 \sim n - 1\) 的颜色为黑色或白色,每个点会向其同色前驱、后继连双向边,若不存在则向 \(0 / n\) 连边。
已知一些点的颜色,求有多少种染色方法使得 \(0\) 到 \(n\) 的最短路 \(\le m\) 。
\(n, m \le 4000\)
设 \(f_{i, j, k, 0/1}\) 表示前 \(i\) 个点,到最后一个白色/黑色点的最短路为 \(j, k\) ,第 \(i\) 个点的颜色为白或黑。若 \(i\) 为白色,则加一个白点后 \(j \to j + 1\) ,加一个黑点后 \(j \to \min(k + 2, j), k \to \min(j + 1, k + 1)\) 。
直接转移是 \(O(nm^2)\) 的,发现状态数过大,首先考虑优化状态数。
设当前点为白色,若 \(j > k + 2\) ,则加一个黑点后 \(j\) 就会变成 \(k + 2\) ,加一个白点后 \(j\) 仍然更大,不会对答案产生贡献,因此只需保留 \(|j - k| \le 2\) 的状态即可。
时间复杂度 \(O(nm)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 4e3 + 7;
int f[2][N][5][2];
char str[N];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int& F(int op, int j, int k, int c) {
return f[op][min(j, k + 2)][min(k, j + 2) - min(j, k + 2) + 2][c];
}
signed main() {
scanf("%d%d%s", &n, &m, str + 1), --n;
f[0][0][2][0] = 1;
for (int i = 0; i < n; ++i) {
memset(f[~i & 1], 0, sizeof(f[~i & 1]));
for (int j = 0; j <= m + 2; ++j)
for (int k = j - 2; k <= j + 2; ++k) {
auto update = [](int &x, int y) {
x = add(x, y);
};
if (str[i + 1] != 'B') {
update(F(~i & 1, j + 1, k, 0), F(i & 1, j, k, 0));
update(F(~i & 1, min(j + 1, k + 1), min(j + 2, k), 0), F(i & 1, j, k, 1));
}
if (str[i + 1] != 'A') {
update(F(~i & 1, j, k + 1, 1), F(i & 1, j, k, 1));
update(F(~i & 1, min(j, k + 2), min(j + 1, k + 1), 1), F(i & 1, j, k, 0));
}
}
}
int ans = 0;
for (int i = 0; i <= m + 2; ++i)
for (int j = i - 2; j <= i + 2; ++j)
if (min(i, j) + 1 <= m)
ans = add(ans, add(F(n & 1, i, j, 0), F(n & 1, i, j, 1)));
printf("%d", ans);
return 0;
}
P9084 [PA 2018] Skwarki
对于一个排列 \(P = p_{1 \sim n}\) ,重复以下操作直到 \(P\) 只剩一个数:将 \(P\) 替换为 \(P\) 的所有局部最大值保持相对顺序形成的数组。
其中 \(p_i\) 是局部最大值当且仅当满足以下两个条件:
- \(i = 1\) 或 \(p_i > p_{i - 1}\) 。
- \(i = n\) 或 \(p_i > p_{i + 1}\) 。
定义 \(f(P)\) 为上述操作的重复次数。
给定 \(n, k, m\) ,求 \(f(p_{1 \sim n}) = k\) 的长度为 \(n\) 的排列数量 \(\bmod m\) 。
\(n \le 5000\)
考虑建立大根笛卡尔树,则一个点会被保留当且仅当其左右儿子均存在,注意特殊处理一下首尾元素的情况,而删去不会保留的点后对每个点儿子的存在性是好维护的。
考虑 DP,设 \(f_{i, j}\) 表示长为 \(i\) 的排列,需要删 \(j\) 次删空,已经确定一个边界的方案数,\(g_{i, j}\) 表示边界的均未确定的方案数。讨论左右子树的存在性,以及根被删除的时间,有转移:
可以前缀和优化到 \(O(n^3)\) 。注意到一次不会保留相邻的元素,因此 \(k > \lfloor \log n \rfloor + 1\) 时无解,因此第二维只需保留 \(O(\log n)\) 级别,时间复杂度 \(O(n^2 \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e3 + 7, LOGN = 15;
int C[N][N], f[N][LOGN], g[N][LOGN], sf[N][LOGN], sg[N][LOGN];
int n, m, Mod;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
signed main() {
scanf("%d%d%d", &n, &m, &Mod);
if (m > __lg(n) + 1)
return puts("0"), 0;
C[0][0] = 1;
for (int i = 1; i <= n; ++i) {
C[i][0] = 1;
for (int j = 1; j <= i; ++j)
C[i][j] = add(C[i - 1][j], C[i - 1][j - 1]);
}
fill(sf[1] + 1, sf[1] + LOGN, f[1][1] = 1);
fill(sg[1] + 1, sg[1] + LOGN, g[1][1] = 1);
for (int i = 2; i <= n; ++i) {
for (int j = 1; j <= __lg(i) + 1; ++j) {
g[i][j] = 2ll * g[i - 1][j] % Mod;
for (int k = 1; k < i - 1; ++k) {
g[i][j] = add(g[i][j], 2ll * C[i - 1][k] * sg[k][j - 1] % Mod * g[i - 1 - k][j] % Mod);
g[i][j] = add(g[i][j], 1ll * C[i - 1][k] * g[k][j - 1] % Mod * g[i - 1 - k][j - 1] % Mod);
}
f[i][j] = add(f[i - 1][j], g[i - 1][j - 1]);
for (int k = 1; k < i - 1; ++k) {
f[i][j] = add(f[i][j], 1ll * C[i - 1][k] * sf[k][j] % Mod * g[i - 1 - k][j - 1] % Mod);
if (j >= 2)
f[i][j] = add(f[i][j], 1ll * C[i - 1][k] * f[k][j] % Mod * sg[i - 1 - k][j - 2] % Mod);
}
}
for (int j = 1; j < LOGN; ++j)
sf[i][j] = add(sf[i][j - 1], f[i][j]), sg[i][j] = add(sg[i][j - 1], g[i][j]);
}
int ans = 2ll * f[n - 1][m] % Mod;
for (int i = 1; i < n - 1; ++i) {
ans = add(ans, 2ll * C[n - 1][i] * f[i][m] % Mod * sf[n - 1 - i][m - 1] % Mod);
ans = add(ans, 1ll * C[n - 1][i] * f[i][m] % Mod * f[n - 1 - i][m] % Mod);
}
printf("%d", ans);
return 0;
}
AT_joisc2012_kangaroo カンガルー
有 \(n\) 个点,每个点有两个权值 \(a_i, b_i\) ,保证 \(a_i > b_i\) 。\(i\) 可以接在 \(j\) 底下当且仅当 \(j\) 是叶子且 \(b_j > a_i\) 。
不难发现最终局面形如若干条链,求不存在两条链可以拼接时最终局面的方案数。
\(n \le 300\)
先将所有点按 \(a\) 排序,这样每条链的 \(a\) 都是递减的,记 \(g_i\) 表示 \(1 \sim i - 1\) 中满足 \(b_j > a_i\) 的点的数量。
设 \(f_{i, j, k}\) 表示考虑了前 \(i\) 个点、形成了 \(j\) 条链、有 \(k\) 条链非法的方案数,其中链非法定义为下面还能接目前存在的链,考虑转移:
- 将第 \(i\) 个点接在非法链下面:\(f_{i, j, k - 1} \gets f_{i - 1, j, k} \times k\) ,因为 \(i\) 底下显然不能接现有的链,所以非法链数量减少 \(1\) 。
- 将第 \(i\) 个点接在合法链下面:\(f_{i, j, k} \gets f_{i - 1, j, k} \times (g_i - (i - 1 - j) - k)\) ,这里 \(g_i - (i - 1 - j)\) 表示可以接 \(i\) 的链的数量,这是因为链上的每个非叶子作为单点时都能接 \(i\) 。而每个非叶子底下一定能接 \(\le i - 1\) ,因此也一定能接 \(i\) ,需要减去 \(k\) 。
- 令第 \(i\) 个点成为单点:\(f_{i, j + 1, g_i - (i - 1 - j)} \gets f_{i - 1, j, k}\) ,这是因为非法链一定能接 \(i\) 。
这里的 \(g\) 实际就是为了优化 DP 的状态维,通过 \(g\) 可以直接算出可以接 \(i\) 的链的数量。
答案即为 \(\sum_{i = 1}^n f_{n, i, 0}\) ,时间复杂度 \(O(n^3)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 3e2 + 7;
pair<int, int> a[N];
int f[2][N][N], g[N];
int n;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d%d", &a[i].first, &a[i].second);
sort(a + 1, a + n + 1, greater<pair<int, int> >());
for (int i = 1; i <= n; ++i)
for (int j = 1; j < i; ++j)
g[i] += (a[j].second > a[i].first);
f[0][0][0] = 1;
for (int i = 1; i <= n; ++i) {
memset(f[i & 1], 0, sizeof(f[i & 1]));
for (int j = 0; j < i; ++j)
for (int k = 0; k <= j; ++k) {
if (k)
f[i & 1][j][k - 1] = add(f[i & 1][j][k - 1], 1ll * f[~i & 1][j][k] * k % Mod);
f[i & 1][j][k] = add(f[i & 1][j][k], 1ll * f[~i & 1][j][k] * (g[i] - (i - 1 - j) - k) % Mod);
f[i & 1][j + 1][g[i] - (i - 1 - j)] = add(f[i & 1][j + 1][g[i] - (i - 1 - j)], f[~i & 1][j][k]);
}
}
int ans = 0;
for (int i = 1; i <= n; ++i)
ans = add(ans, f[n & 1][i][0]);
printf("%d\n", ans);
return 0;
}
mod 2 信息统计
- \(\bmod 2\) 问题首选对消。
- Lucas 定理:\([x \subseteq y] = \binom{y}{x} \bmod 2\) 。
- 调整适应:拿出一个元素,控制结果的奇偶性。
CF1770F Koxia and Sequence
给定 \(n, X, Y\) ,对于所有满足 \(\sum_{i = 1}^n a_i = X\) 且 \(\operatorname{or}_{i = 1}^n a_i = Y\) 的长度为 \(n\) 的序列 \(a_{1 \sim n}\) ,求 \(\oplus_{i = 1}^n a_i\) 的异或和。
\(n \le 2^{40}\) ,\(X \le 2^{60}\) ,\(Y \le 2^{20}\)
当 \(n\) 为偶数时,所有合法序列反转后仍然合法,若回文则异或和为 \(0\) ,因此答案为 \(0\) 。
否则只需统计 \(a_1\) 的异或和即可,因为 \(a_{2 \sim n}\) 是一个序列长度为偶数的子问题,答案为 \(0\) 。
考虑拆位,统计 \(a_i\) 每一位为 \(1\) 的方案数的奇偶性,以下钦定第 \(k\) 位为 \(1\) 。
考虑按位子集反演,设 \(f(z)\) 表示按位或为 \(z\) 子集的方案数,则按位或恰为 \(Y\) 的方案数即为 \(\sum_{y' \subseteq y} (-1)^{|y| - |y'|} f(y')\) 。由于最后统计的权值是异或,因此只需求出其模 \(2\) 意义下的值即可。由于 \(1 \equiv -1 \pmod{2}\) ,因此只要统计 \(\oplus_{z \subseteq y} f(z)\) 即可。
先写出答案的式子(注意此时若 \(2^k \not \subseteq z\) 的答案为 \(0\) ):
考虑逆用 Lucas 定理,得到:
然后可以范德蒙德卷积得到:
直接算即可做到 \(O(y \log y)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
ll n, X;
int Y;
signed main() {
scanf("%lld%lld%d", &n, &X, &Y);
if (~n & 1)
return puts("0"), 0;
int ans = 0;
for (int k = 1; k <= Y; k <<= 1) {
if (~Y & k)
continue;
for (int z = 1; z <= Y; ++z)
if ((z & k) && (z | Y) == Y) {
ll u = n * z - k, v = X - k;
if (0 <= v && v <= u && (v | u) == u)
ans ^= k;
}
}
printf("%d", ans);
return 0;
}
CF979E Kuro and Topological Parity
有 \(n\) 个点,每个点为黑色或白色,有些点颜色未确定。
称一条经过的点为黑白相间的路径为好路径,如果一个图好路径的总数 \(\bmod 2 = p\) ,那么称这个图为好图。
可以在图上加入任意条边,满足其从编号小的点指向编号大的点,对于所有确定未确定颜色的点和加入边的方案,求好图的数量和 \(\bmod (10^9 + 7)\) 。
\(n \le 50\)
判定好图可以通过 DP 判定,枚举到当前点时,若前面存在一个有奇数条路径结尾的异色点,则可以通过控制该点是否连边达到控制好路径奇偶性的目的。否则由于单点也是路径,因此好路径总数的奇偶性会被改变。
因此考虑 DP of DP,设 \(f_{i, j, k, l}\) 表示前 \(i\) 个点,路径条数奇偶性 \(j\) ,\(k, l\) 表示是否存在奇数条路径结尾的黑点和白点,转移不难做到 \(O(n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 5e1 + 7;
int a[N], pw[N], f[N][2][2][2];
int n, p;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
signed main() {
scanf("%d%d", &n, &p);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
pw[0] = 1;
for (int i = 1; i <= n; ++i)
pw[i] = 2ll * pw[i - 1] % Mod;
f[0][0][0][0] = 1;
for (int i = 0; i < n; ++i)
for (int j = 0; j <= 1; ++j)
for (int k = 0; k <= 1; ++k)
for (int l = 0; l <= 1; ++l) {
if (a[i + 1] != 0) {
if (k) {
f[i + 1][j][k][l] = add(f[i + 1][j][k][l],
1ll * f[i][j][k][l] * pw[i - 1] % Mod);
f[i + 1][j ^ 1][k][l | 1] = add(f[i + 1][j ^ 1][k][l | 1],
1ll * f[i][j][k][l] * pw[i - 1] % Mod);
} else
f[i + 1][j ^ 1][k][l | 1] = add(f[i + 1][j ^ 1][k][l | 1],
1ll * f[i][j][k][l] * pw[i] % Mod);
}
if (a[i + 1] != 1) {
if (l) {
f[i + 1][j][k][l] = add(f[i + 1][j][k][l],
1ll * f[i][j][k][l] * pw[i - 1] % Mod);
f[i + 1][j ^ 1][k | 1][l] = add(f[i + 1][j ^ 1][k | 1][l],
1ll * f[i][j][k][l] * pw[i - 1] % Mod);
} else
f[i + 1][j ^ 1][k | 1][l] = add(f[i + 1][j ^ 1][k | 1][l],
1ll * f[i][j][k][l] * pw[i] % Mod);
}
}
int ans = 0;
for (int i = 0; i <= 1; ++i)
for (int j = 0; j <= 1; ++j)
ans = add(ans, f[n][p][i][j]);
printf("%d", ans);
return 0;
}
[AGC050F] NAND Tree
给定一棵 \(n\) 个点的树,每个点上有权值 \(c_i \in \{ 0, 1 \}\) 。
定义一次操作为选择一条边,将这条边的两个端点缩成一个点,点权为两端点点权 NAND 的结果(先与再取反)。
不断操作直到只剩一个点,求使得最终点权为 \(1\) 的方案数模 \(2\) 的结果。
\(n \le 300\)
考虑对消:
-
若相邻两个操作没有选择同一个点,则可以交换顺序使得结果不变,可以对消,总贡献为 \(0\) 。
-
对于两条边 \((x, y), (y, z)\) ,若 \(c_x = c_y\) ,则两次操作交换位置后结果相同,可以对消,贡献为 \(0\) 。
-
那么会有两种操作顺序:\(f(f(c_x, c_y), c_z)\) 、\(f(c_x, f(c_y, c_z))\) ,其中 \(f\) 即为 NAND 运算。若 \(c_x = c_y\) ,则结果相同,可以交换对消,贡献为 \(0\) 。
先考虑 \(2 \nmid n\) 的情况,此时操作次数(边数)为偶数。将操作相邻两两分组,则一组操作相当于:选择一条三个点的链 \((x, y, z)\) ,合并为一个单点,钦定权值为 \(c_x\) 。
由于相邻两组操作有交,因此可以考虑固定保留到最后的点为根,则每次会选择根的一个儿子作为 \(y\) 、\(y\) 的一个儿子作为 \(z\) ,方案数为拓扑序数量。
树上拓扑序数量为 \(\frac{n!}{\prod siz_i}\) ,由于只要求模 \(2\) 的值,因此只要记录分子、分母中 \(2\) 的次幂即可。可以 \(O(n)\) dfs 一遍求出 \(siz\) ,时间复杂度 \(O(n^2)\) 。
\(2 \mid n\) 时只要枚举操作的第一条边即可,时间复杂度 \(O(n^3)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e2 + 7;
struct Graph {
vector<int> e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G, nG;
int a[N], val[N], siz[N], id[N], sctz[N];
bool chosen[N];
int n, ans;
int dfs(int u, int f) {
siz[u] = 1;
for (int v : nG.e[u]) {
if (v == f)
continue;
if (dfs(v, u) == -1)
return -1;
siz[u] += siz[v];
if (!chosen[v]) {
if (!chosen[u])
chosen[u] = true;
else
return -1;
}
}
return chosen[u] ^ 1;
}
inline void solve() {
int all = 0;
for (int i = 1; i <= n / 2; ++i)
all += __builtin_ctz(i);
for (int i = 1; i <= n; ++i) {
if (!val[i])
continue;
memset(chosen + 1, false, sizeof(bool) * n);
if (dfs(i, 0) == -1)
continue;
int cnt = all;
for (int j = 1; j <= n; ++j)
if (chosen[j])
cnt -= __builtin_ctz(siz[j] >> 1);
if (!cnt)
ans ^= 1;
}
}
signed main() {
scanf("%d", &n);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
for (int i = 1; i <= n; ++i)
sctz[i] = sctz[i - 1] + __builtin_ctz(i);
if (n & 1)
memcpy(val + 1, a + 1, sizeof(int) * n), nG = G, solve();
else {
--n;
for (int x = 1; x <= n; ++x)
for (int y : G.e[x])
if (x < y) {
for (int i = 1; i <= n + 1; ++i)
id[i] = i - (i > y);
id[y] = x, nG.clear(n);
for (int i = 1; i <= n + 1; ++i)
for (int j : G.e[i])
if (id[i] != id[j])
nG.insert(id[i], id[j]);
for (int i = 1; i <= n + 1; ++i)
val[id[i]] = a[i];
val[x] = !(a[x] & a[y]), solve();
}
}
printf("%d", ans);
return 0;
}
QOJ1261. Inv
给定 \(n, k\) ,求长度为 \(n\) 、逆序对数为 \(k\) 且满足 \(\forall i \in [1, n], p_{p_i} = i\) 的排列数量模 \(2\) 的值。
\(n \le 500\)
不难发现 \(p_{p_i} = i\) 等价于该排列与其逆排列相同,而该排列与逆排列拥有相同的逆序对数,这是因为原排列的每个逆序对 \((i, j)\) 与逆排列的每个逆序对 \((p_j, p_i)\) 一一对应。
因此答案即为长度为 \(n\) 、逆序对数为 \(i\) 的排列数量模 \(2\) 的值,因为原排列与逆排列不同的排列对消,不难背包 DP 做到 \(O(n^3)\) 。
考虑加强版:对于所有 \(k = 0, 1, \cdots, m\) 均求解以上问题,其中 \(n \le 10^9\) ,\(m \le \min(\binom{n}{2}, 5 \times 10^4)\) 。
写出答案的生成函数:
\[\begin{aligned} ans_{n, k} &= [x^k] \prod_{i = 1}^n \frac{1 - x^i}{1 - x} \\ &= [x^k] (1 - x)^{-n} \prod_{i = 1}^n (1 - x^i) \\ &= [x^k] \left( \sum_{i \ge 0} \binom{n - 1 + i}{i} x^i \right) \prod_{i = 1}^n (1 - x^i) \end{aligned} \]由 Kummer 定理,\(\binom{n - 1 + i}{i} \bmod 2 = [i \operatorname{and} (n - 1) = 0]\) ,因此可以直接求出前一个多项式。
由于 \(1 - x^i \equiv 1 + x_i \pmod{2}\) ,因此算二者卷积可以通过做 \(n\) 次 01 背包求得。由于 \(k \le m\) ,因此只要算 \(\min(n, m)\) 次即可,时间复杂度 \(O(\frac{m^2}{\omega})\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e2 + 7;
int f[N * N];
int n, k;
signed main() {
scanf("%d%d", &n, &k);
f[0] = 1;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= k; ++j)
f[j] ^= f[j - 1];
for (int j = k; j >= i; --j)
f[j] ^= f[j - i];
}
printf("%d", f[k]);
return 0;
}