您的位置:首页 > 汽车 > 新车 > 2024西安铁一中集训DAY28 ---- 模拟赛(简单dp + 堆,模拟 + 点分治 + 神秘dp)

2024西安铁一中集训DAY28 ---- 模拟赛(简单dp + 堆,模拟 + 点分治 + 神秘dp)

2024/11/19 0:48:08 来源:https://blog.csdn.net/weixin_55851276/article/details/140831130  浏览:    关键词:2024西安铁一中集训DAY28 ---- 模拟赛(简单dp + 堆,模拟 + 点分治 + 神秘dp)

文章目录

  • 前言
  • 时间安排及成绩
  • 题解
    • A. 江桥不会做的签到题(简单dp)
    • B. 江桥树上逃(堆,模拟)
    • C. 括号平衡路径(点分治)
    • D. 回到起始顺序(dp,组合数学)

前言

T2好难,T4好难。T3写的好粪,T1好水。。

时间安排及成绩

  • 7:40 开题。看题目说明好像有很签到的题?
  • 7:40 - 7:45 T1看完了,为啥感觉有点难??难道是容斥?但是数据规模也不可能啊。旁边有人叫T1真的水。就我秒不掉吗?
  • 7:45 - 8:05 秉着T1就是签到题的想法,继续想T1。发现不用关心具体填什么数,只需保证相对关系逆序对数就不会变。所以直接暴力dp好像就做完了。写完一边过了所有样例。
  • 8:05 - 8:10 看T2,woc这T2啥东西啊。看着像贪心,又好像模拟。但是这数据规模为啥这么逆天??而且每个点都有牛好像不好搞。
  • 8:10 - 8:50 又想了40minT2,还是只会20pts暴力。特殊性质都不知道该咋写。只好先放弃了。
  • 8:50 - 8:55 T3题看懂了,就是树上每个节点都有一个括号,求树上一条路径,使括号匹配合法并且嵌套数最大。 n n n 的范围为啥是 5 × 1 0 4 5 \times 10^4 5×104。难道要上根号或者 l o g log log 特别多??
  • 8:55 - 9:30 太饿了,好像没啥劲思考。感觉只会 n 2 n^2 n2 的暴力。特殊性质最开始以为是菊花,后来发现还有可能是链或者蒲公英。那写起来就太粪了啊。。。
  • 9:30 - 10:10 吃了个面包,然后嫌教室太吵了,出去想。想到了可以点分治。然后考虑怎样合并两条链。显然需要维护倒着看和正着看两种链,还要维护前缀最大值,最小值啥的。然后好像就可以单 l o g log log ??那这数据范围??一度以为自己假了,后来想想感觉没啥问题。就回去写了。
  • 10:10 - 11:10 写了好长时间,中间忘了点分治的板子了,还回去重新看了看。然后写的很丑,但是感觉思路很清晰。写完自信测样例。卧槽怎么都输出 0 0 0 啊。打表发现我分治的根从第二次开始就都是 0 0 0,好逆天。
  • 11: 10 - 11:30 终于看出了是我的变量重名了。改完之后每次分治的根不是 0 0 0 了,但是输出还是不对。后来发现每次合并是左边链不能从根开始。改完之后怎么还不对??推一下样例发现从子树到根的路径算错了。然后特殊处理了一下这样的路径就把样例都过了。
  • 11:30 - 11:45 线上评测一下。woc,全部MLE了。但是数组只开到了 5 e 4 5e4 5e4 啊,怎么会MLE??后来尝试把代码注释一部分,一点一点找把哪里加上就会MLE。最后发现 void 函数打成了 int,没有返回值。然后就寄了??!!
  • 11:45 - 11:53 火速把T2暴力写了,一开始还一直错。后来发现没开long long。
  • 11:53 - 12:00 尝试写T4 20分,但是由于题目看不懂失败了。。

估分:100 + 20 + 100 + 0 = 220
分数:100 + 20 + 100 + 0 = 220
rk6

点评为没挂分。。

题解

A. 江桥不会做的签到题(简单dp)

在这里插入图片描述
分析:

