Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bài 1: Giải phương trình bậc nhất ax + b = 0

Đề bài

Cho phương trình bậc nhất 1 ẩn: ax + b = 0

Viết hàm giải phương trình với yêu cầu:

  • Code tối ưu, chặt chẽ
  • Xử lý exception hợp lý
  • Có thể reuse
  • Dễ dàng unit test
  • Không dùng third-party libraries
  • Ngôn ngữ: C#

Phân tích

Các trường hợp

1. a ≠ 0 → nghiệm duy nhất x = -b/a
2. a = 0, b = 0 → vô số nghiệm
3. a = 0, b ≠ 0 → vô nghiệm

Edge cases cần xử lý

- a rất gần 0 (floating point precision)
- a hoặc b là NaN, Infinity
- Negative values (valid)
- Overflow khi a rất nhỏ

Solution chính: Double Input

Final Version

public static class LinearEquationSolver
{
    private const double EPSILON = 1e-10;
    
    /// <summary>
    /// Giải phương trình bậc nhất ax + b = 0
    /// </summary>
    /// <returns>Nghiệm x (double)</returns>
    /// <exception cref="ArgumentException">a hoặc b là NaN/Infinity</exception>
    /// <exception cref="InvalidOperationException">Vô nghiệm hoặc vô số nghiệm</exception>
    public static double Solve(double a, double b)
    {
        // Validate input
        if (double.IsNaN(a) || double.IsNaN(b))
            throw new ArgumentException("Input cannot be NaN");
        
        if (double.IsInfinity(a) || double.IsInfinity(b))
            throw new ArgumentException("Input cannot be Infinity");
        
        // Check if a ≠ 0 (với floating point precision)
        if (Math.Abs(a) > EPSILON)
        {
            return -b / a;
        }
        
        // a = 0
        if (Math.Abs(b) < EPSILON)
            throw new InvalidOperationException("Phương trình có vô số nghiệm");
        
        throw new InvalidOperationException("Phương trình vô nghiệm");
    }
    
    /// <summary>
    /// Try solve - không throw exception cho no solution/infinite solutions
    /// </summary>
    public static bool TrySolve(double a, double b, out double? solution)
    {
        // Validate input - vẫn throw cho invalid input
        if (double.IsNaN(a) || double.IsNaN(b))
            throw new ArgumentException("Input cannot be NaN");
        
        if (double.IsInfinity(a) || double.IsInfinity(b))
            throw new ArgumentException("Input cannot be Infinity");
        
        if (Math.Abs(a) > EPSILON)
        {
            solution = -b / a;
            return true;
        }
        
        // a = 0 → vô nghiệm hoặc vô số nghiệm
        solution = null;
        return false;
    }
}

Usage

// Case 1: Nghiệm duy nhất
double x = LinearEquationSolver.Solve(2.5, -5); // x = 2.0
Console.WriteLine($"x = {x}");

// Case 2: Nghiệm thập phân
x = LinearEquationSolver.Solve(3, 1); // x = 0.333...
Console.WriteLine($"x = {x:F10}"); // 0.3333333333

// Case 3: Vô nghiệm
try
{
    LinearEquationSolver.Solve(0, 5);
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.Message); // "Phương trình vô nghiệm"
}

// Case 4: Dùng TrySolve
if (LinearEquationSolver.TrySolve(2.5, -5, out var solution))
{
    Console.WriteLine($"Nghiệm: {solution}");
}
else
{
    Console.WriteLine("Không có nghiệm duy nhất");
}

// Case 5: Invalid input
try
{
    LinearEquationSolver.Solve(double.NaN, 5);
}
catch (ArgumentException ex)
{
    Console.WriteLine(ex.Message); // "Input cannot be NaN"
}

Mở rộng 1: Fraction Result (Nghiệm phân số)

Problem

Khi a, b là số nguyên, nghiệm -b/a có thể biểu diễn chính xác dưới dạng phân số.
Ví dụ: 3x + 1 = 0 → x = -1/3 (chính xác hơn 0.333...)

