首页 > 其他分享 >AT-abc214-g题解

AT-abc214-g题解

时间:2023-07-10 18:45:45浏览次数:44  
标签:ll G3 abc214 题解 sum NTT 删去 我们

题目描述

给定两个排列 \(p, q\),要求统计满足 \(\forall i, r_i\not= p_i, r_i \not= q_i\) 的排列 \(r\) 的数量。对 \(1000000007\) 取模

数据范围 \(n \le 3000\)。

solution

本题要求统计数量,反正我想了半天没想到怎么正向统计(bushi),因此我们考虑容斥。

设 \(h_i\) 为只看其中 \(i\) 条限制,这 \(i\) 条限制都不满足的情况数。因此答案即为 \(\sum \limits_{i=0}^{n}(-1)^ih_i\)。

现在问题转变为了如何求这个 \(h\)。

这里有一个及其巧妙的转化:从 \(p_i\) 向 \(q_i\) 连边,这样一定会出现至少一个环。第 \(i\) 个位置填 \(r_i\) 就意味着第 \(i\) 条边对应着第 \(r_i\) 个点。

而我们考虑不满足要求的情况,就是这条边正好对应着它的起点或者终点。而没有对应的情况我们就可以直接把这条边从图中删去。(注意:下文中所说的选择某条边就意味着不删去这条边)

在一个大小为 \(n\) 的环中,如果我们删去 \(i\) 条边,我们就会剩下 \(i + 1\) 个连通块,如果我们在这个环上不删去任何一条边,那么就意味着这个环上每条边都对应着自己的起点或者终点。这时有两种情况:全都对应自己的起点和全都对应自己的终点,一共两种情况。

而当出现链时,我们考虑这条链上边选择点的方案数。我们可以枚举每个点,这个点左边的边全部选起点,右边的点全部选终点,这样一条链的方案数就是这条链的大小。

图示,相同颜色为对应关系

设 \(dp[n, m]\) 为一个大小为 \(n\) 的*删去 \(m\) 条边的方案数,我们可以枚举第一个点所在链的大小 \(i\),便可推出方程 \(dp_{n, m}=\sum \limits _{i=1}^{n}dp_{n-i,m-1}i\)。这样就求出了链上的情况统计。

我们再考虑环上的统计。设 \(f[n,m]\) 为一个大小为 \(n\) 的删去 \(m\) 条边的方案数。根据上面的分析,我们可以得到边界条件 \(f[n,0]=2\)。我们也可以像上面一样,枚举第一个点所在的连通块的大小,我们可以得到转移方程 \(f_{n, m}=\sum \limits _{i=1}^{n}i^2dp_{n-i,m-1}\)。


关于上面转移方程中 \(i^2\) 的解释(因为我一开始也没搞懂)

我们枚举一号点所在连通块的大小 \(i\),因为删去了边,所以这个连通块一定是一条链。而这条链的选择有 \(i\) 种,大小为 \(i\) 的链的方案数也是有 \(i\) 种的。因此根据乘法原理,应该是 \(i^2\)。


我们统计了一个环的情况,我们再考虑多个环。我们设 \(g[n,m]\) 为前 \(n\) 个环删去 \(m\) 条边的方案数。枚举最后一个环删去多少条边,我们能得到转移方程 \(g_{n, m}=\sum \limits _{i=1}^{v_n}g_{n-1,m-i}f_{v_n,i}\) (\(v_n\) 为第 \(n\) 个环的大小)。

我们已经求出了所有环删去一些边的方案数,这时我们再考虑回去,我们会发现,删去一条边就意味着这条边不对应它的起点或终点,也就意味着它在 \(r\) 排列中合法,那么减去这些删去的边,剩下的就是不满足要求的个数 \(h\) 了。也就是 \(h_i=g_{cnt,n-i}\)。

这时我们看似应该已经做完这题了,但是我们可能还会有一些问题。我们可以想一下哪里有问题。

