[CodeForces - 1236B]Alice and the List of Presents

题目阅读

有n种礼物,每种礼物无限件,m个有标号的( For example, putting the first kind of present into the first box but not into the second box, is different from putting the first kind of present into the second box but not into the first box.可以表示为{1}{}、{}{1} 是不同的情况。如果盒子无标号,这两种就是相同情况,与题意矛盾)盒子。

放置规则为:

1.每个盒子可以放至多1个一种礼物

2.每种礼物至少放在一个盒子里

本题是求多重全排列,在乒乓球模型中礼物相当于有标号的乒乓球,盒子为有标号的盒子

对于一件礼物,其放置的总方法数是2^m种,其中不合法的情况是当所有盒子都不放这个礼物的时候,因此合法情况是2 ^m -1.所以总方法数是(2 ^m -1) ^n。

求解指数很大的幂运算的方法:快速幂

对于指数很大:缩小规模(每层节点数)与计算次数(求解树高度)

基于幂运算的性质am+n=am*an,可以将指数拆分然后分别求解运算规模较小的am、an

问题在于如何划分?我们接触最早的划分问题规模,并缩小问题求解难度的,就是二分法。不妨采用二分的方法将m+n,分为两个规模相似的子问题。之所以说是规模相似,是因为如果m+n为奇数,就需要划分为一个规模较另一个规模大1的两个子问题:求解an+1*an;而如果是偶数,就能划分为求解an*an这两个相同问题。不难看出这种划分有比在其他地方划分的优越性,它的每个问题都能划分为两个子问题,可以表示为一个节点能有两个分支,而如果把m+n为奇数的情况理解为求解an*an*a,那么就能画为满二叉树。它的叶子就是a0=1,任何正整数的0次幂为1.

