Official

F - Candy Redistribution Editorial by sheyasutaka


考察

\(A_1 + \dots + A_N\)\(N\) の倍数でなければ不可能であり,\(N\) の倍数であれば可能です.

\(X := \displaystyle \frac{A_1 + \dots + A_N}{N}\) とします.問題文中の条件は,すべての要素 \(a_1, \dots, a_N\)\(X\) に等しくすることと言い換えられます(ここでは,操作の対象となる数列を \(a_1, \dots, a_N\),入力で与えられる初期値を \(A_1, \dots, A_N\) とおいて区別します).

ある操作列 \((x_1, y_1, z_1), \dots, (x_q, y_q, z_q)\) が条件を満たすとします.このとき,\(N\) 頂点のグラフであって,\(q\) 組の頂点組 \((x_1, y_1), \dots, (x_q, y_q)\) それぞれのあいだに辺を張ったものを考え,その連結成分を \(V_1, \dots, V_s\) とします.このとき,各連結成分 \(V_k\) において,

\[\sum_{v \in V_k} A_v = X \times |V_k|\]

が成り立ちます(操作前後で連結成分内の \(a_v\) の和は変わらず,操作がすべて終わった時点ですべての \(a_v\)\(X\) に等しい). また,連結成分 \(V_k\) がもつ辺の本数は \(|V_k| - 1\) 以上であるため,グラフ全体の辺の本数は \(N - s\) 以上です.

逆に,子供たちの分割 \(V_1 \sqcup \dots \sqcup V_S = \{1, \dots, N\}\) であって,各部分集合 \(V_k\)\(\sum_{v \in V_k} A_v = X \times |V_k|\) を満たすものが与えられたとき,以下のようにして問題文中の条件を満たす長さ \(N - s\) の操作列を構築することができます.

  • 各部分集合 \(V_k\) に属する \(t := |V_k|\) 人の子供を,\(A_{v_{k,i}}\) の降順に \(v_{k, 1}, \dots, v_{k, t}\) とする.\(i = 1, \dots, t-1\) の昇順に,以下の整数組 \((x,y,z)\) によって操作 \((x,y,z)\) を行う.
    • \(x := v_{k,i}\) および \(y := v_{k,i+1}\) とし,\(z := a_{x} - X\) とする(ここで常に \(z \geq 0\) が成り立つことは,\(v_{k,i}\) が降順であることから示せる).

よって,多重集合 \(A = \{A_1, \dots, A_N\}\) を平均が \(X\) である部分集合に分割する方法であって,最も部分集合の個数 \(s\) が多いものを求め,各部分集合で上の操作を行えばよいです.

  • 分割を求めるところは,\(1\) 要素ずつ追加していく bitDP によって \(O(N 2^N)\) 時間で計算できます.
  • 操作列を求めるところは,上の操作を実装することで \(O(N \log N)\) 時間で計算できます.

以上より,\(O(N 2^N)\) 時間でこの問題を解くことができ,十分高速です.

実装例 (C++)

#include <iostream>
using std::cin;
using std::cout;
using std::min;
using std::max;
#include <vector>
using std::vector;
using std::pair;
#include <algorithm>
using std::sort;

typedef long long int ll;

ll n;
vector<ll> a;

vector<ll> dp, subsum, dprev;

vector<ll> x, y, z;

void solve () {
	ll sum = 0;
	for (ll i = 0; i < n; i++) {
		sum += a[i];
	}
	if (sum % n != 0) {
		cout << "-1" << "\n";
		return;
	}
	ll avg = sum / n;
	//
	dp.resize(1LL << n);
	subsum.resize(1LL << n);
	dprev.resize(1LL << n);
	dp[0] = 0;
	subsum[0] = 0;
	dprev[0] = -1;
	for (ll b = 1; !(b >> n); b++) {
		dp[b] = n+1;
		dprev[b] = -1;
		
		subsum[b] = 0;
		for (ll i = 0; i < n; i++) {
			if (b & (1LL << i)) subsum[b] += (a[i] - avg);
		}

		ll item = ((subsum[b] == 0) ? 0 : +1);
		for (ll i = 0; i < n; i++) {
			if (b & (1LL << i)) {
				ll maybe = dp[b - (1LL << i)] + item;
				if (dp[b] > maybe) {
					dp[b] = maybe;
					dprev[b] = i;
				}
			}
		}
	}

	ll cb = (1LL << n) - 1;
	while (cb) {
		vector<pair<ll, ll> > v;
		while (true) {
			ll vi = dprev[cb];
			v.push_back({-a[vi], vi}); // a[i] in decreasing order
			cb -= (1LL << vi);

			if (subsum[cb] == 0) break;
		}

		sort(v.begin(), v.end());
		ll carry = a[v[0].second] - avg;
		for (ll i = 1; i < v.size(); i++) {
			x.push_back(v[i-1].second);
			y.push_back(v[i-0].second);
			z.push_back(carry);

			carry += (a[v[i].second] - avg);
		}
	}

	cout << z.size() << "\n";
	for (ll i = 0; i < z.size(); i++) {
		cout << (x[i] + 1) << " " << (y[i] + 1) << " " << z[i] << "\n";
	}
}

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

	solve();
	

	return 0;
}

posted:
last update: