離散數學:容斥原理
這題答案有點問題,並不能滿分通過,想滿分的移步吧,只提供一個思路,思路是沒問題的
提示:求兩個整數a, b的最小公倍數lcm(a,b),可以利用a, b的最大公約數gcd(a,b)來完成,即lcm(a, b) = a*b/gcd(a, b)
gcd可以這樣寫:
int gcd(int a, int b)
{
return a ? gcd(b % a, a) : b;
}
樣例輸入1
100 2
2
3
樣例輸出1
67
樣例輸入2
100 3
2
5
7
樣例輸出2
66
題目要求:注意程式的時間複雜度,本題目限時1秒。
如果這只是個普通的大一C語言題,那麼我想大部分人的處理做法如下:
#include<stdio.h>
#include<malloc.h>
int main(){
int n,m,t;
int *p;//用來儲存m個數據,因為m為變數所以用指標不用陣列
int i,j;//for迴圈的變數
int number = 0;//用來儲存最後統計的個數
scanf("%d %d",&n,&m);
p = (int *)malloc(sizeof(int) * m);
for(i=0;i<m;i++){
scanf("%d",&t);
*(p+i) = t;
}
for (i=0;i<n;i++){
for(j=0;j<m;j++){
if(i % *(p+j) == 0){
number++;
break;
}
}
}
printf("%d\n",number);
return 0;
}
這種做法思路和程式碼都很簡單直接,可以說與上個推理題是一樣的,都是採用暴力迴圈的方式來解決,這裡程式至少執行外部的N次迴圈,內部迴圈肯定也會被執行數次,這樣下來就出問題了,上個推理題暴力迴圈了一百萬種可能,這個題如果取題目給出的範圍的極限值n=10^9,那麼這個數就是一億,執行的話可能就是數億次,這段程式碼就會沒有意義了,別說一秒,電腦肯定直接炸
下邊來說說我的做法,這個題我不保證自己是對的,裡邊有很多問題,宿舍也只有大佬做出來了,而我們的答案範圍小的時候是一樣的,範圍一旦大了就會不同了,看了半天誰也看不出問題,並且我的程式碼裡有個for迴圈遍歷,正過來反過去答案居然會是兩個結果,大佬也沒搞明白是為什麼,有想明白的可以私信我
我的思路是大佬給的,自己只是簡單地找了找規律翻譯成了C程式碼而已,所以我們在思路上是一致的,也就是通過數學上的找集合的子集的方式,找到所有的序列,然後找到每個序列對應的最小公倍數,再由n/最小公倍數得到一個結果的方式來累加出最後的結果。
用這種方式,不論n取多大,我們都可以把迴圈的次數縮減到2^m次,而m的範圍是1≤m≤15,也就是這個程式最多也就迴圈2^15=32768次,如果像題目給的樣例那樣的話效果肯定感覺不出來,但如果比之上億次的範圍效率就提高了很多
另外,我們覺得老師題目中給出的找最小公倍數與最大公約數的方法並沒有考慮到數足夠大的情況,所以會超出int型別的數值範圍,造成溢位,例如下邊是我們的測試資料
這裡的m是前15個素數,然後每個數前加了個100,這樣找到的最小公倍數就會非常的大,所以我把程式中相對應的一些位置的變數的型別改為了long,學了4年計算機,這還是頭一回用到這種資料型別
下邊是大佬查到的資料,也是給我們思路的一張圖
這裡說一下,所有的序列其實不用如圖上所說的去找第一個0,然後後邊變為1什麼的,那麼囉嗦,自己列舉m=2,3的情況就可以看出所有的序列就是2^m減去全為0的那個序列。也就是所有子集排除掉空集的情況,另外還要注意正負號的問題(與序列中1的個數有關),並且序列中的1,只是代表該位置的數而已(這句話我可能表述的不是很到位,自己看著上邊生成組合那張圖理解一下就明白了,或者看一下我的演算過程)下邊給出我自己做題前的演算紙,自己下載了放到本地調好方向再看吧
終於到程式碼了,這裡我測試時的程式碼只是註釋掉了,並沒有刪除,可以不用管,又想測試的可以開啟註釋看測試輸出結果是什麼
#include<stdio.h>
#include<malloc.h>
#include<math.h>
long gcd(int a, int b);
long lcm(int a, int b);
int dic(int a,int *p11,int m);
int main(){
long n,y,x;
int m,t;
int *p;//用來儲存m個數據,因為m為變數所以用指標不用陣列
int i,j,k;//for迴圈的變數
long number = 0;//最後的輸出結果
int num;//統計1的個數的變數
scanf("%lld %d",&n,&m);
p = (int *)malloc(sizeof(int) * m);
for(i=0;i<m;i++){
scanf("%d",&t);
*(p+i) = t;
}
for(i=1;i<pow(2,m);i++){
int *p11;//p1的指標元素
p11 = (int *)malloc(sizeof(int) * (m+1));//第0號下標位置儲存序列中1的個數
dic(i,p11,m);//i是十進位制數,要把i轉換成二進位制
//對得到的字典序列直接操作,不再去另寫一個for迴圈控制,這裡是第一次寫沒發現,要找的序列就是字典減去全是0的序列,又費力的自己去生成了一遍,沒用的
/*t = m+1;//注意臨界值
j = -1;
do{
t--;
j++;
}while(*(p11+t) == 1 && t>0);
for(k=0;k<j;k++){
*(p11+m-k) = 0;
}
*(p11+m-k) = 1;*/
x = 1;
num = 0;
//找到每一個1的位置,這個for迴圈 跟下面緊鄰的 按說是一樣的,但是結果會不同,想不明白,有想明白的請告訴我一聲
/*for(k=1;k<m+1;k++){
printf("%d ",*(p11+k));
if(*(p11+k) == 1){
//找最小公倍數
num++;
x = lcm(x, *(p+k-1));
//printf("k=%d ",*(p+k-1));
}
}*/
for(k=m;k>0;k--){
//printf("%d ",*(p11+k));
if(*(p11+k) == 1){
//找最小公倍數
num++;
x = lcm(x, *(p+k-1));
//printf("k=%d ",*(p+k-1));
}
}
*(p11+0) = num;//在這裡寫入1的數量
//printf("aa=%d ",num);
//printf("x=%lld ",x);
//printf("i0=%lld n=%lld x=%lld ",*(p11+0),n,x);
if(*(p11+0) % 2 == 0){
y = n/x;
y = -y;
}else{
y = n/x;
}
//y = pow(-1,*(p11+0)+1) * (n / x);//這裡1的個數已經改變了
//printf("y=%lld\n",y);
number += y;
}
printf("%lld\n",number);
return 0;
}
long lcm(int a, int b){
return a*b/gcd(a, b);
}
long gcd(int a, int b){
return a ? gcd(b % a, a) : b;
}
int dic(int a,int *p11,int m){
//該方法中統計1的個數沒用了, 在主方法裡統計了
int i,j,k;
//int num = 0;
//先把P11所有位置置為0
for(i=0;i<m+1;i++){
*(p11+i) = 0;
}
//再將對應位置替換為0或者
for(k=m;a!=0;k--){
j = a % 2;
*(p11+k) = j;//?
a = a / 2;
//if(j == 1){
// num++;//正常累加
//}
}
//*(p11+0) = num;
//字典生成正常
/*for(i=0;i<m+1;i++){
printf("%d ",*(p11+i));
}
printf("\n");*/
return 0;
}
最後說一下執行吧,因為寫的時候要多次執行,有時候還輸入多行資料,很麻煩,大佬就想到了Linux下的管道命令,在我的Windows環境下也可以實現,確實要快很多,每次只要編譯一下,按一下鍵盤的上箭頭敲一下回車就可以了,最多改一下aaa資料夾裡的m值,截圖中的命令分別是在控制檯直接輸出結果和將結果輸出到檔案中
後來發現個挺不錯的容斥原理的文章,主要是圖畫的挺清晰的,給個連結