签到题。

d p i , j dp_{i, j} dpi,j 表示前 i i i 个位置,填 1 ∼ i 1 \sim i 1i,形成了 j j j 个逆序对的方案数。注意这里的 1 ∼ i 1 \sim i 1i 可以理解为 具有相对大小的 i i i 个数字。

然后转移可以枚举第 i i i 位填的是 相对顺序中第几的数字

d p i , j = ∑ k = m a x ( 0 , j − i + 1 ) j d p i − 1 , k dp_{i, j} = \sum_{k=max(0, j - i + 1)}^{j} dp_{i - 1, k} dpi,j=k=max(0,ji+1)jdpi1,k

前缀和随便优化一下。每个限制就是保留一个状态的方案数,其他状态赋值为 0 0 0

CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 5010;
typedef long long LL;
const LL mod = 1e9 + 7;
LL f[N][N], S[N]; // f[i][j] 表示前i个逆序对数为j的方案数 
int n, m;
struct limit {int p, c;
}l[N];
bool cmp(limit x, limit y) {return x.p < y.p;
}
int main() {scanf("%d%d", &n, &m);for(int i = 1; i <= m; i ++ ) {scanf("%d%d", &l[i].p, &l[i].c);}sort(l + 1, l + m + 1, cmp);int k = 1;f[0][0] = 1LL; for(int i = 1; i <= n; i ++ ) {for(int j = 0; j <= 5000; j ++ ) {if(j == 0) S[j] = f[i - 1][j];else S[j] = (S[j - 1] + f[i - 1][j]) % mod;}for(int j = 0; j <= 5000; j ++ ) {f[i][j] = ((S[j] - (j - (i - 1) > 0 ? S[j - (i - 1) - 1] : 0LL)) % mod + mod) % mod;}if(k <= m && i == l[k].p) {for(int j = 0; j <= 5000; j ++ ) {if(j != l[k].c) f[i][j] = 0;}k ++;}}printf("%lld\n", f[n][l[m].c]); return 0;
}

B. 江桥树上逃(堆,模拟)

原题链接

在这里插入图片描述

分析:

感觉这题好难。。。

直接说正解:

首先有一个贪心的性质:如果一个点内还有人,并且它的父边流量还没满,那么把这条边流满肯定更优。也就是说一条边能够流满我们就让它流满。

然后我们假设当前是所有边都流满的状态,设为 G 0 G_0 G0。那么这样的状态会在某个时刻发生改变,这是由于 某个点 x x x 的流入量小于流出量,那么会在每秒流出时消耗它自己点内的人,直到点内的人都流走。这时这个点的父边就没办法流满了,这个点相当于变成了一个 中继站,由儿子流入的量会直接从父边流出,并且父边流不满。那么 x x x 就是无用的,我们考虑这时 x x x 和它的父亲合并成一个点

也就是说边的流量集 G G G 会发生变化,由 G 0 G_0 G0 变为 G 1 G_1 G1,然后再由 G 1 G_1 G1 变成 G 2 G_2 G2 等。这样的变化是由于一个点内原来的人全部净流出去的结果,所以这样的变化应当不超过 n n n 次。或者由于每次变化后我们都把两个点合成一个点,因此合并次数不会超过 n n n 次。

那么我们考虑按照 时间顺序 维护这样的变化。对于一个点,记一个 p a s s i pass_i passi 表示 i i i 号点当前的 净流出量。那么 p a s s i pass_i passi 的计算方式就是父边的流量减去儿子边的流量和。如果当前 i i i 号点的人数为 c i c_i ci,那么前 ⌊ c i p a s s i ⌋ \left \lfloor \frac{c_i}{pass_i} \right \rfloor passici 时刻显然 i i i 号点都 有能力让父边满流。如果超出了这个时间,就可以把 i i i f a i fa_i fai 合成一个点。合并关系可以用 并查集 维护。

