WaWa的奇妙冒险(第六周集训自闭现场)

第六周周记(因为时间不足,且内容较多,所以按知识点分类找特殊题型记录)

  • (一)高精度计算
    • 1.一些细节
    • 2.高精度加法
    • 2.高精度减法
    • 3.高精度乘法
    • 4.高精度除法
    • 5.一些java做的目前C++做不到的操作
  • (二)快速幂
    • 1.普通的快速幂(logn)
    • 2.矩阵快速幂(m^3^logn)
  • (三)GCD/LCM(我懂个锤子的GCD)
    • 利用gcd的性质
      • 1.互质素因数同倍数下的gcd
      • 2.多个数的GCD/LCM
      • 3.求解区间内的互质
    • 扩展欧几里得(扩欧是真的难)
  • (四)同余定理

(一)高精度计算

这周首先提到的一个点就是高精度计算,理解起来也很简单,用字符串模拟整数运算(浮点数不会,也还没了解,有过设想,分两个数组处理,但暂且不做尝试)。

1.一些细节

(1)一般用数组下标0来记录位数,方便输出(乘法不能这么操作)
(2)先算完后进位或者边算边进位皆可,但一般边算边进位,效率较高
(3)善用加法

2.高精度加法

HDU - 1047 Integer Inquiry java代码

import java.util.*;  ##  输入输出的库
import java.math.*;  ##  数学库

public class Main{
	public static void main(String[] args){
		Scanner input = new Scanner(System.in);  ##  定义输入
        int n = input.nextInt();  ##  确认输入的n
        while(n-- > 0){
            BigInteger ans = new BigInteger("0");
            while(input.hasNextBigInteger()){
                BigInteger temp = new BigInteger("0");
                temp = input.nextBigInteger();
                if(!temp.equals(BigInteger.valueOf(0)) ){
                    ans = ans.add(temp);
                }
                else{
                    System.out.println(ans);
                    if(n != 0) System.out.println();
                    break;
                }
            }
        }
        input.close();
	}
}