Solution

/// <summary>
/// Biểu diễn phân số tối giản
/// </summary>
public readonly struct Fraction
{
    public int Numerator { get; }    // Tử số
    public int Denominator { get; }  // Mẫu số (luôn dương)
    
    public Fraction(int numerator, int denominator)
    {
        if (denominator == 0)
            throw new ArgumentException("Denominator cannot be zero");
        
        if (denominator < 0)
        {
            numerator = -numerator;
            denominator = -denominator;
        }
        
        // Rút gọn phân số
        int gcd = GCD(Math.Abs(numerator), denominator);
        Numerator = numerator / gcd;
        Denominator = denominator / gcd;
    }
    
    /// <summary>
    /// Chuyển sang decimal (có làm tròn)
    /// </summary>
    public double ToDouble() => (double)Numerator / Denominator;
    
    /// <summary>
    /// Kiểm tra có phải số nguyên không
    /// </summary>
    public bool IsInteger => Denominator == 1;
    
    public override string ToString()
    {
        if (Denominator == 1)
            return Numerator.ToString();
        return $"{Numerator}/{Denominator}";
    }
    
    public override bool Equals(object obj)
    {
        if (obj is Fraction other)
        {
            return Numerator == other.Numerator && 
                   Denominator == other.Denominator;
        }
        return false;
    }
    
    public override int GetHashCode() => 
        HashCode.Combine(Numerator, Denominator);
    
    /// <summary>
    /// Tìm ước chung lớn nhất
    /// </summary>
    private static int GCD(int a, int b)
    {
        while (b != 0)
        {
            int temp = b;
            b = a % b;
            a = temp;
        }
        return a;
    }
}

/// <summary>
/// Solver trả về kết quả dạng phân số
/// </summary>
public static class FractionEquationSolver
{
    private const double EPSILON = 1e-10;
    
    /// <summary>
    /// Giải phương trình và trả về phân số (chính xác)
    /// </summary>
    public static Fraction Solve(double a, double b)
    {
        if (double.IsNaN(a) || double.IsNaN(b))
            throw new ArgumentException("Input cannot be NaN");
        
        if (double.IsInfinity(a) || double.IsInfinity(b))
            throw new ArgumentException("Input cannot be Infinity");
        
        if (Math.Abs(a) > EPSILON)
        {
            // Làm tròn để lấy tử/mẫu nguyên
            int numerator = (int)Math.Round(-b);
            int denominator = (int)Math.Round(a);
            return new Fraction(numerator, denominator);
        }
        
        if (Math.Abs(b) < EPSILON)
            throw new InvalidOperationException("Vô số nghiệm");
        
        throw new InvalidOperationException("Vô nghiệm");
    }
}

Usage

// 3x + 1 = 0 → x = -1/3
Fraction x = FractionEquationSolver.Solve(3, 1);
Console.WriteLine($"x = {x}"); // x = -1/3
Console.WriteLine($"x = {x.ToDouble():F10}"); // 0.3333333333

// 2x - 4 = 0 → x = 2 (nguyên)
x = FractionEquationSolver.Solve(2, -4);
Console.WriteLine($"x = {x}"); // x = 2
Console.WriteLine($"Is integer: {x.IsInteger}"); // True

// 6x + 3 = 0 → x = -1/2 (rút gọn từ -3/6)
x = FractionEquationSolver.Solve(6, 3);
Console.WriteLine($"x = {x}"); // x = -1/2

Mở rộng 2: Phương trình bậc 2

ax² + bx + c = 0