我们考虑把二元组 ( i (i (i ⌊ c i p a s s i ⌋ ) \left \lfloor \frac{c_i}{pass_i} \right \rfloor) passici) 插入堆中,堆里把 ⌊ c i p a s s i ⌋ \left \lfloor \frac{c_i}{pass_i} \right \rfloor passici 小的放在堆顶。然后把询问按照时间顺序由小到大排序,用一个指针维护处理到哪一个询问了。

  1. 拿出栈顶二元组,如果当前询问时间小于等于栈顶时间,那么这个询问的答案就是 c 1 − p a s s 1 × t i c_1 - pass_1 \times t_i c1pass1×ti t i t_i ti 表示询问的时间。减号是由于 p a s s i pass_i passi 等于 流出量减去流入量,那么 − p a s s 1 -pass_1 pass1 就表示流入量。
  2. 如果当前询问时间大于栈顶时间,那么我们需要检验栈顶的节点 x x x 是否已经合并过,即 F i n d ( i ) Find(i) Find(i) 是否等于 i i i。如果合并过那么这个状态不能用,否则需要把 i i i 合并到 F i n d ( f a i ) Find(fa_i) Find(fai) 的点集中。记 F i n d ( f a i ) Find(fa_i) Find(fai) F a Fa Fa,合并方法是让 p a s s F a pass_{Fa} passFa 加上 p a s s i pass_i passi,让 c F a c_{Fa} cFa 加上 c i c_i ci。( p a s s F a pass_{Fa} passFa 中减去了 i i i 到父亲的流量, p a s s i pass_{i} passi 中加上了 i i i 到父亲的流量,正负抵消)。这个合并可以理解为 这个时刻之后 可以把 初始状态 看作 x x x 和它的父亲缩成了一个点,它们两个之间的边不用管,这个点初始的人的数量就是 c i + c F a c_i + c_{Fa} ci+cFa。然后这个点的 净流量 就是 p a s s x + p a s s F a pass_x + pass_{Fa} passx+passFa。然后这个点能保持父边满流的时间就是 ⌊ c x + c F a p a s s x + p a s s F a ⌋ \left \lfloor \frac{c_x + c_{Fa}}{pass_{x} + pass_{Fa}} \right \rfloor passx+passFacx+cFa。这样在上面算答案时 c 1 − p a s s 1 × t i c_1 - pass_1 \times t_i c1pass1×ti 就可以对应成 1 1 1 初始人数为 c 1 c_1 c1,保持净流入量是 − p a s s 1 -pass_1 pass1,持续了 t t t 时刻 后点内的人数。

时间复杂度 O ( n × l o g 2 n ) O(n \times log_2n) O(n×log2n)
代码不长。
CODE:

// 首先有一个贪心:能满流就让满流,这样一定不劣
// 考虑如果t时间内都是以 flow 的流量流入,那么t时间后这个点的人数就是 t * flow
// 但是一条边不可能一直满流,边的流量情况会出现变化,这个变化是由于某个点里的人为了让它的父边满流因此是负收入,在某一个时刻原来这个点里的人消耗完了
// 那么开一个堆维护这些变化时刻。如果一个点原来的人走完了,那么它实际上就是一个中继点,可以直接把它和它的父亲合并 
#include<bits/stdc++.h>  
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
int n, qc, fa[N], bin[N];
LL c[N], m[N], pass[N], ans[N], sum;
struct node {LL tim; int x;friend bool operator < (node a, node b) {return a.tim > b.tim;}
};
priority_queue< node > q;
struct Q {int t, idx;
}qq[N];
bool cmp(Q a, Q b) {return a.t < b.t;
}
int Find(int x) {return x == bin[x] ? x : bin[x] = Find(bin[x]);}
int main() {scanf("%d%d", &n, &qc);for(int i = 2; i <= n; i ++ ) {scanf("%d%lld%lld", &fa[i], &c[i], &m[i]);pass[i] += m[i], pass[fa[i]] -= m[i];sum += c[i];}for(int i = 1; i <= n; i ++ ) bin[i] = i;for(int i = 1; i <= qc; i ++ ) {scanf("%d", &qq[i].t);qq[i].idx = i;}for(int i = 1; i <= n; i ++ ) {if(pass[i] > 0) q.push((node) {c[i] / pass[i], i});}sort(qq + 1, qq + qc + 1, cmp);int p = 1;while(!q.empty() && p <= qc) {node Tp = q.top(); q.pop();LL tim = Tp.tim; int x = Tp.x;while(p <= qc && qq[p].t <= tim) {ans[qq[p].idx] = c[1] - pass[1] * (1LL * qq[p].t);p ++;}if(Find(x) != x) continue; //刚才被合并过了,那么这个状态不能用 // 可以合并 int Fa = Find(fa[x]); // 找到父亲所在的那个节点 pass[Fa] += pass[x]; c[Fa] += c[x]; bin[x] = Fa;if(pass[Fa] > 0) q.push((node) {c[Fa] / pass[Fa], Fa});}for(int i = p; i <= qc; i ++ ) {ans[qq[i].idx] = sum; // 合成1个点了 }for(int i = 1; i <= qc; i ++ ) printf("%lld\n", ans[i]); return 0;
}

总结:这类 把节点合并简化状态 或者 减少决策 的思路还要多学习。

C. 括号平衡路径(点分治)

原题链接

在这里插入图片描述

分析:

感觉遇到这种 求所有路径中最优值 的题都可以往点分治上想。

对于当前根,考虑如何在 O ( 子树大小 ) O(子树大小) O(子树大小) 的复杂度内求出所有 经过根 的路径的最优值。

还是按照点分治的套路,我们考虑 把两条在不同子树的路径 合起来。我们把左括号看作 1 1 1,右括号看作 − 1 -1 1。同时对于一个点 x x x 到当前根 r t rt rt,维护这条路径 x x x r t rt rt 的前缀最小值 m n 1 [ x ] mn_1[x] mn1[x],前缀最大值 m x 1 [ x ] mx_1[x] mx1[x] 以及 r t rt rt x x x 的前缀最小值 m n 2 [ x ] mn_2[x] mn2[x],前缀最大值 m x 2 [ x ] mx_2[x] mx2[x]。还需要维护 x x x r t rt rt 的路径和 s x s_x sx。那么两条路径 x → r t x \to rt xrt r t → y rt \to y rty 能够合并需要满足以下条件:

  1. s x + s y = 0 s_x + s_y = 0 sx+sy=0
  2. m n 1 [ x ] ≥ 0 mn_1[x] \geq 0 mn1[x]0 s x + m n 2 [ y ] ≥ 0 s_x + mn_2[y] \geq 0 sx+mn2[y]0

如果满足,答案合并后的路径对答案的贡献就是 m a x ( m x 1 [ x ] , s x + m x 2 [ y ] ) max(mx_1[x], s_x + mx_2[y]) max(mx1[x],sx+mx2[y])

可以维护一个桶 v a l val val v a l i val_i vali 表示已经加入的路劲中 s x = i s_x = i sx=i 且满足 m n 1 [ x ] mn_1[x] mn1[x] 的最大的 m x 1 [ x ] mx_1[x] mx1[x]。然后对一条路径 r t → y rt \to y rty 找最优值就是查询 v a l − s y val_{-s_y} valsy

注意:

  1. 由于后加入的路径与前面的路径合并时只能作为后半段,但是实际上它还可以作为前半段,因此需要 正着做一遍,倒着做一遍
  2. 由于两条路径合并时不能都含根节点,因此可以让加入的路径含根节点,查询的路径不含根节点。这是一个边界的细节。

复杂度 O ( n × l o g 2 n ) O(n \times log_2n) O(n×log2n)

CODE:

#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N = 5e4 + 10;
const int INF = 2e6;
int n, fa, a[N], ans, res, root;
int val[N * 2]; // val[i + n]维护的是 sum = val[i + n] 的链,mx的最大值
int mx1[N], mx2[N], mn1[N], mn2[N], s[N]; // 分别表示倒着看的最大值,正着看的最大值,倒着看的最小值,正着看的最小值,和 
bool vis[N];
int sz[N], all, Maxn[N];
char ch[N];
vector< int > E[N];
void getrt(int x, int fa) {sz[x] = 1; Maxn[x] = 0;for(auto v : E[x]) {if(v == fa || vis[v]) continue;getrt(v, x);Maxn[x] = max(Maxn[x], sz[v]);sz[x] += sz[v];}Maxn[x] = max(Maxn[x], all - sz[x]);if(Maxn[x] < Maxn[root]) root = x;
}
void getsz(int x, int fa) {sz[x] = 1;for(auto v : E[x]) {if(v == fa || vis[v]) continue;getsz(v, x);sz[x] += sz[v];}
}
void add(int x) { // 加入 x 作为 倒着 if(mn1[x] >= 0) { // 大于0才加入 val[s[x] + n] = max(val[s[x] + n], mx1[x]);}
}
void del(int x) { // 删去 x 作为 倒着 val[s[x] + n] = -INF;
}
void ask(int x) { // x 为正 if(val[n - s[x]] >= 0 && (-s[x] + mn2[x] >= 0)) { // 存在 res = max(res, max(val[n - s[x]], -s[x] + mx2[x]));}
}
void calc(int x, int fa) { // x子树里作为正着 if(x == fa) {s[x] = mx2[x] = mn2[x] = a[x];}else {	s[x] = s[fa] + a[x];mx2[x] = max(mx2[fa], s[x]);mn2[x] = min(mn2[fa], s[x]);}ask(x);for(auto v : E[x]) {if(v == fa || vis[v]) continue;calc(v, x);}
}
void ins(int x, int fa) { // 插入x子树作为倒着 s[x] = s[fa] + a[x];mx1[x] = max(mx1[fa] + a[x], a[x]);mn1[x] = min(mn1[fa] + a[x], a[x]);if(s[x] == 0 && mn1[x] >= 0) res = max(res, mx1[x]);add(x);for(auto v : E[x]) {if(v == fa || vis[v]) continue;ins(v, x);}
}
void Clear(int x, int fa) {del(x);for(auto v : E[x]) {if(v == fa || vis[v]) continue;Clear(v, x);}
}
void query(int x, int fa) {s[x] = s[fa] + a[x];mx2[x] = max(mx2[fa], s[x]);mn2[x] = min(mn2[fa], s[x]);if(s[x] == 0 && mn2[x] >= 0) res = max(res, mx2[x]);for(auto v : E[x]) {if(v == fa || vis[v]) continue;query(v, x);} 
}
void sol(int rt) { // 以 rt 为根   1是倒着, 2是正着  拿正着和倒着匹配 vis[rt] = 1;mx1[rt] = mn1[rt] = s[rt] = a[rt];mx2[rt] = mn2[rt] = a[rt];add(rt); // 加入根 for(int i = 0; i < E[rt].size(); i ++ ) {int v = E[rt][i];if(vis[v]) continue;calc(v, v); // 解决v子树里的答案 ins(v, rt); // 加入 }for(int i = 0; i < E[rt].size(); i ++ ) { int v = E[rt][i];if(vis[v]) continue;Clear(v, rt); // 把它们的答案删去 }del(rt);for(int i = E[rt].size() - 1; i >= 0; i -- ) { // 倒着做一边 int v = E[rt][i];if(vis[v]) continue;calc(v, v);ins(v, rt);}for(auto v : E[rt]) {if(vis[v]) continue;query(v, rt); // 光处理根 }for(int i = 0; i < E[rt].size(); i ++ ) {int v = E[rt][i];if(vis[v]) continue;Clear(v, rt);}for(auto v : E[rt]) {if(vis[v]) continue;root = 0; all = sz[v];getrt(v, rt);getsz(root, 0);sol(root);}
}
void solve2() {all = n; root = 0; Maxn[0] = INF;getrt(1, 0);getsz(root, 0);sol(root);printf("%d\n", res);
}
int main() {for(int i = 0; i < N * 2; i ++ ) val[i] = -INF;scanf("%d", &n);for(int i = 2; i <= n; i ++ ) {scanf("%d", &fa);E[i].pb(fa); E[fa].pb(i);}for(int i = 1; i <= n; i ++ ) {scanf("\n%c", &ch[i]);if(ch[i] == '(') a[i] = 1;else a[i] = -1;} solve2();return 0;
}

D. 回到起始顺序(dp,组合数学)

原题链接

在这里插入图片描述

分析:

感觉是有很多trick结合的题。

实际上是问你所有 n ! n! n! 1 ∼ n 1 \sim n 1n 的排列的权值的 乘积。一个排列的权值和计算方式:把 i i i a i a_i ai 连一条有向边,所有点的出度为 1 1 1,入度为 1 1 1,形成了若干置换环。这个排列的权值就是 所有环大小的 l c m lcm lcm

分析:

由于 l c m lcm lcm 可能比较大,因此直接求某种 l c m lcm lcm 对应的排列数不现实。我们考虑 每种质因数 的贡献。

f x f_x fx 表示 l c m lcm lcm x x x 的倍数的排列数。那么答案就是

∏ p c ≤ n p f p c \prod_{p^c \leq n} p^{f_{p^c}} pcnpfpc p p p 为质数)