c++高精度加法模板 (摘自https://blog.csdn.net/kele52he/article/details/77484529)

#include  
#include  
#include  
using namespace std;  
const int L=110;  
string add(string a,string b)//只限两个非负整数相加  
{  
    string ans;  
    int na[L]={0},nb[L]={0};  
    int la=a.size(),lb=b.size();  
    for(int i=0;i<la;i++) na[la-1-i]=a[i]-'0';  
    for(int i=0;i<lb;i++) nb[lb-1-i]=b[i]-'0';  
    int lmax=la>lb?la:lb;  
    for(int i=0;i<lmax;i++) na[i]+=nb[i],na[i+1]+=na[i]/10,na[i]%=10;  
    if(na[lmax]) lmax++;  
    for(int i=lmax-1;i>=0;i--) ans+=na[i]+'0';  
    return ans;  
}  
int main()  
{  
    string a,b;  
    while(cin>>a>>b) cout<<add(a,b)<<endl;  
    return 0;  
} 

摘的模板不够优化,其实可以自己按题目要求做一点简单的优化,下面是**wlacm 阶乘和(sum)**中对于加法的简单优化

#include 
#include 
using namespace std;
 
int a[1000] = {0},ans[1000] = {0};
 
void cal(){
    int x = 0;
    ans[0] = ans[0] > a[0] ?ans[0] :a[0];  ##  最高位二取一即可,对于两个大数,同理
    for(int i = 1;i <= ans[0];i++){
        ans[i] += a[i] + x;
        x = ans[i] / 10;
        ans[i] %= 10;
    }
    if(x) ans[++ans[0]] = x;
     
    return;
}
 
int main()
{
    int n;
    cin >> n;
    a[1] = 1;a[0] = 1;
    ans[1] = 1;ans[0] = 1;
    for(int i = 2;i <= n;i++){
        int x = 0;
        for(int j = 1;j <= a[0];j++){
            a[j] = a[j] * i + x;
            x = a[j] / 10;
            a[j] %= 10;
        }
        while(x){
            a[++a[0]] = x % 10;
            x /= 10;
        }
        cal();  ##  自定义函数执行加法
    }
     
    for(int i = ans[0];i >= 1;i--) cout << ans[i];
    cout << endl;
}

2.高精度减法

基本和上面加法同理,不同点是写个while循环进行减位操作操作前比较两个数组大小,这里不再赘述,简单贴个模板,同样来自上面那个blog

#include  
#include  
#include  
using namespace std;  
const int L=110;  
string sub(string a,string b)//只限大的非负整数减小的非负整数  
{  
    string ans;  
    int na[L]={0},nb[L]={0};  
    int la=a.size(),lb=b.size();  
    for(int i=0;i<la;i++) na[la-1-i]=a[i]-'0';  
    for(int i=0;i<lb;i++) nb[lb-1-i]=b[i]-'0';  
    int lmax=la>lb?la:lb;  
    for(int i=0;i<lmax;i++)  
    {  
        na[i]-=nb[i];  
        if(na[i]<0) na[i]+=10,na[i+1]--;  
    }  
    while(!na[--lmax]&&lmax>0)  ;lmax++;  
    for(int i=lmax-1;i>=0;i--) ans+=na[i]+'0';  
    return ans;  
}  
int main()  
{  
    string a,b;  
    while(cin>>a>>b) cout<<sub(a,b)<<endl;  
    return 0;  
}  

3.高精度乘法

和加减法同理,不同点两个高精度大数的乘法,必须要开两倍大的数组进行存储(这个自己想想就好,很简单),其次,数组的0下标位置不能再用来存位数,因为0的特性才使乘法得以成功
wlacm 高精度求积 代码

#include 
#include 
using namespace std;
 
int main()
{
    int m[110] = {0},n[110] = {0},ans[220] = {0};
    string a,b;
    cin >> a >> b;
     
    for(int i = a.size() - 1;i >= 0;i--) m[i] = a[a.size() - i - 1] - '0';
    for(int i = b.size() - 1;i >= 0;i--) n[i] = b[b.size() - i - 1] - '0';   
     
    for(int i = 0;i < a.size();i++){
        for(int j = 0;j < b.size();j++){
            ans[i+j] = ans[i+j] + m[i] * n[j];
        }
    }
    int ansl = a.size() + b.size();
         
    for(int i = 0;i < ansl;i++){
        ans[i + 1] = ans[i + 1] + ans[i] / 10;
        ans[i] %= 10;
    }
     
    while(ansl > 1 && ans[ansl - 1] == 0) ansl--;
     
    for(int i = ansl - 1;i >= 0;i--) cout << ans[i];
    cout << endl;
}

4.高精度除法

做过,但没做过总结归纳,暂且放一放,不过也贴个代码
**hdu 1130 How Many Trees? && hdu 1023 Train Problem II **

#include 
#include 
#include 
#include 
using namespace std;
typedef long long ll;
const ll k = 10000000000;
const int maxn = 105;

ll catalan[maxn][12];

void catalan_table(){
	memset(catalan,0,sizeof(catalan));
	
	catalan[0][0] = catalan[0][1] = 1;
	int i,j;
	for(i = 1;i <= 100;i++){
		for(j = 1;j <= (int)catalan[i - 1][0];j++){
			catalan[i][j] = (4*i-2) * catalan[i - 1][j];
		}
		ll r1 = 0,r2;  ##  此处可以看做一个简单的除法模板,不考虑进位操作的裸模板
		for(j = (int)catalan[i - 1][0];j >= 1;j--){
			r2 = (r1 * k + catalan[i][j]) % (i + 1);
			catalan[i][j] = (r1 * k + catalan[i][j]) / (i + 1);
			r1 = r2;
		}
		for(j = 1;j <= (int)catalan[i - 1][0];j++){
			catalan[i][j + 1] += catalan[i][j] / k;
			catalan[i][j] %= k;
		}
		catalan[i][0] = catalan[i][j] > 0 ?catalan[i - 1][0] + 1 :catalan[i - 1][0];
	}
}

int main()
{
	catalan_table();
	int n;
	while(~scanf("%d",&n)){
		printf("%lld",catalan[n][catalan[n][0]]);
		for(int i = catalan[n][0] - 1;i >= 1;i--){
			printf("%010lld",catalan[n][i]);
		}
		printf("\n");
	}
	return 0;
} 

5.一些java做的目前C++做不到的操作

HDU - 1063 Exponentiation 浮点数高精计算

import java.util.*;
import java.math.*;

public class Main{
	public static void main(String[] args){
		Scanner input = new Scanner(System.in);
		BigDecimal base;
        while(input.hasNextBigDecimal()){
              base = input.nextBigDecimal();
              int n = input.nextInt();
              String ans;
              base = base.pow(n);
              ans = base.stripTrailingZeros().toPlainString();
              if(ans.startsWith("0.")) ans = ans.substring(1);
              System.out.println(ans);
        }
        input.close();
	}
}

(二)快速幂

1.普通的快速幂(logn)

从大一走过来,我们通常喜欢的幂的做法就是pow或者手写循环,但实际上对一些指数较大的幂函数来说,经常会力不从心,这里简单说一下快速幂的思想。

对于ab,我们实际上可以等价于(a2)b/2,这是显而易见的。
但我们不能简单地这么分,因为只有b为偶数时才能这么操作
所以我们把转化分为两种情况
当b & 1,ab == (a*a2)b/2
否则 ab == (a2)b/2

根据这个思想,我们可以把这个操作看做一层套一层的乘积,很显然能想到递归

int pow(int a,int b)
{
    if(b==1)
        return a;
    if(b&1)
        return a*pow(a*a,b/2);
    return pow(a*a,b/2);
}

那在这个思想上再进一步深化,b实际上再整除的过程,形成了一串01串
那么这个式子,实际上,也就成了2k1+2k2+2k3+…+2kn = b
那么,我们不就能把递归转化成一个循环的形式进行优化了吗

int ans=1,base=a;
while(b)
{
    if(b&1)
        ans*=base;
    base*=base;
    b>>=1;
}

快速幂,实际上是一个二分思想上的优化,他不仅仅局限于幂,也同样适用于积
**HDU - 5666 Segment ** (快速积)

#include 
using namespace std;
typedef long long ll;

int main()
{
	ll t;
	cin >> t;
	while(t--){
		ll q,P;
		cin >> q >> P;
		ll a = q - 2ll;
		ll b = q - 1ll;
		if(a & 1) b /= 2;
		else a /= 2;
		
		a %= P;
		ll ans = 0;
		while(b){
			if(b & 1) ans = (ans + a) % P;
			a = (a + a) % P;
			b >>= 1; 
		}
		
		cout << ans << endl;
	}
	return 0;
}

2.矩阵快速幂(m3logn)

求矩阵An,说实话,第一次接触,也没写多少题,大概理解起来就是把简单的递推公式转换为矩阵,然后利用矩阵同样可以快速幂的特性,进行快速幂运算,以解决大数之下单纯递推超时的问题

难点:如何将递推关系转为矩阵
目前对这一块真的不了解,唯一做的题也是题目告诉你关系是怎么转换的,一个简单的finbo,目前就简单地贴一下代码吧
POJ - 3070 Fibonacci(写的很水,也很容易优化)

#include 
#include 
using namespace std;
typedef long long ll;
const ll mod = 10000;

ll maps[2][2],m[2][2];

void cal(){
	ll ms[2][2];
	memset(ms,0,sizeof(ms));
	for(int i = 0;i < 2;i++){
		for(int j = 0;j < 2;j++){
			for(int k = 0;k < 2;k++){
				ms[i][j] += maps[i][k] * m[k][j];
				ms[i][j] %= mod;
			}
		}
	}
	memcpy(m,ms,sizeof(m));
	return;
}

void cal2(){
	ll ms[2][2];
	memset(ms,0,sizeof(ms));
	for(int i = 0;i < 2;i++){
		for(int j = 0;j < 2;j++){
			for(int k = 0;k < 2;k++){
				ms[i][j] += maps[i][k] * maps[k][j];
				ms[i][j] %= mod;
			}
		}
	}
	memcpy(maps,ms,sizeof(maps));
	return;
}

int main()
{
	ll n;
	while(cin >> n && n != -1){
		maps[0][0] = 1;
		maps[0][1] = 1;
		maps[1][0] = 1;
		maps[1][1] = 0;
		
		memset(m,0,sizeof(m));
		m[0][0] = m[1][1] = 1;
		ll k = n;
		while(k){
			if(k & 1) cal();
			cal2();
			k >>= 1;
		}
		
		cout << m[0][1] << endl;
	}
	return 0;
}

另外还有一道进阶题
HDU - 3117 Fibonacci Numbers
可以自行了解,这里特意提一下其中对于斐波那契数列中数取前四位的操作,其实可以成为一个不错的通用思路

斐波那契数列的通项公式为:f(n) = (1 / sqrt(5)) * (((1 + sqrt(5)) / 2) ^ n - ((1 + sqrt(5)) / 2) ^ n),当n >= 40时((1 + sqrt(5)) / 2) ^ n近似为0。

所以我们假设f(n) = t * 10 ^ k(t为小数),所以当两边同时取对数时,log10(t * 10 ^ k) = log10(t) + k = log10((1 / sqrt(5)) * (((1 + sqrt(5)) / 2))) = log10(1 / sqrt(5)) + n * log10(((1 + sqrt(5)) / 2))),
然后减掉整数k,就可以得到log10(t),进而得到t值。

