算法之小数划分数-整数划分

Posted by 许大仙 on July 29, 2017

一.小数化分数2(简直就是个数学题)

小数化分数2

整数化小数,不循环的小数容易化。对于循环小数化分数原理如下:

⑴ 把0.4747……和0.33……化成分数。

例1: 0.4747×100=47.4747 
0.4747×100-0.4747=47.4747-0.4747
(100-1)×0.4747=47
即99×0.4747=47 
那么  0.4747=47/99

例2: 0.33×10=3.33
0.33×10-0.33=3.33-0.33
 (10-1) ×0.33=3
即9×0.33=3
 那么0.33=3/9=1/3

关键:化去无限的循环部分,需要无限循环减去无限循环。

⑵把0.4777……和0.325656……化成分数。

例1:0.4777×10=4.777①
0.4777×100=47.77②
用②-①即得: 
0.4777×90=47-4
所以, 0.4777=43/90

例2:0.325656×100=32.5656①
0.325656×10000=3256.56②
用②-①即得: 
0.325656×9900=3256.5656-32.5656
0.325656×9900=3256-32
所以, 0.325656=3224/9900

综上所述:就是要非循环部分提前到整数部分,之后再做差。

具体公式可以总结为:

设虚线循环小数为:0.abc(efgh)。括号表示为无限循环的部分。

  • 应到求得abcefgh.efgh,和abc.efgh.然后做差得到(abcefgh-abc)
  • 即:abcefgh.efgh-abc.efgh=0.abcefgh*(10^7-10^3)=abcefgh-abc
  • 设非循环体长度为l1,循环体长度为l2。
  • 0.abc(efgh)=(abcefgh-abc)/(10^(l1+l2)-10^l1)
  • 无限循环小数=(非循环体+循环构成的整数-循环体构成整数)/(10^(l1+l2)-10^l1)

代码实现如下:

#include<iostream>
#include<cmath>
using namespace std;
int gcd(int a,int b){
	return b==0? a:gcd(b,a%b);
}
int main(){
	string s;
	ios::sync_with_stdio(false);
	int t;
	cin>>t;
	while(t--,cin>>s){
		int headl=0,head=0;
		int loopl=0,loop=0;
		int totl=s.length();
		int i,fz,fm;
		for(i=2;i<totl;i++){
			if(s[i]=='('){
				break;
			}
			head=head*10+s[i]-'0';
		}
		headl=i-2;
		if(i!=totl){//有无限循环小数的情况
			loopl=totl-2-i;
			for(int j=i+1;j<totl-1;j++){
				loop=loop*10+s[j]-'0';
			}
			fz=head*pow(10.0,loopl)+loop-head;
			fm=pow(10.0,totl-4)-pow(10,headl);
		}
		else{//没有无限循环小数的情况
			fz=head;
			fm=pow(10.0,headl);
		}
		int GCD=gcd(fz,fm);
		fz=fz/GCD;
		fm=fm/GCD;
		printf("%d/%d\n",fz,fm);
		if(t==0)
			break;
	}
	return 0;
} 

ps:化简成分数:是通过求得分子分母的最大公因数之后,分子分母分别除以这个最大公因数得到互质的分子分母。

二.整数划分问题(介绍三种大法解决此问题:递归,动态规划,母函数)

本题参考:算法中的一个经典命题之一:整数划分问题

所谓整数划分,是指把正整数n拆分成许多小项的和,和为n。例如:4=4+0=3+1=2+2=1+1+2=1+1+1+1(一共有五个划分)。(ps:4=1+3和4=3+1被认为是同一个划分。)

现在就是要求出这些划分的划分数量。例如输入n=4.输出划分上=5

算法核心思想:把一个正整数n划分如下形式: n=m1+m2+m3+….+mi;(其中mi为正整数,并且1<=mi<=n),则{m1,m2,m3,….,mi}为n的一个划分。 如果{m1,m2,m3,….,mi}中的最大值不超过m,即max{m1,m2,m3,….,mi} <= m,则称它属于n的一个m划分。这里我们记n的m划分的个数为f(n,m);。这个题目的想法就是:n的划分看成n的一个m划分。该问题是求出n的所有划分个数,即f(n,n)。

1.递归法和动态规划法

