多校冲刺NOIP模拟19 - 乘法——倍增、DP

本文详细介绍了如何计算n!的阶乘并将其转换为十六进制形式,重点讨论了如何处理阶乘中2的因子,通过倍增算法和组合数学的方法优化计算过程,最终实现O(log^3 n log log n)的时间复杂度。文章还提到了代码实现中的关键细节,如快速幂和组合数的模逆运算,并分享了在解决这个问题时与他人思路的对比。
摘要由CSDN通过智能技术生成

此题不提供链接

题目描述

n ! n! n! n < 2 64 n<2^{64} n<264)转成十六进制后,去掉末尾的零的最后 16 位。

题解

n ! = a ⋅ 2 b , a    i s    o d d n!=a\cdot2^b,a\,\,is\,\,odd n!=a2b,aisodd,显然答案应该是 a ⋅ 2 b   m o d   4 a\cdot2^{b\bmod 4} a2bmod4

由于保留十六进制最后 16 位,相当于二进制下保留 64 位,取模 2 64 2^{64} 264,所以全程用 unsigned long long 自然溢出即可。剩下的问题是如何求出 n 以内所有数去除质因子 2 之后相乘。

首先,可以把所有奇数相乘,然后剩下的数除以 2 过后就变成了 { 1 , 2 , 3 , . . . , ⌊ n 2 ⌋ } \{1,2,3,...,\lfloor\frac{n}{2}\rfloor\} {1,2,3,...,2n},再次取所有奇数相乘,把剩下的数除以 2…

所以,我们设答案为 f ( n ) f(n) f(n),设 g ( n ) g(n) g(n) 为 n 以内所有奇数相乘,那么有
f ( n ) = g ( n ) + f ( ⌊ n 2 ⌋ ) f(n)=g(n)+f(\lfloor\frac{n}{2}\rfloor) f(n)=g(n)+f(2n)
由此我们可以想到倍增做法,只需要每次能较快求出 g ( n ) g(n) g(n) 即可。

由于 g ( n ) = ∏ i = 0 n − 1 2 ( 2 i + 1 ) g(n)=\prod_{i=0}^{\frac{n-1}{2}}(2i+1) g(n)=i=02n1(2i+1),我们把括号拆开,就变为所有由若干个 2 i 2i 2i 相乘的项相加。由于取模 2 64 2^{64} 264,所以我们最多只需要关注不超过 63 个 2 i 2i 2i 相乘的项,再大则必定为0。所以对于每个 g ( n ) g(n) g(n),我们只需要求出所有 d p ( n , k ) ( 0 ≤ k < 64 ) dp(n,k)(0\le k<64) dp(n,k)(0k<64) 表示选 k 个 2 i 2i 2i 相乘的和,然后加起来。

再一次转换, d p ( n , k ) dp(n,k) dp(n,k) 怎么求?还是考虑用倍增。假设我们已经求得所有 d p ( n 2 , k ) dp(\frac{n}{2},k) dp(2n,k),相当于知道了前面一半的 ∑ ∏ . . . 2 i \sum\prod_{...}2i ...2i。设后面一半为 d p ′ ( n 2 , k ) = ∑ ∏ . . . ( 2 i + x ) dp'(\frac{n}{2},k)=\sum\prod_{...}(2i+x) dp(2n,k)=...(2i+x),那么有
d p ( n , k ) = ∑ i = 0 k d p ( n 2 , i ) ⋅ d p ′ ( n 2 , k − i ) dp(n,k)=\sum_{i=0}^kdp(\frac{n}{2},i)\cdot dp'(\frac{n}{2},k-i) dp(n,k)=i=0kdp(2n,i)dp(2n,ki)
很像卷积?对啊,我也不知道为什么,可它就是一个普通的状态转移方程。

观察 d p ′ ( n 2 , k ) dp'(\frac{n}{2},k) dp(2n,k) 的式子,发现里面这个括号又可以展开,变为若干个 2 i 2i 2i x x x 的若干次方相乘的和。也就是说,我们可以利用 d p ( n 2 , k ) dp(\frac{n}{2},k) dp(2n,k) 来求出 d p ′ ( n 2 , k ) dp'(\frac{n}{2},k) dp(2n,k)
d p ′ ( n 2 , k ) = ∑ i = 0 k d p ( n 2 , i ) ⋅ x k − i    ( ? ) dp'(\frac{n}{2},k)=\sum_{i=0}^{k}dp(\frac{n}{2},i)\cdot x^{k-i}\,\,(?) dp(2n,k)=i=0kdp(2n,i)xki(?)
搞出来一个貌似正确的式子,可是它连 n = 5 n=5 n=5 都会求错。说明式子肯定不这么简单,后面还要乘上一个系数。经过不断尝试演算,我算出来发现是 ( m − i k − i ) {m-i\choose k-i} (kimi),其中 m 是 n 2 \frac{n}{2} 2n 以内奇数的个数。
d p ′ ( n 2 , k ) = ∑ i = 0 k d p ( n 2 , i ) ⋅ x k − i ⋅ ( m − i k − i ) dp'(\frac{n}{2},k)=\sum_{i=0}^{k}dp(\frac{n}{2},i)\cdot x^{k-i}\cdot{m-i\choose k-i} dp(2n,k)=i=0kdp(2n,i)xki(kimi)
其实也很好理解,因为 ( m − i k − i ) = ( m k ) ( k i ) ( m i ) {m-i\choose k-i}=\frac{{m\choose k}{k\choose i}}{{m\choose i}} (kimi)=(im)(km)(ik)