最后附上代码

#include 
#include 
#include 
#include 
using namespace std;
typedef long long ll;
const ll mod = 10000;

ll maps[2][2],m[2][2];

ll f[40];

void f_table(){
	f[0] = 0;f[1] = 1;f[2] = 1;
	for(int i = 3;i <= 39;i++) f[i] = f[i - 1] + f[i - 2];
	
	return;
}

void cal(){
	ll ms[2][2];
	memset(ms,0,sizeof(ms));
	for(int i = 0;i < 2;i++){
		for(int j = 0;j < 2;j++){
			for(int k = 0;k < 2;k++){
				ms[i][j] += maps[i][k] * m[k][j];
				ms[i][j] %= mod;
			}
		}
	}
	memcpy(m,ms,sizeof(m));
	return;
}

void cal2(){
	ll ms[2][2];
	memset(ms,0,sizeof(ms));
	for(int i = 0;i < 2;i++){
		for(int j = 0;j < 2;j++){
			for(int k = 0;k < 2;k++){
				ms[i][j] += maps[i][k] * maps[k][j];
				ms[i][j] %= mod;
			}
		}
	}
	memcpy(maps,ms,sizeof(maps));
	return;
}

int main()
{
	ll n;
	f_table();
	while(cin >> n){
		if(n <= 39) cout << f[n] << endl;
		else{
			maps[0][0] = 1;
			maps[0][1] = 1;
			maps[1][0] = 1;
			maps[1][1] = 0;
			
			memset(m,0,sizeof(m));
			m[0][0] = m[1][1] = 1;
			ll k = n;
			while(k){
				if(k & 1) cal();
				cal2();
				k >>= 1;
			}
			
			double x = log10(1.0/sqrt(5.0)) + (double)n * log10((1.0+sqrt(5.0)) / 2.0);
			double y = x - (int)(x);
			int ans = (int)(1000.0 * pow(10.0,y));
			printf("%d...%04lld\n",ans,m[0][1]);
		}
	}
	return 0;
}

