にょぐた君と一緒に考察した。面白かった。公式解説は多面体で考えているけど、ここでは LP 双対で解いてみる。
問題概要
種類の水溶液がある。水溶液 は、 として、
- 水溶液全体の重さが g
- 1 g あたりの溶質の重さが g 以上 g 以下
であることがわかっている。
これらの水溶液を有理数 g ずつ混ぜていくことで、
- 重さが g
- 1 g あたりの溶質の重さが g
であるようなの水溶液を作りたい。各水溶液の「1 g あたりの溶質の重さ」に幅があるため、作られる水溶液の「1 g あたりの溶質の重さ」にも幅があるが、その幅を加味した上で、 との最悪時の差分が最小となるようなものを求めよ。
最適な水溶液の、水溶液全体の重さと、溶質の重さを出力せよ。
制約
考えたこと
まず、もとの問題を LP で定式化する。混ぜる水溶液 の重さを g、目標との差分を とすると、次のように定式化できる。
Minimize:
Subject to:
双対をとると、次の LP になる(双対変数を とする)。
Maximize:
Subject to:
まず、 としてよいことがわかる。ある最適解に対して を同じ値だけ増やしても、目的関数の値は変わらず、 は減少するためだ。そこで、改めて とおくと、上の LP は次のように書き直せる。
Maximize:
Subject to:
この LP は、 を固定すると、Greedy に基づく解法で解ける。その部分を考察する。
を固定したときの最適解
簡単のため、 とおくと、 に関する条件は次のようになる。
目的関数の式の形から、 を固定するとき、 は極力小さいことが望ましいので、
とすればよいことがわかる。つまり、次の関数の最大値を求める問題となる。
であることから、 はある程度まで小さくした方がよさそうなことがわかる。ただし、 を小さくしすぎると、 となる が増えていくことに注意しよう。まとめると、次のように整理できる。
- であるような に対する の総和が 以上であるとき、 は小さくした方がよい
- であるような に対する の総和が 未満であるとき、 は大きくした方がよい
よって、 が小さくなる順に を並べて、 として、
となる最小の を見つけて、
とすればよい。以上で、 を固定したときの LP が解けた!!
元の LP に戻り
我々は、 を固定したときの、次の LP の最適解を求めることができた。
Maximize:
Subject to:
次に、 を固定したときの、この LP の最適解を と表して、 の最適解を求めよう。
関数 は、高々 個の微分不可能点をもつ区分線形関数になる。具体的には、各 に対する、
...... ①
となるような点である。この左辺と右辺の大小関係が入れ替わるとき、上の議論における の大小関係が入れ替わるためだ。
最初、にょぐた君と一緒に考えたのは、 通りの候補を調べればよいのだから、それらの差分を素早く更新できれば、 の計算量で解けたのではないか......ということだった。しかしこれは実装が大変だ。
ここで opt さんがスペースにやって来て、ヒントをくれた。関数 は区分線形凹関数だというのだ。確かに、関数 の上側の領域は「一次関数の上側の領域を重ね合わせたもの」として表せるのだから、関数 は凹関数になる!!!
そうであれば、三分探索が使える!!
有理数で答える必要があるから、単純な三分探索ではなく、次のようにする。
- ① を満たす を の範囲で列挙して、 を含めて、小さい順に とする
- 新たに自然数関数 を定義すると、これも凹関数である
- の範囲で三分探索して、 の最大値を求める
この解法は、候補点のソートがボトルネックであり、計算量は となる。
余談
三分探索をするにしても、 を を 等分すれば十分という解法もあるようだ。あと、多面体を用いて幾何的に考察する方法が commentary にある。
コード
#include <bits/stdc++.h> using namespace std; // 有理数 template<class T> struct frac { // inner values T first, second; // constructor frac& normalize() { if (second < 0) first = -first, second = -second; T d = gcd(abs(first), abs(second)); if (d == 0) first = 0, second = 1; else first /= d, second /= d; return *this; } constexpr frac(T f = 0, T s = 1) : first(f), second(s) { normalize(); } constexpr frac& operator = (T a) { *this = frac(a, 1); return *this; } // comparison operators constexpr bool operator == (const frac &r) const { return this->first == r.first && this->second == r.second; } constexpr bool operator != (const frac &r) const { return this->first != r.first || this->second != r.second; } constexpr bool operator < (const frac &r) const { return this->first * r.second < this->second * r.first; } constexpr bool operator > (const frac &r) const { return this->first * r.second > this->second * r.first; } constexpr bool operator <= (const frac &r) const { return this->first * r.second <= this->second * r.first; } constexpr bool operator >= (const frac &r) const { return this->first * r.second >= this->second * r.first; } // arithmetic operators constexpr frac& operator += (const frac &r) { this->first = this->first * r.second + this->second * r.first; this->second *= r.second; this->normalize(); return *this; } constexpr frac& operator -= (const frac &r) { this->first = this->first * r.second - this->second * r.first; this->second *= r.second; this->normalize(); return *this; } constexpr frac& operator *= (const frac &r) { this->first *= r.first; this->second *= r.second; this->normalize(); return *this; } constexpr frac& operator /= (const frac &r) { this->first *= r.second; this->second *= r.first; this->normalize(); return *this; } constexpr frac operator + () const { return frac(*this); } constexpr frac operator - () const { return frac(0) - frac(*this); } constexpr frac operator + (const frac &r) const { return frac(*this) += r; } constexpr frac operator - (const frac &r) const { return frac(*this) -= r; } constexpr frac operator * (const frac &r) const { return frac(*this) *= r; } constexpr frac operator / (const frac &r) const { return frac(*this) /= r; } friend constexpr ostream& operator << (ostream &os, const frac<T> &x) { os << x.first; if (x.second != 1) os << "/" << x.second; return os; } }; void ICPC_Asia_2024_J() { using lfrac = frac<long long>; const long long M = 10000; long long N, S, C, sumA = 0; cin >> N >> S >> C; vector<long long> A(N), L(N), R(N); for (int i = 0; i < N; i++) cin >> A[i] >> L[i] >> R[i], sumA += A[i]; vector<lfrac> vw({lfrac(-1), lfrac(1)}); for (int i = 0; i < N; i++) { for (int j = i+1; j < N; j++) { lfrac f(-L[i]+R[i]+L[j]-R[j], L[i]+R[i]-L[j]-R[j]); if (f >= -1 && f <= 1) vw.push_back(f); } } sort(vw.begin(), vw.end()); vw.erase(unique(vw.begin(), vw.end()), vw.end()); auto func = [&](lfrac w) -> lfrac { vector<pair<lfrac,int>> v; for (int i = 0; i < N; i++) { v.emplace_back(w*lfrac(L[i]+R[i])/2 + lfrac(L[i]-R[i])/2, i); } sort(v.begin(), v.end()); vector<lfrac> y(N, -1); lfrac z, limit = sumA - S; for (auto [val, i] : v) { if (limit >= 0) { limit -= A[i]; y[i] = 0; if (limit < 0) z = -val; } else { y[i] = z + val; } } lfrac obj = z * S + w * C * S; for (int i = 0; i < N; i++) obj -= y[i] * A[i]; return obj; }; int left = 0, right = vw.size()-1; while (right - left > 2) { int m1 = (left * 2 + right) / 3, m2 = (left + right * 2) / 3; if (func(vw[m1]) > func(vw[m2])) right = m2; else left = m1; } lfrac res = max({func(vw[left]), func(vw[(left+right)/2]), func(vw[right])}); res /= M; cout << res.first << " " << res.second << endl; } int main() { ICPC_Asia_2024_J(); }