[luogu p8338] [AHOI2022] 排列

P8338 [AHOI2022] 排列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

TT 组数据。

对于一个长度为 nn 的排列 P=(p1,p2,,pn)P = (p_1, p_2, \ldots, p_n) 和整数 k0k \ge 0,定义 PPkk 次幂

P(k)=(p1(k),p2(k),,pn(k)),P^{(k)} = \left( p^{(k)}_1, p^{(k)}_2, \ldots, p^{(k)}_n \right),

该排列的第 ii 项为

pi(k)={i,k=0,ppi(k1),k>0.p^{(k)}_i = \begin{cases} i, & k = 0, \\ p^{(k - 1)}_{p_i}, & k > 0. \end{cases}

容易证明任意排列的任意次幂都是一个排列。

定义排列 PP循环值 v(P)v(P) 为最小的正整数 kk 使得 P(k+1)=PP^{(k + 1)} = P

给出一个长度为 nn 的排列 A=(a1,a2,,an)A = (a_1, a_2, \ldots, a_n),对于整数 1i,jn1 \le i, j \le n,定义 f(i,j)f(i, j):若存在 k0k \ge 0 使得 ai(k)=ja^{(k)}_i = j,则 f(i,j)=0f(i, j) = 0,否则设排列 Ai,jA_{i, j} 为将排列 AA 的第 iiaia_i 和第 jjaja_j 交换后得到的排列,则 f(i,j)=v(Ai,j)f(i, j) = v(A_{i, j})

i=1nj=1nf(i,j)\sum_{i = 1}^{n} \sum_{j = 1}^{n} f(i, j) 的值。答案可能很大,你只需要输出其对 (109+7)({10}^9 + 7) 取模的结果。

  • 1T51 \le T \le 5

  • 1ain5×1051 \le a_i \le n \le 5 \times 10^5

首先明确一下排列的基本定义:长度为 nn 的排列指的是一个 nn 元组(可以理解为长度为 nn 的数组),满足 11nnnn 个数,每个数都恰好在这个 nn 元组中出现 11 次。


观察题目中排列的幂的定义,从 P(k)P^{(k)}P(k+1)P^{(k + 1)},是通过将 P(k)P^{(k)} 中每一个数 vv 更改为 pvp_v 得到的,也就是一种类似于 ipii \to p_i 的迭代。

考虑对于长度为 nn 的排列 PP ,构造一个有向图 GG,图 GG 中恰有 nn 个编号从 11nn 的点和 nn 条边,第 ii 条边为 ipii \to p_i

发现这个图会是一堆简单环构成的(可以有自环),这是因为图 GG 经过上述连边方式,每个点的入度一定恰好为 11(注意到排列的性质),出度也一定恰好为 11,如此以来一定会形成一堆简单环的集合。

这么建图我们的目的在于将 ipii \to p_i 的迭代具体化——一开始有 nn 个小人,第 ii 个小人在节点 ii,每个小人顺着所在的环走 kk 步后,第 ii 个小人所在的节点就是 P(k)P^{(k)} 的第 ii 个元素。

举个例子:P=(3,1,2)P = (3, 1, 2)。对排列 PP 建图得到:

11 个小人一开始在 11,第 22 个小人一开始在 22,第 33 个小人一开始在 33P0=(1,2,3)P^0 = (1, 2, 3)

现在所有小人顺着走一步。第 11 个小人此时在 33,第 22 个小人此时在 11,第 33 个小人此时在 22,因此 P1=(3,1,2)P^1 = (3, 1, 2)

另外很显然 P1=PP^1 = P

然后所有小人再顺着走一步,第 11 个小人此时在 22,……就可以得到 P2=(2,3,1)P^2 = (2, 3, 1)

然后再走一步,得到 P3=(1,2,3)P^3 = (1, 2, 3)。发现:这等于 P0P^0

那么接下来肯定又会有 P4=P1P^4 = P^1P5=P2P^5 = P^2P6=P3=P0P^6 = P^3=P^0,如此循环下去……发现循环节是 33

事实上所有排列的幂都一定会出现循环,而循环节的长度正是 v(P)v(P) 的定义。


定义排列 PP循环值 v(P)v(P) 为最小的正整数 kk 使得 P(k+1)=PP^{(k + 1)} = P

我们知道,P(k+1)P^ {(k +1)} 代表第 ii 个小人在节点 pip_i,每个人顺着走 kk 步后的结果。

那么 P(k+1)=PP^{(k + 1)} = P 是什么意思? 也就是说,每个小人一开始在 pip_i,能满足所有人挨个顺着走 kk 步后还能回到 pip_iv(P)v(P) 描述了 kk 的最小值。

设总共有 mm’ 个环,第 ii 个环的环长是 rir_i,于是有 v(P)=lcm(r1,r2,,rm)v(P) = \operatorname{lcm}(r_1, r_2, \cdots, r_{m'})。原因:每个人在 pip_i,走 kk 步还能回来,充要条件就是 kk 是所有环长的倍数,那最小的 kk 就是所有环长的最小公倍数。

举个例子,我们对 P=(4,6,8,9,2,5,10,7,1,3)P = (4, 6, 8, 9, 2, 5, 10, 7, 1, 3) 进行循环值分析。首先画个图:

发现总共有 33 个环,长度分别为 4,3,34, 3, 3,于是 v(P)v(P) 就应该等于 lcm(4,3,3)=12 \operatorname{lcm}(4, 3, 3) = 12。因为所有小人都至少走 1212 步后,才能保证所有小人回到原位置。


现在来把视线转移到排列 AA 上。

对于整数 1i,jn1 \le i, j \le n,定义 f(i,j)f(i, j):若存在 k0k \ge 0 使得 ai(k)=ja^{(k)}_i = j,则 f(i,j)=0f(i, j) = 0,否则设排列 Ai,jA_{i, j} 为将排列 AA 的第 iiaia_i 和第 jjaja_j 交换后得到的排列,则 f(i,j)=v(Ai,j)f(i, j) = v(A_{i, j})

分别考虑。


什么时候存在 k0k \ge 0 使得 ai(k)=ja^{(k)}_i = j

先来看 ai(k)a_i^{(k)} 是个什么东西。阅读题目我们发现,它其实是 AkA^k 的第 ii 项。

考虑在 AA 的图上,发现所有的 ai(k)a_i^{(k)} 其实也就是在说图上的 aia_i 这个点走 kk 步。由于 kk 的取值是任意的,那么只要 aia_ijj 在同一个环上,那么 aia_i 就可以走 kk 步到达 jj

也就是说:aia_ijj 在同一个环上,是存在 k0k \ge 0 使得 ai(k)=ja^{(k)}_i = j 的充要条件。


否则,怎么求 v(Ai,j)v(A_{i, j})

考虑交换 aia_iaja_j 会有什么影响。

由于我们现在知道,aia_ijj 不在同一个环上(否则会走上面那个分支),然后 jjaja_j 在同一个环上,因此 aia_iaja_j 不在同一个环上。

交换 aia_iaja_j,相当于在 AA 对应的图基础上,将 iaii \to a_ijajj \to a_j 这两条边分别改为 iaji \to a_jjaij \to a_i,其他的边都不变,形成的新图就是 Ai,jA_{i, j} 的图。

发现这样修改,相当于将 aia_iaja_j 所在的两个环合并成一个大环,环长是原来两个小环长的和。

证明:假设 preipre_iii 的前驱(aia_iii 的后继),假设原来是 preiiaipreipre_i \to i \to a_i \to \cdots \to pre_iprejjajprejpre_j \to j \to a_j \to \cdots \to pre_j 两个小环,那么 Ai,jA_{i, j} 的图里面就会变成 preiiajprejjaipreipre_i \to i \to a_j \to \cdots \to pre_j \to j \to a_i \to \cdots \to pre_i 这样一个大环。

举个例子,假设 A=(4,6,8,9,2,5,10,7,1,3)A = (4, 6, 8, 9, 2, 5, 10, 7, 1, 3)(没错还是那个熟悉的排列)。把图再放一遍:

现在考虑从 AA 扩展到 A5,3A_{5, 3}a5=2,a3=8a_5 = 2, a_3 = 8,那么我们应该把 525 \to 2383 \to 8 这两条边分别改为 585 \to 8323 \to 2

然后就会变成这样:

可以看到确实是 aia_iaja_j 所在的两个环合并了。

所以 v(Ai,j)v(A_{i, j}) 其实就是把 aia_iaja_j 所在的两个环加和,和其他的所有环长取 lcm\operatorname{lcm} 的结果。

上面这个例子中就是 v(A5,3)=lcm(4+3,3)=21v(A_{5, 3}) = \operatorname{lcm}(4 + 3, 3) = 21


到这里已经理论可做了,我们已经大体完成了翻译题目的工作。现在考虑加速最后结果的计算。

还是上面那个图,再放一次:

我们把目标锁定在上面那个环和左下角那个环。会发现,从上面那个环随便选一个点,从左下角那个环随便选一个点,交换,结果肯定是 2121。比如:v(A5,8)=v(A2,7)=v(A6,10)=v(A2,3)=21v(A_{5, 8}) = v(A_{2, 7}) = v(A_{6, 10}) = v(A_{2, 3}) = 21

再把目标锁定在上面那个环和右下角那个环,发现随便选之后答案仍然是 2121v(A8,9)=v(A7,1)=v(A3,4)=v(A9,3)=21v(A_{8, 9}) = v(A_{7, 1}) = v(A_{3, 4}) = v(A_{9, 3}) = 21

那有不是 2121 的吗?如果我们把目标锁定在下面这两个环,那么答案就会恒为 lcm(4,3+3)=12\operatorname{lcm}(4, 3 + 3) = 12

为什么会这样呢?发现他跟我们选取两个点所在的环的长度有关系。选取两个点所在环长度分别是 3,43, 4,那答案就是 2121;选取的两个点所在环长度分别是 3,33, 3,那么答案就是 1212。那为啥没有 4,44, 4 呢,因为没有两个长度为 44 的环,选不出来。

如果两个环的长度相同,我们可以认为他们的本质是相同的,设本质不同的环总共有 mm 个,这个 mm 最多可能有多少个?

我们知道,环长总和不能超过 nn,因此最坏的情况就是环长分别为 1,2,3,,m1, 2, 3, \cdots, m 的情况,即使这样 mm 也是 n\sqrt n 量级的。

这是一个经典结论:如果 Σwi=n\Sigma w_i = n,那么值不同的 wiw_i 的种类是 n\sqrt n 量级的(而且不满)。

因此只要枚举本质不同的环长,进行计算。

我们设 v(i,j)v(i, j) 表示将长度为 ii 的环和长度为 jj 的环合并后排列的循环值,考虑它会对最后的答案产生多少个贡献。我们再设 cic_i 表示长度为 ii 的环的数量。

需要分两种情况讨论:

  • iji \ne j,根据乘法原理,贡献量应该是长度为 ii 的所有环里的所有点的数量(数目为 ci×ic_i \times i,环数量乘环长),乘上长度为 jj 的所有环里所有点的数量(数目为 cj×jc_j \times j),也就是 ci×i×cj×jc_i \times i \times c_j \times j
  • i=ji = j,即 v(i,i)v(i, i) 的贡献。首先肯定需要满足 ci>1c_i > 1,因为至少有 22 个长度为 ii 的环才能实现交换。同样根据乘法原理能得到贡献是 ci×i×(ci1)×ic_i \times i \times (c_i - 1) \times i

根据对称性,上面的第一种情况中,也可以直接把 jji+1i + 1 开始枚举,给贡献乘个 22 就行了。

由于枚举的开销是 m×m=nm \times m= n 的,直接从 n2n^2 优化到线性。

到这里复杂度是 O(n×mlogn)=O(nnlogn)\mathcal{O}(n \times m \log n) = \mathcal{O}(n \sqrt{n} \log n)。虽然拿不到满分也能拿到一个高分了。(这个复杂度似乎是 8080 分)


我们发现原题实际上就是想维护一个可重集合的 lcm\operatorname{lcm},支持询问删除 22 个数,增加 11 个数之后新的 lcm\operatorname{lcm} 的操作。询问之间互相独立(一次询问不会真的增删数)

但是发现 lcm\operatorname{lcm} 有可能非常大,很有可能不能直接算。

于是有了这样的启发:考虑对每个 rir_i 质因数分解,然后取每个质因子的幂次最大值计算结果。不过事实上我们后面还要获取两个环合并后长度的质因数分解,他可能不在 rir_i 中,所以我们需要把 nn 以内的数都预处理质因数分解好。

注意这里有一个小技巧:将 22nn 所有数质因数分解,可以先对 22nn 跑线性筛,同时记录每个数的最小质因子。然后再对 22nn 每个数枚举,对于一个数,不断除以它的最小质因子,直到除尽,再把除尽后剩下的数接着不断除以它的最小质因子……直到分解到剩下 11 为止。这样因式分解的复杂度可以从 O(nn)\mathcal{O}(n \sqrt n) 优化到 O(nlogn)\mathcal{O}(n \log n)

那么就可以转化为维护若干个质数幂(所有环长的质因数分解出来的所有质因子幂的并集,同样,是可重集),支持询问删除 22 个数对应的质因子幂,增加 11 个数对应的质因子幂的情况下,每一个质因子的最大幂的乘积(就是 lcm\operatorname{lcm})。询问互相独立。

然后发现一开始那个集合只用记录下所有质因子幂中,每个质数对应最大的 33 个幂,因为后面的不可能影响到 lcm\operatorname{lcm}。比如如果集合里有 22,23,24,272^2, 2^3, 2^4, 2^7,那么 222^2 就是没有用的。因为删掉两个数后,23,24,272^3, 2^4, 2^7 会至少剩下一个,轮不到 222^2 头上,因此它可以直接放弃统计。

所以询问直接暴力删暴力加暴力获取最大值即可,一次询问中,每个质因数的暴力都是小常数级别的,所以单次询问是 O(logn)\mathcal{O}(\log n) 的。

总复杂度 O(nlogn)\mathcal{O}(n \log n)。小常数可以和跑不太满的 logn\log n 抵消抵消。

另外,实际上不需要建图,只需要写一个维护大小的并查集即可。

单组数据 O(nlogn)\mathcal{O}(n \log n)

#include <bits/stdc++.h>

#define int long long

inline int read() {
    int x = 0;
    bool flag = true;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-')
            flag = false;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = (x << 1) + (x << 3) + ch - '0';
        ch = getchar();
    }
    if(flag)
        return x;
    return ~(x - 1);
}

inline bool updmax(int &x, int y) {
    return y > x ? x = y, true : false;
}

const int maxn = 500005;
const int maxm = 805; // sqrt(maxn)
const int mod = (int)1e9 + 7;

int a[maxn];
int inv[maxn], pfmin[maxn]; // prime factor min,存放了一个数的最小质因子。
int pr[maxn], pcnt = 0;
bool isp[maxn];

int lcm = 1;

std :: vector <std :: pair <int, int> > pfs[maxn];
// pfs[i] 存的是 i 的质因数分解,为一个 pair <int, int> 数组。
// pair 的第一个元素是质因子,第二个元素是质因子对应的幂(不是指数,因为在这个题里没必要)。
// 具体看代码,代码比这些文字好懂。

inline void pre(int n = maxn - 5) {
    inv[1] = pfmin[1] = 1;

    for (int i = 2; i <= n; ++i)
        inv[i] = (mod - mod / i) * inv[mod % i] % mod;
    
    std :: memset(isp, true, sizeof(isp));
    for (int i = 2; i <= n; ++i) {
        if (isp[i]) {
            pr[++pcnt] = i;
            pfmin[i] = i;
        }

        for (int j = 1; j <= pcnt && i * pr[j] <= n; ++j) {
            isp[i * pr[j]] = false;
            pfmin[i * pr[j]] = pr[j];
            if (i % pr[j] == 0)
                break;
        }
    }

    for (int i = 2; i <= n; ++i) {
        int t = i;
        while (t != 1) {
            int p = pfmin[t], q = 1;
            while (t % p == 0) {
                q *= p;
                t /= p;
            }
            pfs[i].emplace_back(p, q);
            // printf("%lld %lld %lld\n", i, p, q);
        }
    }
}

int siz[maxn], fa[maxn];

inline int find(int x) {
    while (x != fa[x])
        x = fa[x] = fa[fa[x]];
    return x;
}

inline void uni(int x, int y) {
    x = find(x);
    y = find(y);

    if (x == y)
        return ;
    if (siz[x] > siz[y])
        x ^= y ^= x ^= y;
    fa[x] = y;
    siz[y] += siz[x];
}

int cnt[maxn];
std :: vector <int> f[maxn];

inline void insert(int x) {
    // printf("%lld\n", x);
    for (auto v : pfs[x]) {
        int p = v.first, q = v.second;
        // printf("%lld %lld\n", p, q);
        f[p].push_back(q);
        std :: sort(f[p].begin(), f[p].end(), std :: greater <int> ());
        // 这里 sort 可以看做常数级别,因为 f[p] 始终大小不超过 3
        if (f[p].size() > 3)
            f[p].pop_back();
    }
    return ;
}

int s[maxm], m;

std :: vector <std :: pair <int, int> > g[maxn];
// g[p][i] 表示第 i 个关于质数 p 的幂的修改,first 表示幂的值,second 表示修改量。
// 这个是我们的修改实现,举例说明:
// 删除 12 这个数,先质因数分解 2 ^ 2 * 3
// 然后转化成删掉两个质因子幂,一个 2 ^ 2,一个 3。
// 相当于我们把 2 ^ 2 和 3 的出现次数在集合中分别削了 1,所以 -1 就是两个修改的修改量
// 具体看代码。

int tcnt[maxn];

inline int getv(int p) {   
    int z = 1;
    for (int q : f[p])
        ++tcnt[q];
    for (auto v : g[p])
        tcnt[v.first] += v.second;
    
    for (int q : f[p]) {
        if (tcnt[q] != 0) {
            // 注意!!为什么这里要写成 != 0 而不能是 > 0!!!
            // 首先,我们想:tcnt[q] 有可能小于 0 吗?
            // 其实是可以构造的,只需要让 f[p][2] 这个质因子幂被删两次就可以了。
            // (也就是说 f[p][2] 和 f[p][3](事实上没有)这两个质因子幂相同,而且恰好都被删,
            // 但是因为 f[p][3] 因为只存前三个的原则并没有记录,所以 f[p][2] 会被记录一次删除两次。
            // 所以 tcnt[q] < 0 是有可能的。
            // 由于我们 tcnt[q] 是边扫边清零的(看下面第二行),为了清零成功,我们需要把 < 0 的也清零。

            // updmax(z, q) 会被影响吗?
            // 我们一次最多删两个数,那么 f[p][2] 被删了两次,f[p][0] 和 f[p][1] 肯定没被删过。
            // 那么这一轮的 z 肯定会成功识别出 f[p][0],所以没有影响。
            updmax(z, q);
            tcnt[q] = 0;
        }
    }

    for (auto v : g[p]) {
        int q = v.first;
        if (tcnt[q] != 0) {
            // 这里同上,不能写 > 0。
            // 这里 < 0 是因为可能会有删掉的质因子幂因为不是前三大没记录在 f 中。
            updmax(z, q);
            tcnt[q] = 0;
        }
    }

    return z;
}

inline void modify(int x, int val) {
    for (auto v : pfs[x]) {
        int p = v.first, q = v.second;
        (lcm *= inv[getv(p)]) %= mod;
        g[p].push_back(std :: make_pair(q, val));
        (lcm *= getv(p)) %= mod;
        // 把 lcm 暴力除以原来 p 这里的贡献,修改之后再暴力乘回去。
    }
}

inline void rec(int x) {
    for (auto v : pfs[x])
        g[v.first].clear();
    // 清空修改
}

inline void init(int n = maxn - 5) {
    std :: fill(siz + 1, siz + 1 + n, 1);
    std :: iota(fa + 1, fa + 1 + n, 1);
    
    std :: memset(cnt, 0, sizeof(cnt));
    std :: memset(s, 0, sizeof(s));
    m = 0;
    for (int i = 1; i <= n; ++i)
        f[i].clear();
    lcm = 1;
}

signed main() {
    int T = read();
    pre();
    while (T--) {
        init();
        int n = read();
        for (int i = 1; i <= n; ++i) {
            a[i] = read();
            uni(i, a[i]);
        }
        
        for (int i = 1; i <= n; ++i) {
            if (find(i) == i) {
                ++cnt[siz[i]];
                insert(siz[i]);
                // printf("%lld ", siz[i]);
            }
        }

        for (int i = 1; i <= n; ++i)
            if (cnt[i] > 0)
                s[++m] = i;
        
        for (int i = 1; i <= n; ++i) {
            if (!f[i].empty())
                (lcm *= f[i][0]) %= mod;
            // 初始 lcm
        }

        // printf("%lld\n", lcm);
        int ans = 0;

        for (int i = 1; i <= m; ++i) {
            int u = s[i];

            if (cnt[u] >= 2) {
                // puts("meitain");
                int org = lcm;
                modify(u << 1, 1);
                modify(u, -2);
                // printf("%lld\n", lcm);
                (ans += lcm * cnt[u] % mod * u % mod * (cnt[u] - 1) % mod * u % mod) %= mod;
                rec(u << 1);
                lcm = org;
            }

            for (int j = i + 1; j <= m; ++j) {
                int v = s[j], org = lcm;
                modify(u + v, 1);
                modify(u, -1);
                modify(v, -1);
                (ans += 2 * lcm % mod * cnt[u] % mod * u % mod * cnt[v] % mod * v % mod) %= mod;
                rec(u + v);
                rec(u);
                rec(v);
                lcm = org;
            }
        }

        printf("%lld\n", ans);
    }
    return 0;
}
posted @   dbxxx  阅读(146)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示