最后再接一句,关于快速幂还有很多地方能用,费马小定理求逆元快速幂取模,它是实现一些算法最基础的部分,要深刻地理解

(三)GCD/LCM(我懂个锤子的GCD)

欧几里得算法,算是接触的最早的算法之一了,但现在才发明,我其实只懂一层皮毛

利用gcd的性质

1.互质素因数同倍数下的gcd

我们都知道,gcd能找出两个数a和b的最大公因数,但我们是否思考过,将a和b除以他们的最大公因数能得到什么?

答案很简单,设最大公因数为k:
gcd(a,b) / k = 1是必然的
而gcd(a,b) / k = 1实际上可以等价于gcd(a/k,b/k) = 1
那我们能够得到的,正好是a和b互质情况下最小的值

再反过来想一想,我们是否就能用这个想法得到一个结论

gcd(ak,bk) 只要a和b互质,那么gcd的值永远不会变化

本来对于这种东西是完全没有思考的,知道我做到了下面这个题目
HDU - 5584 LCM Walk(数论 + GCD + LCM)
题解,来自大佬

①我们可以发现当前位置是(x,y)时,如果x>y,那么当前位置一定是由(x1,y)走到的,如果xy,x=nk,y=mk,当前点就是由(x/(y/k+1),y)走到的,如果x不再是(y+k)的倍数(即:(y/k+1)*k的倍数),则表示不能再逆推

摘自blog:https://blog.csdn.net/qq_31759205/article/details/52628889
下面是代码

#include 
using namespace std;
typedef long long ll;

ll gcd(ll a,ll b){
	return !b ?a :gcd(b,a%b);
}

int main()
{
	ll t,cnt = 0;
	cin >> t;
	while(t--){
		ll x,y,ans = 1;
		cin >> x >> y;
		ll g = gcd(x,y);
		if(x > y) swap(x,y);
		while(!(y % (x + g))){
			ans++;
			x = x;
			y = y / (g + x)*g;
			if(x > y) swap(x,y);
		}
		printf("Case #%lld: %lld\n",++cnt,ans);
	}
	return 0;
}

2.多个数的GCD/LCM

这个性质还是比较好想的,上个学期也是做过多个数GCD的一个简单dp了,两个两个处理过去就好,新生的和老的继续GCD,LCM也是如此。
(这个就不列举题目了。。。)

3.求解区间内的互质

gcd(x,n) = 1,1 <= x <= n;
本质上因为这个一块不应该放在gcd里面讲的,但因为自己第一次写是从gcd里得到的灵感,姑且写到gcd里面。

做一个简单的假设,假设x和n互质,那么没有问题,这就是我们要找的数
如果不是,那么就有意思了,x*k和n必然不可能互质,可以结合我们讲的一点来看
那么我们如果从2开始枚举,利用筛法的思路,把所有gcd(x,n) != 1的值为基数,做一次筛法,是不是我们就能
把所有和n不互质的数找出来了,剩下的不就都是互质的了吗
(当时不知道欧拉函数和容斥原理)

后面结合容斥原理和质因数分解定理,很容易就能写这题
当然直接写欧拉函数更好

扩展欧几里得(扩欧是真的难)

简单来说,扩欧就是用来找二元一次方程的特解,然后根据题目来确认是否找通解之类的

设方程ax+by = n  gcd(a,b) = k;
a = ka',n = kb' ax + by = k(a'x + b'y) = n
如果所有数都为整数,那么n必须为gcd的倍数才有解
很多时候,为了简化问题,判断完是否有解后,直接ax+by = gcd(a,b)
算出特解后整数倍扩大即可

实际上emm 扩欧学的还是很差,只能写写一些裸题,也只写了两道来着
POJ - 1061 青蛙的约会
自己写的时候非常的粗暴。。。套书上的意思,改一下方程的形式,写成类似不定方程的样子直接扩欧解的,写的时候还是很青涩,ac的也有一点莫名其妙
后面附上大佬的详细证明题解
来自洛谷 https://www.luogu.org/problemnew/solution/P1516

#include 
#include 
#include 
#include 
using namespace std;
typedef long long ll;

ll extend_gcd(ll a,ll b,ll &x1,ll &y1,ll &ans){
	if(!b){
		x1 = 1;y1 = 0;
		return a;
	}
	ans = extend_gcd(b,a%b,x1,y1,ans);
	ll t = x1;
	x1 = y1;
	y1 = t - a/b*y1;
	return ans;
}

int main()
{
	ll x,y,m,n,l,x1,y1,ans;
	cin >> x >> y >> m >> n >> l;
	ll a = x-y,b = n-m;
	if(b < 0){
		a = -a;
		b = -b;
	}
	ans = extend_gcd(b,l,x1,y1,ans);
	
	if(a % ans != 0) cout << "Impossible" << endl;
	else cout << ((x1*(a/ans)) % (l/ans) + (l/ans))%(l/ans) << endl;
	return 0;
}

HDU - 1576 A/B
这题emm 刚开始写一直卡自己的想的样例,看了题解才发现有可能有负数情况
直接附上大佬题解,自己只是硬套扩欧求不定方程的模板,实际上理解还是不行
https://blog.csdn.net/infinity_izayoi/article/details/51910343

#include 
#include 
#include 
#include 
using namespace std;
typedef long long ll;
const ll mod = 9973;

void extend_gcd(ll a,ll b,ll &x,ll &y){
	if(!b){
		x = 1;y = 0;
		return;
	}
	extend_gcd(b,a%b,x,y);
	ll temp = x;
	x = y;
	y = temp - (a/b)*y;
	
	return;
}

int main()
{
	ll t;
	cin >> t;
	while(t--){
		ll n,b,x,y;
		cin >> n >> b;
		extend_gcd(b,mod,x,y);
		x = (x + mod) % mod;
		x = (x*n) % mod;
		cout << x << endl;
	}
	return 0;
}

(四)同余定理

简单表述同余定理,即a和b对c取余的余数相同,一般用于解决鸽笼问题用等式方式处理整除关系求逆元

pis:关于用用等式方式处理整除关系逆元结合这一块,知之甚少,或者说,理解很不到尾,不敢随意做总结,估计放一放

1.鸽笼原理
何为鸽笼原理

你可能感兴趣的:(萌新级)