Official

F - Loud Cicada Editorial by sheyasutaka


実装方針

\(A_i\) の倍数であるという条件を「条件 \(i\)」と呼ぶことにします.

\(N\) 個の条件からなる全体集合に対して部分集合 \(S \in \{1, \dots, N\}\) を考えます.

各部分集合 \(S\) について,\(S\) に属する条件は満たし,かつ他の条件は満たさないという条件を「一致条件 \(S\)」と呼ぶことにします.

このとき,すべての部分集合についてその一致条件を満たす \(1\) 以上 \(Y\) 以下の整数の個数が求まれば,\(|S| = M\) である部分集合 \(S\) すべてについてその合計を求めることで答えが求まります.

ここで,部分集合 \(T\) について,\(T\) に属する条件は満たし,他の条件は満たしても満たさなくてもよいという条件を「部分条件 \(T\)」と呼ぶことにします.

部分条件は一致条件よりも考えやすく,部分条件 \(\{i_1, \dots, i_k\}\) は「\(A_{i_1}, \dots, A_{i_k}\) の最小公倍数の倍数である」と簡潔に言い換えられます.この最小公倍数を \(x\) とおくと,この部分条件を満たす \(1\) 以上 \(Y\) 以下の整数の個数は \(\lfloor \frac{Y}{x} \rfloor\) で得られます.最小公倍数を求める際,\(Y\) を上回るならば \(Y+1\) として扱う,できる限り乗算ではなく除算を使って判定・計算するといった工夫をすることで,オーバーフローの危険を避けられます.

各部分条件を満たすものの個数が求まったら,これをゼータ変換の逆変換 (メビウス変換といいます) にかけることによって各一致条件を満たすものの個数が求まり,よって答えが求まります.これは,簡潔にいえば「条件 \(1, \dots, k\) については一致条件,条件 \(k+1, \dots, N\) については部分条件」というような条件の持ち方をし,部分条件から \(1\) つずつ一致条件に移していく形で計算することができます.

実装例 (C++)

#include <iostream>
using std::cin;
using std::cout;

typedef long long int ll;


ll n, m, y;
ll a[21];
ll zeta[1LL << 21];

ll mygcd (ll l, ll r) {
	if (r == 0) return l;
	return mygcd(r, l % r);
}

int main (void) {
	cin >> n >> m >> y;
	for (ll i = 0; i < n; i++) {
		cin >> a[i];
	}

	for (ll b = 0; !(b >> n); b++) {
		ll prod = 1;
		for (ll j = 0; j < n; j++) {
			if (b & (1LL << j)) {
				ll g = mygcd(prod, a[j]);
				if ((prod / g) > y / a[j]) {
					prod = y+1;
					break;
				} else {
					prod = (prod / g) * a[j];
				}
			}
		}
		zeta[b] = y / prod;
	}

	// zeta[01] <- mobius(zeta[*1])
	for (ll bi = 0; bi < n; bi++) {
		for (ll b = 0; !(b >> n); b++) {
			// [0] <- [*] - [1]
			if (!(b & (1LL << bi))) {
				zeta[b] = zeta[b] - zeta[b | (1LL << bi)];
			}
		}
	}

	ll ans[n+1];
	for (ll i = 0; i <= n; i++) {
		ans[i] = 0;
	}
	for (ll b = 0; !(b >> n); b++) {
		ll cnt = 0;
		for (ll j = 0; j < n; j++) {
			if (b & (1LL << j)) {
				cnt++;
			}
		}
		ans[cnt] += zeta[b];
	}

	cout << ans[m] << "\n";

	
	return 0;
}

posted:
last update: