P5366 [SNOI2017] 遗失的答案
当仁不让 希言自然 莫向外求 气冲斗牛 ——题记
- 题记注:这与解题过程没什么关系,不过...
题意
-
已知在
1\sim N 间选了至少一个数,给出这些数的最大公约数G ,最小公倍数L 。 -
回答
Q 组询问,内容为有多少种方案选了x 。
数据范围
-
-
题面未保证
G\mid L ,但在实际数据中,G\mid L 。
解析
0.题意转化
-
首先,容易看出对于
G\nmid x 或x\nmid L ,x 是不可能被选上的。 -
从而考虑令
n=\dfrac{N}{G},g=\dfrac{G}{G},l=\dfrac{L}{G} 。当然,实际数据肯定有G=1 的情况。 -
由
\gcd 与\operatorname{lcm} 的本质(\gcd=\prod p^{\min(k_1,k_2,\dots)},\operatorname{lcm}=\prod p^{\max(k_1,k_2,\dots)} ),容易想到对l 做质因数分解。 -
记
\omega(x) 为x 的质因数个数,d(x) 为x 的约数个数,t_i 表示l 中第i 个质因数的次数。 -
手动打表发现,对于
l\leqslant 10^8 ,有\omega(l)\leqslant 8 ,d(l)\leqslant 768 。 -
显然这是在要我们状压。考虑令
sta1 表示当前是否有对应质因数次数为0 的因数(即向下顶满,使g=1 ),sta2 表示对应质因数次数是否恰为t_i (即向上顶满,使l=l (意会一下))。
1.dp 设计
-
容易想到如下 dp:
-
令
\mathit{dp}_ {i,sta1,sta2} 表示当前已经考虑完了前i 个l 的约数(非约数不可能被选上,注意>n 的也不可能被选上),后两维含义参上,的总方案数。 -
初始化很显然,
\mathit{dp}_ {0,0,0}=1 。 -
考虑到状态似乎比较稀疏而且我就是不想写状态转移方程,我们来写一下递推转移式。记
f_i 为第i 个约数。 -
-
其中
sta1_{f_{i+1}} 之类的表示f_{i+1} 对sta 的影响,显然容易预处理出,故转移O(1) 。状态数为d(l)\times 2^{2\omega(l)}\approx 5\times 10^7 ,还算能接受。 -
但我们无法回答询问:看起来这个 dp 只能每次暴力钦定必选某个跑一遍,T 的无药可救。
2.针对询问的 dp 修正
-
考虑一种惯用手法:正反(指从前向后和从后向前)做两遍 dp。从而询问
f_i (不是约数的询问的答案显然是0 )时可以暴力枚举左右进行配对。 -
然而配对显然不可做(
2^{4\omega(l)} ),考虑只枚举一边。 -
枚举左边之后容易得到左边与
f_i 的影响或上后所缺的sta ,从而问题转化为了对右边的反向 dp 求一个sta 的超集和,为了方便我们肯定是把两个sta 压到一起了。 -
很遗憾我不会 FWT,也不会 FMT,所以我们来一个稍微乱搞一点的做法(似乎本质还是 FMT)。
-
把整个反向 dp 的结果在
sta 上取反,把所缺的sta 也取反。从而我们现在是要求一个sta 的子集和。 -
显然暴力枚举子集和刚才的复杂度一样,往递推之类的方向思考。
-
考虑这样一种 dp 来求子集和:
-
定义
\mathit{sum}_ {i,sta} 为考虑了前(认为位权为1 的位最靠前)i 位可以与当前的sta 不同的所有子集的和,注意这个做法没考虑它自己(没有任何一位不一样),得特判(其实就是初始化)。 -
容易看出,当
sta 第i 位为0 时,\mathit{sum}_ {i,sta}=\mathit{sum}_ {i-1,sta} 。 -
当
sta 第i 位不为0 时,可以分类讨论。- 首先强制该位为
1 ,则同上(两者都相当于该位没有变,所以同上)。 - 然后强制该位为
0 ,打表后大眼硬瞪仔细观察可以发现,强制该位为0 的方案数恰好就是\mathit{sum}_ {i-1,sta\bigoplus 2^i} 。 - 实质即强制该位为
0 后可以递归求解(当然没有必要真的递归)。
- 首先强制该位为
-
更进一步地,容易看出这个 dp 可以直接压维,毕竟该轮被用到的不会在该轮被更新。于是初始化显然。
-
算一下复杂度。单次求
sum 为O(2\omega(l)\times 2^{2\omega(l)}\approx 1\times 10^6) ,考虑上极限情况下要求d(l) 次,复杂度上界大约为8\times 10^8 。 -
乍一看也没什么,可若以普遍理性而论,确实是 T 了?
-
不是的。首先时限是
2s ,然后作为复杂度瓶颈的求sum 主要是加法和位运算(手动取模),实际常数并不大,实测最大点360ms 跑的飞起,诶嘿。
示范代码
- 这篇是解题报告,注释我就不删了,那是当时边做边想的思考过程,上面的讲的不清楚可以看下面。
#include<bits/stdc++.h>
#define il inline
#define ri register
#define For(i,a,b) for(ri int i=(a);i<=(b);++i)
#define foR(i,a,b) for(ri int i=(a);i>=(b);--i)
#define uint unsigned int
#define ll long long
#define ull unsigned long long
#define b_s basic_string
#define u_map unordered_map
#define pii pair<int,int>
#define m_p make_pair
#define fir first
#define sec second
#define nsync ios::sync_with_stdio(0),cin.tie(0)
#define mem(a,b) memset(a,b,sizeof(a))
using namespace std;
il ll rd(){
ll x=0;bool f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=0;ch=getchar();}
while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
return f?x:-x;
}
char wtst[66];
int wtps;
il void wt(ll x){
if(x<0) putchar('-'),x=-x;
while(x>9) wtst[++wtps]=(x%10+'0'),x/=10;
wtst[++wtps]=x+'0';
while(wtps) putchar(wtst[wtps--]);
}
il void wtk(ll x){wt(x);putchar(' ');}
il void wth(ll x){wt(x);putchar('\n');}
const int maxp=1e4+7,usep=1e4;
bitset<maxp> np;
int p[maxp],nump;
il void euler(){
np[0]=np[1]=1;
For(i,2,usep){
if(!np[i]) p[++nump]=i;
for(ri int j=1;j<=nump && i*p[j]<=usep;++j){
np[i*p[j]]=1;
if(!(i%p[j])) break;
}
}
}
int N,G,L;
int usen,useg,usel;
int pf[9],numpf,numpf2;//prime factor
int lt[9];
int full;
const int maxf=769;
struct factor{
int v,sta;
bool operator<(const factor &b)const{return v<b.v;}
}f[maxf];
int numf;
u_map<int,short> refl;
il void init(){
N=rd(),G=rd(),L=rd();
usen=N/G,usel=L/G,useg=G/G;
//基本读入
euler();
int nowl=usel,t=0;
For(i,1,nump){
while(!(nowl%p[i])) ++t,nowl/=p[i];
if(t) pf[++numpf]=p[i],lt[numpf]=t,t=0;
}
if(nowl>1) pf[++numpf]=nowl,lt[numpf]=1;//注意这里是从 1 开始存的,实际使用中必须减 1
numpf2=(numpf<<1),full=(1<<numpf2)-1;
//对 L 质因数分解
int usei,tnow[9];
for(ri int i=1;i*i<=usel && i<=usen;++i)
if(!(usel%i)){
usei=i; mem(tnow,0);
f[++numf].v=usei;
For(j,1,numpf){
while(!(usei%pf[j])) ++tnow[j],usei/=pf[j];
f[numf].sta=f[numf].sta|((!tnow[j])<<(numpf2-j))|((tnow[j]==lt[j])<<(numpf-j));
}
usei=usel/i;
if(usei==i || usei>usen) continue;
f[++numf].v=usei;
For(j,1,numpf){
tnow[j]=lt[j]-tnow[j];
f[numf].sta=f[numf].sta|((!tnow[j])<<(numpf2-j))|((tnow[j]==lt[j])<<(numpf-j));
}
}
sort(f+1,f+1+numf);
For(i,1,numf) refl[f[i].v]=i;
//生成所有约数
}
const int lim=(1<<16),mod=1e9+7;//够不到
int dpf[maxf][lim],dpb[maxf][lim];//ll 根本开不开,必须步步模 //f 从左向右,b 从右向左
bitset<lim> vis;
il void add(int &a,int b){a=a+b; if(a>=mod) a=a-mod;}//大胆一点。
il void DP(){
dpf[0][0]=1;
For(i,0,numf-1)
For(sta,0,full)
if(dpf[i][sta]){
add(dpf[i+1][sta],dpf[i][sta]);
add(dpf[i+1][sta|f[i+1].sta],dpf[i][sta]);
}
//正向
dpb[numf+1][0]=1;
foR(i,numf+1,2)
For(sta,0,full)
if(dpb[i][sta]){
add(dpb[i-1][sta],dpb[i][sta]);
add(dpb[i-1][sta|f[i-1].sta],dpb[i][sta]);
}
//反向
}
int ans[maxf];
int sum[maxf][lim];
il void work(){
int stanow;
for(ri int l=0,i=1,r=2;i<=numf;++l,++i,++r){
if(r<=numf) For(sta,0,full) sum[r][sta]=dpb[r][full^sta];
else sum[r][full]=1;
For(j,0,numpf2-1)
For(sta,0,full)
if(sta&(1<<j))
add(sum[r][sta],sum[r][sta^(1<<j)]);
For(sta,0,full)
if(dpf[l][sta]){
stanow=(sta|f[i].sta);
// fsta=(stanow^full);//取两遍反,相当于没取
add(ans[i],1ll*dpf[l][sta]*sum[r][stanow]%mod);
}
}
}
il void answer(){
int Q=rd(),x;
while(Q--){
x=rd();
if(x%G || L%x){wth(0); continue;}
x=x/G;
wth(ans[refl[x]]);
}
}
int main(){
init();
DP();
work();
answer();
return 0;
}
/*
首先,我们将题意转化。
若 G>1,则将 N,L,G 同除 G
显然,N%G!=0 部分不可能被选,故此变换等价
只要在询问时处理一下即可。
接下来我们对 L 做质因数分解。
打表可得 1e8 一定拆不出来 23(在尽量让不同质因数尽量多的前提下)
故不同的质因数至多为 2 3 5 7 11 13 17 19,即 8 种,记为 P
由约数个数定理随便乱搞一下,容易发现,
不同的约数至多有 768 种,记为 D
有了这些,我们就有能力度量复杂度了。
基于 gcd 和 lcm 的实质,考虑一个简单 dp:
dp[i][sta1][sta2] 表示考虑完毕了前 i 个约数,sta1 表示各个质因子是否有 0 次方,sta2 表示各个质因子是否有 max 次方,内容为总方案数。
则实际上的状态大小为 D*2^P*2^P=D*2^2P=4e7
考虑如何转移。预处理出每个约数及其质因数分解。
进一步地,我们可以给每个约数整个 state1,state2 表示对应质因子是否为 0 次方,是否为 max 次方。
这一部分显然不是复杂度瓶颈。
于是递推转移为 dp[i][sta1][sta2]->dp[i+1][~][~] 以及 dp[i+1][sta1|state1][sta2|state2]
固然可以写转移方程,但是我们知道这样递推因为带 if 可能常数小一点
则我们有了一个 O(1) 的转移。故,总复杂度为 4e7
考虑如何回答询问。容易想到一个简单的暴力:强制选择这个数然后暴力 DP
当然,我们知道,这是绝无希望能过的。所以,
考虑一个稍微能接受一点的做法:
离线询问。
从左向右、从右向左分别 DP,
对于每个询问,在它左边枚举,在它右边枚举,加上它自己的 state,合法则合法。
然而不能接受,这个枚举的复杂度是 2^4P=2^32
所以考虑只枚举一边,复杂度骤降至单个询问 2^2P,故总复杂度为 D*2^2P,与 DP 同阶
只枚举一边之后,我们就得到了一个当前的 state,譬如 100 001
从而对面必须是 x11 11x,即 011 110 的超集
显然这个东西长得不美妙,所以我们得把两个 sta 合到一起,规定 GCD 的在前面
现在的问题就变成了对于给定的二进制数,怎么求它的超集和。
复杂度要求...对于所有的二进制数总共是 O(2^2P),毕竟外面还有一个 D,你可能得对于每个约数都求一遍,当然这里才 4e7 有些冗余的
超集我不会,转成子集试试
将第二个 DP 的 sta 取反,则 011110 的超集和就变成了 100001 的子集和。
还算能接受,我去接杯水,等下继续
显然暴力枚举所有子集根本行不通,那跟刚才的没区别
所以得考虑一点带递推性质或者别的什么的东西
我们知道这实际上好像是 FMT,但没关系。
考虑这样递推它:
先把子集和拆成以下几部分:
只有第 0 位(位权 1)不一样的,只有第 1 位不一样的,...,etc.,记为 just[i][sta],表示对 sta 来说只有第 i 位不一样的子集的和
众所周知,"恰好"在信竞中约等于不可做
所以先把它转成
第 0 位及以下不一样的,第 1 位及以下不一样的,...,etc.,记为 sum[i][sta],容易看出有 sum[i][sta]=sum[i-1][sta]+just[i][sta]
接下来分类讨论这个 just 怎么处理
当 sta&(1<<i)==0,显然有 just[i][sta]=0,即 sum[i][sta]=sum[i-1][sta]
当 sta&(1<<i)==1,啊,这就有点难办了
举个例子会比较显然。对于 111 来说
just-1(完全一样): 111
just0: 110
just1: 100 101
just2: 000 010 001 011
观察到什么了吗?不显然吗?那再看一下 just2
发现,just[2][111]=sum[1][11] !
从而似乎可以递推。sum[i][sta]=sum[i-1][sta]+sum[i-1][sta^(1<<i)]
OK,复杂度为 2P*2^(2P)
总纸面复杂度 8e8,但是时限 2s
更进一步地这个做法常数不大,很有希望过
*/
总结与反思
1.知识点
- 学会了子集和的递推求法。
- 学到了精度较高的复杂度估算。
2.解题中的失误
- 缺的
sta 取反一遍,因为sum 是取过反的要再取反一遍,相当于不取。 - 经典边界写错。
3.其他
- 码力提升(大概)。我其实觉得这个 initialize 最难写。
后记
- 坚持精确,坚持客观,坚持严谨,坚持诚实,敢作敢为。这也许就是这道题的复杂度分析又一次教给我们的。