¿Cuál es la mejor forma de comparar fracciones?

Buenasss. Estuve resolviendo este problema: Codeforces - The Fair Nut and Rectangles.
Es de las primeras veces que implemento el Convex Hull Trick y me encontré con un gran problema al momento de encontrar intersecciones entre rectas: comparar fracciones.

Dejo mi código por si alguien quiere ver cómo va
#include <iostream>
#include <assert.h>
#include <algorithm>
#include <vector>
#include <deque>

#define  DBG(x         ) cerr<<#x<<" = "<<(x)<<endl
#define DBG2(x, y      ) cerr<<#x<<" = "<<(x)<<", "<<#y<<" = "<<(y)<<endl
#define DBG3(x, y, z   ) cerr<<#x<<" = "<<(x)<<", "<<#y<<" = "<<(y)<<", "<<#z<<" = "<<(z)<<endl
#define DBG4(x, y, z, w) cerr<<#x<<" = "<<(x)<<", "<<#y<<" = "<<(y)<<", "<<#z<<" = "<<(z)<<", "<<#w<<" = "<<(w)<<endl
#define RAYA cerr<<" =============== "<<endl

#define forn(i, n) for(int i = 0; i < int(n); i++)
#define fort(i, n) for(int i = 0; i <= int(n); i++)
#define fori(i, a, n) for(int i = a; i < int(n); i++)
#define forit(i, a, n) for(int i = a; i <= int(n); i++)

#define all(v) v.begin(), v.end()
#define rall(v) v.rbegin(), v.rend()

using namespace std;

template<typename T>
ostream & operator<<(ostream &os, const vector<T> &v){
    os<<"[";
    forn(i, (int) v.size()){
        if(i) os<<", ";
        os<<v[i];
    }
    os<<"]";
    return os;
}

template<typename S, typename T>
ostream & operator<<(ostream &os, const pair<S, T> &p){
	os<<"("<<p.first<<", "<<p.second<<")";
	return os;
}

typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;

typedef ll T;
typedef pair<T,T> Fraction;

struct Line{
	T m, c;
	
	T getY(T x){
		return m * x + c;
	}
	Fraction interX(const Line& o){
		return {o.c - c, m - o.m};
	}
};

bool operator >= (Fraction a, Fraction b){
	if(a.first == 0 || b.first == 0) return a.first >= b.first;
	if(a.first / a.second > b.first / b.second) return 1;
	if(b.first / b.second > a.first / a.second) return 0;
	a.first %= a.second;
	b.first %= b.second;
	return a.first * b.second >= b.first * a.second;
}


bool bad(Line& a, Line& b, Line& c){
	auto p = b.interX(a), q = c.interX(b);
	return p >= q;
}

bool operator >= (pair<T,T> a, T b){
	return a.first >= b * a.second;
}

struct Rect{
	T x, y, a;
	
	bool operator < (const Rect& o) const{
		return x < o.x;
	}
};

void solve(){
	
	int n;
	cin>>n;
	
	vector<Rect> rects(n);
	for(auto& rect : rects) cin>>rect.x>>rect.y>>rect.a;
	
	sort(all(rects));
	
	ll ans = 0;
	deque<Line> ch{{0,0}};
	for(auto rect : rects){
		int sz = ch.size();
		while(sz >= 2 && ch[sz-1].interX(ch[sz-2]) >= rect.y){
			ch.pop_back(), sz--;
		}
		T c = ch.back().getY(rect.y) + rect.x * rect.y - rect.a;
		ans = max(ans,ll(c));
		Line aux = {-rect.x,c};
		while(sz >= 2 && bad(aux, ch[0], ch[1])) ch.pop_front(), sz--;
		ch.push_front(aux);
	}
	
	cout<<ans<<"\n";

}

int main(){

	cin.tie(NULL);
	ios::sync_with_stdio(0);

    #ifdef LOCAL
        freopen("input.in", "r", stdin);
    #else
        //~ freopen("input.in", "r", stdin);
        //~ freopen("output.out", "w", stdout);
    #endif

    #ifdef LOCAL
    
    int tcs;
    cin>>tcs;
    while(tcs--) solve();
    
    #else
    
    solve();
    
    #endif

    return 0;
}

Entendiendo lo siguiente:

\frac{a}{b} \geq \frac{c}{d}
a \times d \geq c \times b

Me surgió pensar en qué pasaría si a = 0 y d < 0 o c = 0 y b < 0. Podría generar una respuesta errónea, como el siguiente ejemplo.

\frac{5}{-5} \geq \frac{0}{10}
5 \times 10 \geq 0 \times -5
50 \geq 0