剩下的问题就是数据范围了,我们可以注意到我们上面写的所有转移方程好像都是 \(O(n^3)\) 的(枚举 \(n, m, i\)),这必然无法通过 \(n \le 3000\) 的数据。所以我们需要考虑优化。

我们再把上面的那几个转移方程拿下来一个一个分析。

\[dp_{n, m}=\sum \limits _{i=1}^{n}dp_{n-i,m-1}i \]

这个很明显是 \(O(n^3)\) 的方程。我们考虑如何快速求出后面这一项。首先我们很容易就能注意到:这是一个极其明显的卷积形式。我们自然可以想到 NTT,但是首先复杂度只能到 \(O(n^2\log n)\),可能也过不去。而且取模数也不是通常的 \(998244353\),而是 \(1000000007\),只能使用 MTT,而 MTT 的常数...就看下面 MTT 的代码吧。

NTT(A[0], G1, 1), NTT(B[0], G1, 1);
for(int i = 0; i < lim; i ++ ) a1[i] = (ll)A[0][i] * B[0][i] % G1;
NTT(a1, G1, -1);

NTT(A[1], G2, 1), NTT(B[1], G2, 1);
for(int i = 0; i < lim; i ++ ) a2[i] = (ll)A[1][i] * B[1][i] % G2;
NTT(a2, G2, -1);

NTT(A[2], G3, 1), NTT(B[2], G3, 1);
for(int i = 0; i < lim; i ++ ) a3[i] = (ll)A[2][i] * B[2][i] % G3;
NTT(a3, G3, -1);

for(int i = 0; i < lim; i ++ )
{
    __int128 M = (__int128)G1 * G2 * G3;
    ans[i] = ((__int128)a1[i] * M / G1 * inv(M / G1, G1) % M
                + (__int128)a2[i] * M / G2 * inv(M / G2, G2) % M + 
                (__int128)a3[i] * M / G3 * inv(M / G3, G3) % M) % M % p;
}

9 遍 NTT 根本跑不动好吧......

因此我们考虑如何才能把转移弄成 \(O(1)\)。我们可以类似前缀和的思想,下面有一个引理可以帮助我们完成构建前缀和的任务。


引理:一个常数数列(例如 \(1, 1, 1, 1\cdots\)) 的 \(k\) 阶前缀和是一个 \(k\) 次多项式,而一个 \(k\) 次多项式的 \(k\) 阶差分为常数数列。

这里就先显然带过,我们只需要数列 \(1, 1, 1, 1\cdots\) 的二阶(下面还有三阶)前缀和。