/// <summary>
/// Kết quả giải phương trình bậc 2
/// </summary>
/// <param name="HasSolutions">Có nghiệm thực không</param>
/// <param name="X1">Nghiệm thứ nhất (nhỏ hơn)</param>
/// <param name="X2">Nghiệm thứ hai (lớn hơn, null nếu nghiệm kép)</param>
public readonly record struct QuadraticResult(
    bool HasSolutions,
    double X1,
    double? X2
)
{
    /// <summary>
    /// Số nghiệm thực (0, 1, hoặc 2)
    /// </summary>
    public int SolutionCount => !HasSolutions ? 0 : (X2.HasValue ? 2 : 1);
    
    /// <summary>
    /// Nghiệm kép (nếu có)
    /// </summary>
    public double? RepeatedRoot => X2.HasValue ? null : X1;
}

public static class QuadraticEquationSolver
{
    private const double EPSILON = 1e-10;
    
    /// <summary>
    /// Giải phương trình bậc 2: ax² + bx + c = 0
    /// </summary>
    /// <returns>
    /// QuadraticResult với:
    /// - HasSolutions = false: vô nghiệm thực
    /// - HasSolutions = true, X2 = null: nghiệm kép X1
    /// - HasSolutions = true, X2.HasValue: 2 nghiệm phân biệt X1, X2
    /// </returns>
    public static QuadraticResult Solve(double a, double b, double c)
    {
        // Validate
        if (double.IsNaN(a) || double.IsNaN(b) || double.IsNaN(c))
            throw new ArgumentException("Input cannot be NaN");
        
        if (double.IsInfinity(a) || double.IsInfinity(b) || double.IsInfinity(c))
            throw new ArgumentException("Input cannot be Infinity");
        
        // a = 0 → suy biến thành bậc nhất
        if (Math.Abs(a) < EPSILON)
        {
            double x = LinearEquationSolver.Solve(b, c);
            return new QuadraticResult(HasSolutions: true, X1: x, X2: null);
        }
        
        // Tính delta
        double delta = b * b - 4 * a * c;
        
        if (delta > EPSILON)
        {
            // 2 nghiệm phân biệt
            double sqrtDelta = Math.Sqrt(delta);
            double x1 = (-b - sqrtDelta) / (2 * a);
            double x2 = (-b + sqrtDelta) / (2 * a);
            
            // Đảm bảo x1 < x2
            if (x1 > x2)
                (x1, x2) = (x2, x1);
            
            return new QuadraticResult(HasSolutions: true, X1: x1, X2: x2);
        }
        else if (Math.Abs(delta) < EPSILON)
        {
            // 1 nghiệm kép
            double x = -b / (2 * a);
            return new QuadraticResult(HasSolutions: true, X1: x, X2: null);
        }
        else
        {
            // Vô nghiệm thực
            return new QuadraticResult(HasSolutions: false, X1: 0, X2: null);
        }
    }
    
    /// <summary>
    /// Try solve - trả về QuadraticResult, không throw exception
    /// </summary>
    public static QuadraticResult TrySolve(double a, double b, double c)
    {
        try
        {
            return Solve(a, b, c);
        }
        catch
        {
            return new QuadraticResult(HasSolutions: false, X1: 0, X2: null);
        }
    }
}

Usage

// 2 nghiệm phân biệt: x² - 5x + 6 = 0 → x = 2, 3
var result = QuadraticEquationSolver.Solve(1, -5, 6);
Console.WriteLine($"Số nghiệm: {result.SolutionCount}"); // 2
Console.WriteLine($"x1 = {result.X1}, x2 = {result.X2}"); // x1 = 2, x2 = 3

// 1 nghiệm kép: x² - 4x + 4 = 0 → x = 2
result = QuadraticEquationSolver.Solve(1, -4, 4);
Console.WriteLine($"Nghiệm kép: {result.RepeatedRoot}"); // 2
Console.WriteLine($"x1 = {result.X1}, x2 = {result.X2}"); // x1 = 2, x2 = null

// Vô nghiệm: x² + x + 1 = 0
result = QuadraticEquationSolver.Solve(1, 1, 1);
Console.WriteLine($"Có nghiệm: {result.HasSolutions}"); // False