下面我们考虑求f(n,m)的方法:(n划分中的所有小项必须<=m)

根据n和m的关系,考虑下面几种情况:
(1)当n=1时,不论m的值为多少(m>0),只有一种划分,即{1};
(2)当m=1时,不论n的值为多少(n>0),只有一种划分,即{1,1,....1,1,1};
(3)当n=m时,根据划分中是否包含n,可以分为两种情况:
	(a)划分中包含n的情况,只有一个,即{n};
	(b)划分中不包含n的情况,这时划分中最大的数字也一定比n小,即n的所有(n-1)划分=f(n,n-1);
  因此,f(n,n) = 1 + f(n, n - 1)。
(4)当n<m时,n的划分中最大值不过是n,划分中不可能出现负数,因此就相当于n<m时候f(n,m)=f(n,n);
(5)当n>m时,根据划分中是否包含m,可以分为两种情况:
	(a)划分中包含m的情况,即{m,{x1,x2,x3,...,xi}},
		其中{x1,x2,x3,...,xi}的和为n-m,并且{x1,x2,x3,...,xi}可能再次出现m,因此是(n-m)的m划分,因此这种划分个数为f(n-m, m);
	(b)划分中不包含m的情况,则划分中所有值都比m小,即n的(m-1)划分,个数为f(n, m - 1);
 因此,f(n,m) = f(n - m,m) + f(n, m - 1)。 ##### a.递归 综合以上各种情况,可以看出,上面的结论具有递归定义的特征:
  • 其中(1)和(2)属于递归的终止条件条件
  • (3)和(4)属于特殊情况
  • 情况(5)为通用情况,属于递归的方法,其本质主要是通过减少n或m以达到递归终止条件,结束递归调用,从而解决问题。

递归表达式如下: 递归表达式

递归代码(时间复杂度高,n很大时运行慢,递归进行了很多重复计算,很可能TLE):

#include<iostream>
using namespace std;
int divide(int n,int m){//n的m划分。就是n中划分出来的所有当个值均小于等于m 
	if(n==1)
		return 1;
	if(m==1)
		return 1;
	if(n<m)
		return divide(n,n);
	if(n>m)
		return divide(n-m,m)+divide(n,m-1);
	if(n==m)
		return 1+divide(n,m-1);
}
int main(){
	int n;
	while(scanf("%d",&n)!=EOF){
		printf("%d\n",divide(n,n));//n的划分,就是divide(n,n)
	}
	return 0;
}

递归优化版(此版本使用数组标记,如果之前计算过,则直接调用数组中内容,否则计算子递归,这样保证了每次计算一次,减少冗余量):

#include <iostream>  
#define MAXNUM 100  //最高次数  
unsigned  long ww[MAXNUM*11][MAXNUM*11];  
unsigned long dynamic_GetPartitionCount(int n, int max);  
  
using namespace std;  
int main(int argc, char **argv)  
{  
    int n;  
    int m;  
    unsigned long count;  
      
    while(1)  
    {  
        cin>>n;  
        cout<<dynamic_GetPartitionCount(n,n)<<endl;  
    }  
      
    return 0;  
}  
  
unsigned long dynamic_GetPartitionCount(int n, int max)  
{  
    if(n == 1 || max == 1)  
    {  
        ww[n][max]=1;  
        return 1;  
    }  
    if(n < max)  
    {  
        ww[n][n]=ww[n][n]? ww[n][n] : dynamic_GetPartitionCount(n, n);  
        return ww[n][n];  
    }  
    if(n == max)  
    {  
        ww[n][max]=ww[n][n-1]?1+ww[n][n-1]:1 + dynamic_GetPartitionCount(n, n - 1);  
        return ww[n][max];  
    }  
    else  
    {  
        ww[n][max]=ww[n - max][max]? (ww[n - max][max]) : dynamic_GetPartitionCount(n - max, max);  
        ww[n][max]+=ww[n][max-1]? (ww[n][max-1]): dynamic_GetPartitionCount(n, max - 1);      
        return ww[n][max];  
    }  
}  
b.动态规划

考虑到使用递归中,很多的子递归重复计算,这样不仅在时间开销特别大,这也是运算太慢的原因,比如计算130的时候需要27秒钟,在计算机200的时候….计算10分钟还没计算出来。。。鉴于此,可以使用动态规划的思想进行程序设计(减少重复计算),原理如同上面一样,分成三种大情况,只是使用一个数组来代替原有的递归。

