在 OI 中,有大量的題目要求對一些數字取模,這便是本文寫作的背景。
背景介紹
這些題目要麼是因為答案太大,不方便輸出結果,例如許多計數 dp;要麼是因為答案是浮點數,出題人不願意寫一個確定精度的 Special Judge,例如很多期望概率題;要麼是因為這道題目直接考察了模的性質和運用,比如大量的 998244353 類的多項式題目。
過去的做法
在這種要求之下,取模運算就成為了程式設計中不可缺少的一部分。下面以式子 \(\texttt{ans}=(x+y+z)\times u\) 為例介紹幾種寫法。
第一種 直接取模
這種方法是直接取模,簡單直接,清晰明瞭。
constexpr int p=998244353;
int ans=1ll*(((x+y)%p+z)%p)*u%p;
但是這種方法有著嚴重的缺陷,一是容易忘記大括號,二是容易中間運算時搞錯運算順序、忘記取模,三是式子太長、括號太多、不易檢驗。
因此,不推薦運用這種方法。
第二種 函式取模
這種方法有效地解決了直接取模的忘記取模的漏洞。
constexpr int p=998244353;
int add(int a,int b){
return a+b>=p?a+b-p:a+b;
}
int sub(int a,int b){
return a<b?a-b+p:a-b;
}
int mul(int a,int b){
return 1ll*a*b%p;
}
int ans=mul(add(add(x,y),z),u);
但是,這種寫法的式子依舊太長,不易檢驗,並且如果編譯器沒有任何優化(現在不存在這種情況了)的話,大量的函式呼叫將會耗費不少的時間。並且如果要對多個模數取模,則需要寫多個函式,顯得程式碼冗長。
泛型程式設計
考慮到函式取模的優點,我們不妨通過類的運算子過載來進一步優化 add
等函式。
同時為了解決多個模數的問題,我們考慮泛型程式設計,將模數直接包含在型別中。
template<typename T,const T p>
class modint{
private:
T v;
public:
modint(){}
modint(const T& x){assert(0<=x&&x<p);v=x;}
modint operator+(const modint& a)const{
return v+a.v>=p?v+a.v-p:v+a.v;
}
modint operator-(const modint& a)const{
return v<a.v?v-a.v+p:v-a.v;
}
modint operator*(const modint& a)const{
return 1ll*v*a.v%p;
}
T operator()(void)const{
return v;
}
};
modint<int,998244353> x(),y(),z(),u();
modint<int,998244353> ans=(x+y+z)*u;
這樣使用的時候,一方面減少了心智負擔,不用操心運算時忘記取模;另一方面採取了常數更小的加減法操作,運算更快。
唯一的缺點就是型別名難寫,但是模數個數少的時候可以縮寫,即寫成:
typedef modint<int,998244353> modInt1;
這樣就解決了型別名長的缺點。