DLS采花 解题报告

题目背景

超水的签到题

题目描述

DLS 有N个花田,每个花田里有a_i 朵花。

DLS 喜欢稀奇古怪的花田,他希望重新排列花田,然后去采花。
但 DLS 采花又有一个癖好:他会从左往右采花。
若当前采到第i个花田,在之前有一个花田的花的数量,是第i个花田的花的数量的因子的话,那么 DLS 不会采这个花田的花。

现在,DLS 想知道对于所有排列花田的方案,他能够采到的花的数量的和是多少。
由于答案会比较大,请对998244353取模。

输入输出格式

输入格式:

第一行一个正整数N
第二行是一个长度为N的序列a

输出格式:

共一行,表示所有方案中采花的数量和对998244353取模的结果。

 

题目来源:https://www.luogu.org/problemnew/show/P5216

Solution(由暴力到正解)

1、枚举所有排列,时间复杂度为O(n^2*n!),能通过50%的数据(N \leq 9)

2、我们换一种思路,统计每个花田的贡献值;

所谓贡献值,就是每种花田对答案贡献了多少,它由花田中花的数量以及花田被统计的次数决定,例如:

在输入为2 3 4中,有六种排列:

2 3 4

2 4 3

3 2 4

3 4 2

4 2 3

4 3 2

对于花的数量为4的花田的贡献值,我们发现:在第四、五、六种排列中它被统计入采花数量和(因一、二、三种排列中4的前面均有2这个因子),因此它的贡献值为4*3 == 12.

那么如何求贡献值呢?

我们考虑一下一个花田要满足什么条件才能被统计,显然地,要把它的因子全放在这块花田的后面。因此,这就是一个组合数学问题了。

设一个花田在这N块花田中因子数目为x,且这块花田含a_i朵花;我们可以枚举这个花田应该放的位置,并在后面选x个位置放置它的因子,然后将所有花田排列一下,那么就是a_i*(n-x-1)!\sum_{i=0}^{n-x}P_{n-i}^x,预处理阶乘逆元后,这里运算用到的时间复杂度为O(n),由于有n块花田,所以求解总时间复杂度为O(n^2).

那么如何求得x呢?直接暴力枚举同样也是O(n^2),因此只能通过80%的数据(N \leq 1000),而最大数据N \leq 10^5,a_i \leq 10^5,我们需要更优的方法。

3、看到其它题解中有用桶排序的方法,时间复杂度为O(n),我就分享下我的O(n \sqrt {max\{a_i}\})的做法吧!

先说说求x的方法:用num数组记录每个数字出现的次数,然后枚举每块花田,但是第二重循环并非暴力地枚举其它花田,而是枚举数字找因子——这样可以保证只枚举到\sqrt {a_i}来实现优化目的;因为枚举到的每个因子对应着两个因子,例如通过求得a[i]的因子j,可以知道它还对应着另一个因子\frac {a[i]}{j},因此我们只需枚举到[\sqrt{a_i}],也就是说可以在\sqrt {a_i}时间内把一个数的所有因子统计出来,然后加上每个因子出现的次数即为因子总数;值得注意的是,在因子为1和刚好为\sqrt {a_i}时有特殊情况需判断,详见代码;

那么对于求解的过程怎么优化呢?其实我们不必枚举某个花田的位置,可以直接利用组合先选出x+1个位置放置该花田即它的因子,然后排列一下,每块花田的贡献值就是就是:C_n^{x+1}x!(n-x-1)!,这样就可以在O(n)时间内求解答案。

总时间复杂度为O(n \sqrt {max\{a_i}\}),期望得分100分。

#include<cstdio>
#include<cmath>
#define mod 998244353
using namespace std;
    int n;
    int a[100001];
    int x[100001];
    int num[100001];
    long long mul[100001];
    long long inverse[100001];
    long long ans;
long long C(long long a,long long b)
{
    long long t=1;
    t=t*mul[b]%(long long)mod;
    t=t*inverse[a]%(long long)mod;
    t=t*inverse[b-a]%(long long)mod;
    return t;
}
int main()
{
    scanf("%d",&n);
    mul[0]=1;
    mul[1]=1;
    for (int i=2;i<=100000;i++) //预处理阶乘
        mul[i]=mul[i-1]*(long long)i%(long long)mod;
    inverse[0]=1;
    inverse[1]=1;
    for (int i=2;i<=100000;i++) //预处理逆元
        inverse[i]=(long long)(mod-mod/i)*inverse[mod%i]%(long long)mod;
    for (int i=2;i<=100000;i++) //预处理阶乘逆元
        inverse[i]=inverse[i-1]*inverse[i]%(long long)mod;
    for (int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        num[a[i]]++;
    }
    for (int i=1;i<=n;i++)
        for (int j=1;j<=int(sqrt(a[i]));j++) //枚举因子
            if (a[i]%j==0)
            {
            	if (a[i]==1&&j==1) //特殊情况1:因子和花的数目都为1
            	{
            		x[i]+=num[1]-1;
            		break;
            	}
            	else if (j==1) x[i]--; //特殊情况2:因子为1,花的数目不为1
            	else if (j*j==a[i]) //特殊情况3:因子是花的数目的算术平方根
            	{
            		x[i]+=num[j];
            		break;
            	}
                x[i]+=num[j]+num[a[i]/j];
            }
    for (int i=1;i<=n;i++)
    {
        ans+=(long long)(a[i])*C(x[i]+1,n)%mod*mul[x[i]]%mod*mul[n-x[i]-1]%mod;
        ans%=mod;
    }
    printf("%lld",ans);
}

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值