毅毅:统计不是数学(断章取义 ed)。故删掉了数学标签。 其实并没有。
#A. 二分图染色
http://222.180.160.110:61235/contest/6181/problem/1
首先只看红色。显然一个左部点最多只能用红边连接一个右部点,反之同理。把左部点视为下标,其用红边相连的右部点视为值,则一个合法的方案为 n 的只保留一部分元素的排列。显然为 f(n)=n∑i=0Cin⋅Ain。
再加上蓝色,f2(n) 会给一条边涂两种颜色,故钦定有两种颜色的边数,容斥得到 n∑i=0(−1)i⋅Cin⋅Ain⋅f2(n−i)。发现 f 的计算可能需要优化一下。考虑已经知道 f(n−1),此时对于新增的第 n 对点:
- 任意连边(显然两个当中只能有一个点发出边),共有 2n−1 种方案,因为 (n,n) 会被算重。
- 不连,共 1 种方案。
- 发现 1 中可能连到已经有连边的点上了,新边的目的地有 n−1 个选项,目的地原本连接的点也有 n−1 个选项,去掉两边共 4 个点,非法的即为 (n−1)2⋅f(n−2)。
#include <bits/stdc++.h>
const int mod = 1e9 + 7;
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int n;
std::cin >> n;
std::vector<long long> f(n + 1), fac(n + 1), inv(n + 1);
f[0] = 1ll, f[1] = 2ll;
fac[0] = 1ll, fac[1] = 1ll, inv[0] = 1ll;
for (int i = 2; i <= n; ++i) {
fac[i] = fac[i - 1] * i % mod;
f[i] = (f[i - 1] * 2 * i % mod + mod - f[i - 2] * (i - 1) % mod * (i - 1) % mod) % mod;
}
auto qkp = [](long long x, int y) {
auto res = 1ll;
for (; y; (x *= x) %= mod, y >>= 1)
if (y & 1)
(res *= x) %= mod;
return res;
};
inv[n] = qkp(fac[n], mod - 2);
for (int i = n - 1; i; --i)
inv[i] = inv[i + 1] * (i + 1) % mod;
auto C = [&](int n, int m) {
return fac[n] * inv[n - m] % mod * inv[m] % mod;
};
auto A = [&](int n, int m) {
return fac[n] * inv[n - m] % mod;
};
long long res = 0ll;
for (int i = 0, k = 1; i <= n; ++i, k = mod - k)
(res += k * C(n, i) % mod * A(n, i) % mod * f[n - i] % mod * f[n - i]) %= mod;
std::cout << res << '\n';
return 0;
}
查看代码
#B. 七选五
http://222.180.160.110:61235/contest/6181/problem/2
首先 p 固定,钦定有 x 个数相等,有 Cxk 个方案,剩下的就是从 n−x 个元素里选出 k−x 个来错排,考虑钦定相同的个数来容斥:
k−x∑i=0(−1)i⋅Cik−x⋅Ak−x−in−x−i
#include <bits/stdc++.h>
const int mod = 1e9 + 7;
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int n, k, x;
std::cin >> n >> k >> x;
std::vector<long long> fac(n + 1), inv(n + 1);
fac[0] = 1ll, fac[1] = 1ll, inv[0] = 1ll;
for (int i = 2; i <= n; ++i)
fac[i] = fac[i - 1] * i % mod;
auto qkp = [](long long x, int y) {
auto res = 1ll;
for (; y; (x *= x) %= mod, y >>= 1)
if (y & 1)
(res *= x) %= mod;
return res;
};
inv[n] = qkp(fac[n], mod - 2);
for (int i = n - 1; i; --i)
inv[i] = inv[i + 1] * (i + 1) % mod;
auto C = [&](int n, int m) {
return fac[n] * inv[n - m] % mod * inv[m] % mod;
};
auto A = [&](int n, int m) {
return fac[n] * inv[n - m] % mod;
};
long long res = C(k, x), sum = 0ll;
for (int i = 0, p = 1; i <= k - x; ++i, p = mod - p)
(sum += p * C(k - x, i) % mod * A(n - x - i, k - x - i) % mod) %= mod;
std::cout << res * sum % mod << '\n';
return 0;
}
查看代码
#D. 硬币购物
http://222.180.160.110:61235/contest/6181/problem/4
每次多重背包会超时,考虑用钦定每种硬币是否超额来容斥。令 fs 表示凑出 s 的方案数(完全背包),那么 fs−c1×(d1+1) 就表示钦定第 1 种硬币超额时凑出 s 的方案数,以此类推。
#include <bits/stdc++.h>
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int c[4], T, m = 1e5;
std::vector<long long> f(m + 1);
f[0] = 1;
for (int i = 0; i < 4; ++i) {
std::cin >> c[i];
for (int j = c[i]; j <= m; ++j)
f[j] += f[j - c[i]];
}
std::cin >> T;
for (int d[4], m; T--; ) {
for (int i = 0; i < 4; ++i)
std::cin >> d[i];
std::cin >> m;
long long res = 0ll;
for (int i = 0; i < (1 << 4); ++i) {
int s = m;
for (int j = 0; j < 4; ++j)
if ((i >> j) & 1)
s -= (d[j] + 1) * c[j];
if (s >= 0) {
int k = (__builtin_popcount(i) & 1) ? -1 : 1;
res += k * f[s];
}
}
std::cout << res << '\n';
}
return 0;
}
查看代码
#E. Distinct Multiples
http://222.180.160.110:61235/contest/6181/problem/5
推式子题。题意转化为,给定 n 个位置可选的值集合,要求任意两个位置值不等,问方案数。
如果把两个位置取等视作在团上选中边,那么有朴素做法钦定选边的集合 S 然后容斥,考虑它们构成的连通块 {s}⊆S,有 res=∑S⊆V(−1)|S|⋅∏si⌊mlcmsi⌋。
有个很神奇的操作是把 S 丢掉,直接枚举 {s} 尝试子集 DP。有:res=∑{s}∏sf(s)⋅⌊mlcms⌋。其中 f(s) 表示选取一个 s 的导出子图的容斥系数之和,其中次数为导出子图中边数。
怎么把 f 算出来呢?容易发现其值只与 |s| 有关,考虑钦定与 1 连通的点数容斥,则有:
f(n)=n−1∑i=0(−1)i×Ci−1n−1×f(i)⋅m←i×(i−1)÷2∑j=0(−1)j⋅Cjm
我们知道,二项式定理取 a=1,b=−1 有 0m=[m=0]=∑mj=0(−1)j⋅Cjm,代入得:
f(n)=n−1∑i=0(−1)i×Cn−i−1n−1×f(n−i)⋅[m=0⟺i=1]=(1−n)⋅f(n−1)
然后就能线性求出。再用一个子集 DP,为了保证顺序枚举最小的未被确定的点所在连通块进行转移。
#include <bits/stdc++.h>
const int mod = 998244353;
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int n, l;
long long m;
std::cin >> n >> m, l = 1 << n;
std::vector<long long> d(n), f(n + 1), dp(l), g(l);
f[1] = 1ll;
for (int i = 1; i <= n; ++i) {
std::cin >> d[i - 1];
if (i >= 2)
f[i] = (1 + mod - i) * f[i - 1] % mod;
// printf("f[%d] = %lld\n", i, f[i]);
}
for (int i = 0; i < l; ++i) {
long long now = 1ll;
for (int j = 0; j < n; ++j)
if ((i >> j) & 1) {
if (now / std::__gcd(now, d[j]) > m / d[j])
goto nosol;
now = now / std::__gcd(now, d[j]) * d[j];
}
g[i] = (m / now) % mod;
// printf("lcm = %lld, g[%d] = %lld\n", now, i, g[i]);
nosol: ;
}
dp[0] = 1ll;
auto lowbit = [](int x) {
return x & -x;
};
for (int i = 1; i < l; ++i) {
int to = std::__lg(lowbit(i));
for (int j = i; j; j = (j - 1) & i)
if ((j >> to) & 1)
(dp[i] += dp[i ^ j] * f[__builtin_popcount(j)] % mod * g[j] % mod) %= mod;
}
std::cout << dp[l - 1] << '\n';
return 0;
}
查看代码
#F. Permutation
http://222.180.160.110:61235/contest/6181/problem/6
如果全是 <
,方案数为 1。
现在把其中一些 <
变成 ?
,比如 <<??<?<<<<
的方案数,太好了是多重集排列,我们没救了 显然被 <
连接起来的一段元素只有一种排列方式,所以可以视为多重集排列,方案数为 11!3!×2!×5!。
似乎只需要枚举把 >
变成 <
或 =
的 2k 种情况再计算就可以了,可惜 k 有点大。但我们发现它在一定程度上是没有后效性的,比如 <<??<
和 <<?<<
,前面的 <<
不会对后面的内容带来影响。
故令 fi 表示对于前 i 个元素的方案数,枚举最后一个被钦定为 ?
的 >
j(即 [j+1,i−1] 间的 >
都被替换为 <
),带上容斥系数,有 fi=∑sj='>'(−1)cnt'>'[j+1,i−1]×fj×1(i−j)!。
#include <bits/stdc++.h>
const int mod = 1e9 + 7;
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int n;
std::cin >> n;
std::vector<long long> fac(n + 1), inv(n + 1);
fac[0] = 1ll, inv[0] = 1ll;
for (int i = 1; i <= n; ++i)
fac[i] = fac[i - 1] * i % mod;
auto qkp = [](long long x, int y) {
auto res = 1ll;
for (; y; (x *= x) %= mod, y >>= 1)
if (y & 1)
(res *= x) %= mod;
return res;
};
inv[n] = qkp(fac[n], mod - 2);
for (int i = n - 1; i; --i)
inv[i] = inv[i + 1] * (i + 1) % mod;
std::vector<char> s(n + 1);
for (int i = 1; i <= n; ++i)
std::cin >> s[i];
std::vector<long long> f(n + 1);
s[0] = '>', f[0] = 1ll;
for (int i = 1; i <= n; ++i) {
int cnt = 0;
for (int j = i - 1; ~j; --j)
if (s[j] == '>') {
long long k = (cnt & 1) ? mod - 1 : 1;
(k *= f[j]) %= mod, (k *= inv[i - j]) %= mod;
(f[i] += k) %= mod;
++cnt;
}
// printf("f[%d] = %lld\n", i, f[i]);
}
std::cout << f[n] * fac[n] % mod << '\n';
return 0;
}
查看代码
忳郁邑余侘傺兮,余独穷困乎此时也。
#H. ~K Perm Counting
http://222.180.160.110:61235/contest/6181/problem/8
考虑钦定令几个元素不满足条件进行容斥,即答案为 n∑i=0(−1)i⋅f(i)。但我们发现 [n−k,n+k] 都有两个不能选的值,直接取 f(i)=∏2 肯定会选到相同值,一个自然(?)的想法是再容斥一遍,可惜手玩一下发现似乎容不动。考虑把玩意儿拍在棋盘上:

其中 × 是非法格子,边是非法格子间的不同选关系,同颜色的边隶属同一条链。容易发现如果棋盘继续扩大,这些链还会继续延长。
会发现这些链互不干扰,就是说我选了这条链上的某个点和链外的点没有任何关系(显然)。把这些链首尾相连拼起来,要做的就是拼接处可以选相邻,其余位置不能选相邻,选出来 i 个的方案数。提前预处理出来整个序列,令 tagj 表示 j 是否能和 j−1 同选,设 dpj,i,0/1 表示 DP 到了 j,已经选了 i 个数,第 j 个元素(不)选的方案数,那么有:
dpj,i,0=dpj−1,i,0+dpj−1,i,1dpj,i,1={dpj−1,i−1,1+dpj−1,i−1,0tagj=1dpj−1,i,0otherwise
大力 DP 即可。f(i) 即为 (n−i)!×(dpm,i,0+dpm,i,1),其中 m 为总链长。
注意不滚动可能会 MLE
#include <bits/stdc++.h>
const int mod = 924844033;
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int n, k;
std::cin >> n >> k;
std::vector<int> tag(1);
std::vector<long long> fac(n + 1);
std::vector<std::vector<int> > vis(n + 1, std::vector<int> (n + 1));
auto calc = [&](int i, int j, int s) {
tag.push_back(1);
for (; ; s ^= 1) {
// printf("(%d, %d) ", i, j);
vis[i][j] = 1;
if (!s) {
i = j + k;
if (i <= n)
tag.push_back(0);
else
break;
}
else {
j = i + k;
if (j <= n)
tag.push_back(0);
else
break;
}
}
// puts("");
return;
};
fac[0] = 1ll;
for (int i = 1; i <= n; ++i) {
fac[i] = fac[i - 1] * i % mod;
int j = i - k;
if (j >= 1 && !vis[i][j])
calc(i, j, 1);
j = i + k;
if (j <= n && !vis[i][j])
calc(i, j, 0);
}
int m = (int)tag.size() - 1;
// printf("m = %d\n", m);
std::vector<std::vector<std::vector<long long> > > dp(2, std::vector<std::vector<long long> > (n + 1, std::vector<long long> (2)));
dp[0][0][0] = 1ll;
for (int j = 1, now = 1; j <= m; ++j, now ^= 1) {
for (int i = 0; i <= n && i <= j; ++i) {
dp[now][i][0] = dp[!now][i][0];
dp[now][i][1] = 0ll;
if (i) {
(dp[now][i][0] += dp[!now][i][1]) %= mod;
dp[now][i][1] = dp[!now][i - 1][0];
if (tag[j]) {
(dp[now][i][1] += dp[!now][i - 1][1]) %= mod;
// assert(0);
}
}
// printf("dp[%d][%d] = %lld / %lld\n", j, i, dp[j][i][0], dp[j][i][1]);
}
}
long long res = 0ll;
for (int i = 0, p = 1; i <= n; ++i, p = mod - p)
(res += p * fac[n - i] % mod * (dp[m & 1][i][0] + dp[m & 1][i][1]) % mod) %= mod;
std::cout << res << '\n';
return 0;
}
查看代码
#A - Positions in Permutations
https://codeforces.com/problemset/problem/285/E
(看向上一题)这照片是你吗?
在上一题的基础上,令 k=1。但是有个小小的问题——所求的「至少」不为 0,无法简单容斥。具体地,如果一个排列一共有 p 个非法排列,那么它会被 f(i) 统计 Cip 次。令 g(p) 表示非法格子数恰好为 p 的排列的真实数量,则 f(i)=n∑j=iCij⋅g(j),二项式反演即可得到真实值 g(m)=n∑j=m(−1)j−m⋅Cmj⋅f(j)。
#include <bits/stdc++.h>
const int mod = 1e9 + 7;
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int n, k;
std::cin >> n >> k;
std::vector<int> tag(1);
std::vector<long long> fac(n + 1), inv(n + 1);
std::vector<std::vector<int> > vis(n + 1, std::vector<int> (n + 1));
auto calc = [&](int i, int j, int s) {
tag.push_back(1);
for (; i <= n && j <= n; s ^= 1) {
vis[i][j] = 1;
if (!s) {
i = j + 1;
if (i <= n)
tag.push_back(0);
}
else {
j = i + 1;
if (j <= n)
tag.push_back(0);
}
}
return;
};
auto qkp = [&](long long x, int y) {
auto res = 1ll;
for (; y; (x *= x) %= mod, y >>= 1)
if (y & 1)
(res *= x) %= mod;
return res;
};
fac[0] = 1ll, inv[0] = 1ll;
for (int i = 1; i <= n; ++i) {
fac[i] = fac[i - 1] * i % mod;
int j = i - 1;
if (j >= 1 && !vis[i][j])
calc(i, j, 1);
j = i + 1;
if (j <= n && !vis[i][j])
calc(i, j, 0);
}
inv[n] = qkp(fac[n], mod - 2);
for (int i = n - 1; i; --i)
inv[i] = inv[i + 1] * (i + 1) % mod;
int m = (int)tag.size() - 1;
std::vector<std::vector<std::vector<long long> > > dp(2, std::vector<std::vector<long long> > (n + 1, std::vector<long long> (2)));
dp[0][0][0] = 1ll;
for (int j = 1, now = 1; j <= m; ++j, now ^= 1) {
for (int i = 0; i <= n && i <= j; ++i) {
dp[now][i][0] = dp[!now][i][0];
dp[now][i][1] = 0ll;
if (i) {
(dp[now][i][0] += dp[!now][i][1]) %= mod;
dp[now][i][1] = dp[!now][i - 1][0];
if (tag[j])
(dp[now][i][1] += dp[!now][i - 1][1]) %= mod;
}
}
}
auto C = [&](int n, int m) {
return fac[n] * inv[n - m] % mod * inv[m] % mod;
};
long long res = 0ll;
for (int i = k, p = 1; i <= n; ++i, p = mod - p) {
// printf("%d: %lld\n", i, C(i, k) * fac[n - i] % mod * (dp[m & 1][i][0] + dp[m & 1][i][1]) % mod);
(res += p * C(i, k) % mod * fac[n - i] % mod * (dp[m & 1][i][0] + dp[m & 1][i][1]) % mod) %= mod;
}
std::cout << res << '\n';
return 0;
}
查看代码
#D - All Pairs Similarity P
https://www.luogu.com.cn/problem/P11458
省流:求 ∀i,fi=n∑j=1|ai∩aj||ai∪aj|。
首先分离常数,有:
fi=n∑j=1|ai∩aj||ai∪aj|=n∑j=1|ai|+|aj|−|ai∪aj||ai∪aj|=n∑j=1|ai|+|aj||ai∪aj|−1
尽量把分子变得更简:
fi=n∑j=1|ai|+|aj||ai∪aj|−1=|ai|⋅(n∑j=11|ai∪aj|)−n+n∑j=1|aj||ai∪aj|
问题转化为求解 n∑j=11|ai∪aj| 和 n∑j=1|aj||ai∪aj|,以 ∗=n∑j=11|ai∪aj| 为例。令 bi 为 ai 补集,则:
∗=n∑j=11|ai∪aj|=n∑j=11k−|bi∩bj|
为什么要做这个转换呢?相比起并集运算,交集运算有着优秀的性质:s⊆(bi∩bj)⟺s⊆bi∧s⊆bj,直接取或当然也有相似的性质,但是太烧脑了。
基于这个性质,我们有一个想法:对于所有 j,在 ∀s⊆bj 处放置 1k−|s| 的贡献;对于 i,将 ∀s⊆bi 处的贡献求和。但是这样肯定会拿到很多我们不想要的贡献,例如 ∀s⊂(bi∩bj)。
考虑精细布置贡献——构造 g(|s|) 满足 n∑j=1∑s⊆bjg(|s|)=∗。
这里可以二项式反演得到 g,具体地,令 F(|S|)=1k−|S|=∑s⊆Sg(|s|)=|S|∑j=0Cj|S|g(j),则 g(i)=i∑j=0Cji⋅(−1)i−jk−j。
再令 h(s)=∑bj⊇sg(|s|)=g(|s|)⋅∑bj⊇s1,那么 h 就是高维后缀和。我们正在做的事情就是求解 ∗=∑s⊆bih(s),这就又是一个高维前缀和了。
对于 n∑j=1|aj||ai∪aj| 呢,令 h(s)=∑bj⊇sg(|s|)⋅|aj|=g(|s|)⋅∑bj⊇sk−|bj|,改变高维后缀和求和对象即可。
复杂度就是 O(n+k⋅2k),其中 k⋅2k 来自整体高维前 / 后缀和,n⋅k 来自枚举 i。
#include <bits/stdc++.h>
const int mod = 1e9 + 7;
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int n, k, l;
std::cin >> n >> k, l = 1 << k;
std::vector<int> a(n + 1), b(n + 1), cnt(l);
for (int i = 1; i <= n; ++i) {
std::cin >> a[i], b[i] = (l - 1) ^ a[i];
++cnt[b[i]];
}
std::vector<long long> g(k + 1), fac(k + 1);
auto qkp = [&](long long x, int y = mod - 2) {
auto res = 1ll;
for (; y; (x *= x) %= mod, y >>= 1)
if (y & 1)
(res *= x) %= mod;
return res;
};
auto C = [&](int n, int m) {
return fac[n] * qkp(fac[n - m]) % mod * qkp(fac[m]) % mod;
};
fac[0] = 1ll;
for (int i = 1; i <= k; ++i)
fac[i] = fac[i - 1] * i % mod;
for (int i = 0; i <= k; ++i) {
for (int j = 0, p = (i & 1) ? mod - 1 : 1; j <= i; ++j, p = mod - p)
(g[i] += C(i, j) * p % mod * qkp(k - j) % mod) %= mod;
// printf("g[%d] = %lld\n", i, g[i]);
}
// for (int i = 0; i <= k; ++i) {
// long long F = 0ll;
// for (int j = 0; j <= i; ++j)
// (F += C(i, j) * g[j] % mod) %= mod;
// printf("%d: %lld / %lld\n", i, F, qkp(k - i));
// }
std::vector<long long> h(l);
std::copy(cnt.begin(), cnt.end(), h.begin());
for (int i = 0; i < k; ++i)
for (int j = l - 1; ~j; --j)
if (!((j >> i) & 1))
(h[j] += h[j ^ (1 << i)]) %= mod;
auto f1(h);
for (int i = 0; i < l; ++i)
(f1[i] *= g[__builtin_popcount(i)]) %= mod;
for (int i = 0; i < k; ++i)
for (int j = 0; j < l; ++j)
if ((j >> i) & 1)
(f1[j] += f1[j ^ (1 << i)]) %= mod;
for (int i = 0; i < l; ++i)
h[i] = cnt[i] * (k - __builtin_popcount(i));
for (int i = 0; i < k; ++i)
for (int j = l - 1; ~j; --j)
if (!((j >> i) & 1))
(h[j] += h[j ^ (1 << i)]) %= mod;
auto f2(h);
for (int i = 0; i < l; ++i)
(f2[i] *= g[__builtin_popcount(i)]) %= mod;
for (int i = 0; i < k; ++i)
for (int j = 0; j < l; ++j)
if ((j >> i) & 1)
(f2[j] += f2[j ^ (1 << i)]) %= mod;
for (int i = 1; i <= n; ++i) {
// fprintf(stderr, "%d: %d * %lld - %d + %lld\n", i, __builtin_popcount(a[i]), f1[b[i]], n, f2[b[i]]);
std::cout << (__builtin_popcount(a[i]) * f1[b[i]] % mod + mod - n + f2[b[i]]) % mod << '\n';
}
return 0;
}
查看代码
#E - Prefix XORs
https://atcoder.jp/contests/arc137/tasks/arc137_d
省流:做 k 次前缀和,k=1,2,⋯,m,分别询问:令 xi←ai 对 sn 的贡献次数,则 ∑ai⋅(ximod2)?
对于 ap,容易发现第一轮其对任意 sq 有 1 次贡献,拉开来就是常数列;第二轮有 (q−p+1) 次贡献,是公差为 1 的等差数列;第三轮是类三角形数——至此,肯定和组合数有关了。手玩可以发现第 k 轮 ap 对 sn 的贡献为 Cn−pn−p+(k−2),尽量令其中一个不动,得到 Ck−1n−p+(k−2)。
但是我们发现直接枚举每轮每个数会起飞,而且模数为 2 似乎只能 Lucas(再带个 log),怎么办呢?
那就 Lucas 呗。由其观察容易发现 Ck−2n−p+(k−2)mod2=1 当且仅当 (k−2)⊆(n−p+k−2)⟺(k−2)⊆∁U(n−p),故问题转化为高维后缀和,当然你也可以做一次 and-FWT,那么 resk=∑(Ck−2n−p+(k−2)mod2)⋅ap=Sk−2,其中 S 为高维后缀和,初值为 S∁u(n−i)←ai。
#include <bits/stdc++.h>
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int n, m, l, k;
std::cin >> n >> m, k = std::__lg(n + m) + 1, l = 1 << k;
std::vector<int> s(l);
for (int i = 1; i <= n; ++i)
std::cin >> s[l - 1 - (n - i)];
for (int i = 0; i < k; ++i)
for (int j = l - 1; ~j; --j)
if (!((j >> i) & 1))
s[j] ^= s[j ^ (1 << i)];
for (int i = 1; i <= m; ++i)
std::cout << s[i - 1] << ' ';
std::cout << '\n';
return 0;
}
查看代码
#B - The Top Scorer
https://codeforces.com/problemset/problem/1096/E
鉴于每种方案等概率(翻译没说,但原题面有提到),考虑计算小明能够取胜的方案数,再对总方案数做除法。先枚举最高分 k≥r,再枚举包括小明在内恰有 c≥1 个人 拿到 k 分。但剩下的人呢?显然是不能插板随机分配的,因为存在 <k 的限制。
考虑容斥,钦定剩下的 p−c 个人中拿到 ≥k 分的人数后再可空地插板即可,则总方案数为:
s∑k=rp∑c=1Cc−1p−1c⋅p−c∑i=0Cip−c⋅Cp−c−1s−k⋅(c+i)+(p−c−1)
其中分母上的 c 来源于等概率分配给最高分,虽然不是整数,但也代表着「小明获胜可行方案数」。最后将答案除上总方案数 Cp−1s−r+p−1 即可。
#include <bits/stdc++.h>
const int mod = 998244353;
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int p, s, r;
std::cin >> p >> s >> r;
std::vector<long long> fac(s + p), inv(s + p);
fac[0] = inv[0] = 1ll;
for (int i = 1; i < s + p; ++i) {
fac[i] = fac[i - 1] * i % mod;
}
auto qkp = [&](long long x, int y) {
auto res = 1ll;
for (; y; (x *= x) %= mod, y >>= 1)
if (y & 1)
(res *= x) %= mod;
return res;
};
inv.back() = qkp(fac.back(), mod - 2);
for (int i = s + p - 2; i > 0; --i)
inv[i] = inv[i + 1] * (i + 1) % mod;
auto C = [&](int n, int m) {
if (n == -1 && m == -1)
return 1ll;
return fac[n] * inv[n - m] % mod * inv[m] % mod;
};
auto res(0ll);
for (int k = r; k <= s; ++k)
for (int c = 1; c <= p; ++c)
if ((p - c) * (k - 1) + c * k >= s) {
auto t(0ll);
for (int i = 0, now = 1; i <= p - c && k * (c + i) <= s; ++i, now = mod - now)
(t += now * C(p - c, i) % mod * C(s - k * (c + i) + (p - c - 1), p - c - 1) % mod) %= mod;
(res += C(p - 1, c - 1) * qkp(c, mod - 2) % mod * t % mod) %= mod;
}
std::cout << res * qkp(C(s - r + p - 1, p - 1), mod - 2) % mod << '\n';
return 0;
}
查看代码
#C - Sky Full of Stars
https://codeforces.com/problemset/problem/997/C
发现用 3n×n 减去任意一行一列不同色的方案就是答案。
考虑一元容斥,如令 fi 表示至少 i 行 i 列同色的方案,但会发现 f0 中包含 0 行 1 列同色等与预期不符的情况。受此启发考虑令 fi,j 表示至少 i 行 j 列同色进行二元容斥。容易发现当 i×j≠0 时,被选中的 i 行 j 列全部连通,应为同一种颜色;将它们挪到角落,可以发现未被选中的格子共有 (n−i)×(n−j) 个。故有:
fi,j={Cjn×3j×3n×(n−j)i=0Cin×3i×3n×(n−i)j=0Cin×Cjn×3×3(n−i)×(n−j)otherwise
令 gi,j 表示恰好 i 行 j 列同色的方案数,那么答案为 3n×n−g0,0。二项式反演 / 容斥原理得 g0,0=n∑i=0n∑j=0(−1)i+j⋅fi,j。很惊讶地发现这是 O(n2) 的!考虑优化。把 f1→n,1→n 合并同类项,得到:
g0,0=[n∑j=0(−1)j⋅f0,j]+n∑i=1(−1)i⋅fi,0+n∑j=1(−1)i+j⋅Cin×Cjn×3(n−i)×(n−j)+1=[n∑j=0(−1)j⋅f0,j]+n∑i=1(−1)i⋅fi,0+(−1)i⋅Cin×3n2−n×i+1×n∑j=1(−1)j⋅Cjn×3j×(−n+i)=[n∑j=0(−1)j⋅f0,j]+n∑i=1(−1)i⋅fi,0+(−1)i⋅Cin×3n2−n×i+1×[(n∑j=0(−1)j⋅Cjn×(3−n+i)j)−1]=[n∑j=0(−1)j⋅f0,j]+n∑i=1(−1)i⋅fi,0+(−1)i⋅Cin×3n2−n×i+1×[(−3−n+i+1)n−1]
由此便可 O(nlogn) 计算。担心超时可以把所有 3−n+i 和 3n×i 线性预处理出来,复杂度不会变就是了。
这里解释一下最后一步的二项式定理,非常遗憾地发现 −1 和 3−n+i 都是 j 次的,没办法把二者相加减做二项式定理;但由于次数相同,这提示我们可以把 −1 乘到 3−n+i 里去,给每一项配上 1n−k 就可以做 −3n−i(注意不是 (−3)n−i) 和 1 的二项式定理。
#include <bits/stdc++.h>
const int mod = 998244353;
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
long long n;
std::cin >> n;
std::vector<long long> fac(n + 1), inv(n + 1), invn(n + 1), invi(n + 1), powi(n + 1), pown(n + 1);
auto qkp = [&](long long x, long long y) {
auto res(1ll);
for (; y; (x *= x) %= mod, y >>= 1)
if (y & 1)
(res *= x) %= mod;
return res;
};
const long long pnn = qkp(3ll, n * n), pn = qkp(3ll, n);
fac[0] = inv[0] = invn[0] = invi[0] = powi[0] = pown[0] = 1ll;
for (int i = 1; i <= n; ++i) {
fac[i] = fac[i - 1] * i % mod;
powi[i] = powi[i - 1] * 3 % mod;
pown[i] = pown[i - 1] * pn % mod;
}
inv[n] = qkp(fac[n], mod - 2);
invi[n] = qkp(powi[n], mod - 2);
invn[n] = qkp(pown[n], mod - 2);
for (int i = n - 1; i; --i) {
inv[i] = inv[i + 1] * (i + 1) % mod;
invi[i] = invi[i + 1] * 3 % mod;
invn[i] = invn[i + 1] * pn % mod;
}
auto C = [&](int n, int m) {
return fac[n] * inv[n - m] % mod * inv[m] % mod;
};
long long g = 0ll;
auto f = [&](int i, int j) { // make sure (!i || !j)
if (i == 0 && j == 0)
return pnn;
else if (i == 0)
return C(n, j) * powi[j] % mod * pown[n - j] % mod;
return C(n, i) * powi[i] % mod * pown[n - i] % mod;
};
for (int j = 0, p = 1; j <= n; ++j, p = mod - p)
(g += p * f(0, j)) %= mod;
for (int i = 1, p = mod - 1; i <= n; ++i, p = mod - p)
(g += p * f(i, 0) % mod + p * C(n, i) % mod * pnn % mod * invn[i] % mod * 3 % mod * (qkp(mod - invi[n - i] + 1, n) + mod - 1) % mod) %= mod;
std::cout << (pnn + mod - g) % mod << '\n';
return 0;
}
查看代码
#D - 去 M / NoM
https://www.luogu.com.cn/problem/P11316
假设 f(i) 为至少有 i 对不合法元素的方案数,则容斥得到答案 n∑i=0(−1)i⋅f(i)。考虑怎么计算 f(i)。
M 整除 dis(x,y)⟺(posx−posy)modM=0⟺posx≡posy(modM)。
考虑把关于 M 同余的位置放在一个组,也就是说需要让一对元素不能选同一组的位置。
考虑容斥,令 f(i) 表示至少有 i 对点选到一组的方案数,考虑怎么计算。假设我们要在第 k 组(size 为 sk)中选出 x 对位置,实际上只需要选择 2x 个位置然后任意分配给这 x 对数,即 A2xsk。设 dpi,j 表示 DP 到第 i 个组,已经选了 j 对,那么有 dpi,j=j∑k=0Cj−kn−(j−k)×dpi−1,j−k×A2ksi。乍一看好像是 O(n3) 的,但是别忘了 ∑si=2n,所以只有 O(n2)。f(i) 即为 dpm,i×(2n−2i)!。
#include <bits/stdc++.h>
const int mod = 1e9 + 7;
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int n, m;
std::cin >> n >> m;
std::vector<int> s(m + 1);
std::vector<long long> fac(2 * n + 1), inv(2 * n + 1);
fac[0] = inv[0] = 1ll;
for (int i = 1; i <= 2 * n; ++i)
++s[i % m + 1], fac[i] = fac[i - 1] * i % mod;
auto qkp = [](long long x, int y) {
auto res = 1ll;
for (; y; (x *= x) %= mod, y >>= 1)
if (y & 1)
(res *= x) %= mod;
return res;
};
inv[2 * n] = qkp(fac[2 * n], mod - 2);
for (int i = 2 * n - 1; i; --i)
inv[i] = inv[i + 1] * (i + 1) % mod;
auto A = [&](int n, int m) {
return fac[n] * inv[n - m] % mod;
};
auto C = [&](int n, int m) {
return fac[n] * inv[n - m] % mod * inv[m] % mod;
};
std::vector<std::vector<long long> > dp(m + 1, std::vector<long long> (n + 1));
dp[0][0] = 1ll;
for (int i = 1; i <= m; ++i)
for (int j = 0; j <= n; ++j) {
for (int k = 0; k <= j && 2 * k <= s[i]; ++k)
(dp[i][j] += C(n - (j - k), k) * dp[i - 1][j - k] % mod * A(s[i], 2 * k) % mod) %= mod;
// printf("s = %d, dp[%d][%d] = %lld\n", s[i], i, j, dp[i][j]);
}
long long res = 0ll;
for (int i = 0, p = 1; i <= n; ++i, p = mod - p)
(res += p * dp[m][i] % mod * fac[2 * n - 2 * i] % mod) %= mod;
std::cout << res << '\n';
return 0;
}
查看代码
#E - 「KDOI-11」彩灯晚会
https://www.luogu.com.cn/problem/P11292
考虑 cnti2 的含义,即在所有颜色为 i、长度为 l 的链中有放回地选两次的方案数。
显然复杂度里是不能包含 k 的,所以放弃直接统计 cnti 的想法。显然每种颜色是等价的,考虑计算选择两条链可以给答案带来的贡献。我们发现答案的形式是个和式,对于两条同色链 p,q,假设颜色为 i,那么会对 cnti2 带来 kn−2l−|p∩q| 的贡献,对总答案就会带来 kn−2l−|p∩q|+1 的贡献。
然而如果要枚举计算 |p∩q| 就无法避免 O(n5),考虑更有潜力的方法:将问题转化为对于每个 x,求交集大小恰好为 x 的链对个数。然而「恰好」是不方便计算的——假如当前两链在 u 次重合,如果我假设下一次在 v 次重合,就需要保证在 u,v 之间不能重合——这并不好操作。二项式反演将恰好转化为至少,则限制减弱。
令 fu,c,l1,l2 表示 p,q 当前在 u 处重合,视野内的 p,q 长度为 l1,l2,至少已经重合了 c 次的方案数。预处理出走到 u 步数为 l1,l2 的方案数为初始值。枚举可能的后续重合点 v(满足 v 的拓扑序 >u)有 fv,c+1,l′1,l′2←fv,c+1,l′1,l′2+fu,c,l1,l2。预处理出 u→v 长度为 Δ 的方案数(这是 O(n3l) 的)优化转移,在算出 nexu,l 表示从 u 出发走 l 步的方案数处理答案,则 F(i)=∑u,l1,l2fu,l1,l2⋅nexu,l−l1⋅nexu,l−l2,枚举 u,c,l1,l2,v,l′1,l′2,就可以达到优秀的 O(n2l5+n3l)!简直是令人震撼
给出第一个优化:发现 l1 和 l2 的转移彼此不干扰,考虑建立一个临时数组 g,先从 fu,c 转移 l1 到 g,再从 g 转移 l2 到 fv,c+1,则复杂度降为 O(n2l4+n3l),仍不足以通过。
DP 过程上的优化已经万策尽,考虑从式子本身消元减少 DP 维度入手。令 h(i) 表示交集大小恰好恰好为 i 的方案数,则此时答案式为 l∑i=0kn−2l+i+1×h(i)。又 F(i)=l∑j=iCij⋅h(j),二项式反演得:
res=l∑i=0kn−2l+i+1⋅l∑j=i(−1)j−i⋅Cij⋅F(j)=kn−2l+1⋅l∑j=0j∑i=0ki⋅(−1)j−i⋅Cij⋅F(j)=kn−2l+1⋅l∑j=0(k−1)j⋅F(j)
注意这里利用了二项式反演的系数可以和交换求和顺序后的 i 次项(或 j−i 次项,参见 Sky Full of Stars 中最后一步的处理)组成二项式定理的特点,以便基于式子结构尽可能消元。
那么此时答案式已经和 c 无关,可以丢掉 c 这一维,和 c 有关的计算已经在转移时处理了。则 fv,l′1,l′2=∑fu,l1,l2×(k−1),复杂度降为 O(n2l3+n3l)。
记得还要算上 F(0),即任选一条合法链的方案数平方。
// 兄弟你好香
// 兄弟你是依托打分,我踏马吃吃吃吃吃
#include <bits/stdc++.h>
const int mod = 998244353;
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen("party3.in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
using arr = std::vector<long long>;
using arrr = std::vector<arr>;
using arrrr = std::vector<arrr>;
int n, k, l, m;
std::cin >> n, std::cin >> n >> k >> l >> m;
std::vector<int> deg(n + 1), id;
std::vector<std::vector<std::pair<int, int> > > g(n + 1);
for (int x, y, c; m--; ) {
std::cin >> x >> y >> c;
++deg[y], g[x].emplace_back(y, c);
}
arrrr to(n + 1, arrr(n + 1, arr(l + 1)));
arrr pre(n + 1, arr(l + 1)), nex(n + 1, arr(l + 1));
{
std::queue<int> q;
for (int i = 1; i <= n; ++i)
if (!deg[i])
q.push(i);
for (int u; !q.empty(); ) {
u = q.front(), q.pop();
to[u][u][0] = 1ll, id.push_back(u);
for (auto i : id)
for (int j = 0; j <= l; ++j) {
(pre[u][j] += to[i][u][j]) %= mod;
// printf("to[%d][%d][%d] = %lld\n", i, u, j, to[i][u][j]);
}
// for (int j = 0; j <= l; ++j)
// printf("pre[%d][%d] = %lld\n", u, j, pre[u][j]);
for (auto [v, c] : g[u]) {
for (auto i : id)
for (int j = 1; j <= l; ++j)
(to[i][v][j] += to[i][u][j - 1] * c) %= mod;
if (!--deg[v])
q.push(v);
}
}
for (int u = 1; u <= n; ++u)
for (int v = 1; v <= n; ++v)
for (int i = 0; i <= l; ++i)
(nex[u][i] += to[u][v][i]) %= mod;
}
arrrr f(n + 1, arrr(l + 1, arr(l + 1)));
{
for (int i = 1; i <= n; ++i)
for (int l1 = 1; l1 <= l; ++l1)
for (int l2 = 1; l2 <= l; ++l2)
f[i][l1][l2] = pre[i][l1 - 1] * pre[i][l2 - 1] % mod * (k - 1) % mod;
for (auto i = 0; i < n; ++i) {
int u = id[i];
// for (int l1 = 1; l1 <= l; ++l1)
// for (int l2 = 1; l2 <= l; ++l2)
// printf("f[%d][%d][%d] = %lld\n", u, l1, l2, f[u][l1][l2]);
for (auto j = i + 1; j < n; ++j) {
arrr g(l + 1, arr(l + 1));
int v = id[j];
for (int l1 = 1; l1 < l; ++l1)
for (int _l1 = l1 + 1; _l1 <= l; ++_l1) {
auto K = to[u][v][_l1 - l1];
if (K)
for (int l2 = 1; l2 < l; ++l2)
(g[_l1][l2] += f[u][l1][l2] * K) %= mod;
}
for (int _l1 = 2; _l1 <= l; ++_l1)
for (int l2 = 1; l2 < l; ++l2)
if (g[_l1][l2])
for (int _l2 = l2 + 1; _l2 <= l; ++_l2)
(f[v][_l1][_l2] += g[_l1][l2] * to[u][v][_l2 - l2] % mod * (k - 1)) %= mod;
}
}
}
auto qkp = [](long long x, int y) {
auto res(1ll);
for (; y; (x *= x) %= mod, y >>= 1)
if (y & 1)
(res *= x) %= mod;
return res;
};
auto res(0ll);
for (int u = 1; u <= n; ++u)
for (int v = 1; v <= n; ++v)
(res += to[u][v][l - 1]) %= mod;
(res *= res) %= mod;
for (int u = 1; u <= n; ++u)
for (int l1 = 1; l1 <= l; ++l1)
for (int l2 = 1; l2 <= l; ++l2)
(res += f[u][l1][l2] * nex[u][l - l1] % mod * nex[u][l - l2] % mod) %= mod;
res = res % mod * (qkp(k, n + 1) * qkp(qkp(k, 2 * l), mod - 2) % mod) % mod;
std::cout << res << '\n';
return 0;
}
查看代码
#G - 小星星
https://www.luogu.com.cn/problem/P3349
首先考虑比较暴力的做法,那么有 fi,j,S 表示在 i 这个子树里面选了集合 S,且 i 的颜色为 j 的方案数,维数里之所以有 j 是为了满足连边限制。
然后树上子集 DP,发现是 O(n4⋅2n) 的,
#CF785D Anton and School - 2
https://codeforces.com/problemset/problem/785/D
容易想到枚举每个 (
作为分界点的情况,那么钦定当前枚举的 (
是要选的。对于当前 (
,若其左边(不含)的 (
有 n 个而右边的 )
有 m 个,枚举除了当前 (
还要选 i 个 (
,那么答案为 ∑i=0Ci−1n⋅Cim。
发现这个形式可以范德蒙德卷积:∑i=0Ci−1n⋅Cim=∑i=0Cn−i+1n⋅Cim=Cn+1n+m。
关于怎么记忆范德蒙德卷积,发现上下相加,也可以从组合意义记忆:在 n 个球中选出 k−i 个球,再从 m 个球中选出 i 个球的总方案就是从 n+m 个球中直接选出 k 个球的方案。
注意判断右侧没有 )
的时候贡献为 0。
#include <bits/stdc++.h>
const int mod = 1e9 + 7;
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
std::string s;
std::cin >> s;
int n = s.length();
s = " " + s;
std::vector<int> cnt1(n + 1), cnt2(n + 1);
std::vector<long long> fac(n + 1), inv(n + 1);
fac[0] = inv[0] = 1ll;
for (int i = 1; i <= n; ++i) {
fac[i] = fac[i - 1] * i % mod;
cnt1[i] = cnt1[i - 1] + (s[i] == '(');
}
auto qkp = [](long long x, int y) {
auto res(1ll);
for (; y; (x *= x) %= mod, y >>= 1)
if (y & 1)
(res *= x) %= mod;
return res;
};
inv[n] = qkp(fac[n], mod - 2);
cnt2[n] = (s[n] == ')');
for (int i = n - 1; i; --i) {
cnt2[i] = cnt2[i + 1] + (s[i] == ')');
inv[i] = inv[i + 1] * (i + 1) % mod;
}
long long res = 0ll;
auto C = [&](int n, int m) {
if (n < m)
return 0ll;
return fac[n] * inv[n - m] % mod * inv[m] % mod;
};
for (int i = 1; i <= n; ++i)
if (s[i] == '(')
(res += C(cnt1[i] + cnt2[i] - 1, cnt1[i])) %= mod;
std::cout << res << '\n';
return 0;
}
查看代码
#CF1332E Height All the Same
https://codeforces.com/problemset/problem/1332/E
容易发现第一个操作是用来改变两个数奇偶性的;而第二个操作能在全图奇偶性相同的任何情况下达成要求。
注意到如果我们想同时仅改变任意两个数的奇偶性,可以在棋盘上任找一条路径一路使用操作一。只要某种奇偶性的元素个数共有偶数个,就能通过若干次操作一把它们全部变成另一种奇偶性。
令 K1 为 L∼R 中奇数的个数,K2 为偶数,那么有:
res=nm∑i=0Cinm×K1i⋅K2nm−i⋅[imod2=0]
我们发现这个东西和二项式定理简直像得不能再像了!但多了一个系数导致没办法省略枚举过程。如果进行变形呢?
res=(K1+K2)nm−nm∑i=0Cinm×K1i⋅K2nm−i⋅[imod2=1]
似乎依然没有出路!但这里有个神奇的操作:
res=(−K1+K2)nm+nm∑i=0Cinm×K1i⋅K2i⋅[imod2=1]
二式相加就可以消元,得到 2⋅res=(K1+K2)nm+(K1−K2)nm。这启示我们二项式定理中的符号和奇偶性的深切联系。
如果你使用费马小定理对次数进行了处理,你可能需要注意次数可能为 mod−1 的倍数。
#include <bits/stdc++.h>
const int mod = 998244353;
const int inv2 = 499122177;
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
long long n, m, l, r, k1, k2;
std::cin >> n >> m >> l >> r, k1 = (r - l + 1) / 2, k2 = (r - l + 1) - k1;
auto qkp = [](long long x, long long y) {
auto res(1ll);
for (; y; (x *= x) %= mod, y >>= 1)
if (y & 1)
(res *= x) %= mod;
return res;
};
if (n * m % 2)
std::cout << qkp(r - l + 1, n * m) << '\n';
else
std::cout << (qkp(r - l + 1, n * m) + qkp((k1 + mod - k2) % mod, n * m)) % mod * inv2 % mod << '\n';
return 0;
}
查看代码
#A - 交错序列
https://www.luogu.com.cn/problem/P4456

容易想到把答案用二项式定理拆开:
res=∑yfy⋅yb⋅(n−y)a=∑yfy⋅yb⋅a∑i=0Cia⋅ni⋅(−y)a−i=∑ya∑i=0fy⋅Cia⋅ni⋅(−1)a−i⋅ya+b−i=a∑i=0Cia⋅ni⋅(−1)a−i⋅∑yfy⋅ya+b−i
然后发现后面那个 sigma 不太有办法求。一个比较优雅的方法是把 ya+b−i 直接作为系数而非下标塞到 f 里去,即令 fi 表示 ∀y,∑yi 之和。
具体地,令 dpn,i,0/1 表示当前 DP 到第 n 位,要求幂次为 i,最后一位为 0/1 的答案。则显然有 dpn,i,0=dpn−1,i,0+dpn−1,i,1。对于 dpn,i,1,因为此时 ∀k,k←k+1,则 (k+1)i=i∑j=0Cji⋅kj 即 dpn,i,1=i∑j=0Cji⋅dpn−1,j,0。发现 i,j 的范围是 90,很恐怖的事情是这是可以矩阵的。
就像我们都知道的那样,矩阵在加完之后再取模就会快很多……
#include <bits/stdc++.h>
int mod;
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int n, a, b;
std::cin >> n >> a >> b >> mod;
std::vector<std::vector<long long> > C(a + b + 1, std::vector<long long>(a + b + 1));
for (int i = 0; i <= a + b; ++i) {
C[i][0] = 1ll;
for (int j = 1; j <= i; ++j)
C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % mod;
}
struct mat {
int n, m;
std::vector<std::vector<long long> > a;
mat(int n, int m, bool flag = 0): n(n), m(m), a(n, std::vector<long long> (m)) {
if (flag)
for (int i = 0; i < n; ++i)
a[i][i] = 1ll;
return;
}
mat operator* (const mat &q) const {
mat res(n, q.m);
for (int i = 0; i < n; ++i)
for (int k = 0; k < q.m; ++k) {
for (int j = 0; j < m; ++j)
res.a[i][k] += a[i][j] * q.a[j][k];
res.a[i][k] %= mod;
}
return res;
}
mat operator^ (int q) const {
mat res(n, n, 1), x(*this);
for (; q; x = x * x, q >>= 1)
if (q & 1)
res = res * x;
return res;
}
} f(1, 2 * (a + b + 1)), op(2 * (a + b + 1), 2 * (a + b + 1));
auto fun = [&](int i, int j) {
return i + j * (a + b + 1);
};
f.a[0][fun(0, 0)] = 1ll;
for (int i = 0; i <= a + b; ++i)
op.a[fun(i, 0)][fun(i, 0)] = op.a[fun(i, 1)][fun(i, 0)] = 1ll;
for (int i = 0; i <= a + b; ++i)
for (int j = 0; j <= i; ++j)
op.a[fun(j, 0)][fun(i, 1)] = C[i][j];
f = f * (op ^ n);
// for (int i = 0; i <= n; ++i) {
// if (i)
// f = f * op;
// for (int j = 0; j <= a + b; ++j)
// printf("f[%d][%d] = %lld / %lld\n", i, j, f.a[0][fun(j, 0)], f.a[0][fun(j, 1)]);
// }
auto res = 0ll;
for (int i = 0; i <= a; ++i) {
static auto p(1ll), k((a % 2) ? mod - 1ll : 1ll);
(res += C[a][i] * p % mod * k % mod * (f.a[0][fun(a + b - i, 0)] + f.a[0][fun(a + b - i, 1)]) % mod) %= mod;
(p *= n) %= mod, k = mod - k;
}
std::cout << res << '\n';
return 0;
}
查看代码
#B - Different Subsets For All Tuples
https://www.luogu.com.cn/problem/CF660E
首先你可能需要知道,如果已知一个序列,如何得到答案?
令 fi 表示在 i 处取一个子序列的末尾,枚举上一个元素 x,找到 i 之前最靠后的一个 aj=x,那么有 fi=∑fj,换言之需要保证 (j,i) 范围内没有 x 出现。
此时序列未知,令 fi,x 表示在 i 处取一个子序列末尾,且 i 的值为 x;按照贡献的视角来看待,每个可以和 i 组成新子序列的方案可以带来 mi−1 的贡献(因为 ai 已经固定为 x,其他位置可以任选),那么有 fi,x=mi×∑j<i,yfj,y×(m−1)i−j−1。
然后就惊讶地发现式子和 x 这一维没有关系了。所以直接带上系数得到:
fi=mi×∑j<ifj×(m−1)i−j−1×m=mi×(m−1)i−1×m⋅∑j<ifj×(m−1)−j
前缀和优化一下就可以快速求了。最终的答案就是 res=mn+∑fi×(m−1)n−i。
#include <bits/stdc++.h>
const int mod = 1e9 + 7;
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int n, m;
std::cin >> n >> m;
if (m == 1)
std::cout << (n + 1) << '\n';
else {
std::vector<long long> f(n + 1), s(n + 1), pm1(n + 1);
pm1[0] = 1ll;
for (int i = 1; i <= n; ++i)
pm1[i] = pm1[i - 1] * (m - 1) % mod;
auto qkp = [](long long x, int y) {
auto res(1ll);
for (; y; (x *= x) %= mod, y >>= 1)
if (y & 1)
(res *= x) %= mod;
return res;
};
auto res(qkp(m, n));
const auto invm = qkp(m - 1, mod - 2);
for (int i = 1; i <= n; ++i) {
static long long p(m), inv(invm);
f[i] = (p + pm1[i - 1] * m % mod * s[i - 1] % mod) % mod;
s[i] = (s[i - 1] + f[i] * inv % mod) % mod;
(res += f[i] * pm1[n - i] % mod) %= mod;
(p *= m) %= mod, (inv *= invm) %= mod;
}
std::cout << res << '\n';
}
return 0;
}