ps:考虑到计算ww[10][10]=ww[10][9]+1;所以在每次计算中都是用到之前的记录,这样就可以先从小到大计算出程序,使得计算较大数的时候调用已经计算出的较小的记录(因此循环从1到n进行),程序直接是用循环就可以完成任务,避免了重复计算和空间栈的开销。

递归代码如下:

#include<iostream>
#include<memory.h>
using namespace std;
int dp[1000][1000];
int main(){
	int n;
	while(scanf("%d",&n)!=EOF){
		memset(dp,0,sizeof(dp));
			int m=n;
		for(int i=1;i<=n;i++){
			for(int j=1;j<=m;j++){
				if(j==1||i==1){
					dp[i][j]=1;
					continue;
				}
				if(i<j)
					dp[i][j]=dp[i][i];
				else if(i==j){
					dp[i][j]=1+dp[i][j-1];
				}
				else if(i>j){
					dp[i][j]=dp[i-j][j]+dp[i][j-1];
				}
				
			}
		}
		cout<<dp[n][n]<<endl;
	} 
	
	return 0;
}

2.母函数

生成函数即母函数,是组合数学中尤其是计数方面的一个重要理论和工具。生成函数有普通型生成函数和指数型生成函数两种。形式上说,普通型生成函数用于解决多重集的组合问题,而指数型母函数用于解决多重集的排列问题。母函数还可以解决递归数列的通项问题(例如使用母函数解决斐波那契数列的通项公式)。

所谓母函数,即为关于x的一个多项式G(x):有G(x) = a0 + a1x + a2x^2 + a3*x^3 + ……则我们称G(x)为序列(a0, a1, a2,…..)的母函数。也就是,构造这么一个多项式函数g(x),使得x的n次方系数为f(n)。 如:序列{0,1,2,3,4,5…n}的生成函数(母函数)为:g(x)=0+x+2x^2+3x^3+4x^4+…+nx^n

普通生成函数 指数生成型函数

以上介绍完母函数,先从母函数这个角度来考虑整数拆分的问题:

  • 我们从整数划分考虑,假设n的某个划分中,1的出现个数记为a1,2的个数记为a2,…..,i的个数记为ai,显然有:ak <= n/k(0<= k <=n)因此n的划分数f(n,n),也就是从1到n这n个数字抽取这样的1,2,3……n组合,每个数字理论上可以无限重复出现,即个数随意,使它们的综合为n。
  • 显然,数字i可以有如下可能,出现0次(即不出现),1次,2次,……,k次等等。把数字i出现一次用(x^i)表示,出现k次的数字i用(x^(i*k))表示,不出现用x^0=1表示。
    • 例如,数字2用x^2表示,2个2用x^4表示,3个2用x^6表示,k个2用x^2k表示。

    • 对于1这个数字可以出现n/1=n次,用母函数表示为: (1 + x + x^2 + x^3 + … + x^n)
    • 对于2这个数字可以出现n/2次(除不尽的均向下取整),用母函数表示为:(1 + x^2 + x^4 + x^6 + ….+x^(n/2))
    • 分别考虑1,2……n的所有数字的出现次数的母函数……
    • 则对于从1到N的所有可能组合结果我们可以表示为:     - G(x) = ( 1 + x + x^2 + x^3 + … + x^n)* (1 + x^2 + x^4 + x^6 + ….)….(1 + x^n)= g(x,1)* g(x,2)* g(x,3)* ….* g(x,n)= a0 + a1* x + a2* x^2 +…+ an*x^n + ….//展开式
  • 上面的表达式中,每个括号内的多项式代表了数字i的参与到划分中的所有可能情况。因此,该多项式展开后,由于x^a *x^b = x^(a+b),因此x^i(x^(a+b))就代表了i(a+b)的划分(i被划分为a+b),展开后(x^i)项的系数也就是i的所有划分个数,即n的划分数就是x^n的系数f(n,n) = an。

ps:例如5*x^4,就是整数4的划分数为5.这个次数4可以是4+0,1+3,2+2,1+1+2,1+1+1+1组成,也就是x^4=x^(4+0)=x^(1+3)=x^(2+2)=x^(1+1+2)=x^(1+1+1+1)