Cuando los valores reales para las fracciones serían:

-1 \geq 0 \text{ (no tiene sentido)}

En el problema que estaba resolviendo no hubo mucho problema porque se podían hacer los cálculos en cierto orden tal que el denominador nunca quede en negativo.

struct Line{
	T m, c;
	T getY(T x){
		return m * x + c;
	}
	Fraction interX(const Line& o){
		return {o.c - c, m - o.m};
	}
};

Al estar ordenadas las rectas por pendiente, el método interX se debe llamar desde la recta con mayor pendiente para evitar hacer un if.

Otro de los problemas fue el overflow del long long. En mi código, cada estado de la programación dinámica dp_i lo agrego como recta de la forma -\text{rectangulo}_x \times x + dp_i lo que hace que para insertarla tenga que usar el código:

Line aux = {-rect.x,c};
while(sz >= 2 && bad(aux, ch[0], ch[1])) ch.pop_front(), sz--;
ch.push_front(aux);

La función bad tiene lo siguiente:

bool bad(Line& a, Line& b, Line& c){
	auto p = b.interX(a), q = c.interX(b);
	return p >= q;
}

Y acá el gran problema: Si comparamos las fracciones p y q de la forma que mostré arriba, es decir \text{p}_\text{fst} \times \text{q}_\text{snd} \geq \text{q}_\text{fst} \times \text{p}_\text{snd} puede ocurrir que \text{p}_\text{fst} o \text{q}_\text{fst} lleguen a 10^{18} por lo que multiplicarlos por otro valor que puede llegar a 10^9 hace que supere el límite del long long. Para solucionar esto, hice el siguente código:

bool operator >= (Fraction a, Fraction b){
	if(a.first == 0 || b.first == 0) return a.first >= b.first;
	if(a.first / a.second > b.first / b.second) return 1;
	if(b.first / b.second > a.first / a.second) return 0;
	a.first %= a.second;
	b.first %= b.second;
	return a.first * b.second >= b.first * a.second;
}

Lo cual soluciona el problema, pero lo hace más lento al involucrar varias divisiones (la solución ocupa 1093 ms).

Siento que no debería ser tan difícil trabajar con fracciones o tal vez no debieron ser las protagonistas en mi solución cuando el problema trataba de Convex Hull Trick y por eso surge mi duda:

¿Hay una forma más fácil y eficiente de trabajar y comprar fracciones?

¡Hola!

Para el tema del denominador negativo, lo mejor es tener en el struct Fraccion ya siempre normalizado y que el denominador siempre sea positivo. Al crearlo en el constructor con numerador y denominador, si ve que el denominador es negativo se puede negar ambos y usar entonces (-numerador) / (-denominador) que es la misma fracción. Con eso el operator< que se implementa de la fracción siempre sabe ya que denominador es positivo y se simplifica.

Para el tema del overflow, se puede usar __int128 que probablemente sea lo más eficiente. Si llega a no haber __int128 o si encima __int128 no alcanza, ahí se pone molesta la cuestión y dependerá del problema si hay que usar biginteger o qué (a veces el problema está hecho para que con double y epsilons entra, pero en general si hay que comparar fracciones totalmente arbitrarias que no entran en int128 entonces existe algún caso de prueba patológico donde los correspondientes long doubles son idénticos porque necesitarían más de 128 bits de precisión relativa). Quizás en un caso así es que la solución iba por otro lado sin fracciones (en chull esencialmente aparece esa cuenta de comparar fracciones, que no por casualidad es exactamente la misma cuenta del producto cruz, por dualidad punto línea / el truquito alfa-beta).

Pero si tenemos fracciones razonables casi siempre lo mejor es trabajar en forma exacta con fracción, y en esos casos __int128 alcanza.

1 me gusta

Hola. Según este blog de codeforces [Tutorial] Convex Hull Trick — Geometry being useful (que casualmente habla de ese mismo problema) para el CHT no debería haber problema en comparar utilizando long double cuando solo se pregunta por coordenadas enteras. Incluso funcionaría usando siempre el piso de la división (que es lo que he visto que usan en varios notebook, por ejemplo este https://github.com/kth-competitive-programming/kactl/blob/main/content/data-structures/LineContainer.h).

2 Me gusta

No conocía eso de quedarse en enteros dividiendo y “redondeando” los puntos intersección aunque ya no se guarde la verdadera envolvente :open_mouth: Igualmente teniendo en cuenta que el post original iba a tratar de tunear la constante, hacer una división entera (por un número arbitrario) es famosamente una operación super lenta en los procesadores habituales, así que esperaría que usar int128 sea mucho más rápido. ¡Pero habría que testearlo!

1 me gusta