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
| Aspect | Double Input | Integer Input |
|---|---|---|
| EPSILON | ✅ Cần | ❌ Không |
| Comparison | Math.Abs() > EPSILON | != 0 |
| Validation | NaN, Infinity | Không cần |
| Precision | Floating point errors | Exact |
| Complexity | Phứ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 định | Lý do |
|---|---|
| Static class | Không cần state, tiết kiệm memory |
| Exception cho errors | Global exception handling đã có |
| TrySolve pattern | Cho expected no solution cases |
| EPSILON = 1e-10 | Cân bằng giữa precision và practical |
| Fraction struct | Exact representation cho integer inputs |
| Record struct | Type-safe, pattern matching cho quadratic |
Performance
Time: O(1) - chỉ vài phép tính
Space: O(1) - không allocate extra memory