// Pattern matching
var (hasSolutions, x1, x2) = QuadraticEquationSolver.Solve(1, -5, 6);
if (hasSolutions)
{
    if (x2.HasValue)
        Console.WriteLine($"2 nghiệm: {x1} và {x2.Value}");
    else
        Console.WriteLine($"Nghiệm kép: {x1}");
}
else
{
    Console.WriteLine("Vô nghiệm");
}

// Suy biến thành bậc nhất: 0x² + 2x - 4 = 0 → x = 2
result = QuadraticEquationSolver.Solve(0, 2, -4);
Console.WriteLine($"x = {result.X1}"); // 2

Unit Tests

using Xunit;

public class LinearEquationSolverTests
{
    [Fact]
    public void Solve_NonZeroA_ReturnsCorrectSolution()
    {
        // 2.5x - 5 = 0 → x = 2
        double x = LinearEquationSolver.Solve(2.5, -5);
        Assert.Equal(2.0, x);
    }
    
    [Fact]
    public void Solve_NegativeCoefficients_ReturnsCorrectSolution()
    {
        // -3x + 9 = 0 → x = 3
        double x = LinearEquationSolver.Solve(-3, 9);
        Assert.Equal(3.0, x);
    }
    
    [Fact]
    public void Solve_FractionalSolution_ReturnsDecimal()
    {
        // 3x + 1 = 0 → x = -1/3
        double x = LinearEquationSolver.Solve(3, 1);
        Assert.Equal(-1.0 / 3.0, x, 10);
    }
    
    [Fact]
    public void Solve_ZeroA_ZeroB_ThrowsException()
    {
        Assert.Throws<InvalidOperationException>(() => 
            LinearEquationSolver.Solve(0, 0));
    }
    
    [Fact]
    public void Solve_ZeroA_NonZeroB_ThrowsException()
    {
        Assert.Throws<InvalidOperationException>(() => 
            LinearEquationSolver.Solve(0, 5));
    }
    
    [Fact]
    public void Solve_NaNInput_ThrowsException()
    {
        Assert.Throws<ArgumentException>(() => 
            LinearEquationSolver.Solve(double.NaN, 5));
    }
    
    [Fact]
    public void TrySolve_Valid_ReturnsTrue()
    {
        var success = LinearEquationSolver.TrySolve(2.5, -5, out var x);
        Assert.True(success);
        Assert.Equal(2.0, x);
    }
    
    [Fact]
    public void TrySolve_NoSolution_ReturnsFalse()
    {
        var success = LinearEquationSolver.TrySolve(0, 5, out var x);
        Assert.False(success);
        Assert.Null(x);
    }
}

public class FractionEquationSolverTests
{
    [Fact]
    public void Solve_ReturnsFraction()
    {
        var x = FractionEquationSolver.Solve(3, 1);
        Assert.Equal(-1, x.Numerator);
        Assert.Equal(3, x.Denominator);
        Assert.Equal("-1/3", x.ToString());
    }
    
    [Fact]
    public void Solve_SimplifiesFraction()
    {
        var x = FractionEquationSolver.Solve(6, 3);
        Assert.Equal(-1, x.Numerator);
        Assert.Equal(2, x.Denominator);
    }
    
    [Fact]
    public void Solve_IntegerResult()
    {
        var x = FractionEquationSolver.Solve(2, -4);
        Assert.Equal(2, x.Numerator);
        Assert.Equal(1, x.Denominator);
        Assert.True(x.IsInteger);
        Assert.Equal("2", x.ToString());
    }
}

public class QuadraticEquationSolverTests
{
    [Fact]
    public void Solve_TwoDistinctSolutions_ReturnsCorrectRoots()
    {
        // x² - 5x + 6 = 0 → x = 2, 3
        var result = QuadraticEquationSolver.Solve(1, -5, 6);
        Assert.True(result.HasSolutions);
        Assert.Equal(2, result.SolutionCount);
        Assert.Equal(2.0, result.X1);
        Assert.Equal(3.0, result.X2);
        Assert.False(result.RepeatedRoot.HasValue);
    }
    
