24暑假集訓day3上午

Yantai_YZY發表於2024-08-03

進位制轉換

一個 \(m\) 進位制的數字 \(\overline{a_0a_1\cdots a_k}\) 實際上是 \(a_0m^k+a_1m^{k-1}+\cdots+a_k\)

\(10\) 進位制轉 \(m\) 進位制:每次除以 \(m\) 並取出餘數。

\(m\) 進位制轉 \(10\) 進位制:計算 \(a_0m^k+a_1m^{k-}+\cdots+a_k\)

進位制轉換

問題簡述:將 \(n\) 進位制數轉換成 \(m\) 進位制數

思路:先轉換成 \(10\) 進位制,再轉換成 \(m\) 進位制

std:

#include <iostream>
#include <string>

using namespace std;

int n;          //轉化前為n進位制
int m;          //轉化後為m進位制
int num_10 = 0;	//轉化成的10進位制
string num_n;   //轉化前的n進位制
string num_m;   //轉化後的m進位制

int main(void)
{
	cin >> n;
	cin >> num_n;
    cin >> m;
	
	//n進位制轉為10進位制
	int len_n = num_n.length();
	for(int i = 0; i < len_n; i++)
	{
		num_10 *= n;
		num_10 += (num_n[i] >= 'A' && num_n[i] <= 'F') ? (num_n[i] - 'A' + 10) : (num_n[i] - '0');
	}
    
	while(num_10)
	{
		num_m = (char)((num_10 % m >= 10) ? (num_10 % m - 10 + 'A') : (num_10 % m + '0')) + num_m;
		num_10 /= m;
	}
	
	cout << num_m;
	
	return 0;
}

高精度表示

\(\text{int}, \text{long long}\) 分別只能表示 \([−2^31,2^31 ),[−2^63,2^63 )\) 內的數字,超過這個範圍就不能用基礎資料型別直接表示。

我們可以用一個陣列來表示一個高精度數。
例如陣列 \([3,2,1]\) 表示十進位制下的 \(123\)\([3,2,5,1,1]\) 表示 \(11523\) 。(相當於是將數翻轉,再拆位)

高精度加減法

與列豎式一樣,從低位向高位依次考慮。

做加法時,如果進位,此位 \(−=10\),更高一位 \(+=1\)

做減法時,如果借位,此位 \(+=10\),更高一位 \(−=1\)

程式碼實現:

#include <bits/stdc++.h>
using namespace std;
char a[1005], b[1005];//a,b 兩數 
int c[1005], d[1005], e[1005];//c是整數形式的a d是整數>形式的b e是和 
int main()
{
  cin>>a>>b;
  int la = strlen(a);//a的長度 
  int lb = strlen(b);//b的長度 
  //轉整數: 
  for(int i = 1;  i <= la;  i++)
  {
  	c[i] = a[i-1] - '0';
  }
  for(int i = 1;  i <= lb;  i++)
  {
  	d[i] = b[i-1] - '0';
  }
  //倒序存放: 
  reverse(c+1, c+la+1);
  reverse(d+1, d+lb+1);
  int j = la;
  if(j < lb) j = lb;//最大長度
  //相加並判斷是否進位: 
  for(int i = 1;  i <= j;  i++)
  {
  	e[i] += c[i] + d[i];
  	if(e[i] >= 10)
  	{
  		e[i+1]++;
  		e[i] = e[i] - 10;
  	}
  }
  //如果進了一位,說明位數多了一位,j++(位數加一): 
  if(e[j+1] == 1){
  	j++;
  }
  //倒序輸出: 
  for(int i=j;i>=1;i--){
  	cout<<e[i];
  }
  return 0;
}

高精度乘法

還是與列豎式類似,逐個數位相乘,最後化簡。
如果是 \(A\) 的第 \(i\) 位乘以 \(B\) 的第 \(j\) 位,則實際上指的是 \((A[i]×10^i )×(B[j]×10^j )\),貢獻給結果的第 \(i+j\) 位。

