主頁 > 後端開發 > C++值元編程

C++值元編程

2020-09-12 03:07:36 後端開發

——永遠不要在OJ上使用值元編程,過于簡單的沒有優勢,能有優勢的編譯錯誤,

背景

2019年10月,我在學習演算法,有一道作業題,輸入規模很小,可以用打表法解決,具體方案有以下三種:

  1. 運行時預處理,生成所需的表格,根據輸入直接找到對應項,稍加處理后輸出;

  2. 一個程式生成表格,作為提交程式的一部分,后續與方法1相同,這樣就省去了運行時計算的步驟;

  3. 以上兩種方法結合,編譯期計算表格,運行時直接查詢,即元編程(metaprogramming),

做題當然是用方法1或2,但是元編程已經埋下了種子,時隔大半年,我來補上這個坑,

題目

北京大學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\)

輸出

對于每組測驗資料,輸出以下三行資料:

第一行: \(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[c][i][j] = \sum_{n=1}^{min\{i-1,j\}}dp[c-1][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式值元編程

constexpr開始講,是因為它與我們在C++中慣用的編程范式——程序式范式是一致的,

constexpr關鍵字在C++11中被引入,當時,constexpr函式中只能包含一條求值陳述句,就是return陳述句,回傳值可以用于初始化constexpr變數,作模板引數等用途,如果需要分支陳述句,用三目運算子?:;如果需要回圈陳述句,用函式遞回實作,比如,計算階乘:

constexpr int factorial(int n)
{
    return n <= 1 ? 1 : (n * factorial(n - 1));
}

對于編譯期常量ifactorial(i)產生編譯期常量;對于運行時值jfactorial(j)產生運行時值,也就是說,constexpr可以視為對既有函式的附加修飾,

然而,多數函式不止有一句return陳述句,constexpr對函式體的限制使它很難用于中等復雜的計算任務,為此C++14放寬了限制,允許定義區域變數,允許if-elseswitch-casewhilefor等控制流,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::arrayoperator[]從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 = https://www.cnblogs.com/jerry-fuyi/p/X + 1;
};

template
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 = https://www.cnblogs.com/jerry-fuyi/p/calculate();
};

void f()
{
    std::cout << SquareAndIncrease<5>::value << std::endl;
}

程序式方法同樣可以用于分支和回圈結構,以下省略;函式式方法可以相似地用于值元編程與型別元編程,所以我更青睞(主要還是逼格更高),

分支

C++模板元編程實作分支的方式是模板特化與模板引數匹配,用一個額外的帶默認值的bool型別模板引數作匹配規則,特化falsetrue的情形,另一種情形留給主模板,

比如,計算 \(x\) 的絕對值:

template<int X, bool Pos = (X > 0)>
struct AbsoluteHelper
{
    static constexpr int value = https://www.cnblogs.com/jerry-fuyi/p/X;
};

template
struct AbsoluteHelper
{
    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 = https://www.cnblogs.com/jerry-fuyi/p/N * Factorial::value;
};

template<>
struct Factorial<0>
{
    static constexpr int value = 1;
};

或許階乘的遞回定義很大程度上來源于數學,那就再看一個平方和的例子:

template<int N>
struct SquareSum
{
    static constexpr int value = https://www.cnblogs.com/jerry-fuyi/p/SquareSum::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 = https://www.cnblogs.com/jerry-fuyi/p/Cur * Cur + SquareSumR::value;
};

template
struct SquareSumR
{
    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 = https://www.cnblogs.com/jerry-fuyi/p/SubT::value;
};
// partial specialization for end of recursion criterion
template
struct Sqrt {
    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 = https://www.cnblogs.com/jerry-fuyi/p/Accumulator::value + Partition::value;
};

template
struct Accumulator
{
    static constexpr int value = 0;
};

template
struct Partition
{
    static constexpr int value = Accumulator::value;
};

template
struct Partition<1, Num, Max>
{
    static constexpr int value = Num <= Max;
};

template
struct Store
{
    static inline void store(int* dst)
    {
        Store::store(dst);
        dst[Num] = Partition::value;
    }
};

