speed pack 1.
收录数学题
1. qoj7759/集训队互测2023 - Permutation Counting 2
tag:絶滅反演;link。
\(x\) 个相邻上升数对等价于 \(n-x\) 个连续上升段(等价于 \(n-x-1\) 个相邻下降数对)。所以现在考虑从连续上升段的角度计算。设原排列构成 \(x\) 个连续段,逆排列构成 \(y\) 个连续段。
一个观察:考虑依次加入逆排列的每个连续段,相当于从小到大往原排列中填数(比如逆排列 \([1,2]\) 为 \(\{1,4\}\) 的上升段,就令 \(p_1=1,p_4=2\)),那么任意时刻原排列中每个连续段被填了一个前缀。设 \(a_{i,j}\) 表示逆排列第 \(i\) 个连续段插入到原排列第 \(j\) 个连续段的数个数,那么满足每行、每列和非 \(0\) 且总和为 \(n\) 的 \(a\) 数量(设为 \(g_{x,y}\))即为钦定原排列构成 \(x\) 个连续段,逆排列构成 \(y\) 个连续段的方案数。容斥,钦定 \(a\) 行 \(b\) 列非空的答案为 \(h_{a,b}=\dbinom{ab+n-1}{ab-1}\),二维絶滅反演得:
容易优化至 \(O(n^3)\)。
但是每两个连续段可能可以合并,所以设 \(f_{x,y}\) 为答案,则有(\(g_{n-x,n-y}\) 为钦定 \(x,y\) 个相邻下降数对的方案数):
二维絶滅反演得:
容易优化至 \(O(n^3)\)。
点击查看代码
//qoj7759
#include <bits/stdc++.h>
using namespace std;
const int N = 510;
typedef long long ll;
int n, m;
ll P, f[N][N], g[N][N], h[N][N];
ll C[N][N], fac[N*N+N], inv[N*N+N];
ll qp(ll x, ll y){
ll ans = 1;
while(y){
if(y & 1){
ans = ans * x % P;
}
x = x * x % P;
y >>= 1;
}
return ans;
}
int fy(int x){
return (x&1) ? P-1 : 1;
}
ll Co(int x, int y){
return fac[x] * inv[y] % P * inv[x-y] % P;
}
int main(){
cin >> n >> P;
fac[0] = 1;
m = n * n + n;
for(int i = 1; i <= m; ++ i){
fac[i] = fac[i-1] * i % P;
}
inv[m] = qp(fac[m], P-2);
for(int i = m-1; i >= 0; -- i){
inv[i] = inv[i+1] * (i+1) % P;
}
C[0][0] = 1;
for(int i = 1; i <= n; ++ i){
C[i][0] = C[i][i] = 1;
for(int j = 1; j < i; ++ j){
C[i][j] = (C[i-1][j] + C[i-1][j-1]) % P;
}
}
for(int x = 1; x <= n; ++ x){
for(int y = 1; y <= n; ++ y){
for(int i = 1; i <= y; ++ i){
h[x][y] = (h[x][y] + C[y][i] * fy(y-i) % P * Co(x*i+n-1, x*i-1)) % P;
}
for(int i = 1; i <= x; ++ i){
g[x][y] = (g[x][y] + C[x][i] * fy(x-i) % P * h[i][y]) % P;
}
}
}
memset(h, 0, sizeof(h));
for(int x = 0; x < n; ++ x){
for(int y = 0; y < n; ++ y){
for(int i = y; i <= n; ++ i){
h[x][y] = (h[x][y] + C[i][y] * fy(i-y) % P * g[n-x][n-i]) % P;
}
}
}
for(int x = 0; x < n; ++ x){
for(int y = 0; y < n; ++ y){
for(int i = x; i <= n; ++ i){
f[x][y] = (f[x][y] + C[i][x] * fy(i-x) % P * h[i][y]) % P;
}
printf("%lld ", f[x][y]);
}
puts("");
}
return 0;
}
2. CF1973F - Maximum GCD Sum Queries
tag:高维差分;link。
不妨 \((a_1,b_1)\) 不交换。那么答案的 \((x,y)\) 只可能有 \(O(d(a_i)^2)\) 大约 \(6\times 10^5\) 种,可以放入状态中,因为一定有 \(x|a_1,y|b_1\)(所以限定不交换的目的是减少状态数)。
设 \(f_{x,y}\) 表示答案为 \(a_1\) 的第 \(x\) 个因数,\(b_1\) 的第 \(y\) 个因数的最小代价。为了维护是否可行,设 \(cnt_{x,y}\) 表示过程中可以贡献的 \((a,b)\) 对数量。最后使用 \(cnt_{x,y}=n-1\) 的 \((x,y)\) 来计算答案。
那么每对 \((a,b)\) 的贡献是(为了方便描述,下文中下标指对应的第 \(x\) 个因数本身而不是 \(x\)):
- \(cnt_{(a,a_1),(b,b_1)}\) 以及下标的因数 \(+1\)。
- \(cnt_{(b,a_1),(a,b_1)}\) 以及下标的因数 \(+1\)。
- \(cnt_{(a,b,a_1),(a,b,b_1)}\) 以及下标的因数 \(-1\),因为这部分的可行计数不能加两次。
- \(f_{(b,a_1),(a,b_1)}\) 以及下标的因数 \(+c\),表示一次交换。
- \(f_{(a,b,a_1),(a,b,b_1)}\) 以及下标的因数 \(-c\),这部分不需要交换就可以。
发现这类似于高维差分,只不过每一维对应一个质因数。所以维护过程是:
- 求解过程中,只更新上述的 \(5\) 个下标;
- 最后还原答案,枚举 \(a\) 的每个质因数 \(p\) 以及下标 \(x\),若 \(p|x\) 则令 \(f_{x/p,y}\) 加上 \(f_{x,y}\)
- 枚举 \(b\) 的每个质因数,同理求解。
最后将所有 \(cnt_{x,y}=n-1\) 的 \((f_{x,y},x+y)\) 提取出来扫描线即可。
复杂度瓶颈是 \(O(d(a)^2\omega(a))\),能过。
点击查看代码
//CF1973F
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10, M = 780;
int n, q, a[N], b[N], c[N], ans[N];
typedef long long ll;
ll d[N];
int cnt[M][M];
ll f[M][M];
vector<int> pa, pb, pra, prb;
unordered_map<int, int> ma, mb;
void solve(){
memset(f, 0, sizeof(f));
memset(cnt, 0, sizeof(cnt));
pra.clear();
prb.clear();
pa.clear();
pb.clear();
ma.clear();
mb.clear();
for(int i = 1; i * i <= a[1]; ++ i){
if(a[1] % i == 0){
pa.push_back(i);
if(i * i != a[1]){
pa.push_back(a[1] / i);
}
}
}
int k = a[1];
for(int i = 2; i * i <= k; ++ i){
if(k % i == 0){
pra.push_back(i);
while(k % i == 0){
k /= i;
}
}
}
if(k > 1){
pra.push_back(k);
}
for(int i = 1; i * i <= b[1]; ++ i){
if(b[1] % i == 0){
pb.push_back(i);
if(i * i != b[1]){
pb.push_back(b[1] / i);
}
}
}
k = b[1];
for(int i = 2; i * i <= k; ++ i){
if(k % i == 0){
prb.push_back(i);
while(k % i == 0){
k /= i;
}
}
}
if(k > 1){
prb.push_back(k);
}
sort(pa.begin(), pa.end());
sort(pb.begin(), pb.end());
for(int i = 0, k = pa.size(); i < k; ++ i){
ma[pa[i]] = i + 1;
}
for(int i = 0, k = pb.size(); i < k; ++ i){
mb[pb[i]] = i + 1;
}
for(int i = 2; i <= n; ++ i){
int p = __gcd(a[i], b[i]);
++ cnt[ma[__gcd(a[i], a[1])]][mb[__gcd(b[i], b[1])]];
++ cnt[ma[__gcd(b[i], a[1])]][mb[__gcd(a[i], b[1])]];
-- cnt[ma[__gcd(p , a[1])]][mb[__gcd(p , b[1])]];
f[ma[__gcd(b[i], a[1])]][mb[__gcd(a[i], b[1])]] += c[i];
f[ma[__gcd(p , a[1])]][mb[__gcd(p , b[1])]] -= c[i];
}
for(int i : pra){
for(int x = pa.size(); x >= 1; -- x){
for(int y = pb.size(); y >= 1; -- y){
int val = pa[x-1];
if(val % i == 0){
int p = ma[val / i];
cnt[p][y] += cnt[x][y];
f[p][y] += f[x][y];
}
}
}
}
for(int i : prb){
for(int x = pa.size(); x >= 1; -- x){
for(int y = pb.size(); y >= 1; -- y){
int val = pb[y-1];
if(val % i == 0){
int p = mb[val / i];
cnt[x][p] += cnt[x][y];
f[x][p] += f[x][y];
}
}
}
}
vector<pair<ll, int> > v;
for(int x = pa.size(); x >= 1; -- x){
for(int y = pb.size(); y >= 1; -- y){
if(cnt[x][y] == n-1){
v.emplace_back(f[x][y], - pa[x-1] - pb[y-1]);
}
}
}
for(int i = 1; i <= q; ++ i){
v.emplace_back(d[i], i);
}
sort(v.begin(), v.end());
int mx = 0;
for(auto i : v){
if(i.second > 0){
ans[i.second] = max(ans[i.second], mx);
} else {
mx = max(mx, -i.second);
}
}
}
int main(){
scanf("%d%d", &n, &q);
for(int i = 1; i <= n; ++ i){
scanf("%d", &a[i]);
}
for(int i = 1; i <= n; ++ i){
scanf("%d", &b[i]);
}
for(int i = 1; i <= n; ++ i){
scanf("%d", &c[i]);
}
for(int i = 1; i <= q; ++ i){
scanf("%lld", &d[i]);
}
solve();
for(int i = 1; i <= q; ++ i){
d[i] -= c[1];
}
swap(a[1], b[1]);
solve();
for(int i = 1; i <= q; ++ i){
printf("%d ", ans[i]);
}
return 0;
}
3. ARC114E - Paper Cutting 2
tag:期望;link。被剪开了/ll
把选择直线的操作写成一个排列,那么对于任意一个排列,会进行其中的若干操作(不一定是前缀)。由于期望的线性性,考虑计算每一条直线在多少排列中会被操作。
容易发现,一条直线 \(x\) 能够被操作当且仅当穿过黑格的所有直线以及与它平行且在它与黑格之间的所有直线还未被操作,即在排列中这些直线在它之后。所以设这样的直线有 \(a\) 个,那么直线 \(x\) 的贡献即为 \(\dfrac{1}{a+1}\)。
于是统计所有直线的贡献,并且最后 \(+1\) 表示选择了一条穿过黑格的直线使得操作结束即可。
点击查看代码
//AT_arc114_e
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll P = 998244353;
const int N = 2e5 + 10;
ll inv[N], w, h, x, y, xx, yy, ans;
int main(){
cin >> h >> w >> x >> y >> xx >> yy;
if(x > xx){
swap(x, xx);
}
if(y > yy){
swap(y, yy);
}
inv[1] = 1;
for(int i = 2; i < N; ++ i){
inv[i] = (P - P / i) * inv[P%i] % P;
}
for(int i = 1; i < h; ++ i){
if(i >= x && i < xx){
continue;
} else if(i < x){
ll a = (yy - y) + (xx - i);
ans = (ans + inv[a]) % P;
} else if(i >= xx){
ll a = (yy - y) + (i - x + 1);
ans = (ans + inv[a]) % P;
}
}
for(int i = 1; i < w; ++ i){
if(i >= y && i < yy){
continue;
} else if(i < y){
ll a = (xx - x) + (yy - i);
ans = (ans + inv[a]) % P;
} else if(i >= yy){
ll a = (xx - x) + (i - y + 1);
ans = (ans + inv[a]) % P;
}
}
printf("%lld\n", (ans + 1) % P);
return 0;
}
类似的题目:CF1924E - Paper Cutting Again。
发现对于一条直线,不能在它之后选的直线依旧是一个集合,于是一样做即可。
4. LuoguP8967 - 追寻 | Pursuit of Dream
tag:期望;link。
设 \(q_i\) 表示不考虑散入天际时,点 \(i\) 到终点的概率。则有当 \(\forall j,d_j\ge a_{i,j}\) 时:
其中 \(s_i=\sum d_j-a_{i,j}\)。
接着考虑散入天际的情况,设答案为 \(f_i\),则:
其中 \(G\) 表示散入天际后的期望,\(H\) 表示散入天际前的期望。
发现 \(G\) 对于所有 \(i\) 是不变的,有 \(G=\sum \dfrac{p_i}{\sum p}f_i\)。
考虑 \(H\) 怎么算:
- 不考虑到达终点,期望步数为 \(\dfrac 1{\sum p}\);
- 考虑到达终点,要减去走到终点再失足的情况 \(q_i(s_i+\dfrac 1{\sum p})\)。
然后将 \(f\) 带入 \(G\) 解出 \(G\) 后,使用 \(G\) 解出 \(f_0\) 即可。
点击查看代码
//P8967
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 110, K = 1e4 + 10, M = 1e7 + 10;
const ll P = 998244353, iv = 205817851;
int n, k;
ll d[N], a[K][N], p[K], pp;
ll fac[M], q[K], g;
ll qp(ll x, ll y){
ll ans = 1;
while(y){
if(y & 1){
ans = ans * x % P;
}
x = x * x % P;
y >>= 1;
}
return ans;
}
int main(){
fac[0] = 1;
for(int i = 1; i < M; ++ i){
fac[i] = fac[i-1] * i % P;
}
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; ++ i){
scanf("%lld", &d[i]);
}
for(int i = 1; i <= k; ++ i){
for(int j = 1; j <= n; ++ j){
scanf("%lld", &a[i][j]);
}
scanf("%lld", &p[i]);
p[i] = p[i] * iv % P;
pp = (pp + p[i]) % P;
}
for(int i = 0; i <= k; ++ i){
bool flg = 1;
ll sum = 0, mul = 1;
for(int j = 1; j <= n; ++ j){
if(d[j] < a[i][j]){
flg = 0;
break;
}
sum += d[j] - a[i][j];
mul = mul * fac[d[j]-a[i][j]] % P;
}
if(flg){
mul = mul * qp(n, sum) % P;
q[i] = qp(mul, P-2) * fac[sum] % P * qp(P + 1 - pp, sum) % P;
}
}
ll fz = 1, fm = 0, ip = qp(pp, P-2);
for(int i = 1; i <= k; ++ i){
ll tmp = p[i] * (1 - q[i] + P) % P * ip % P;
fm = (fm + tmp * ip) % P;
fz = (fz + P - tmp) % P;
}
g = fm * qp(fz, P-2) % P;
ll ans = (1 + P - q[0]) * (g + ip) % P;
printf("%lld\n", ans);
return 0;
}
5. ARC122E - Increasing LCMs
考虑每次找到一个合法的 \(a_i\) 放到末尾,然后判断是否能找到 \(n\) 次,若能则有解否则无解。
一个 \(a_i\) 合法当且仅当 \(a_i\not| \operatorname{lcm}_{j\neq i}\{a_j\}\),所以每次找到一个数放到末尾限制只会变松(\(\operatorname{lcm}\) 变为原本的一个因数),所以这么做是正确的。
考虑如何判断 \(a_i\not| \operatorname{lcm}_{j\neq i}\{a_j\}\):
- 转化为 \(\gcd(a_i,\operatorname{lcm}_{j\neq i}\{a_j\})<a_i\);
- 将 \(\gcd\) 与 \(\operatorname{lcm}\) 换顺序(多个数的 \(\operatorname{lcm}\) 不方便求);
- 得到 \(\operatorname{lcm}_{j\neq i}\{\gcd(a_i,a_j)\}<a_i\),可以直接求解(左侧大于的时候直接判不合法)。
每次暴力找到一个 \(a_i\),复杂度 \(O(n^3\log V)\)。
点击查看代码
// Problem: E - Increasing LCMs
// Contest: AtCoder - Tokio Marine & Nichido Fire Insurance Programming Contest 2021(AtCoder Regular Contest 122)
// URL: https://atcoder.jp/contests/arc122/tasks/arc122_e
// Memory Limit: 1024 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 110;
int n;
ll a[N], b[N];
bool chk(int pos, int len){
__int128 nw = 0;
for(int i = 1; i <= len; ++ i){
if(i == pos){
continue;
}
__int128 g = __gcd(a[i], a[pos]);
if(nw == 0){
nw = g;
} else {
nw = nw / __gcd(nw, g) * g;
}
if(nw >= a[pos]){
return 0;
}
}
return 1;
}
int main(){
int T = 1;
while(T--){
scanf("%d", &n);
for(int i = 1; i <= n; ++ i){
scanf("%lld", &a[i]);
}
for(int i = n; i >= 1; -- i){
bool flg = 0;
for(int j = 1; j <= i; ++ j){
if(chk(j, i)){
b[i] = a[j];
for(int k = j; k < i; ++ k){
a[k] = a[k+1];
}
flg = 1;
break;
}
}
if(!flg){
puts("No");
return 0;
}
}
puts("Yes");
for(int i = 1; i <= n; ++ i){
printf("%lld ", b[i]);
}
puts("");
}
return 0;
}

浙公网安备 33010602011771号