泛型程式設計與 OI——modint

diaearth發表於2022-02-01

部落格連結

在 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;

這樣就解決了型別名長的缺點。

相關文章