複雜度為 \(O(l_A l_B )\)

程式碼實現:

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int c[5005],d[5005],e[10010];
char a[5005],b[5005];
int main(){
    scanf("%s%s",a,b);
    int la=strlen(a),lb=strlen(b);
    for(int i=1;i<=la;i++){
        c[i]=a[i-1]-'0';
    }
    for(int i=1;i<=lb;i++){
        d[i]=b[i-1]-'0';
    }
    reverse(c+1,c+la+1);
    reverse(d+1,d+lb+1);
    for(int i=1;i<=la;i++){
        int x=0;
        for(int j=1;j<=lb;j++){
            e[i+j-1]=c[i]*d[j]+x+e[i+j-1];
            x=e[i+j-1]/10;
            e[i+j-1]%=10;

        }
        e[i+lb]=x; 
    }
    int lc=la+lb;
    while(e[lc]==0&&lc>1){
        lc--;
    }
    reverse(e+1,e+lc+1);
    for(int i=1;i<=lc;i++)cout<<e[i];
    return 0;
}

高精度除以低精度

與豎式除法類似,從高位向低位考慮。

豎式除法每次“帶下去”的那個數實際上是目前的餘數,這個餘數在考慮下一位時位權會 \(×10\)

pair<vector<int>,int> div(vector<int> A, int B){
    vector<int> quotient(A.size());
    int remainder =0;
    for(int i=A.size()-1;i>=0;i--){
        quotient[i]=(A[i]+remainder*10)/ B,
        remainder=(A[i]+remainder *10)%B;
        while(quotient.back()== 0){
            quotient.pop_back();
        }
    }
    return {quotient,remainder};
}

高精度壓位

實際上陣列每個位置不僅僅可以裝一個數位。

例如我們可以讓 \([12,34,567]\) 表示 \(7654321\)
可以限制每個位置裝載 \([0,10^9 )\) 的數字。這樣兩個數字相加不會溢位,但乘法會溢位,需要轉為 \(\text{long long}\) 計算。

階乘之和

問題簡述:

用高精度計算出 \(S = 1! + 2! + 3! + \cdots + n!\)\(n \le 50\))。

其中 ! 表示階乘,定義為 \(n!=n\times (n-1)\times (n-2)\times \cdots \times 1\)。例如,\(5! = 5 \times 4 \times 3 \times 2 \times 1=120\)

思路:用高精度乘上低精度,每次拿出 \(a_{i-1}\times i\) 即可

std:

#include<iostream>
#include<cstring>
using namespace std;
int n,a[90],b[90],c[90],f[90],d=0,len_a,len_b=1,len_c=1,len_ans,m=1;
string s;
int main(){
    cin>>n;
    b[0]=1;
    for(int i=1;i<=n;i++){ 
        len_a=0; 
        int p=i;
        while(p>0){
            a[len_a++]=p%10;
            p/=10;
        }
        for(int j=0;j<len_a;j++) 
            for(int k=0;k<=len_b;k++)
                c[j+k]+=a[j]*b[k];
        for(int j=0;j<len_c;j++) 
            if(c[j]>9) c[j+1]+=c[j]/10,c[j]%=10;
        if(c[len_c]) len_c++; 
        len_ans=len_b,len_b=len_c,m=max(m,len_c); 
        for(int k=len_c-1;k>=0;k--) b[k]=c[k]; 
        len_c=len_a+len_ans;
        memset(c,0,sizeof(c)); 
        for(int j=0;j<m;j++){ 
            f[j]+=b[j];
            if(f[j]>9) f[j+1]+=f[j]/10,f[j]%=10; 
        }
    }
    while(!f[m]&&m>0) m--; 
    for(int i=m;i>=0;i--) cout<<f[i]; 
    return 0; 
}

組合數學基礎

加法原理:做完一件事有 \(n\) 類方法,每類方法有 \(a_i\) 個方法,那麼做完這件事有 \(a_1+a_2+…+a_n\) 個方法。

