[多校联考2021] 模拟赛2
总结
\(\tt tly,yyds!\) 今天他直接碾压第二名百来分,\(\tt A\) 掉了两个题。
怎么说呢,我觉得正是这种难度的比赛才能看得出思维能力,这种比赛的特点就是淡化套路,\(\tt NOI\) 的出题风格也确实向这方面靠近了(我觉得这套题和 \(\tt NOI2020\space Day2\) 很像),我认为 \(\tt tly\) 巨佬是最有钻研精神的一个,我常常看到他在走廊上独自思考,而且他从来不听讲评总是自己想题,最重要的是即使这么强他也从不摸鱼,永远再挑战更高的境界。
我在想,这也许就是所谓不忘初心吧 \(...\)
还有考试策略的问题,这次我口胡了很多暴力分上去,导致最后都没有时间打。下次应该先预估比赛难度,如果像这种难度的比赛就边想边打暴力,这次第二题写动态动规直接飞黄腾达。
wsm
题目描述
定义两个非递减数列 \(A,B\) 之间的笛卡尔和数列 \(C\) 为任意的 \((A_i+B_j)\) 排序之后得到的非递减数列。
\(T\) 组数据,给定 \(C\) 的长度 \(n\),\(A\) 的长度 \(m\),问有多少对合法的 \((A,B)\) 满足笛卡尔和数列为 \(C\),答案对 \(998244353\) 取模。
\(1\leq T\leq 500,1\leq n,m\leq 10^{12},n\bmod m=0\)
解法
感谢 \(\tt lsk\) 学长的耐心讲解,他在考试时 \(\tt A\) 了这个题 \(\%\%\%\)
首先 \(\tt yy\) 一下最后的结果是怎么样的?想象我们把 \(C\) 排成一个序列,然后序列 \(A\) 的功能是在这个序列上面打标记,\(B\) 的功能是平移这个标记,最后的要求是让标记覆盖所有的 \(C\)(\(\tt lsk\) 学长的神奇转化)
我们考虑在 \(C\) 的这个序列上 \(dp\),设 \(dp(i,j)\) 表示已经填满了前 \(i\) 个空,用了 \(j\) 个 \(A\) 序列的方案数,那么转移考虑把前面的序列 \(\tt copy\) 一份(因为 \(B\) 的作用是整体平移,所以考虑 \(\tt copy\)),转移:
-
考虑添加 \(B\) 序列中元素:$$dp(k\cdot i,j)\leftarrow dp(i,j)$$
-
考虑添加 \(A\) 序列中元素:$$dp(k\cdot i,k\cdot j)\leftarrow dp(i,j)$$
边界条件是 \(dp(1,1)=1\),但是这样 \(dp\) 会有问题,如果 \(S\) 能转移到 \(SS\) 和 \(SSSS\),然后 \(SS\) 又能转移到 \(SSSS\),那么显然会算多,所以我们强制第一种转移和第二种转移交替进行(状态增加一维就行)。
但是这样写转移肯定优化不了,因为最后我们只需要求 \(dp(n,m)\),所以把转移写成递归的形式,由于转移要交替进行我们定义 \(f,g\) 两个 \(dp\) 数组,下面的转移一定要把条件写清哦:
现在优化的方式很唯一,就是研究转移路径,因为转移都是做的除法,所以考虑质因数分解。这里把 \(\frac{n}{m}\) 和 \(m\) 质因数分解就行了,记分解出来的质因数集合分别为 \(S\) 和 \(T\),发现第一种转移就是在 \(S\) 中取若干质因数,第二种转移就是在 \(T\) 中取若干质因数。
那么再把题目做一个转化,有 \(S,T\) 两个小球集合,有大约 \(20\) 种颜色 \(50\) 个小球,问交替划分小球集合的方案数。设 \(f(i)\) 为 \(S\) 划分出 \(i\) 个非空盒子的方案数,\(g(i)\) 为 \(T\) 划分出 \(i\) 个非空盒子的方案数,答案可以直接合并:
上式就是交替放置,不难理解。那么现在算出 \(f(i)\) 就行了(\(g(i)\) 同理算),我们知道允许空盒子的情况是特别好算的,这就是考虑每种颜色的球,然后问不定方程解的个数,就是隔板法,设 \(a(i)\) 表示质因数 \(p_i\) 的指数:
现在要求盒子非空,那么容斥回去就可以了,这里我用的是二项式反演:
时间复杂度瓶颈在于质因数分解,我们预处理一下 \(10^6\) 以内的质数就能少掉一个 \(\log\) 了,好消息是不卡常。
小小的总结一下吧,考试时候总以为要推结论然后就没敢做。
方法还是老方法,先分析结果有什么特点是最重要的(这个需要强大的感知能力),这道题就是直接 \(dp\) 然后考虑优化的一道妙妙题,优化需要强大的观察能力哦(才能观察到质因数),这道题确实没有什么套路。
#include <cstdio>
const int N = 105;
const int M = 1000005;
const int MOD = 998244353;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int T,n,m,cnt,ans,p[M],vis[M],C[N][N],a[N],f[N],g[N];
void init(int n)
{
for(int i=2;i<=n;i++)
{
if(!vis[i]) p[++cnt]=i;
for(int j=1;j<=cnt && i*p[j]<=n;j++)
{
vis[i*p[j]]=1;
if(i%p[j]==0) break;
}
}
C[0][0]=1;
for(int i=1;i<=100;i++)
{
C[i][0]=1;
for(int j=1;j<=i;j++)
C[i][j]=(C[i-1][j]+C[i-1][j-1])%MOD;
}
}
void fuck(int x,int *f)
{
//质因数分解
a[0]=0;
for(int i=1;p[i]*p[i]<=x;i++)
if(x%p[i]==0)
{
a[++a[0]]=0;//只用计数就行了吧
while(x%p[i]==0)
x/=p[i],a[a[0]]++;
}
if(x>1) a[++a[0]]=1;
//下面是容斥
for(int i=1;i<=50;i++) f[i]=1;
for(int i=1;i<=a[0];i++)//枚举每种球
{
for(int j=1;j<=50;j++)//j个盒子里面乱几把放
f[j]=f[j]*C[a[i]+j-1][j-1]%MOD;
}
for(int i=50;i>=1;i--)//高往低容斥
{
for(int j=i-1;j>=1;j--)
{
if((i-j)&1) f[i]=(f[i]-f[j]*C[i][j])%MOD;
else f[i]=(f[i]+f[j]*C[i][j])%MOD;
}
}
}
signed main()
{
freopen("wsm.in","r",stdin);
freopen("wsm.out","w",stdout);
T=read();
init(1e6);
while(T--)
{
n=read();m=read();ans=0;
if(n==m || m==1)
{
puts("1");
continue;
}
fuck(n/m,f);//对n质因数分解后容斥
fuck(m,g);//对n/m质因数分解后容斥
for(int i=1;i<=50;i++)
ans=(ans+f[i]*(2*g[i]+g[i-1]+g[i+1])%MOD)%MOD;
printf("%lld\n",(ans+MOD)%MOD);
}
}