Senの競技プログラミング備忘録

こけた問題を自分用の解説で載せる。けんちょんさんのブログを目指したい。質的にも量的にも。こけた問題だけに限定するけど

2019-2020 ICPC Asia Taipei-Hsinchu Regional Contest - L Largest Quadrilateral

Attachments - 2019-2020 ICPC Asia Taipei-Hsinchu Regional Contest - Codeforces

概要

N点与えられる。そのうちの適切な順番で4つの点を選び、作れる最大の四角形の面積を求めよ。

制約: 4 <= N <= 4096, 0 <= X_i, Y_i <= 10^9

ACするまで

1. なるべく大きい四角形を考えると、凸包に含まれてる点から選ばないと行けなさそう。
2. わかりません。


3. 凸包は、時には円のように考えてみるとうまくいきやすい!
4. 作る四角形の対角線のうちの1本を固定するとする。固定したうえでの面積最大化を考えてみる。
5. その状態で、対角線の一端を固定、もう一端を少しずらしてみると、面積が最大化されるような四角形の残りの2つの頂点の動き方は、広義単調であると気付く。->変則しゃくとり(命名、僕)でいけそう 例外処理忘れずに(すべて1本の直線上とか、凸包上の点が3つしかないとか)
6. AC!幾何ライブラリを持っておくと、2次元計算が楽になるのでいいですね。

考察

まず、点の集合のうち、自明に凸包上にある点が候補になるとわかる。いろいろ面倒なのは考えずに、ここでは、凸包上の点の数が4つ以上存在してるとして考えていく。


凸包の上からいかにして4点を選ぶか?

という問題に対して、凸包では次のように考えるとうまくいきやすい。


Point
凸包上での動き方は、円の上での動き方を似た感じに適用できる場合が多い。

今回の場合は、この考えでうまくいく。つまり、次のような問題を考えてみる。


円上に何点かあり、それらが作る四角形の最大面積を求めよ。

大学受験でのテクニックではあるが、円に内接する四角形は、対角線を1本固定すると、それを底辺とした三角形2つそれぞれの面積最大化を独立に考えればよい。今回もこの考えでいく。

f:id:Sen_comp:20200210164923j:plain

atcoder.jp
紙の最後で言及してるしゃくとり法は、この問題の尺取法と同じ。次の記事で詳しく書かれている。
sen-comp.hatenablog.com

ACソースコード

#include<iostream>
#include<algorithm>
#include<cmath>
#include<vector>
 
using namespace std;
 
typedef long long ll;
 
namespace v2d {
 
	struct Vec2 {
		//2次元ベクトルのクラス
 
		double x, y;
		Vec2(double _x, double _y) {
			x = _x, y = _y;
		}
		Vec2() {
			x = 0, y = 0;
		}
 
		Vec2 operator+() const {
			return *this;
		}
		Vec2 operator-() const {
			return{ -x, -y };
		}
		Vec2 operator+ (const Vec2& b) const {
			return{ x + b.x, y + b.y };
		}
		Vec2 operator- (const Vec2& b) const {
			return{ x - b.x, y - b.y };
		}
		Vec2 operator* (const double b) const {
			return{ x * b, y * b };
		}
		Vec2 operator/ (const double b) const {
			return{ x / b, y / b };
		}
		Vec2 operator+= (const Vec2& b) {
			x += b.x, y += b.y;
			return *this;
		}
		Vec2 operator-= (const Vec2& b) {
			x -= b.x, y -= b.y;
			return *this;
		}
		Vec2 operator*= (const double b) {
			x *= b, y *= b;
			return *this;
		}
		Vec2 operator/= (const double b) {
			x /= b, y /= b;
			return *this;
		}
		bool operator== (const Vec2& b) {
			return b.x == x && b.y == y;
		}
 
		double lengthSquare() {
			return (x * x + y * y);
		}
		double length() {
			return std::sqrt(lengthSquare());
		}
		double dot(const Vec2& b) {
			return x * b.x + y * b.y;
		}
		double cross(const Vec2& b) {
			//Generally, cross product is vector, but in 2D, cross product is also scalar.
			return x * b.y - y * b.x;
		}
		double distanceFrom(const Vec2& b) {
			return std::sqrt((x - b.x) * (x - b.x) + (y - b.y) * (y - b.y));
		}
		Vec2 normalized() {
			return{ x / length(), y / length() };
		}
		bool isZero() {
			return x == 0.0 && y == 0.0;
		}
	};
 