乘法原理:做完一件事有 \(n\) 個步驟,每個步驟有 \(a_i\) 個方法,那麼做完這件事有 \(a_1×a_2×…×a_n\) 個方法。

排列數

\(n\) 個不同的元素中任取 \(m\) 個元素,按照一定順序排成一列,其方案數稱為 \(A_n^m\)

\[A_n^m=n(n-1)(n-2)\cdots (n-m+1) = \frac{n!}{(n-m)!} \]

第一個位置有 \(n\) 種取法,第二個位置有 \(n−1\) 種取法,第 \(i\) 個位置有 \(n−i+1\) 種取法。

全排列(即 \(m=n\))時,\(A_n^n=n!\)

\[A_n^m=A_{n-1}^m+mA_{n-1}^{m-1} \]

考慮第 \(n\) 號元素是否選取。

如果不選取,則需要在前 \(n−1\) 個元素中選取 \(m\) 個排成一列,方案數為 \(A_(n−1)^m\)

如果選取,則首先需要給 \(n\) 號元素指定一個位置(\(m\) 種可能),然後從前 \(n−1\) 個元素中選取 \(m−1\) 個排成一列。

組合數

\(n\) 個不同的元素中任取 \(m\) 個元素,組成一個集合,其方案數稱為 \(C_n^m\)

\(C_n^m\)\(A_n^m\) 的區別在於其不關注選出元素的順序,只關注選取哪些元素。

每一個大小為 \(m\) 的集合都對應著 \(m!\) 個排列。所以 \(A_n^m=m!C_n^m\)

所以有

\[C_n^m=\frac{n!}{m!(n-m)!} \]

組合數 \(C_n^m\) 又常寫作 \(\binom{n}{m}\)。注意 \(n\)\(m\) 上下顛倒了。

\[C_n^m=C_{n-1}^m+C_{n-1}^{m-1} \]

同樣考慮第 \(n\) 號元素是否選取。

如果不選取,則需要在前 \(n−1\) 個元素中選取 \(m\) 個組成集合,方案數為 \(C_{n-1}^m\)

如果選取,則需要在前 \(n−1\) 個元素中選取 \(m−1\) 個組成集合。方案數為 \(C_{n-1}^{m-1}\) 。這裡我們不需要給第 \(n\) 號元素指定位置。

組合數問題

思路:

用遞推式 \(C_n^m=C_{n-1}^m+C_{n-1}^{m-1}\) 計算每個\(C_n^m\mod k\) 的值。

然後使用二維字首和預處理 \(𝑖 ≤ 𝑛,𝑗 ≤ 𝑚\)\(C_i^j\) 有多少個 \(𝑘\) 的倍數。

std:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
#include<set>
#include<unordered_map>
#include<bitset>
#define int long long
using namespace std;
int C[20005][2005],sum[20005][2005];  
signed main(){
int t,k;
	cin >> t >> k;
	for (int i=0;i<=2000;i++){
		C[i][0] = 1;
		for (int j=1;j<=i;j++)
			C[i][j] =(C[i-1][j-1] + C[i-1][j])%k;
	}
	for (int i=1;i<=2000;i++)
		for (int j=1;j<=2000;j++){
			sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1]; 
			if(C[i][j]==0&&i>j)sum[i][j]++;
		}
			
	for (int q=1;q<=t;q++){
		int n,m;
		cin >> n >> m;
		cout<<sum[n][min(n,m)]<<"\n";
	}
}

排列數與組合數的性質

\[C_n^0+C_n^1+\cdots c_n^n=2^n \]

這是因為左式相當於從 \(n\) 個元素中選出一個子集,每個元素都有選/不選兩種選擇。

範德蒙德卷積公式:

\[\binom{n+k}{m}=\sum_{i=0}^m\binom{n}{i}\binom{k}{m-i} \]

左式相當於在 \(n+k\) 個元素中選 \(m\) 個,右式相當於列舉在前 \(n\) 個元素中選 \(i\) 個,那麼應當在後 \(k\) 個元素中選 \(m−i\) 個。


