【GDOI2022PJD1T4 小学生计数题】题解
D1T4 小学生计数题
题目
作为 GDOI 的组题人,小 Y 需要整理手中已有的题目,考虑它们的难度以及所考察的知识点,然后将它们组成数套题目。
小 Y 希望先能组出第一套题目,为了整套题目具有良好的区分度,在一套题目中:
- 所有题目的难度需要能排成等差数列;(也就是说,若将所有题目按难度从小到大排序,那么每相邻两题的难度的差相等,这个差叫做公差)
- 每道题目的难度都是公差的倍数,公差不为 0;
- 需要有不少于 \(L\) 道题,不多于 \(R\) 道题。
现在小 Y 手里已经有了 \(m\) 道题目,其中难度为 \(a_i\) 的题有 \(c_i\) 道 \((1 ≤ i ≤ n)\)。
小 Y 希望能够知道,他有多少种不同的方式能够组出一套题目。
在这道题目中,我们认为两种组题方式不同当且仅当 \(∃k(1 ≤ k ≤ m)\),使得一种方案包含第 \(k\) 道题而另一种方案不包含。
由于答案可能很大,输出答案对 \(998244353\) 取模。
思路
先不考虑题目数量,我们把所有等差数列抽出来。满足这些等差数列都不满足一个完全包含另一个。
我们把这种等差数列定义为特殊等差数列。
那么此时对于任意一种难度 \(a_i\),它最多会被多少个特殊等差数列包含?
显然,对于所有特殊等差数列,其公差必然互不相同。
于是,这种难度 \(a_i\),它的因数个数必然小于100个(打个程序可以验证),所以包含它的特殊等差数列个数必然小于100个。
然后我们就可以把所有特殊等差数列处理出来了。上面的时间复杂度是 \(O(n\log n)\)
我们把这些抽出来的等差数列分别处理,看看其有多少个符合要求的等差数列。
暴力的思想使枚举开头和结尾,然后再暴力循环一遍求其方案,复杂度为 \(O(n^3)\),总复杂度为 \(O(n^4\log n)\)。
显然,最后一重暴力循环不必要,我们可以求个乘积前缀和,然后套费马小定理求逆元再求乘积,复杂度 \(O(n^3\log n)\)。
然后我们发现逆元也可以再做一个前缀和,然后再省掉一重循环,变为 \(O(n^2\log n\log n)\),还有一个 \(\log\) 是因为求逆元有个快速幂需要 \(\log\)。(我不知道线性求逆元在此题可不可行,感兴趣可以试一试)
时间复杂度看起来会爆,然而,每个特殊等差数列。假如其个数多,那么长度小。假如个数小,长度多。因此均摊下来是 \(O(n\log n\log n)\) 的。
官方数据似乎不需要逆元前缀和的优化,但民间数据卡了。
Code
#include<bits/stdc++.h>
using namespace std;
#define int long long
inline int read(){int f=1,x=0;char ch=getchar();while(ch<'0'||
ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;}
#define N 800010
//#define M
#define mo 998244353
struct node
{
int x, y;
}ax[N];
int n, m, i, j, k, T;
int a[N], b[N], c[N], d[N], x[N], s[N], mp[N];
int ans, L, R, sx[N], sy[N];
map<int, int>dp[N];
int sum, anss;
bool cmp(node x, node y)
{
return x.x<y.x;
}
int kuai(int a, int b)
{
int ans=1;
while(b)
{
if(b&1) ans=ans*a%mo;
a=a*a%mo;
b>>=1;
}
return ans;
}
//int chu(int a, int b)
//{
// return a*kuai(b, mo-2)%mo;
//}
//int pan(int l, int r, int k)
//{
// int i, j, ans=0;
// for(i=1; i<=r; ++i)
// for(j=i-k+1; j<=i; ++j)
// {
// ans+=chu(s[i], s[j-1]);
// ans=(ans+mo)%mo;
// }
// return ans;
//}
int panp(int n, int l, int r)
{
int i, j, ans=0;
for(i=l; i<=n; ++i)
{
sum=(sy[i-l]-sy[max(1ll, i-r+1)-2])%mo;
ans+=s[i]*sum%mo;
ans=(ans+mo)%mo;
}
return ans;
}
void check(int i, int k)
{
int m=0, j;
while(i) d[++m]=i, i=dp[i][k];
for(i=1, j=m; i<=m; ++i, --j) x[i]=d[j];
// for(i=1; i<=m; ++i) printf("%lld ", a[x[i]]);
// printf(":%lld\n", k);
for(i=1; i<=m; ++i) d[i]=c[x[i]];
for(i=s[0]=1; i<=m; ++i) s[i]=s[i-1]*d[i]%mo;
for(i=0; i<=m; ++i) sx[i]=kuai(s[i], mo-2)%mo;
for(i=0; i<=m; ++i) sy[i]=((i-1==-1 ? 0 : sy[i-1])+sx[i])%mo;
// for(i=1; i<=m; ++i) printf("%lld ", s[i]);
// printf("\n");
// ans+=(pan(1, m, R)-pan(1, m, L-1));
ans+=panp(m, L, R);
ans=(ans%mo+mo)%mo;
}
signed main()
{
// freopen("counting.in", "r", stdin);
// freopen("counting.out", "w", stdout);
// freopen("tiaoshi.in", "r", stdin);
// freopen("tiaoshi.out", "w", stdout);
n=read(); m=read(); L=read(); R=read();
for(i=1; i<=n; ++i)
ax[i].x=read(), ax[i].y=read();
sort(ax+1, ax+n+1, cmp);
for(i=1; i<=n; ++i)
a[i]=ax[i].x, c[i]=ax[i].y;
// for(i=1; i<=n; ++i) printf("%lld ", a[i]);
for(i=1; i<=n; ++i) mp[a[i]]=i;
for(i=1; i<=n; ++i)
{
// if(a[i]==0) continue;
m=0;
if(a[i]==0)
{
for(j=2; j<=n; ++j)
dp[j][a[j]]=1;
continue;
}
for(j=1; j*j<=a[i]; ++j)
if(a[i]%j==0)
{
b[++m]=j; b[++m]=a[i]/j;
if(j*j==a[i]) --m;
}
sort(b+1, b+m+1);
// printf("%lld:", a[i]);
for(j=1; j<=m; ++j)
{
// printf("%lld ", b[j]);
if(k=mp[a[i]+b[j]]) dp[k][b[j]]=i;
else check(i, b[j]);
}
// printf("\n");
}
printf("%lld", ans);
return 0;
}
4.25更新:此题需要特判一下 \(a_i=0\) 的情况(官方数据没卡,但万恶的民间出题人卡了)
总结
首先对于这道题,首先要想到的是制造特殊等差数列,因为很多情况就可以归到一起来考虑了。
然后是对于题目中等差数列公差的特殊定义可以去思考,然后发现特殊等差数列个数受限于数的因数个数。
然后后面的优化就是凭经验了。对于多次求逆元,可以考虑逆元前缀和进行优化。
本文来自博客园,作者:zhangtingxi,转载请注明原文链接:https://www.cnblogs.com/zhangtingxi/p/16180001.html