考虑一个排列的 l c m lcm lcm 如果包含 p c p^c pc,那么这一部分贡献将会在 p f p p^{f_{p}} pfp p f p 2 p^{f_{p^2}} pfp2,…, p f p c p^{f_{p^c}} pfpc 分别被计算一次。那么总共会被计算 c c c,贡献不会少。 p c ≤ n p^c \leq n pcn 是因为一个 l c m lcm lcm 中包含某个质因数 p p p 的幂一定小于等于 n n n(任何一个环的长度都小于等于 n n n,因此环长质因数分解后 p p p 的幂肯定小于等于 n n n。那么 l c m lcm lcm 中包含 p p p 的幂一定也小于等于 n n n)。

考虑怎样求 f x f_x fx。我们发现如果 x x x 是某个质数的幂,那么还有一个好处:满足 l c m lcm lcm x x x 的倍数的排列一定至少存在一个置换环的长度是 x x x 的倍数。这个性质很好想: l c m lcm lcm 是每个环长质因数分解后每种质因数取幂次最大的那个。那么一定存在一个环长的质因数的幂次大于等于 x x x 的幂次。这个环长就是 x x x 的倍数。

我们考虑枚举 x x x,然后 d p dp dp
f i f_i fi 表示长度为 i i i 的排列,满足所有置换环的长度都是 x x x 的倍数的排列数。
g i g_i gi 表示长度为 i i i 的排列,没有一个置换环 的长度是 x x x 的倍数的方案数。