由此我们找到了关于整数划分的母函数G(x);剩下的问题就是,我们需要求出G(x)的展开后的所有系数。为此,我们首先要做多项式乘法:我们把一个关于x的多项式用一个整数数组a[]表示,a[i]代表x^i的系数,即:g(x) = a[0] + a[1]x + a[2]x^2 + … + a[n]x^n; 则关于多项式乘法的代码如下,其中数组a和数组b表示两个要相乘的多项式,结果存储到数组c中。

代码如下:

#include <iostream>  
#include <string.h>  
#include <stdio.h>  
using namespace std;  
const int N=10005;  
int c1[N],c2[N]; //c1储存最后的答案,表示当前已经乘上的多项式的各项系数值。c[i]=m表示次幂为i的x^i项的系数为m 
//c2用于过渡 
int main()  
{  
	int n,i,j,k;  
	while(cin>>n)  
	{  
	if(n==0) break;  
	for(i=0;i<=n;i++)  
	{  
		c1[i]=1;  
		c2[i]=0;  
	}  
	for(i=2;i<=n;i++)//i表示要构成整数n的小项.一个i对应一整个括号的多项式  
	{  
		for(j=0;j<=n;j++) //比如n=4,虽然组合数=(1+x+x^2+x^3+x^4)*(1+x^2+x^4)*(1+x^3)*(1+x^4)
   		//但是这个表达式能够达到的最高次幂是4+4+3+4=15次幂,但是我们只要找到x^4次幂.所以这里遍历的之后都n作为终止
	//大于n的次幂,不需要计算 
	//观察会发现每个每次乘完一个多项式,结果的次幂都是连续的从0-n+(n/i)*i。所以从0-n肯定次幂更是连续的。所以每次遍历只要j++从0-n次幂遍历即可 
		for(k=0;k+j<=n;k+=i) //k用于遍历新的多项式的次幂 ,j是前几个多项式的次幂 
   		 		c2[k+j]+=c1[j]; //因为新的多项式系数都为1,所以新多项式系数*前几个多项式系数=前几个多项式系数,所以对c1来说这个操作相当于没做。
		//只需要把次幂相同的项的系数(默认已经一个个小项乘完)累加即可。 
		for(j=0;j<=n;j++)  
		{  
			c1[j]=c2[j];  //结果赋给c1 
			c2[j]=0; //赋0方便累加 
   			}  
	}  
	cout<<c1[n]<<endl;  
   		}  
return 0;  
}  

附题:HDU:1028:Ignatius and the Princess III

三.复杂的正整数划分

第二部分的参考中还有对于五边形数定理和树的算法分析。

对于简单的正整数划分只需要divide(n,m),把n数做m的划分,即n划分成的k份中,每份的值都小于m. 可以参考二点中所述

复杂的正整数划分

简单的正整数划分是针对n个数划分的所有可能。那么如果题目有要求划分的条件,那么状态转移方程将有所不同。

可能出现的划分要求有:

  • 将n划分成若干正整数之和的划分数。(简单正整数划分)
  • 将n划分成k个正整数之和的划分数。
  • 将n划分成最大数不超过k的划分数。
  • 将n划分成若干奇正整数之和的划分数。
  • 将n划分成若干不同整数之和的划分数。

不同情况下的状态转移方程

1.将n划分成不大于m的划分法(对划分每份的上限的要求做分类,状态量是需要划分的数以及每份数值上限)
1).若是划分多个整数可以存在相同的:
	dp[n][m]= dp[n][m-1]+ dp[n-m][m]  

	含义:dp[n][m]表示整数 n 的划分中,每个数不大于 m 的划分数。

	划分数可以分为两种情况:
		a.划分中每个数都小于 m,相当于每个数不大于 m- 1, 故划分数为 dp[n][m-1].
		b.划分中有一个数为 m. 那就在 n中减去 m ,剩下的就相当于把 n-m 进行划分, 故划分数为 dp[n-m][m];
		(因为划分的整数可以相同,所以n-m后划分的最大上限还是m)

