Equal Rational Numbers
HardPrompt
Given two strings s and t, each of which represents a non-negative rational number, return true if and only if they represent the same number. The strings may use parentheses to denote the repeating part of the rational number.
A rational number can be represented using up to three parts: <IntegerPart>, <NonRepeatingPart>, and a <RepeatingPart>. The number will be represented in one of the following three ways:
<IntegerPart>- For example,
12,0, and123.
- For example,
<IntegerPart><.><NonRepeatingPart>- For example,
0.5,1.,2.12, and123.0001.
- For example,
<IntegerPart><.><NonRepeatingPart><(><RepeatingPart><)>- For example,
0.1(6),1.(9),123.00(1212).
- For example,
The repeating portion of a decimal expansion is conventionally denoted within a pair of round brackets. For example:
1/6 = 0.16666666... = 0.1(6) = 0.1666(6) = 0.166(66).
Example 1:
Input: s = "0.(52)", t = "0.5(25)"
Output: true
Explanation: Because "0.(52)" represents 0.52525252..., and "0.5(25)" represents 0.52525252525..... , the strings represent the same number.Example 2:
Input: s = "0.1666(6)", t = "0.166(66)"
Output: trueExample 3:
Input: s = "0.9(9)", t = "1."
Output: true
Explanation: "0.9(9)" represents 0.999999999... repeated forever, which equals 1. [See this link for an explanation.]
"1." represents the number 1, which is formed correctly: (IntegerPart) = "1" and (NonRepeatingPart) = "".
Constraints:
- Each part consists only of digits.
- The
<IntegerPart>does not have leading zeros (except for the zero itself). 1 <= <IntegerPart>.length <= 40 <= <NonRepeatingPart>.length <= 41 <= <RepeatingPart>.length <= 4
Approaches
2 approaches with complexity analysis and trade-offs.
This approach converts each string representation of a rational number into a floating-point double and then compares these two double values for equality. While generally unsafe due to precision issues with floating-point numbers, it can work for this specific problem because of the small constraints on the lengths of the number parts and the specific behavior of standard library parsing functions.
Algorithm
- Define a helper function
convertToDouble(String str). - In the helper function, check if the string contains a repeating part (i.e., a '(' character).
- If there is no repeating part, parse the string directly to a
doubleand return it. - If there is a repeating part, extract the part before the parenthesis (non-repeating) and the part inside the parenthesis (repeating).
- Construct a new string by concatenating the non-repeating part with the repeating part appended enough times (e.g., 20) to saturate the precision of a
double. - Parse this newly constructed long string into a
doubleand return it. - In the main function
isRationalEqual, callconvertToDoublefor both input stringssandt. - Return
trueif the two resultingdoublevalues are equal,falseotherwise.
Walkthrough
The core idea is to approximate the rational number by expanding its decimal representation to a length that exceeds the precision of a double (typically 15-17 decimal digits).
A helper function is created to perform this conversion.
- Parsing: The input string is parsed to identify the integer, non-repeating, and repeating parts.
- Expansion: A new string is built. It starts with the integer part, a decimal point, and the non-repeating part. Then, the repeating part is appended multiple times (e.g., 20 times) to ensure the string is long enough to capture the number's value up to the limit of
doubleprecision. - Conversion: The expanded string is converted to a
doubleusingDouble.parseDouble(). A crucial observation is thatDouble.parseDouble("0.9999999999999999")evaluates to1.0, which correctly handles cases like0.9(9) == 1. - Comparison: The main function calls this helper for both input strings
sandtand returnstrueif the resultingdoublevalues are equal.
class Solution { public boolean isRationalEqual(String s, String t) { return convertToDouble(s) == convertToDouble(t); } private double convertToDouble(String s) { int lParen = s.indexOf('('); if (lParen == -1) { return Double.parseDouble(s); } String nonRepeatingPart = s.substring(0, lParen); String repeatingPart = s.substring(lParen + 1, s.length() - 1); StringBuilder sb = new StringBuilder(nonRepeatingPart); for (int i = 0; i < 20; i++) { sb.append(repeatingPart); } return Double.parseDouble(sb.toString()); }}Complexity
Time
O(1), since the length of the input strings is bounded by a small constant (max length is 4 + 1 + 4 + 1 + 4 = 14). The expansion and parsing operations take a constant amount of time.
Space
O(1), as the expanded string created for parsing has a bounded, constant length.
Trade-offs
Pros
Simple and concise to implement.
Leverages built-in, highly optimized floating-point parsing functions.
Cons
Relies on floating-point arithmetic, which is inherently imprecise and generally unsuitable for exact equality checks.
The solution is not robust. It works only because the problem's constraints are small enough to fit within
doubleprecision.It's a 'trick' rather than a fundamentally correct algorithmic solution for comparing rational numbers.
This approach provides a mathematically robust solution by converting each string representation into its canonical fractional form (p/q). Two rational numbers are equal if and only if their canonical fractions are identical. This method avoids the pitfalls of floating-point imprecision.
Algorithm
- Define a helper class
Fractionto store a numerator and a denominator. The constructor should simplify the fraction by dividing by the GCD. - Define a helper function
convertToFraction(String str)that returns aFractionobject. - Inside
convertToFraction, parse the string to get the integer, non-repeating, and repeating parts. - Case 1: No decimal point. The fraction is
(Integer, 1). - Case 2: Decimal point, no repeating part (
I.N). The value isI + N / 10^|N|. Convert this to a single fraction(I * 10^|N| + N) / 10^|N|. - Case 3: Decimal point and repeating part (
I.N(R)). The value isI + N/10^|N| + R/((10^|R|-1)*10^|N|). Combine these into a single fraction. The numerator will beI * den + N*(10^|R|-1) + Rand the denominator will be10^|N| * (10^|R|-1). Uselongfor calculations to avoid overflow. - Create a new
Fractionobject with the calculated numerator and denominator, which will also handle simplification via GCD. - In the main function
isRationalEqual, callconvertToFractionfor both input stringssandt. - Return
trueif the numerators and denominators of the two resultingFractionobjects are equal,falseotherwise.
Walkthrough
The fundamental principle is that any terminating or repeating decimal can be expressed as a fraction of two integers. A number represented as I.N(R), where I is the integer part, N is the non-repeating decimal part, and R is the repeating decimal part, can be converted to a fraction using a standard formula.
The algorithm involves:
- Parsing: A helper function parses the input string to extract the integer (
I), non-repeating (N), and repeating (R) parts. - Fraction Conversion: It calculates the numerator and denominator of the number. Due to potential size,
longshould be used for these calculations. For a numberI.N(R), the value can be expressed asI + (value of 0.N(R)). The fractional part0.N(R)is equivalent to(value of NR - value of N) / ( (10^|R| - 1) * 10^|N| ). - Simplification: The resulting fraction (numerator, denominator) is simplified to its canonical form by dividing both parts by their greatest common divisor (GCD). The GCD can be found using the Euclidean algorithm.
- Comparison: The main function converts both
sandtto their canonical fractions and compares them. If the numerators and denominators are respectively equal, the numbers are the same.
class Solution { class Fraction { long num, den; public Fraction(long n, long d) { long common = gcd(n, d); this.num = n / common; this.den = d / common; } private long gcd(long a, long b) { return b == 0 ? a : gcd(b, a % b); } } public boolean isRationalEqual(String s, String t) { Fraction f1 = convertToFraction(s); Fraction f2 = convertToFraction(t); return f1.num == f2.num && f1.den == f2.den; } private Fraction convertToFraction(String s) { int dot = s.indexOf('.'); if (dot == -1) { return new Fraction(Long.parseLong(s), 1); } long intPart = Long.parseLong(s.substring(0, dot)); int lParen = s.indexOf('('); if (lParen == -1) { // No repeating part String nonRepeating = s.substring(dot + 1); if (nonRepeating.isEmpty()) { return new Fraction(intPart, 1); } long den = (long) Math.pow(10, nonRepeating.length()); long num = Long.parseLong(nonRepeating); return new Fraction(intPart * den + num, den); } // Has repeating part String nonRepeating = s.substring(dot + 1, lParen); String repeating = s.substring(lParen + 1, s.length() - 1); long den1 = (long) Math.pow(10, nonRepeating.length()); long den2 = (long) Math.pow(10, repeating.length()) - 1; long den = den1 * den2; long num1 = 0; if (!nonRepeating.isEmpty()) { num1 = Long.parseLong(nonRepeating); } long num2 = Long.parseLong(repeating); // Total fractional part is nonRepeating/den1 + repeating/(den2*den1) long totalNum = num1 * den2 + num2; // Add integer part return new Fraction(intPart * den + totalNum, den); }}Complexity
Time
O(1). The string length is bounded by a constant. Parsing and arithmetic operations on `long`s are constant time. The `gcd` function's complexity is logarithmic with respect to the values, but since the values are bounded by `~10^12`, this is also effectively constant time.
Space
O(1). We only need to store a few variables and the `Fraction` objects, which take up constant space.
Trade-offs
Pros
Mathematically sound and exact. It is the canonical way to compare rational numbers.
Robust and not dependent on floating-point precision limitations. It would work even with much larger constraints by using
BigInteger.Handles all cases, including
0.9(9) == 1, correctly and explicitly through fractional arithmetic.
Cons
Implementation is more complex than the floating-point approach, requiring careful handling of fraction arithmetic and GCD calculation.
Solutions
Solution
class Solution {public boolean isRationalEqual(String S, String T) { int[] rationalS = getRational(S); int[] rationalT = getRational(T); return rationalS[0] == rationalT[0] && rationalS[1] == rationalT[1]; }public int[] getRational(String str) { boolean positive = true; if (str.charAt(0) == '-') { str = str.substring(1); positive = false; } int dotIndex = str.indexOf('.'); if (dotIndex < 0) { int integer = Integer.parseInt(str); if (!positive) integer = -integer; int[] rational = {integer, 1}; return rational; } int length = str.length(); if (dotIndex == length - 1) { int integer = Integer.parseInt(str.substring(0, dotIndex)); if (!positive) integer = -integer; int[] rational = {integer, 1}; return rational; } String integerPart = str.substring(0, dotIndex); int integer = Integer.parseInt(integerPart); String decimalPart = str.substring(dotIndex + 1); int decimalPartLength = length - dotIndex - 1; int repeatingIndex = str.indexOf('('); if (repeatingIndex < 0) { int numerator = Integer.parseInt(decimalPart); int denominator = (int)Math.pow(10, decimalPartLength); int gcd = gcd(numerator, denominator); numerator /= gcd; denominator /= gcd; int[] rational = {numerator, denominator}; rational[0] += integer * denominator; if (!positive) rational[0] = -rational[0]; return rational; } else { if (repeatingIndex - dotIndex == 1) { int numerator = Integer.parseInt(str.substring(repeatingIndex + 1, length - 1)); int denominator = (int)Math.pow(10, decimalPartLength - 2) - 1; int gcd = gcd(numerator, denominator); numerator /= gcd; denominator /= gcd; int[] rational = {numerator, denominator}; rational[0] += integer * denominator; if (!positive) rational[0] = -rational[0]; return rational; } else { int nonRepeatingLength = repeatingIndex - dotIndex - 1; int repeatingLength = length - 2 - repeatingIndex; int nonRepeating = Integer.parseInt(str.substring(dotIndex + 1, repeatingIndex)); int numerator = nonRepeating * (int)Math.pow(10, repeatingLength) + Integer.parseInt(str.substring(repeatingIndex + 1, length - 1)) - nonRepeating; int denominator = (int)(Math.pow(10, repeatingLength) - 1) * (int)(Math.pow(10, nonRepeatingLength)); int gcd = gcd(numerator, denominator); numerator /= gcd; denominator /= gcd; int[] rational = {numerator, denominator}; rational[0] += integer * denominator; if (!positive) rational[0] = -rational[0]; return rational; } } }public int gcd(int a, int b) { if (a == 0 && b == 0) return 1; while (a > 0 && b > 0) { if (a > b) { int temp = a; a = b; b = temp; } b %= a; } return a == 0 ? b : a; }}# 972. Equal Rational Numbers # https://leetcode.com/problems/equal-rational-numbers/ class Solution : def isRationalEqual ( self , s : str , t : str ) -> bool : def g ( s ): s2 = "" rep1 = "" ok = False for x in s : if x == "(" or x == ")" : ok = True continue if ok : rep1 += x else : s2 += x return ( s2 , rep1 ) os1 , rep1 = g ( s ) os2 , rep2 = g ( t ) s1 = os1 + rep1 * 30 s2 = os2 + rep2 * 30 if len ( rep1 ) == 0 and len ( rep2 ) == 0 : return s1 == s2 or float ( s1 ) == float ( s2 ) if s1 == s2 or float ( s1 ) == float ( s2 ): return True def good ( s1 , s2 ): count = 1 curr = s1 [ - 1 ] roundCount = len ( s2 ) - 2 for i in range ( len ( s1 ) - 2 , - 1 , - 1 ): if curr == s1 [ i ]: count += 1 else : return False if i > 0 and s1 [ i ] != s1 [ i - 1 ] and count >= 30 : ss = s1 [: i + 1 ] r = float ( ss ) + float ( "0." + "0" * ( len ( ss ) - 3 - bool ( s1 [ i - 1 ] == "." )) + "1" ) if r == float ( s2 ): return True return False return good ( s1 , s2 ) or good ( s2 , s1 )// OJ: https://leetcode.com/problems/equal-rational-numbers/ // Time: O(1) // Space: O(1) struct Number { string prefix , repeat ; Number ( string p , string r ) : prefix ( p ), repeat ( r ) { int N = r . size (), len = 1 ; // find the minimal repeat part for (; len <= N / 2 ; ++ len ) { int i = 0 ; while ( i < N && repeat [ i ] == repeat [ i % len ]) ++ i ; if ( i == N ) break ; } if ( len <= N / 2 ) repeat = repeat . substr ( 0 , len ); if ( repeat == "0" ) repeat = "" ; normalizePrefix (); } void normalizePrefix () { if ( prefix . find_first_of ( "." ) == string :: npos ) { prefix += '.' ; } else if ( repeat . empty ()) { // only pop trailing zeroes if repeat is empty while ( prefix . back () == '0' ) prefix . pop_back (); } } }; class Solution { string increment ( string & s ) { int i = s . size () - 1 , carry = 1 ; for (; i >= 0 && carry ; -- i ) { if ( s [ i ] == '.' ) continue ; carry += s [ i ] - '0' ; s [ i ] = '0' + carry % 10 ; carry /= 10 ; } if ( carry ) s . insert ( begin ( s ), '1' ); return s ; } Number getNumber ( string & s ) { auto i = s . find_first_of ( "(" ); if ( i == string :: npos ) return Number ( s , "" ); auto ans = Number ( s . substr ( 0 , i ), s . substr ( i + 1 , s . size () - i - 2 )); if ( ans . repeat == "9" ) { ans . repeat = "" ; ans . prefix = increment ( ans . prefix ); ans . normalizePrefix (); } return ans ; } public: bool isRationalEqual ( string s , string t ) { auto a = getNumber ( s ), b = getNumber ( t ); if ( a . repeat . size () != b . repeat . size ()) return false ; if ( a . repeat . size () == 0 ) return a . prefix == b . prefix ; if ( a . prefix . size () > b . prefix . size ()) swap ( a , b ); int i = 0 , N = b . prefix . size (); for (; i < N ; ++ i ) { if ( i < a . prefix . size ()) { if ( a . prefix [ i ] != b . prefix [ i ]) return false ; } else { if ( a . repeat [( i - a . prefix . size ()) % a . repeat . size ()] != b . prefix [ i ]) return false ; } } i = ( i - a . prefix . size ()) % a . repeat . size (); return a . repeat . substr ( i ) + a . repeat . substr ( 0 , i ) == b . repeat ; } };Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.