	inline Vec2 operator*(double a, const Vec2& b) {
		return{ b.x * a, b.y * a };
	}
 
	template <class Char>
	inline std::basic_ostream<Char>& operator <<(std::basic_ostream<Char>& os, const Vec2& v)
	{
		return os << Char('(') << v.x << Char(',') << v.y << Char(')');
	}
 
	template <class Char>
	inline std::basic_istream<Char>& operator >> (std::basic_istream<Char>& is, Vec2& v)
	{
		return is >> v.x >> v.y;
	}
 
}
 
using namespace v2d;
 
namespace convex_h {
 
	bool comp_x(const v2d::Vec2& p, const v2d::Vec2& q) {
		if (p.x != q.x)return p.x < q.x;
		return p.y < q.y;
	}
	bool comp_y(const v2d::Vec2& p, const v2d::Vec2& q) {
		if (p.y != q.y)return p.y < q.y;
		return p.x < q.x;
	}
 
	struct convex_hull {
 
		const double EPS = 1e-10;
 
		vector<int> ans;
		vector<v2d::Vec2> candidate;
		vector<pair<v2d::Vec2, int>> taiou;
 
		void calc(bool is_cover = true) {
			for (int i = 0; i < candidate.size(); i++) {
				taiou.push_back(make_pair(candidate[i], i));
			}
			sort(taiou.begin(), taiou.end(), [](pair<v2d::Vec2, int> &a, pair<v2d::Vec2, int> &b) {
				return comp_x(a.first, b.first);
			});
			int k = 0;
			ans.resize(2 * taiou.size());
			for (int i = 0; i < taiou.size(); i++) {
				while (k > 1 && (is_cover && ((candidate[ans[k - 1]] - candidate[ans[k - 2]]).
					cross(taiou[i].first - candidate[ans[k - 1]]) <= 0) ||
					(!is_cover && (candidate[ans[k - 1]] - candidate[ans[k - 2]]).
						cross(taiou[i].first - candidate[ans[k - 1]]) < 0)))k--;
				ans[k++] = taiou[i].second;
			}
 
			for (int i = taiou.size() - 2, t = k; i >= 0; i--) {
				while (k > t && (is_cover && (candidate[ans[k - 1]] - candidate[ans[k - 2]]).
					cross(taiou[i].first - candidate[ans[k - 1]]) <= 0) ||
					((!is_cover && (candidate[ans[k - 1]] - candidate[ans[k - 2]]).
						cross(taiou[i].first - candidate[ans[k - 1]]) < 0)))k--;
				ans[k++] = taiou[i].second;
			}
			ans.resize(k - 1);
		}
 
		double farthest_dis() {
			//Using effective algorithm, 
			//this function can calculate farthest distance of all point in convex hull int O(n).
			//This algorithm name is Rptating Calipers, according to 蟻本.
 
			double res = 0;
			int n = ans.size();
			if (ans.size() == 2) {
				return candidate[ans[0]].distanceFrom(candidate[ans[1]]);
			}
			int i = 0, j = 0;
			for (int k = 0; k < n; k++) {
				if (!comp_x(candidate[ans[i]], candidate[ans[k]]))i = k;
				if (comp_x(candidate[ans[j]], candidate[ans[k]]))j = k;
			}
			int si = i, sj = j;
			while (i != sj || j != si) {
				res = max(res, candidate[ans[i]].distanceFrom(candidate[ans[j]]));
				int nxti = (i + 1) % n, nxtj = (j + 1) % n;
 
				if ((candidate[ans[nxti]] - candidate[ans[i]]).cross(candidate[ans[nxtj]] - candidate[ans[j]]) < 0) {
					i = (i + 1) % n;
				}
				else {
					j = (j + 1) % n;
				}
			}
			return res;
		}
 
		double hull_distance() {
			double ret = candidate[ans[0]].distanceFrom(candidate[ans.back()]);
			for (int i = 1; i < ans.size(); i++) {
				ret += candidate[ans[i]].distanceFrom(candidate[ans[i - 1]]);
			}
			return ret;
		}
 
	};
 
}
 
