——永遠不要在OJ上使用值元編程,過於簡單的沒有優點,能有優點的編譯錯誤。ios
2019年10月,我在學習算法。有一道做業題,輸入規模很小,能夠用打表法解決。具體方案有如下三種:算法
運行時預處理,生成所需的表格,根據輸入直接找到對應項,稍加處理後輸出;編程
一個程序生成表格,做爲提交程序的一部分,後續與方法1相同,這樣就省去了運行時計算的步驟;數組
以上兩種方法結合,編譯期計算表格,運行時直接查詢,即元編程(metaprogramming)。編程語言
作題固然是用方法1或2,可是元編程已經埋下了種子。時隔大半年,我來補上這個坑。ide
北京大學OpenJudge 百練4119 複雜的整數劃分問題函數式編程
將正整數 \(n\) 表示成一系列正整數之和,\(n = n_1 + n_2 + ... + n_k\),其中 \(n_1 \geq n_2 \geq ... \geq n_k \geq 1\),\(k \geq 1\)。正整數 \(n\) 的這種表示稱爲正整數 \(n\) 的劃分。函數
標準的輸入包含若干組測試數據。每組測試數據是一行輸入數據,包括兩個整數 \(N\) 和 \(K\)。( \(0 \le N \leq 50\),\(0 \le K \leq N\) )工具
對於每組測試數據,輸出如下三行數據:oop
第一行: \(N\) 劃分紅 \(K\) 個正整數之和的劃分數目
第二行: \(N\) 劃分紅若干個不一樣正整數之和的劃分數目
第三行: \(N\) 劃分紅若干個奇正整數之和的劃分數目
5 2
2 3 3
第一行: 4+1,3+2
第二行: 5,4+1,3+2
第三行: 5,1+1+3,1+1+1+1+1+1
標準的動態規劃題。用dp[c][i][j]
表示把i
分紅c
個正整數之和的方法數,其中每一個數都不超過j
。
第一行。初始化:由 \(i \leq j\) 是否成立決定dp[1][i][j]
的值,當 \(i \leq j\) 時爲1
,劃分爲 \(i = i\),不然沒法劃分,值爲0
。
遞推:爲了求dp[c][i][j]
,對 \(i = i_1 + i_2 + ... + i_c\),\(i_1 \geq i_2 \geq ... \geq i_c\) 中的最大數 \(i_1\) 分類討論,最小爲 \(1\),最大不超過 \(i - 1\),由於 \(c \geq 2\),同時不超過 \(j\),由於定義。最大數爲 \(n\) 時,對於把 \(i - n\) 分紅 \(c - 1\) 個數,每一個數不超過 \(n\) 的劃分,追加上 \(n\) 可得 \(i\) 的一個劃分。\(n\) 只有這些取值,沒有漏;對於不一樣的 \(n\),因爲最大數不同,兩個劃分也不同,沒有多。故遞推式爲:
dp[K][N][N]
即爲所求ans1[K][N]
。
第二行。能夠把遞推式中的dp[c - 1][i - n][n]
修改成dp[c - 1][i - n][n - 1]
後從新計算。因爲只需一個與c
無關的結果,能夠省去c
這一維度,相應地改變遞推順序,每輪累加。
另外一種方法是利用已經計算好的ans1
數組。設 \(i = i_1 + i_2 + ... + + i_{c-1} + i_c\),其中 \(i_1 \ge i_2 \ge ... \ge i_{c+1} \ge i_c \ge 0\),則 \(i_1 - \left( c-1 \right) \geq i_2 - \left( c-2 \right) \geq ... \geq i_{c-1} - 1 \geq i_c \ge 0\),且 \(\left( i_1 - \left( c-1 \right) \right) + \left( i_2 - \left( c-2 \right) \right) + ... + \left( i_{c-1} - 1 \right) + \left( i_c \right) = i - \frac {c \left( c-1 \right)} {2}\),故把i
劃分紅c
個不一樣正整數之和的劃分數目等於ans[c][i - c * (c - 1) / 2]
,遍歷c
累加即得結果。
第三行。想法與第二行類似,也是找一個對應,此處從略。另外,數學上能夠證實,第二行和第三行的結果必定是同樣的。
#include <iostream> #include <algorithm> constexpr int max = 50; int dp[max + 1][max + 1][max + 1] = { 0 }; int ans1[max + 1][max + 1] = { 0 }; int ans2[max + 1] = { 0 }; int ans3[max + 1] = { 0 }; int main() { int num, k; for (int i = 1; i <= max; ++i) for (int j = 1; j <= max; ++j) dp[1][i][j] = i <= j; for (int cnt = 2; cnt <= max; ++cnt) for (int i = 1; i <= max; ++i) for (int j = 1; j <= max; ++j) { auto min = std::min(i - 1, j); for (int n = 1; n <= min; ++n) dp[cnt][i][j] += dp[cnt - 1][i - n][n]; } for (int cnt = 1; cnt <= max; ++cnt) for (int i = 1; i <= max; ++i) ans1[cnt][i] = dp[cnt][i][i]; for (int i = 1; i <= max; ++i) for (int cnt = 1; cnt <= i; ++cnt) { int j = i - cnt * (cnt - 1) / 2; if (j <= 0) break; ans2[i] += ans1[cnt][j]; } for (int i = 1; i <= max; ++i) for (int cnt = 1; cnt <= i; ++cnt) { int j = i + cnt; if (j % 2) continue; j /= 2; ans3[i] += ans1[cnt][j]; } while (std::cin >> num) { std::cin >> k; std::cout << ans1[k][num] << std::endl; std::cout << ans2[num] << std::endl; std::cout << ans3[num] << std::endl; } }
元編程是指計算機程序能把其餘程序做爲它們的數據的編程技術。在目前的C++中,元編程體現爲用代碼生成代碼,包括宏與模板。當咱們使用了std::vector<int>
中的任何一個名字時,std::vector
類模板就用模板參數int, std::allocator<int>
實例化爲std::vector<int, std::allocator<int>>
模板類,這是一種元編程,不過咱們一般不這麼講。
狹義的C++模板元編程(template metaprogramming,TMP)包括值元編程、類型元編程,以及二者的相交。本文討論的是值元編程,即爲編譯期值編程。
在C++中有兩套工具可用於值元編程:模板和constexpr
。C++模板是圖靈徹底的,這是模板被引入C++之後才被發現的,並非C++模板的初衷,所以用模板作計算在C++中算不上一等用法,致使其語法比較冗長複雜。constexpr
的初衷是提供純正的編譯期常量,後來才取消對計算的限制,但不能保證計算必定在編譯期完成。總之,這兩套工具都不完美,因此本文都會涉及。
嚴格來講,constexpr
不符合上述對元編程的定義,但它確實能夠提供運行時程序須要的數據,因此也納入元編程的類別。
從constexpr
開始講,是由於它與咱們在C++中慣用的編程範式——過程式範式是一致的。
constexpr
關鍵字在C++11中被引入。當時,constexpr
函數中只能包含一條求值語句,就是return
語句,返回值能夠用於初始化constexpr
變量,做模板參數等用途。若是須要分支語句,用三目運算符?:
;若是須要循環語句,用函數遞歸實現。好比,計算階乘:
constexpr int factorial(int n) { return n <= 1 ? 1 : (n * factorial(n - 1)); }
對於編譯期常量i
,factorial(i)
產生編譯期常量;對於運行時值j
,factorial(j)
產生運行時值,也就是說,constexpr
能夠視爲對既有函數的附加修飾。
然而,多數函數不止有一句return
語句,constexpr
對函數體的限制使它很難用於中等複雜的計算任務,爲此C++14放寬了限制,容許定義局部變量,容許if-else
、switch-case
、while
、for
等控制流。factorial
函數能夠改寫爲:
constexpr int factorial(int n) { int result = 1; for (; n > 1; --n) result *= n; return result; }
也許你會以爲factorial
函數的遞歸版本比循環版本易懂,那是由於你學習遞歸時接觸的第一個例子就是它。對於C++開發者來講,大多數狀況下首選的仍是循環。
計算單個constexpr
值用C++14就足夠了,可是傳遞數組須要C++17,由於std::array
的operator[]
從C++17開始纔是constexpr
的。
整數劃分問題的constexpr
元編程實現須要C++17標準:
#include <iostream> #include <utility> #include <array> constexpr int MAX = 50; constexpr auto calculate_ans1() { std::array<std::array<std::array<int, MAX + 1>, MAX + 1>, MAX + 1> dp{}; std::array<std::array<int, MAX + 1>, MAX + 1> ans1{}; constexpr int max = MAX; for (int i = 1; i <= max; ++i) for (int j = 1; j <= max; ++j) dp[1][i][j] = i <= j; for (int cnt = 2; cnt <= max; ++cnt) for (int i = 1; i <= max; ++i) for (int j = 1; j <= max; ++j) { auto min = std::min(i - 1, j); for (int n = 1; n <= min; ++n) dp[cnt][i][j] += dp[cnt - 1][i - n][n]; } for (int cnt = 1; cnt <= max; ++cnt) for (int i = 1; i <= max; ++i) ans1[cnt][i] = dp[cnt][i][i]; return ans1; } constexpr auto calculate_ans2() { constexpr auto ans1 = calculate_ans1(); std::array<int, MAX + 1> ans2{}; constexpr int max = MAX; for (int i = 1; i <= max; ++i) for (int cnt = 1; cnt <= i; ++cnt) { int j = i - cnt * (cnt - 1) / 2; if (j <= 0) break; ans2[i] += ans1[cnt][j]; } return ans2; } int main() { constexpr auto ans1 = calculate_ans1(); constexpr auto ans2 = calculate_ans2(); for (int cnt = 1; cnt <= 10; ++cnt) { for (int i = 1; i <= 10; ++i) std::cout << ans1[cnt][i] << ' ';+ std::cout << std::endl; } std::cout << std::endl; for (int i = 1; i <= 50; ++i) std::cout << ans2[i] << ' '; std::cout << std::endl; int num, k; while (std::cin >> num) { std::cin >> k; std::cout << ans1[k][num] << std::endl; std::cout << ans2[num] << std::endl; std::cout << ans2[num] << std::endl; } }
模板式與C++11中的constexpr
式相似,必須把循環化爲遞歸。事實上C++模板是一門函數式編程語言,對值元編程和類型元編程都是如此。
程序控制流有三種基本結構:順序、分支與循環。
在函數式編程中,數據都是不可變的,函數老是接受若干參數,返回若干結果,參數和結果是不一樣的變量;修改原來的變量是不容許的。對於C++模板這門語言,函數是類模板,也稱「元函數」(metafunction);參數是模板參數;運算結果是模板類中定義的靜態編譯期常量(在C++11之前,經常使用enum
來定義;C++11開始用constexpr
)。
好比,對於參數 \(x\),計算 \(x + 1\) 和 \(x ^ 2\) 的元函數:
template<int X> struct PlusOne { static constexpr int value = X + 1; }; template<int X> struct Square { static constexpr int value = X * X; };
這裏假定運算數的類型爲int
。從C++17開始,能夠用auto
聲明非類型模板參數。
順序結構,是對數據依次進行多個操做,能夠用函數嵌套來實現:
std::cout << PlusOne<1>::value << std::endl; std::cout << Square<2>::value << std::endl; std::cout << Square<PlusOne<3>::value>::value << std::endl; std::cout << PlusOne<Square<4>::value>::value << std::endl;
或者藉助constexpr
函數,迴歸熟悉的過程式範式:
template<int X> struct SquareAndIncrease { static constexpr int calculate() { int x = X; x = x * x; x = x + 1; return x; } static constexpr int value = calculate(); }; void f() { std::cout << SquareAndIncrease<5>::value << std::endl; }
過程式方法一樣能夠用於分支和循環結構,如下省略;函數式方法能夠類似地用於值元編程與類型元編程,因此我更青睞(主要仍是逼格更高)。
C++模板元編程實現分支的方式是模板特化與模板參數匹配,用一個額外的帶默認值的bool
類型模板參數做匹配規則,特化false
或true
的情形,另外一種情形留給主模板。
好比,計算 \(x\) 的絕對值:
template<int X, bool Pos = (X > 0)> struct AbsoluteHelper { static constexpr int value = X; }; template<int X> struct AbsoluteHelper<X, false> { static constexpr int value = -X; };
若是你怕用戶瞎寫模板參數,能夠再包裝一層:
template<int X> struct Absolute : AbsoluteHelper<X> { }; void g() { std::cout << Absolute<6>::value << std::endl; std::cout << Absolute<-7>::value << std::endl; }
標準庫提供了std::conditional
及其輔助類型std::conditional_t
用於模板分支:
template<bool B, class T, class F> struct conditional;
定義了成員類型type
,當B == true
時爲T
,不然爲F
。
模板匹配其實是在處理switch-case
的分支,bool
只是其中一種簡單狀況。對於對應關係不太規則的分支語句,能夠用一個constexpr
函數把參數映射到一個整數或枚舉上:
enum class Port_t { PortB, PortC, PortD, PortError, }; constexpr Port_t portMap(int pin) { Port_t result = Port_t::PortError; if (pin < 0) ; else if (pin < 8) result = Port_t::PortD; else if (pin < 14) result = Port_t::PortB; else if (pin < 20) result = Port_t::PortC; return result; } template<int Pin, Port_t Port = portMap(Pin)> struct PinOperation; template<int Pin> struct PinOperation<Pin, Port_t::PortB> { /* ... */ }; template<int Pin> struct PinOperation<Pin, Port_t::PortC> { /* ... */ }; template<int Pin> struct PinOperation<Pin, Port_t::PortD> { /* ... */ };
若是同一個模板有兩個參數分別處理兩種分支(這已經從分支上升到模式匹配了),或同時處理分支和循環的特化,總之有兩個或以上維度的特化,須要注意兩個維度的特化是否會同時知足,若是有這樣的情形但沒有提供兩參數都特化的模板特化,編譯會出錯。見problem2::Accumulator
,它不須要提供兩個參數同時特化的版本。
如前所述,循環要化爲遞歸,循環的開始與結束是遞歸的起始與終點或二者對調,遞歸終點的模板須要特化。好比,仍是計算階乘:
template<int N> struct Factorial { static constexpr int value = N * Factorial<N - 1>::value; }; template<> struct Factorial<0> { static constexpr int value = 1; };
或許階乘的遞歸定義很大程度上來源於數學,那就再看一個平方和的例子:
template<int N> struct SquareSum { static constexpr int value = SquareSum<N - 1>::value + N * N; }; template<> struct SquareSum<0> { static constexpr int value = 0; };
(\(1^2 + 2^2 + \cdots + n^2 = \frac {n \left( n + 1 \right) \left( 2n + 1\right)} {6}\))
好吧,仍是挺數學的,去下面看實例感受一下吧,那裏還有break
——哦不,被我放到思考題中去了。
加羣是交換羣,求和順序不影響結果,上面這樣的順序寫起來方便。有些運算符不知足交換律,須要逆轉順序。還以平方和爲例:
template<int N, int Cur = 0> struct SquareSumR { static constexpr int value = Cur * Cur + SquareSumR<N, Cur + 1>::value; }; template<int N> struct SquareSumR<N, N> { static constexpr int value = N * N; };
遞歸在過程式中是一種高級的結構,它能夠直接轉化爲函數式的遞歸,後面會提到二者的異同。
好比,計算平方根,這個例子來源於C++ Templates: The Complete Guide 2e:
// primary template for main recursive step template<int N, int LO = 1, int HI = N> struct Sqrt { // compute the midpoint, rounded up static constexpr auto mid = (LO + HI + 1) / 2; // search a not too large value in a halved interval using SubT = std::conditional_t<(N < mid * mid), Sqrt<N, LO, mid - 1>, Sqrt<N, mid, HI>>; static constexpr auto value = SubT::value; }; // partial specialization for end of recursion criterion template<int N, int S> struct Sqrt<N, S, S> { static constexpr auto value = S; };
這個遞歸很容易化爲循環,有助於你對循環化遞歸的理解。
實際應用中咱們可能不須要把全部計算出來的值存儲起來,但在打表的題目中須要。存儲一系列數據須要用循環,循環的實現方式依然是遞歸。好比,存儲階乘(Factorial
類模板見上):
template<int N> inline void storeFactorial(int* dst) { storeFactorial<N - 1>(dst); dst[N] = Factorial<N>::value; } template<> inline void storeFactorial<-1>(int* dst) { ; } void h() { constexpr int MAX = 10; int factorial[MAX + 1]; storeFactorial<MAX>(factorial); for (int i = 0; i <= MAX; ++i) std::cout << factorial[i] << ' '; std::cout << std::endl; }
多維數組同理,例子見下方。注意,函數模板不能偏特化,但有靜態方法的類模板能夠,這個靜態方法就充當原來的模板函數。
雖然咱們是對數組中的元素挨個賦值的,但編譯器的生成代碼不會這麼作,即便不能優化成全部數據一塊兒用memcpy
,至少能作到一段一段拷貝。
類內定義的函數隱式成爲inline
,手動寫上inline
沒有語法上的意義,可是對於一些編譯器,寫上之後函數被內聯的可能性更高,因此寫inline
是一個好習慣。
#include <iostream> #include <algorithm> constexpr int MAX = 50; namespace problem1 { template<int Count, int Num, int Max> struct Partition; template<int Count, int Num, int Loop> struct Accumulator { static constexpr int value = Accumulator<Count, Num, Loop - 1>::value + Partition<Count, Num - Loop, Loop>::value; }; template<int Count, int Num> struct Accumulator<Count, Num, 0> { static constexpr int value = 0; }; template<int Count, int Num, int Max = Num> struct Partition { static constexpr int value = Accumulator<Count - 1, Num, std::min(Num - 1, Max)>::value; }; template<int Num, int Max> struct Partition<1, Num, Max> { static constexpr int value = Num <= Max; }; template<int Count, int Num> struct Store { static inline void store(int* dst) { Store<Count, Num - 1>::store(dst); dst[Num] = Partition<Count, Num>::value; } }; template<int Count> struct Store<Count, 0> { static inline void store(int* dst) { ; } }; template<int Count> inline void store(int (*dst)[MAX + 1]) { store<Count - 1>(dst); Store<Count, MAX>::store(dst[Count]); } template<> inline void store<0>(int (*dst)[MAX + 1]) { ; } inline void store(int(*dst)[MAX + 1]) { store<MAX>(dst); } } namespace problem2 { template<int Num, int Count = Num, int Helper = Num - Count * (Count - 1) / 2, bool Valid = (Helper > 0)> struct Accumulator { static constexpr int value = Accumulator<Num, Count - 1>::value + problem1::Partition<Count, Helper>::value; }; template<int Num, int Count, int Helper> struct Accumulator<Num, Count, Helper, false> { static constexpr int value = Accumulator<Num, Count - 1>::value; }; template<int Num, int Helper, bool Valid> struct Accumulator<Num, 0, Helper, Valid> { static constexpr int value = 0; }; template<int Num> inline void store(int* dst) { store<Num - 1>(dst); dst[Num] = Accumulator<Num>::value; } template<> inline void store<0>(int* dst) { ; } inline void store(int* dst) { store<MAX>(dst); } } int ans1[MAX + 1][MAX + 1]; int ans2[MAX + 1]; int main() { problem1::store(ans1); problem2::store(ans2); int num, k; while (std::cin >> num) { std::cin >> k; std::cout << ans1[k][num] << std::endl; std::cout << ans2[num] << std::endl; std::cout << ans2[num] << std::endl; } }
請對照運行時版本自行理解。
constexpr
不保證計算在編譯期完成,大部分編譯器在Debug模式下把全部能夠推遲的constexpr
計算都推遲到運行時完成。但constexpr
能夠做爲一個強有力的優化提示,本來在最高優化等級都不會編譯期計算的代碼,在有了constexpr
後編譯器會盡力幫你計算。若是編譯器實在作不到,根據你是否強制編譯期求值,編譯器會給出錯誤或推遲到運行時計算。在不一樣的編譯器中,這類行爲的表現是不一樣的——衆所周知MSVC對constexpr
的支持很差。
目前(C++17)沒有任何方法能夠檢查一個表達式是不是編譯期求值的,可是有方法可讓編譯器對於非編譯期求值表達式給出一個錯誤,把指望constexpr
的表達式放入模板參數或static_assert
表達式都是可行的:若是編譯期求值,則編譯經過;不然編譯錯誤。
(C++20:consteval
、is_constant_evaluated
)
若是咱們把Sqrt
中的遞歸替換爲以下語句:
static constexpr auto value = (N < mid * mid) ? Sqrt<N, LO, mid - 1>::value : Sqrt<N, mid, HI>::value;
顯然計算結果是相同的,看上去還更簡潔。可是問題在於,編譯器會把Sqrt<N, LO, mid - 1>
和Sqrt<N, mid, HI>
兩個類都實例化出來,儘管只有一個模板類的value
會被使用到。這些類模板實例繼續致使其餘實例產生,最終將產生 \(O \left( n \log n \right)\) 個實例。相比之下,把兩個類型名字傳給std::conditional
並不會致使類模板被實例化,std::conditional
只是定義一個類型別名,對該類型求::value
纔會實例化它,一共產生 \(O \left( \log n \right)\) 個實例。
還有一個很常見的工具是變參模板,我沒有介紹是由於暫時沒有用到,並且我怕寫出非多項式複雜度的元程序。若是我還有機會寫一篇類型元編程的話,確定會包含在其中的。
循環的一次迭代每每須要上一次迭代的結果,對應地在遞歸中就是函數對一個參數的結果依賴於對其餘 \(n\) 個參數的結果。有些問題用遞歸解決比較直觀,可是若是 \(n \geq 2\),計算過程就會指數爆炸,好比:
int fibonacci(int n) { if (n <= 2) return 1; else return fibonacci(n - 2) + fibonacci(n - 1); }
計算fibonacci(30)
已經須要一點點時間了,而計算fibonacci(46)
(4字節帶符號整型能容納的最大斐波那契數)就很慢了。把這種遞歸轉化爲循環,就是設計一個動態規劃算法的過程。然而函數式中的遞歸與過程式中的循環可能有相同的漸近複雜度:
template<int N> struct Fibonacci { static constexpr int value = Fibonacci<N - 2>::value + Fibonacci<N - 1>::value; }; template<> struct Fibonacci<1> { static constexpr int value = 1; }; template<> struct Fibonacci<2> { static constexpr int value = 1; };
由於只有Fibonacci<1>
到Fibonacci<46>
這46個類模板被實例化,是 \(O \left( n \right)\) 複雜度的。
在題目中,因爲表中的全部數據都有可能用到,而且運行時不能執行計算,因此要把全部數據都計算出來。實際問題中可能只須要其中一個值,好比我如今就想知道不一樣整數的劃分問題對 \(50\) 的答案是多少,就寫:
std::cout << problem2::Accumulator<50>::value << std::endl;
那麼problem1::Partition
的Count
參數就不會超過10
,不信的話你能夠加一句static_assert
。實例化的模板數量一共只有2000多個,而在完整的問題中這個數量要翻100倍不止。這種性質稱爲惰性求值,即用到了才求值。惰性求值是必需的,總不能窮盡模板參數的全部可能組合一一實例化出來吧?
函數式編程語言能夠在運行時實現這些特性。
我愧對這個小標題,由於C++值元編程根本沒有性能,時間和空間都是。類型元編程也許是必需,至於值元編程,emm,作點簡單的計算就能夠了,這整篇文章都是反面教材。
思考題2用GCC編譯,大概須要10分鐘;用MSVC編譯,出現我聞所未聞的錯誤:
由於編譯器是32位的,4GB內存用完了就爆了。
一個頗有趣的問題是編譯器對於死循環的行爲。根據圖靈停機問題,編譯器沒法判斷它要編譯的元程序是否包含死循環,那麼它在遇到死循環時會怎樣表現呢?固然不能跟着元程序一塊兒死循環,constexpr
的循環次數與模板的嵌套深度都是有限制的。在GCC中,能夠用-fconstexpr-depth
、-fconstexpr-loop-limit
和-ftemplate-depth
等命令行參數來控制。
problem2::Accumulator
從Count == 0
到Count == Num
都要實例化,但其實只需實例化到 \(O \left( \sqrt{n} \right)\) 就能夠了,試改寫之。
洛谷 NOIp2016提升組D2T1 組合數問題,用元編程實現。
只需完成 \(n \leq 100, m \leq 100\) 的任務點;
使用64位編譯器(指編譯器自己而非目標代碼),給編譯器億點點時間;
不要去網站上提交,我已經試過了,編譯錯誤。
組合數 \(\binom {n} {m}\) 表示的是從 \(n\) 個物品中選出 \(m\) 個物品的方法數。舉個例子,從 \(\left( 1, 2, 3 \right)\) 三個物品中選擇兩個物品能夠有 \(\left( 1, 2 \right), \left( 1, 3 \right), \left( 2, 3 \right)\) 這三種選擇方法。根據組合數的定義,咱們能夠給出計算組合數 \(\binom {n} {m}\) 的通常公式
其中 \(n! = 1 \times 2 \times \cdots \times n\);特別地,定義 \(0! = 1\)。
小蔥想知道若是給定 \(n\),\(m\) 和 \(k\),對於全部的 \(0 \leq i \leq n, 0 \leq j \leq \min \left( i, m \right)\) 有多少對 \(\left( i, j \right)\) 知足 \(k \mid \binom {i} {j}\)。
第一行有個兩個整數 \(t, k\),其中 \(t\) 表明該測試點總共有多少組測試數據,\(k\) 的意義見問題描述。
接下來 \(t\) 行每行兩個整數 \(n, m\),其中 \(n, m\) 的意義見問題描述。
共 \(t\) 行,每行一個整數表明全部的 \(0 \leq i \leq n, 0 \leq j \leq \min \left( i, m \right)\) 有多少對 \(\left( i, j \right)\) 知足 \(k \mid \binom {i} {j}\)。
【輸入#1】
1 2 3 3
【輸出#1】
1
【輸入#2】
2 5 4 5 6 7
【輸出#2】
0 7
【樣例1說明】
在全部可能的狀況中,只有 \(\binom {2} {1} = 2\) 一種狀況是 \(2\) 的倍數。
【子任務】
測試點 | \(n\) | \(m\) | \(k\) | \(t\) |
---|---|---|---|---|
1 | \(\leq 3\) | $ \leq 3$ | \(= 2\) | $ = 1$ |
2 | \(= 3\) | \(\leq 10^4\) | ||
3 | \(\leq 7\) | $ \leq 7$ | \(= 4\) | $ = 1$ |
4 | \(= 5\) | \(\leq 10^4\) | ||
5 | \(\leq 10\) | $ \leq 10$ | \(= 6\) | $ = 1$ |
6 | \(= 7\) | \(\leq 10^4\) | ||
7 | \(\leq 20\) | $ \leq 100$ | \(= 8\) | $ = 1$ |
8 | \(= 9\) | \(\leq 10^4\) | ||
9 | \(\leq 25\) | $ \leq 2000$ | \(=10\) | $ = 1$ |
10 | \(=11\) | \(\leq 10^4\) | ||
11 | \(\leq 60\) | $ \leq 20$ | \(=12\) | $ = 1$ |
12 | \(=13\) | \(\leq 10^4\) | ||
13 | \(\leq 100\) | $ \leq 25$ | \(=14\) | $ = 1$ |
14 | \(=15\) | \(\leq 10^4\) | ||
15 | $ \leq 60$ | \(=16\) | $ = 1$ | |
16 | \(=17\) | \(\leq 10^4\) | ||
17 | \(\leq 2000\) | $ \leq 100$ | \(=18\) | $ = 1$ |
18 | \(=19\) | \(\leq 10^4\) | ||
19 | $ \leq 2000$ | \(=20\) | $ = 1$ | |
20 | \(=21\) | \(\leq 10^4\) |