怎么求组合数呢?由于 k − i k-i ki 很小,所以可以考虑用同一行的组合数递推。但是需要模意义下除法,模数不为质数不好求逆元怎么办?

由于组合数是整数,所以递推的过程中保证分母之积整除分子之积,分子的质因子2的次数肯定大于等于分母的。所以我们可以先把所有分子分母的2的都取出来,最后再乘回去。把2取出来后,剩下的分母必定与 2 64 2^{64} 264 互质,直接预处理求逆元即可。

最后有一些小细节,比如奇数个数翻倍后多1或少1,需要用背包把dp再推一步或倒推一步。

两个倍增合在一起处理,复杂度可达到 O ( log ⁡ 3 n ) O(\log^3n) O(log3n)。我没有预处理 x x x 的次幂和2的次幂,直接使用快速幂,所以复杂度是 O ( log ⁡ 3 n log ⁡ log ⁡ n ) O(\log^3n\log\log n) O(log3nloglogn)

代码

自己推了好久,发现居然就是 O n e I n D a r k \rm O\color{red}neInDark OneInDark 考场上的做法!我还是被 O n e I n D a r k \rm O\color{red}neInDark OneInDark 远远地甩在后面!

#include<cstdio>//JZM yyds!!
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<ctime>
#include<vector>
#include<queue>
#include<stack>
#include<map>
#include<set>
#define ll long long
#define uns unsigned
#define MOD 
#define MAXN 
#define INF 1e17
#define IF (it->first)
#define IS (it->second)
using namespace std;
inline ll read(){
	ll x=0;bool f=1;char s=getchar();
	while((s<'0'||s>'9')&&s>0)f^=(s=='-'),s=getchar();
	while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+(s^48),s=getchar();
	return f?x:-x;
}
int pt[30],lp;
inline void print(ll x,char c='\n'){
	if(x<0)putchar('-'),x=-x;
	pt[lp=1]=x%10;
	while(x>9)x/=10,pt[++lp]=x%10;
	while(lp)putchar(pt[lp--]^48);
	putchar(c);
}
inline uns ll lowbit(uns ll x){return x&-x;}
uns ll n;
char p16[16]={'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
inline void print16(uns ll x,char c='\n'){
	pt[lp=1]=x&15ull;
	while(x>15ull)x>>=4,pt[++lp]=x&15ull;
	while(lp)putchar(p16[pt[lp--]]);
	putchar(c);
}
inline uns ll ksm(uns ll a,uns ll b){
	uns ll res=1;
	for(;b;b>>=1,a*=a)if(b&1)res*=a;
	return res;
}
uns ll inv[105];
const int w=64;
struct itn{
	uns ll f[w];
	int n;
	itn(){memset(f,0,sizeof(f)),n=0;}
};
uns ll ans;
inline int Log2(double x){
	if(x==1)return 0;
	return ((*(unsigned long long*)&x>>52)&1023)+1;
}
inline int coun(uns ll n){return Log2(lowbit(n));}
inline uns ll del(uns ll n){return n/lowbit(n);}
inline itn C(uns ll n){
	itn res;
	res.n=min(n+1ull,w+0ull),res.f[0]=1;
	uns ll k=res.n-1;
	itn cnt;cnt.n=res.n;
	for(uns ll i=1;i<=k;i++){
		res.f[i]=res.f[i-1]*del(n-i+1)*inv[del(i)];
		cnt.f[i]=cnt.f[i-1]+coun(n-i+1)-coun(i);
	}
	for(uns ll i=1;i<=k;i++)res.f[i]*=ksm(2,cnt.f[i]);
	return res;
}
inline itn solve(uns ll n){
	if(n==1){
		itn res;res.f[0]=1,res.n=1;
		return res;
	}
	uns ll m=(n>>1),ad=m-(~m&1)+1;
	if(n&1)m++;
	uns ll c=((n>>1)+1)>>1,d;
	itn a=solve(n>>1),b,res;
	b.n=min(a.n+1,w),res.n=min(a.n+b.n-1,w);
	itn ch[b.n];
	for(int i=0;i<b.n;i++)ch[i]=C(c-i);
	for(int i=b.n-1;~i;i--)for(int j=i;~j;j--)
		b.f[i]+=a.f[j]*ksm(ad,i-j)*ch[j].f[i-j];
	for(int i=res.n-1;~i;i--)for(int j=i;~j;j--)
		res.f[i]+=a.f[j]*b.f[i-j];
	c<<=1;
	while(m>c){
		c++,d=(c<<1)-1ull;
		res.n=min(res.n+1,w);
		for(int i=res.n-1;i>0;i--)res.f[i]+=res.f[i-1]*(d-1ull);
	}while(m<c){
		d=(c<<1)-1ull,c--;
		for(int i=1;i<res.n;i++)res.f[i]-=res.f[i-1]*(d-1ull);
		if(res.n>0&&!res.f[res.n-1])res.n--;
	}
	uns ll cg=0;
	for(int i=0;i<res.n;i++)cg+=res.f[i];
	ans*=cg;
	return res;
}
signed main()
{
	freopen("multiplication.in","r",stdin);
	freopen("multiplication.out","w",stdout);
	for(int i=1;i<=65;i+=2)inv[i]=ksm(i,(1ull<<63)-1);
	for(int T=read();T--;){
		scanf("%llu",&n);
		uns ll m=n;
		int e=0;
		while(m>1)e+=(m>>1)&3ull,e&=3,m>>=1;
		ans=1;
		solve(n);
		while(e--)ans<<=1;
		print16(ans);
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值