总结
( t tly,yyds!) 今天他直接碾压第二题百来分,( t A) 掉了两个题。
怎么说呢,我觉得正是这种难度的比赛才能看得出思维能力,这种比赛的特点就是淡化套路,( t NOI) 的出题风格也确实向这方面靠近了(我觉得这套题和 ( t NOI2020space Day2) 很像),我认为 ( t tly) 巨佬是最有钻研精神的一个,我常常看到他在走廊上独自思考,而且他从来不听讲评总是自己想题,最重要的是即使这么强他也从不摸鱼,永远再挑战更高的境界。
我在想,这也许就是所谓不忘初心吧 (...)
还有考试策略的问题,这次我口胡了很多暴力分上去,导致最后都没有时间打。下次应该先预估比赛难度,如果像这种难度的比赛就边想边打暴力,这次第二题写动态动规直接飞黄腾达。
wsm
题目描述
定义两个非递减数列 (A,B) 之间的笛卡尔和数列 (C) 为任意的 ((A_i+B_j)) 排序之后得到的非递减数列。
(T) 组数据,给定 (C) 的长度 (n),(A) 的长度 (m),问有多少对合法的 ((A,B)) 满足笛卡尔和数列为 (C),答案对 (998244353) 取模。
(1leq Tleq 500,1leq n,mleq 10^{12},nmod m=0)
解法
感谢 ( t lsk) 学长的耐心讲解,他在考试时 ( t A) 了这个题 (\%\%\%)
首先 ( t yy) 一下最后的结果是怎么样的?想象我们把 (C) 排成一个序列,然后序列 (A) 的功能是在这个序列上面打标记,(B) 的功能是平移这个标记,最后的要求是让标记覆盖所有的 (C)(( t lsk) 学长的神奇转化)
我们考虑在 (C) 的这个序列上 (dp),设 (dp(i,j)) 表示已经填满了前 (i) 个空,用了 (j) 个 (A) 序列的方案数,那么转移考虑把前面的序列 ( t copy) 一份(因为 (B) 的作用是整体平移,所以考虑 ( t copy)),转移:
-
考虑添加 (B) 序列中元素:$$dp(kcdot i,j)leftarrow dp(i,j)$$
-
考虑添加 (A) 序列中元素:$$dp(kcdot i,kcdot 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
",(ans+MOD)%MOD);
}
}