ll S_2times(Vec2 a, Vec2 b) {
	return abs(a.x * b.y - a.y * b.x);
}
 
ll min3(ll a, ll b, ll c) {
	return min(a, min(b, c));
}
 
int main() {
	int T;
	cin >> T;
	while (T--) {
		int N;
		cin >> N;
		vector<Vec2> P(N);
		for (int i = 0; i < N; i++) {
			cin >> P[i];
		}
 
		convex_h::convex_hull ch;
		ch.candidate = P, ch.calc(false);
		//P[ch.ans[i]]
 
		if (ch.ans.size() <= 2) {
			cout << 0 << endl;
			continue;
		}
		if (ch.ans.size() == 3) {
			ll ans = 0;
			Vec2 a = P[ch.ans[0]], b = P[ch.ans[1]], c = P[ch.ans[2]];
			for (int i = 0; i < N; i++) {
				if (a == P[i] || b == P[i] || c == P[i])continue;
				ans = max(ans, S_2times(b - a, c - a) -
					min3(
						S_2times(P[i] - a, c - a),
						S_2times(P[i] - a, b - a),
						S_2times(P[i] - b, P[i] - c)
					)
				);
			}
			if (ans % 2 == 0)cout << ans / 2 << endl;
			else cout << ans / 2 << ".5" << endl;
			continue;
		}
 
		ll ans = -1145141919810;
		for (int i = 0; i < ch.ans.size(); i++) {
			int s1 = i, s2 = i + 2;
			ll maxs1 = 0, maxs2 = 0;
			for (int j = i + 2; j < ch.ans.size() + i; j++) {
				while (s1 + 1 < j && maxs1 <= S_2times(P[ch.ans[j % ch.ans.size()]] - P[ch.ans[i]], P[ch.ans[j % ch.ans.size()]] - P[ch.ans[(s1 + 1) % ch.ans.size()]])) {
					maxs1 = S_2times(P[ch.ans[j % ch.ans.size()]] - P[ch.ans[i]], P[ch.ans[j % ch.ans.size()]] - P[ch.ans[(s1 + 1) % ch.ans.size()]]);
					s1++;
				}
				while (s2 + 1 < ch.ans.size() + i && maxs2 <= S_2times(P[ch.ans[j % ch.ans.size()]] - P[ch.ans[i]], P[ch.ans[j % ch.ans.size()]] - P[ch.ans[(s2 + 1) % ch.ans.size()]])) {
					maxs2 = S_2times(P[ch.ans[j % ch.ans.size()]] - P[ch.ans[i]], P[ch.ans[j % ch.ans.size()]] - P[ch.ans[(s2 + 1) % ch.ans.size()]]);
					s2++;
				}
 
				ans = max(ans, maxs1 + maxs2);
				
				if (s2 == j)s2++;
				else {
					maxs1 = S_2times(P[ch.ans[(j + 1) % ch.ans.size()]] - P[ch.ans[i]], P[ch.ans[(j + 1) % ch.ans.size()]] - P[ch.ans[s1 % ch.ans.size()]]);
					maxs2 = S_2times(P[ch.ans[(j + 1) % ch.ans.size()]] - P[ch.ans[i]], P[ch.ans[(j + 1) % ch.ans.size()]] - P[ch.ans[s2 % ch.ans.size()]]);
				}
			}
		}
		if(ans % 2 == 0)cout << ans / 2 << endl;
		else cout << ans / 2 << ".5" << endl;
	}
	return 0;
}

GYMの問題なので、普通の人は人の提出コードが見れない。なので、ソースコードは長いけど貼った。
2次元座標クラスは、cross()が2次元版外積に相当し、dot()がない積に相当。
凸包クラスのcalc()の引数は、falseならば、凸包に含まれる点で必ず凸包が曲がるようになる。

一言

このRegionalセット、解法の文献が乏しすぎる。日本語資料はじゅぴろさんの実況っぽいやつしかないし、中国語資料はコード、ぺ!wしかない。(ついでに言うと中国語資料のコードは理解できなかった)
解法のアイディアをくれたHyadoくん、ありがとう!

まとめ


Point
凸包上での動き方は、円の上での動き方を似た感じに適用できる場合が多い。