template
struct Store
{
    static inline void store(int* dst)
    {
        ;
    }
};

template
inline void store(int (*dst)[MAX + 1])
{
    store(dst);
    Store::store(dst[Count]);
}

template<>
inline void store<0>(int (*dst)[MAX + 1])
{
    ;
}

inline void store(int(*dst)[MAX + 1])
{
    store(dst);
}

}

namespace problem2
{

template 0)>
struct Accumulator
{
    static constexpr int value = Accumulator::value + problem1::Partition::value;
};

template
struct Accumulator
{
    static constexpr int value = Accumulator::value;
};

template
struct Accumulator
{
    static constexpr int value = 0;
};

template
inline void store(int* dst)
{
    store(dst);
    dst[Num] = Accumulator::value;
}

template<>
inline void store<0>(int* dst)
{
    ;
}

inline void store(int* dst)
{
    store(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

constexpr不保證計算在編譯期完成,大部分編譯器在Debug模式下把所有可以推遲的constexpr計算都推遲到運行時完成,但constexpr可以作為一個強有力的優化提示,原本在最高優化等級都不會編譯期計算的代碼,在有了constexpr后編譯器會盡力幫你計算,如果編譯器實在做不到,根據你是否強制編譯期求值,編譯器會給出錯誤或推遲到運行時計算,在不同的編譯器中,這類行為的表現是不同的——眾所周知MSVC對constexpr的支持不好,

目前(C++17)沒有任何方法可以檢查一個運算式是否是編譯期求值的,但是有方法可以讓編譯器對于非編譯期求值運算式給出一個錯誤,把期望constexpr的運算式放入模板引數或static_assert運算式都是可行的:如果編譯期求值,則編譯通過;否則編譯錯誤,

(C++20:constevalis_constant_evaluated

模板

如果我們把Sqrt中的遞回替換為如下陳述句:

static constexpr auto value = https://www.cnblogs.com/jerry-fuyi/p/(N < mid * mid) ? Sqrt::value
                                              : Sqrt::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 = https://www.cnblogs.com/jerry-fuyi/p/Fibonacci::value + Fibonacci::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::PartitionCount引數就不會超過10,不信的話你可以加一句static_assert,實體化的模板數量一共只有2000多個,而在完整的問題中這個數量要翻100倍不止,這種性質稱為惰性求值,即用到了才求值,惰性求值是必需的,總不能窮盡模板引數的所有可能組合一一實體化出來吧?

函式式編程語言可以在運行時實作這些特性,

性能

我愧對這個小標題,因為C++值元編程根本沒有性能,時間和空間都是,型別元編程也許是必需,至于值元編程,emm,做點簡單的計算就可以了,這整篇文章都是反面教材,

思考題2用GCC編譯,大概需要10分鐘;用MSVC編譯,出現我聞所未聞的錯誤:

因為編譯器是32位的,4GB記憶體用完了就爆了,

停機問題

一個很有趣的問題是編譯器對于死回圈的行為,根據圖靈停機問題,編譯器無法判斷它要編譯的元程式是否包含死回圈,那么它在遇到死回圈時會怎樣表現呢?當然不能跟著元程式一起死回圈,constexpr的回圈次數與模板的嵌套深度都是有限制的,在GCC中,可以用-fconstexpr-depth-fconstexpr-loop-limit-ftemplate-depth等命令列引數來控制,

思考題

  1. problem2::AccumulatorCount == 0Count == Num都要實體化,但其實只需實體化到 \(O \left( \sqrt{n} \right)\) 就可以了,試改寫之,

  2. 洛谷 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}\) 的一般公式

\[\binom {n} {m} = \frac {n!} {m! \left( n-m \right) !} \,, \]

其中 \(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\)
  • 對于全部的測驗點,保證 \(0 \leq n, m \leq 2 \times 10^3, 1 \leq t \leq 10^4\)

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/9928.html

標籤:C++

上一篇:L1-018 大笨鐘 (10分)

下一篇:L1-019 誰先倒 (15分)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more