この問題を思い出した!
問題概要
素数 と、
個の 1 以上
以下の整数
が与えられる。
を満たす整数 が存在するような
の組の個数を数え上げよ。
制約
考えたこと
一瞬、原始根を考えたくなったが、原始根ではなく「位数」を考えた方が計算量的によいことが多いことは、次の問題で学習済みだ。
mod において、整数
の位数とは、
を満たす最小の正の整数 のことである。今後、
の位数を
と書くことにする。位数の便利な性質として、次のものがある。
【位数の法則】
の倍数でない整数
に対して、
が成り立つとき、 は
の倍数である。
このことは比較的容易に示せる。 を
で割った余り
が存在すると仮定すると、
となるが、位数の定義から、
でなければならないので矛盾である。
位数の法則から、特に、 は
の約数であることが従う (あとでこれを使う)。
位数の活用
結論から言えば、 の倍数ではない整数
に対して、
を満たす整数
が存在
⇔ が
の倍数
が成り立つ。この性質は、原始根を知っていると予想がつきやすい。証明も原始根を用いる。原始根については、この記事にて。ちゃんとした証明は公式 editorial にある。
ここでは感覚的に納得することを目指す。
原始根を とすると、
はそれぞれ
を法として
と表せる。原始根を用いると、
を法とした整数の掛け算に関する議論は「指数部分の足し算に関する議論」へと置き換えることができるのだ。
そして、これら 個の数は、指数部分の
との最大公約数によって類別できる。たとえば
のとき、次の表のようになる。
指数の |
位数 | 種類 |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
このように、「指数と との最大公約数」と「位数」の積は
となっている。指数を用いた議論をすると、
の指数をそれぞれ
として、
を満たす整数
が存在
⇔ を満たす整数
が存在
⇔ を満たす整数
が存在
⇔ が
の倍数
⇔ が
の倍数
となる。ここで、「指数と との最大公約数」と「位数」の積は
となっていることから、
を満たす整数
が存在
⇔ が
の倍数
となることが示された。
位数の求め方
位数は
から開始して
の各素因数
について、
である限り
を
で割り続ける
という簡単な方法で求められる。この方法で計算量は となる。
最後の詰め
こうして、 の位数を順に求めていき、位数の値ごとに個数を集計していく。この処理に要する計算量は
であり、十分間に合う。
位数としてありうる値は、 の約数の個数なので、十分小さい。具体的には
より、おおよそ
個程度である。
よって、約数倍数関係にあるような位数のペアについて、その個数の積を求め、合算すればよい。これも の約数の個数を
として
の計算量で実施できる。
コード
なので、愚直にやると
long long
型だと足りないなどで大変だった。128 ビット整数を用いた。
#include <bits/stdc++.h> using namespace std; // 128 ビット整数のラッパー struct i128 { // inner value __int128 val; // constructor constexpr i128() : val(0) {} constexpr i128(long long v) : val(v) {} i128(const string &s) : val(0) { parse(s); } void parse(const string &s) { val = 0; for (auto c : s) { if (isdigit(c)) val = val * 10 + (c - '0'); } if (s[0] == '-') val *= -1; } constexpr __int128 get() const { return val; } constexpr i128 abs() { if (val < 0) return -val; else return val; } // comparison operators constexpr bool operator == (const i128 &r) const { return this->val == r.val; } constexpr bool operator != (const i128 &r) const { return this->val != r.val; } constexpr bool operator < (const i128 &r) const { return this->val < r.val; } constexpr bool operator > (const i128 &r) const { return this->val > r.val; } constexpr bool operator <= (const i128 &r) const { return this->val <= r.val; } constexpr bool operator >= (const i128 &r) const { return this->val >= r.val; } // arithmetic operators constexpr i128& operator += (const i128 &r) { val += r.val; return *this; } constexpr i128& operator -= (const i128 &r) { val -= r.val; return *this; } constexpr i128& operator *= (const i128 &r) { val *= r.val; return *this; } constexpr i128& operator /= (const i128 &r) { val /= r.val; return *this; } constexpr i128& operator %= (const i128 &r) { val %= r.val; return *this; } constexpr i128 operator + () const { return i128(*this); } constexpr i128 operator - () const { return i128(0) - i128(*this); } constexpr i128 operator + (const i128 &r) const { return i128(*this) += r; } constexpr i128 operator - (const i128 &r) const { return i128(*this) -= r; } constexpr i128 operator * (const i128 &r) const { return i128(*this) *= r; } constexpr i128 operator / (const i128 &r) const { return i128(*this) /= r; } constexpr i128 operator % (const i128 &r) const { return i128(*this) %= r; } // other operators constexpr i128& operator ++ () { ++val; return *this; } constexpr i128& operator -- () { --val; return *this; } constexpr i128 operator ++ (int) { i128 res = *this; ++*this; return res; } constexpr i128 operator -- (int) { i128 res = *this; --*this; return res; } friend istream& operator >> (istream &is, i128 &x) { string s; is >> s; x.parse(s); return is; } friend ostream& operator << (ostream &os, const i128 &x) { auto tmp = x.val < 0 ? -x.val : x.val; char buffer[128]; char *d = end(buffer); do { --d; *d = "0123456789"[tmp % 10]; tmp /= 10; } while (tmp != 0); if (x.val < 0) { --d; *d = '-'; } int len = end(buffer) - d; if (os.rdbuf()->sputn(d, len) != len) { os.setstate(ios_base::badbit); } return os; } }; template<class T> T mod_pow(T a, T n, T m) { T res = 1; while (n > 0) { if (n % 2 == 1) res = res * a % m; a = a * a % m; n /= 2; } return res; }; template<class T> vector<pair<T, T>> prime_factorize(T n) { vector<pair<T, T>> res; for (T p = 2; p * p <= n; ++p) { if (n % p != 0) continue; T num = 0; while (n % p == 0) { ++num; n /= p; } res.push_back(make_pair(p, num)); } if (n != 1) res.push_back(make_pair(n, 1)); return res; } // 位数の計算 template<class T> T calc_order(T a, T P, const vector<pair<T, T>> &pf) { T res = P - 1; for (auto pa : pf) { T p = pa.first; while (res % p == 0 && mod_pow(a, res/p, P) == 1) res /= p; } return res; } int main() { long long N, P; cin >> N >> P; vector<long long> A(N); for (int i = 0; i < N; ++i) cin >> A[i]; // P-1 の素因数分解 auto pf = prime_factorize(i128(P - 1)); map<i128, long long> ma; for (auto a : A) { i128 order = calc_order(i128(a), i128(P), pf); ++ma[order]; } long long res = 0; for (auto [v1, num1] : ma) { for (auto [v2, num2] : ma) { if (v2 % v1 == 0) { res += num1 * num2; } } } cout << res << endl; }