看這題之前先複習一下容斥原理,不然肯定看不懂,呃,如果第一次接觸容斥原理的題,可能弄懂了容斥原理你還是看不懂代碼,是的,等會你就知道了。
容斥原理簡介:在計數時,爲了使重疊部分不被重複計算,人們研究出一種新的計數方法:先不考慮重疊的情況,把包含於某內容中的所有對象的數目先計算出來,然後再把計數時重複計算的數目排斥出去,使得計算的結果既無遺漏又無重複,這種計數方法稱爲容斥原理。
兩個集合的容斥原理:
n(A∪B)=n(A)+n(B) -n(A∩B)
要計算幾個集合並集的大小,我們要先將所有單個集合的大小計算出來,然後減去所有兩個集合相交的部分,再加回所有三個集合相交的部分,再減去所有四個集合相交的部分,依此類推,一直計算到所有集合相交的部分。
也就是加上奇數的數量,減去偶數的數量。
原理還是很好理解的,做題的時候因爲有位運算的優化,就需要多思考。
Problem Description
Now you get a number N, and a M-integers set, you should find out how many integers which are small than N, that they can divided exactly by any integers in the set. For example, N=12, and M-integer set is {2,3}, so there is another set {2,3,4,6,8,9,10}, all the integers of the set can be divided exactly by 2 or 3. As a result, you just output the number 7.
給你一個數字N,以及一個有M個整數的數組集合S,求出小於數字N的所有整數集合R,使得集合R中的這些數能被S中的數整除。
比如N=12,集合S是{2, 3},那麼還有一個集合R{2, 3, 4, 6, 8, 9, 10},所有整數都能被2或者3整除。輸出R的元素個數。
Input
There are a lot of cases. For each case, the first line contains two integers N and M. The follow line contains the M integers, and all of them are different from each other. 0<N<2^31,0<M<=10, and the M integer are non-negative and won’t exceed 20.
多個測試用例,第一行包含數字N和集合S的元素個數M。接着輸入集合S的M個數字。N用長整型。
Output
For each case, output the number.
Sample Input
12 2
2 3
Sample Output
7
解題思路:
1、對於數字N,用M中的每個數去整除,然後計算數量,肯定有重複的。
2、通過容斥原理,可以知道n(A∪B∪C...)=n(A)+n(B)+n(C)...-n(A∩B)-n(A∩C)-n(B∩C)...+n(A∩B∩C)...
其中A、B、C表示集合S中的M個數依次整除N所能得到的集合。
對於n(A∩B∩...),只需要取得最小公倍數整除N得到集合。
3、本題性能優化主要有3處
一個是在一開始就去掉不需要重複計算的數字,比如N爲12的時候,如果M個數是{2,3,4},首先可以把4去掉,因爲能被4整除的肯定能被2整除。
一個是沒有用遞歸。
一個是用到了位運算。位運算這個比較難理解,看代碼註釋慢慢研究吧,另外可以設置一些打印語句查看整個運行過程,做其他題目也一樣,便於理解前後因果。
4、註釋比代碼還多,與前面那個組合的問題有點相似,數學題目就是這樣,難以理解,代碼精簡,解題的性能差距比較大。
源代碼:G++ 78ms HDU排名93
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<vector>
#include<fstream>
using namespace std;
long long N;
long long s[100];
vector<long long > q;
//求最大公約數
int gcd(int a, int b)
{
if (b == 0)
return a;
else
return gcd(b, a % b);
}
long long solve(int size)
{
long long ans = 0;
//將符合條件的m個數,看作m位,每位是0或者是1,那麼一共有2^m種狀態
//1<<x就是2的x次方,如1<<0是2的0次方,1<<1是2的1次方
for (long long i = 1; i < (1 << size); i++)
{
long long p = 1;
int cnt = 0;
for (long long j = 0; j < size; j++)
{
//與操作確定該數字是否參與組合
//比如這裏的size爲3,那麼二進制(1<<size)-1表示爲111
//i的值用二進制表示依次爲1,10,11,100,101,110,111
//1<<j的值依次爲1,10,100
//如果i的值爲110,則1<<j在10和100的時候&i就能大於0
if ((1 << j)&i)
{
//計算最小公倍數
//lcm(a,b) = a*b/gcd(a,b)
p = p / gcd(p, q[j]) * q[j];
cnt++;
}
}
//cnt表示組合有幾個數,奇數次減去,偶數次加上
//這裏爲啥是(n-1)/p,比如(n-1)=11,p=2,那麼被2分的數字肯定是(n-1)/p
//依次爲2、4、6、8、10
if (cnt & 1)
ans += (N - 1) / p;
else
ans -= (N - 1) / p;
}
return ans;
}
//可以將測試數據放在文件裏面,便於調試
//fstream ifs("test.txt");
//#define cin ifs
int main()
{
int M = 0;
//輸入N和M
while (cin >> N >> M)
{
q.clear();
int flag = 0;
//輸入M個數的集合S
for (int i = 0; i < M; i++)
{
cin >> s[i];
}
//排序
sort(s, s + M);
//去掉重複的數,比如2和6,去掉6,注意這一步去重非常影響性能
for (int i = 0; i < M; i++)
{
if (s[i] <= 0)
continue;
for (int j = i + 1; j < M; j++)
{
//是否能夠被整除,能夠被整除說明不需要參與組合計算
//比如2和6,能被6整除的肯定能被2整除
if (s[j] % s[i] == 0)
s[j] = -1;
}
q.push_back(s[i]);
}
long long ans = solve(q.size());
cout << ans << endl;
}
return 0;
}
個人公衆號:ACM算法日常
專注於基礎算法的研究工作,深入解析ACM算法題,五分鐘閱讀,輕鬆理解每一行源代碼。內容涉及算法、C/C++、機器學習等。