2).若是划分多个不同的整数:
	dp[n][m]= dp[n][m-1]+ dp[n-m][m-1]   

	含义:dp[n][m]表示整数 n 的划分中,每个数不大于 m 的划分数。

	划分情况分为两种情况:
		a.划分中每个数都小于m,相当于每个数不大于 m-1,划分数为 dp[n][m-1].
		b.划分中有一个数为 m.在n中减去m,剩下相当对n-m进行划分,并且每一个数不大于m-1,故划分数为 dp[n-m][m-1].
		(由于划分出的每份整数要求不同,所以n-m划分的最大上限是m-1)
2.将n划分成k个数的划分法:(对划分分数做分类,状态量是需要划分的数以及划分的份数)
dp[n][k]= dp[n-k][k]+ dp[n-1][k-1];

含义:dp[n][k],表示整数n的划分中,需要划分k份

方法可以分为两类:(分出的k份中是否包含值为1的份)
	第一类: n 份中不包含 1 的分法。
			为保证每份都 >= 2,可以先拿出 k 个 1 分。到每一份,然后再把剩下的 n- k 分成 k 份即可,分法有: dp[n-k][k]
			(剩下n-k的值还要再分k分和原来拿出的k份1合并,使得每份必>1(即>=2))
	第二类: n 份中至少有一份为 1 的分法。
			可以先拿出一个 1 作为单独的1份,剩下的 n- 1 再分成 k- 1 份即可,分法有:dp[n-1][k-1]
	(已经分出完成的一份1了,剩下n-1的值还需要分k-1份就可以)
3.将n划分成若干奇数的划分法:(计算奇数划分时候同时考虑偶数,状态量为需要划分的数和划分的份数)

方法一:

g[i][j] = f[i - j][j]; 
f[i][j] = f[i - 1][j - 1] + g[i - j][j];

含义:g[i][j]:将i划分为j个偶数
	f[i][j]:将i划分为j个奇数

方法可以分为两个步骤:
  第一步:g[i][j] = f[i - j][j]; 
		i中拿出j个1分到每一份中,将剩余的i-j分成j个奇数
		(这j个奇数和原来拿出的1变成j份偶数。分i为j个偶数(用g表示分偶数)=分i为1+j个奇数=分i-j为j个奇数(用f表示分奇数))
		
  第二步:将i划分为j个奇数,等同于两种子问题的和
		a.j份中包含有一份为1的情况:一份包含奇数1,剩余的i-1分成j-1个奇数(f表示分成奇数值);
		b.j份中不包含一份为1的情况:每份至少大于1,将j个1拿出来分到每一份中,其余i-j分成j份的偶数(j份的偶数和原来j份的1形成j份奇数f[i][j])

方法二:

含义:f[i][j]表示数i划分成j个正奇数的方案数

初始值:
对于i为奇数  f[i][1]=1;f[i][2]=0;
对于i为偶数   f[i][1]=0;f[i][2]=(i/2+1)/2;

考虑f[i][j],需满足i>=j,j>2时
若最小的正奇数是1即j份中包含有一份为1的情况。
对应的方案数是 f[i-1][j-1]
若最小的正奇数>1即j份中不包含一份为1的情况。
从每个正整数中抽调2,对应的方案数f[i-2*j][j];
(为什么要抽2个给每一份,而不是1个,原因是抽两个后还是把i-2*j的值分成j份的奇数值。如果凑1个给每份,就要引入分成j份的偶数值,也就是方法一中的g的含义)

状态转移方程:f[i][j]=f[i-1][j-1]+f[i-2*j][j];

对应代码如下:

        for (int i=1;i<=n;i++){
			if (i%2) 
            {
              f[i][1]=1;
              f[i][2]=0;
            }
        else
          {
            f[i][1]=0;
            f[i][2]=(i/2+1)/2;
          }
		}
        for (int i=3;i<=n;i++){
          for (int j=1;j<=i;j++)
            {
              f[i][j]=f[i-1][j-1];
              if (i-2*j>=j)
                f[i][j]+=f[i-2*j][j];
            }
		}
        for (int i=1;i<=n;i++)
          ans+=f[n][i];
        cout<<ans<<endl;//输出所有i分奇数组合的可能

		//如果是i分成k份奇数值,那么直接输出F[i][k]
       }

类似题目也可以考虑用母函数来解决。