一些小問題

\(𝐼\) 每個球都有 \(𝑚\) 種選擇。答案為 \(𝑚^𝑛\)

\(𝐼𝐼\) 第一個球有 \(𝑚\) 種選擇,第 \(𝑖\) 個球有 \(𝑚−𝑖+1\) 種選擇,答案為 \(𝐴_𝑚^𝑛\)

\(𝑉\) 如果能放下都是一樣的。答案為 \(𝑛≤𝑚\)

\(𝑋𝐼\) 答案也是 \(𝑛≤𝑚\)

\(𝑉𝐼𝐼𝐼\) 相當於找出 \(𝑛\) 個盒子放下一個球。答案為 \(𝐶_𝑚^𝑛\)

\(𝐼𝑋\) 插板法,從 \(𝑛−1\) 個空隙中選擇 \(𝑚−1\) 個板子。答案為 \(C_{n-1}^{m-1}\)

\(𝑉𝐼𝐼\)憑空變出來 \(𝑚\) 個球,這樣就變成上一個問題了。答案為 \(C_{n+m-1}^{m-1}\)


最大公約數與最小公倍數

\(\gcd⁡(𝑎,𝑏)\) 表示 \(𝑎\)\(𝑏\) 的最大公約數,\(\text{lcm}(𝑎,𝑏)\) 表示 \(𝑎\)\(𝑏\) 的最小公倍數。

\(𝑎≥𝑏\)\(\gcd⁡(𝑎,𝑏)=\gcd⁡(𝑎−𝑏,𝑏)\)

證明:

\[d|a∧d|b⇔d|(a−b)∧d|b \]

從而可以推出 \(\gcd⁡(𝑎,𝑏)=\gcd⁡(𝑎 \bmod 𝑏,𝑏)\),可以在 $O(\log \max⁡(a,b)) $ 時間內求出 \(\gcd⁡(𝑎,𝑏)\)

此稱為歐幾里得演算法。

/*
int gcd(int a, int b){
	if(a == 0 && b == 0){
		return a + b;
	}
	if(a >= b){
		return gcd(a % b, b);
	} else {
		return gcd(b % a, a);
	}
}*/
int gcd(int a, int b){
	return b ? gcd(b, a % b) : a;
}
int lcm(int a, int b){
	return a / gcd(a, b) * b;
}

最大公約數和最小公倍數問題

思路:

\(𝑥_0 ∤ 𝑦_0\),那麼無解。

考慮某一個質因子 \(𝑝\) ,若 \(𝑓_𝑝 (𝑦_0)\) \(>\) \(𝑓_𝑝 (x_0)\),則有兩種選擇;若\(𝑓_𝑝 (𝑦_0)\) = \(𝑓_𝑝 (x_0)\) ,則有一種選擇。

答案即為\(2^{w(\frac{y_0}{x_0})}\) ,其中\(w(x)\) 表示 \(x\) 的質因子數。對\(𝑦_0 𝑥_0\)進行因數分解即可

std:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
#include<set>
#include<unordered_map>
#include<bitset>
#define int long long
using namespace std;
const int MAXN=100005;

inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while (ch<'0'||ch>'9'){
		  if (ch=='-') f=-1;
		  ch=getchar();
	}
	while (ch>='0'&&ch<='9'){
		  x=x*10+ch-48;
		  ch=getchar();
	}
	return x*f;
}
int gcd(int a,int b){
	return b?gcd(b,a%b):a;
}
int lcm(int x,int y){
	return x/gcd(x,y)*y;
}
signed main(){
	int x,y;
	x=read();
	y=read();
	int ans=0;
	if(x==y)ans--;
	y*=x;
	for(int i=1;i<=sqrt(y);i++){
		if(y%i==0&&gcd(i,y/i)==x){
			ans+=2;
		}
	}
	cout<<ans;
	return 0;
}


同餘問題

裴蜀定理