那么有转移:

f i = ∑ j ≤ i , x ∣ j C i − 1 j − 1 × f i − j × ( j − 1 ) ! f_i = \sum_{j \leq i,x|j}C_{i - 1}^{j - 1} \times f_{i - j} \times (j - 1)! fi=ji,xjCi1j1×fij×(j1)!
g i = i ! − ∑ j ≤ i , x ∣ j C i j × f j × g i − j g_i = i! - \sum_{j \leq i,x | j}C_{i}^{j} \times f_{j} \times g_{i - j} gi=i!ji,xjCij×fj×gij

最后 n ! − g n n! - g_n n!gn 就是 l c m lcm lcm x x x 的倍数的排列数。

f i f_i fi 的转移可以理解为枚举 1 1 1 号位置所在的置换环大小 j j j,然后这个置换环的其它位置需要在 i − 1 i - 1 i1 个位置里面选 j − 1 j - 1 j1 个,还要乘一个圆排列 ( j − 1 ) ! (j - 1)! (j1)! 表示这个环内的顺序。剩下 i − j i - j ij 个位置要接着划分为若干个大小是 x x x 的倍数的置换环。

g i g_i gi 的转移是一个容斥:总的排列数减去存在某些环的长度是 x x x 的倍数的排列数。枚举这些长度是 x x x 的倍数的置换环的总长度 j j j,这一部分的方案是 f j f_j fj,然后剩下 i − j i - j ij 个位置需要划分成长度都不是 x x x 的倍数的环,方案是 g i − j g_{i - j} gij,最后还要乘上 C i j C_{i}^{j} Cij 表示在 i i i 个位置里选 j j j 个位置去构建长度是 x x x 的倍数的置换环。