    [Fact]
    public void Solve_OneRepeatedSolution_ReturnsSingleRoot()
    {
        // x² - 4x + 4 = 0 → x = 2
        var result = QuadraticEquationSolver.Solve(1, -4, 4);
        Assert.True(result.HasSolutions);
        Assert.Equal(1, result.SolutionCount);
        Assert.Equal(2.0, result.X1);
        Assert.Null(result.X2);
        Assert.Equal(2.0, result.RepeatedRoot);
    }
    
    [Fact]
    public void Solve_NoRealSolutions_ReturnsNoSolutions()
    {
        // x² + x + 1 = 0
        var result = QuadraticEquationSolver.Solve(1, 1, 1);
        Assert.False(result.HasSolutions);
        Assert.Equal(0, result.SolutionCount);
        Assert.Null(result.RepeatedRoot);
    }
    
    [Fact]
    public void Solve_ZeroA_DelegatesToLinear()
    {
        // 0x² + 2x - 4 = 0 → 2x - 4 = 0 → x = 2
        var result = QuadraticEquationSolver.Solve(0, 2, -4);
        Assert.True(result.HasSolutions);
        Assert.Equal(1, result.SolutionCount);
        Assert.Equal(2.0, result.X1);
    }
    
    [Fact]
    public void Solve_X1LessThanX2()
    {
        // Đảm bảo X1 < X2
        var result = QuadraticEquationSolver.Solve(1, -5, 6);
        Assert.True(result.X1 < result.X2);
    }
    
    [Fact]
    public void TrySolve_InvalidInput_ReturnsNoSolutions()
    {
        var result = QuadraticEquationSolver.TrySolve(double.NaN, 1, 1);
        Assert.False(result.HasSolutions);
    }
}

Tham khảo: Integer Input

public static class LinearEquationSolver_Int
{
    /// <summary>
    /// Giải phương trình bậc nhất (integer version - đơn giản hơn)
    /// </summary>
    public static double Solve(int a, int b)
    {
        if (a != 0)
        {
            return -(double)b / a; // Cast để tránh integer division
        }
        
        if (b == 0)
            throw new InvalidOperationException("Vô số nghiệm");
        
        throw new InvalidOperationException("Vô nghiệm");
    }
}

// Usage
double x = LinearEquationSolver_Int.Solve(2, -4); // x = 2.0

Lưu ý: Integer version đơn giản hơn vì không cần EPSILON, so sánh trực tiếp == 0 được.


Tóm tắt

So sánh Double vs Integer

AspectDouble InputInteger Input
EPSILON✅ Cần❌ Không
ComparisonMath.Abs() > EPSILON!= 0
ValidationNaN, InfinityKhông cần
PrecisionFloating point errorsExact
ComplexityPhức tạp hơnĐơn giản hơn

QuadraticResult Tuple

// Record struct cho type-safe result
public readonly record struct QuadraticResult(
    bool HasSolutions,
    double X1,
    double? X2
);

// Benefits so với array:
✅ Type-safe: Biết rõ ý nghĩa từng field
✅ Self-documenting: HasSolutions, X1, X2 rõ ràng
✅ Pattern matching: var (has, x1, x2) = ...
✅ Extension: Thêm SolutionCount, RepeatedRoot

Design Decisions

Quyết địnhLý do
Static classKhông cần state, tiết kiệm memory
Exception cho errorsGlobal exception handling đã có
TrySolve patternCho expected no solution cases
EPSILON = 1e-10Cân bằng giữa precision và practical
Fraction structExact representation cho integer inputs
Record structType-safe, pattern matching cho quadratic

Performance

Time: O(1) - chỉ vài phép tính
Space: O(1) - không allocate extra memory

← Thuật toán & Giải thuật | Xem bài tiếp theo →