(其实这玩意也有应用,例如这题


因此我们考虑如何拼凑出来转移方程前面的 \(i\)。我们可以记录两个变量 \(sum_1\) 和 \(sum_2\),前者存储上面所说的常数数列,后者存储 \(sum_1\) 的前缀和数列(注意这里说的数列的意思是 \(dp\) 数组的系数)。然后 \(sum_2\) 就是我们要转移到的值。

画图理解就是:

同理,\(f_{n, m}=\sum \limits _{i=1}^{n}i^2dp_{n-i,m-1}\) 也可以这样优化。\(i^2\) 是个二次多项式,我们就需要 \(3\) 个变量来计算它。

而 \(g_{n, m}=\sum \limits _{i=1}^{v_n}g_{n-1,m-i}f_{v_n,i}\) 其实根本不需要优化。我们考虑下把这三个循环写出来是什么样子:

for i = 1 ~ cnt
    for j = 1 ~ n
        for k = 1 ~ v[i]
            转移

我们可以发现复杂度即为 \(O(n \sum v)\),就是 \(O(n^2)\)。

因此现在三个转移方程复杂度都为 \(O(n^2)\) 了,我们就可以愉快的 AC 了!

代码这里放一下吧,因为这题算思维题里面码量大的了,而且边界也很多。

#define LOCAL
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
typedef pair<int, int> PII;

const int mod = 1e9 + 7, N = 3010;
ll f[N][N], g[N][N], dp[N][N], h[N];
ll fact[N];
ll p[N], q[N];
ll ne[N], la[N];
bitset<N> vis;
vector<int> d;
ll n, ans;

int main()
{
    n = read();

    fact[0] = 1;
    for(int i = 1; i <= n + 1; i ++ ) 
    {
        fact[i] = fact[i - 1] * i % mod;
        dp[1][i] = i;
    }

    for(int i = 2; i <= n + 1; i ++ )
    {
        ll sum1 = 0, sum2 = 0;
        sum1 = sum2 = dp[i - 1][i - 1];
        for(int j = i; j <= n + 1; j ++ )
        {
            dp[i][j] = sum2;
            sum1 = (sum1 + dp[i - 1][j]) % mod;
            sum2 = (sum2 + sum1) % mod;
        }
    }

    for(int i = 1; i <= n + 1; i ++ )
    {
        f[1][i] = i * i % mod;
        f[0][i] = 2;
    }

    for(int i = 2; i <= n + 1; i ++ )
    {
        ll sum1 = dp[i - 1][i - 1];
        ll sum2 = dp[i - 1][i - 1];
        ll sum3 = dp[i - 1][i - 1];
        for(int j = i; j <= n + 1; j ++ )
        {
            f[i][j] = sum3;
            sum2 = (sum2 + sum1 * 2) % mod;
            sum1 = (sum1 + dp[i - 1][j]) % mod;
            sum2 = (sum2 + dp[i - 1][j]) % mod;
            sum3 = (sum3 + sum2) % mod;
        }
    }

    for(int i = 1; i <= n; i ++ ) p[i] = read();
    for(int i = 1; i <= n; i ++ ) 
    {
        q[i] = read();
        ne[p[i]] = q[i];
        la[q[i]] = p[i];
    }

    for(int i = 1; i <= n; i ++ )
    {
        if(!vis[p[i]])
        {
            int num = 1;
            int st = p[i];
            vis[st] = true;
            while(ne[st] != p[i])
            {
                st = ne[st];
                num ++;
                vis[st] = true;
            }
            d.push_back(num);
        }
    }

    ll sum = 0;
    for(int i = 0; i < d.size(); i ++ )
    {
        ll v = d[i];
        sum += v;
        if(i == 0)
        {
            if(v == 1)
            {
                g[i][0] = 1;
                g[i][1] = 1;
                continue;
            }
            g[i][0] = 2;
            for(int j = 1; j <= v; j ++ )
                g[i][j] = f[j][v];
            continue;
        }
        if(v == 1)
        {
            for(int j = 0; j <= sum; j ++ )
            {
                g[i][j] = g[i - 1][j];
                if(j) g[i][j] = (g[i][j] + g[i - 1][j - 1]) % mod;
            }
            continue;
        }
        for(int j = 0; j <= sum; j ++ )
            for(int k = max(j - v, 0ll); k <= sum - v; k ++ )
                g[i][j] = (g[i][j] + g[i - 1][k] * f[j - k][v] % mod) % mod;
    }

    for(int i = 0; i <= n; i ++ )
        h[n - i] = g[d.size() - 1][i];

    for(int i = 0; i <= n; i ++ )
    {
        if(i & 1) ans = (ans - h[i] * fact[n - i] % mod + mod) % mod;
        else ans = (ans + h[i] * fact[n - i] % mod) % mod;
    }
    
    cout << ans << endl;

    return 0;
}

标签:ll,G3,abc214,题解,sum,NTT,删去,我们
From: https://www.cnblogs.com/crimsonawa/p/17542002.html

相关文章

  • P3599题解
    本题是一道比较典的构造题,拿来扩宽扩宽大家的眼界吧。Task1试判断能否构造并构造一个长度为\(n\)的\(1\simn\)的排列,满足其\(n\)个前缀和在模\(n\)的意义下互不相同。很容易想到的一点是:\(n\)一定排在第一位,因为如果排在别的位,加上\(n\)后在模\(n\)意义下是不......
  • CF1421E题解
    题目链接本题作为一道本人思考了50分钟没想出来的大思维题,我觉得可以用来扩宽一下大家的视野。本题中,我们每次都会选取两个相邻的数\(a_i\)和\(a_{i+1}\),同时将这两位变为一位,这一位上填的数为\(-(a_i+a_{i+1})\)。很容易想到的一个\(O(n^3)\)的dp做法是区间dp,设\(f[......
  • NOIP2013-2023题解
    本文章主要是为了不想卷题的时候不是特别颓废而准备本文章是为了总结NOIP最近的题目(为了今年NOIP做准备),目前还没写完,尽量做的全面一些。2013积木大赛给定一个长度为\(n\)的序列\(h_i\),初始有一个全为\(0\)的序列,每次操作可以任意选择\(L,R\),使得\([L,R]\)这段区......
  • CF1545D-题解
    题目链接题目描述有\(n\)个人和\(k\)个间隔相同时间的时刻,每个人都向正方向做匀速直线运动。给出每个时刻(\(0\simk-1\))的所有人的坐标集合(无序),在这\(nk\)个数中有一个数是错误的,找出这个错误的数并将其改正。数据范围:\(5\len\le1000\),\(7\lek\le1000\)。加......
  • 【问题解决】docker login报错 org.freedesktop.Secret.Error.IsLocked: Cannot creat
    问题场景环境docker24.0.2社区版UbuntuServer18.04LTS刚刚执行dockerlogin登录仓库报错:hellxz@bigdata:~/dockerTest$dockerloginharbor.xxx.com.cnUsername:hellxzPassword:**Message:17:26:21.611:Remoteerrorfromsecretservice:org.freedesktop.Se......
  • CF1827D 题解
    problem&blog。很好的题。用到一些关于重心的trick。不妨认为只有一个重心\(\text{maxx}\)。设当前节点数为\(n\),重儿子所在的子树的大小为\(\text{maxsiz}\),那么答案即\(n-2\times\text{maxsiz}\),方法是往重儿子的那个子树爆加节点。因此需要在线维护\(\text{maxx}......
  • CF1601F Two Sorts 题解--zhengjun
    link这里提供一种不用meetinmiddle的方法,速度比较可观。发现性质开始简单的推一下式子。\(\sum(i-a_i)\bmodp=\sum(rk_i-i-p\times\lfloor\frac{rk_i-i}{p}\rfloor)=-p\times\sum\lfloor\frac{rk_i-i}{p}\rfloor,p=998244353\)于是,只需求出\(\sum\lfloor\frac{rk_i-......
  • C++题解——格子游戏
    题目链接:一本通TFLSOJ思路:使用并查集给点连接,如果在连接过程中遇到已连接的点二次连接,就输出代码:#include<bits/stdc++.h>usingnamespacestd;structnode{ intx,y;};nodef[205][205];intn,m;nodefind(nodek){ if(f[k.x][k.y].x==k.x&&f[k.x][k.y].y==k.y)retur......
  • Windows下安装python2和python3双版本及问题解决
    现在大家常用的桌面操作系统有:Windows、MacOS、ubuntu,其中MacOS和ubuntu上都会自带python。这里我们只介绍下Windows(我用的Win10)环境下的python2.x和python3.x的安装,以及python2.x与python3.x共存时的配置问题。本节内容python下载安装Python2.x安装Python3.x当前存......
  • 洛谷P1443:马的遍历--题解
    写在前面这是蒟蒻第一篇题解。作为一名没带脑子的初中生的第一篇题解,本题解必定存在诸多错误,给您带来的不便敬请谅解。对于不足之处与错误,还请多多包涵,并欢迎批评指正!本题目来自于洛谷,网址https://www.luogu.com.cn/problem/P1443。非营利性,侵权请联系删除。题目详情马的遍历......