还要注意一下由于求出的排列数要作为指数去算答案,因此它的计算过程中模数应该为 m o d − 1 mod - 1 mod1。这个数不一定是质数,因此需要避免逆元。上面的过程中组合数可以预处理求。

然后直接暴力 dp 复杂度是 O ( n 2 × l o g 2 n ) O(n^2 \times log_2n) O(n2×log2n) 的:
枚举 x x x n n n,枚举排列的长度 i i i 加上转移是 ∑ i = 1 n i x ≤ n × n x \sum_{i = 1}^{n} \frac{i}{x} \leq n \times \frac{n}{x} i=1nxin×xn。那么总复杂度是 ∑ x = 1 n n × n x = n × ∑ x = 1 n n x = n 2 × l o g 2 n \sum_{x = 1}^{n} n\times \frac{n}{x} = n \times \sum_{x = 1}^{n} \frac{n}{x} = n^2 \times log_2n x=1nn×xn=n×x=1nxn=n2×log2n

但是根据转移方程式我们发现: f i f_i fi 的转移中只有 i ≡ 0 ( m o d x ) i \equiv 0(mod \ x) i0(mod x) 的状态有用, g i g_i gi 的转移中只有 i ≡ n ( m o d x ) i \equiv n(mod \ x) in(mod x) 的状态有用。因此不用全部转移,只需要转移 n x \frac{n}{x} xn 个状态。每个状态转移 n x \frac{n}{x} xn 次。

复杂度就是 ∑ x = 1 n n x × n x = ∑ x = 1 n n 2 x 2 ≈ n 2 \sum_{x = 1}^{n} \frac{n}{x} \times \frac{n}{x} = \sum_{x = 1}^{n} \frac{n^2}{x^2} \approx n^2 x=1nxn×xn=x=1nx2n2n2

这个约等于可以让电脑打表输出一下,发现确实不会超过两倍。

CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 7550;
typedef long long LL;
bool vis[N];
int n;
LL m1, m2, c[N][N], f[N], g[N]; // f[i], g[i] 分别表示长度为i的置换所有环的长度都是x的倍数,所有环的长度都不是x的倍数的排列数 
LL res = 1LL, fac[N];
inline LL Pow(LL x, LL y, LL mod) {LL res = 1LL, k = x % mod;while(y) {if(y & 1) res = (res * k) % mod;y >>= 1;k = (k * k) % mod;}return res;
}
int main() {cin >> n >> m1;m2 = m1 - 1;for(int i = 0; i < N; i ++ ) for(int j = 0; j <= i; j ++ ) {if(!j) c[i][j] = 1LL;else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % m2;}fac[0] = 1LL;for(int i = 1; i < N; i ++ ) fac[i] = fac[i - 1] * (1LL * i) % m2;for(int i = 2; i <= n; i ++ ) {if(!vis[i]) {for(int j = i; j * i <= n; j ++ ) {vis[i * j] = 1;}}}for(int i = 2; i <= n; i ++ ) {if(vis[i]) continue;else { // 质数 int x = i; LL p = 1LL * i, cnt = 0; while(x <= n) { // 计算答案 g[0] = f[0] = 1LL;for(int j = 1; j <= n; j ++ ) {g[j] = f[j] = 0;if(j % x == 0 % x) { // 转移 f for(int k = x; k <= j; k += x) f[j] = (f[j] + f[j - k] * c[j - 1][k - 1] % m2 * fac[k - 1] % m2) % m2;}if(j % x == n % x) { // 转移 g g[j] = fac[j];for(int k = x; k <= j; k += x) g[j] = ((g[j] - g[j - k] * f[k] % m2 * c[j][k] % m2) % m2 + m2) % m2;}}cnt = (cnt + ((fac[n] - g[n]) % m2 + m2) % m2) % m2;x = x * p;}res = (res * Pow(p, cnt, m1)) % m1;}}printf("%lld\n", res);return 0;
}

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com