递推形式
f ( n ) = { f ( n / 2 ) ∗ f ( n / 2 ) n 为 偶 数 f ( n / 2 ) ∗ f ( n / 2 ) ∗ a n 为 奇 数 1 n = 0 f(n)=\begin{cases} f(n/2)*f(n/2)& n为偶数 \\ f(n/2)*f(n/2)*a & n为奇数\\ 1 & n=0 \end{cases} f(n)=f(n/2)f(n/2)f(n/2)f(n/2)a1nnn=0

不难据此写出递归函数。注意到划分的两个子问题f(n/2)是相同数的乘积形式,每次递归语句可以写为求一次f(n/2),并将结果的平方,这样避免了两次调用f(n/2),这就是利用对称性简化求解次数。而在其他地方划分的优越性会出现不平衡的二叉树,这会出现一个问题划分为一个较大一个较小的两个子问题,进而形成两个不对称且高度比二分高的树,因为同样多的节点下不平衡的树一定比平衡二叉树高度更大,而无论怎样划分,子问题数总是相同的(证明见慕课: 算法设计与分析 的5.4 动态规划算法的递归实现 ,课上的划分是每个问题的划分位置不同,可以说是更加一般化的划分)。由于高度的大小和计算次数的大小成正比,那么不难看出二分较其他分割方式更平衡,计算更快

实际上这个递归形式的时间复杂度把线性累乘底数的O(m+n)降到O(log(m+n)),即O(log(指数))。而这种尾递归很容易写成迭代的形式。

那么,我们从指数的二进制角度看二分指数,二进制形式的奇偶对应最低位的数值1,0。如果指数是奇数,f(n)=an/2*an/2*a将进行1+(n/2)2的指数运算,而1就是最低位数值,n/2可用c语言的n>>1表示最低位可用n&1获取数值,平方用自乘表示f(n>>1)*f(n>>1),则指数n为奇数时表达式为f(n>>1)f(n>>1);同理,指数为偶数时,将进行*(n/2)2的指数运算**,即f(n>>1)*f(n>>1)

通过分析,我们能够得到f(n)的另一个表达形式:
f ( n ) = { f ( n > > 1 ) ∗ f ( n > > 1 ) n & 1 = = 0 f ( n > > 1 ) ∗ f ( n > > 1 ) ∗ a n & 1 = = 1 1 n = 0 f(n)=\begin{cases} f(n>>1)*f(n>>1)& n\&1==0 \\ f(n>>1)*f(n>>1)*a & n\&1==1\\ 1 & n=0 \end{cases} f(n)=f(n>>1)f(n>>1)f(n>>1)f(n>>1)a1n&1==0n&1==1n=0

//求a^b%m,递归写法
long long binaryPow(long long a, long long b, long long m)
{
	if (b == 0)
		return 1;//递归基a^0=1
	//b为奇数,转化为b-1
	if (b & 1)
    {
        long long mul = binaryPow(a, b >>1, m);
        return a *mul *mul% m;
    }
		
	else//b为偶数转化为b/2
	{
		long long mul = binaryPow(a, b >>1, m);
		return mul*mul %m;
	}
}

注意:1.a、b、m必须都是LL类型,否则大数算错

将递归写为更高效的迭代,需要学会将求幂的形式转化为更高效的每位递推关系

如a98765

将指数按位权展开( 位权,是指数制中每一固定位置对应的单位值。如209的百位位权为100)

=a9*104+8*103+7*102+6*101+5*100

我们能够看到指数各位的位权是连续且固定倍数倍增”,如果我们利用amn=am*n以及am+n=am*an,我们可以把式子转换为以a位权为底,以数码为指数的累乘形式

=(a1049*(a1038*(a1027*(a1016*(a1005

这种形式的好处是我们可以通过把求a位权的值视为子问题,每个因数的求解视作对相同子问题的累乘,整体则是因数的乘积,而每一位都是一个子问题,子问题之间有明显的递推关系,即后一问是前一问结果的a10,我们把a位权称为基底。求解子问很容易用递归、DP实现,每步求解的时间复杂度是O(1)。而对原始问题中指数每一位的求解过程可以看做由多个求基底的数码次幂的子任务的累乘

其实问题的规模并不是n,问题的规模是描述问题所需的空间的量。对于这个问题由二分可以得出问题规模实质是r=logn,近似n的位数。在解空间表示上,由于对称性每步求解只取满二叉树的单个分支,造成最终求解的节点个数恰为树高。

那么线性求幂的时间复杂度为O(2r),这里的求解的时间复杂度实际为O(2r)=O(r),一个r从低位到高位求位权,另一个r是求各子任务的解。二者差异是巨大的。

十进制可以理解这种问题形式的转换与求解,但由于数码的存在并不快,而二进制则更为高效,因为二进制的基底的关系是a位权*2=(a位权)2=a位权*a位权=a下一位的位权,即基底的自乘可以得到下一位的基底.由于一个数的二进制和十进制是一一对应的,那么相应的子任务也是基底的数码次幂,而这里的数码只有0、1。相对于十进制的子任务的累乘底数,对二进制形式的指数而言,子任务只有乘(该位数码为1)与不乘(该位数码为0)基底得到因子,从而简化了原本的累乘循环为数码的条件判断,提高了效率。注意到迭代每一位时虽然某些位的数码可能为0,但仍要为更高位求解计算该位基底,实现前后子问题的递推关系。

long long power(long long a,long long n){
    long long pow=1,p=a;//0(1+1) pow为从低位到当前位置累乘的临时结果,p为当前位置的基底
    while(0<n){//0(1ogn) 判断是否遍历结束
        if(n& 1)//0(1) 判断当前位数码是否为1
        pow*=p;//0(1) 当前数码为1时,则累乘基底
        n>>=1;//0(1) 通过将数码逐次后移来实现对n的二进制形式的遍历
        p*=p;//0(1) 根据递推关系得到下一位的基底
    }
    return pow;//0(1) 返回最终累乘结果
}

最终代码

#include<iostream>
using namespace std;
const int mo=1e9+7;
int mul(int a,int b){
   int ans=1,c=a;
   while(b){
   	if(b%2)ans*=c%mo;//ans累乘基底 
   	c*=c;//基底倍增 
   	b/=2;
   }
   return ans;
}
int main(){
   int n,m;
   freopen("test.txt","r",stdin);
   cin>>n>>m;
   int l=mul(mul(2,m-1)-1,n);
   cout<<l<<endl;
   return 0;
} 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值