Hướng dẫn giải của Kiểm tra số chính phương 2


Chỉ dùng lời giải này khi không có ý tưởng, và đừng copy-paste code từ lời giải này. Hãy tôn trọng người ra đề và người viết lời giải.
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.

Lời giải này đang bị ẩn cho đến khi bạn chọn mở ra.

Chúng tôi khuyên bạn nên tự thử giải bài trước. Việc mở lời giải có thể làm lộ mất ý tưởng chính trước khi bạn có cơ hội tự giải.

Bạn phải đăng nhập để mở lời giải này.

Đăng nhập

Tác giả: Hiếu Nguyễn, tri_88, lephuochauhungvuong, oqtn75

Hiểu bài toán

Bài toán yêu cầu kiểm tra một số nguyên dương N (với N ≤ 10^100) có phải là số chính phương hay không. Nếu không, in ra 'NO'. Nếu có, in ra số nguyên dương M sao cho M^2 = N. Do giới hạn của N rất lớn, không thể sử dụng các kiểu dữ liệu nguyên (integer) thông thường mà phải xử lý số lớn (big integer) dưới dạng chuỗi ký tự.

Các cách tiếp cận

Cách Tìm kiếm nhị phân (Binary Search)
#include <bits/stdc++.h>
using namespace std;

// So sánh hai chuỗi số lớn
int cmp(const string &a, const string &b) {
    if (a.size() != b.size()) return a.size() < b.size() ? -1 : 1;
    if (a == b) return 0;
    return a < b ? -1 : 1;
}

// Nhân số lớn (string) với số lớn (string)
multiply(const string &a, const string &b) {
    int n = a.size(), m = b.size();
    vector<int> res(n + m, 0);
    for (int i = n - 1; i >= 0; i--) {
        for (int j = m - 1; j >= 0; j--) {
            int mul = (a[i] - '0') * (b[j] - '0');
            int sum = res[i + j + 1] + mul;
            res[i + j + 1] = sum % 10;
            res[i + j] += sum / 10;
        }
    }
    string s;
    for (int c : res) if (!(s.empty() && c == 0)) s.push_back(c + '0');
    return s.empty() ? "0" : s;
}

// Cộng 1 vào số lớn (string)
string addOne(const string &s) {
    string a = s;
    int i = a.size() - 1, carry = 1;
    while (i >= 0 && carry) {
        int d = a[i] - '0' + carry;
        a[i] = (d % 10) + '0';
        carry = d / 10;
        i--;
    }
    if (carry) a = '1' + a;
    return a;
}

// Chia đôi số lớn (string)
string divTwo(const string &s) {
    string res = "";
    int temp = 0;
    for (char c : s) {
        temp = temp * 10 + (c - '0');
        res += (temp / 2) + '0';
        temp %= 2;
    }
    while (res.size() > 1 && res[0] == '0') res.erase(res.begin());
    return res;
}

int main() {
    string n;
    cin >> n;
    string l = "1", r = n;
    // Tối ưu hóa biên: căn bậc hai của 10^100 là 10^50
    // Tuy nhiên, ta vẫn để r = n và dùng tìm kiếm nhị phân chuẩn

    while (cmp(l, r) <= 0) {
        string mid = divTwo(addOne(l + "0")); // (l+r)/2 nhưng an toàn
        // Tính mid = (l + r) / 2
        // Cách khác:
        string sum = "";
        // ... (cần hàm cộng)
        // Thay vào đó, dùng phép chia 2 trực tiếp sau khi cộng

        // Để đơn giản hóa code trong editorial:
        // Giả sử có hàm add, sub, div2
        // mid = div2(add(l, r));
    }
    // Code đầy đủ cần các hàm helper
    return 0;
}
  • Time Complexity: O(K^2 * log N), với K là số digits của N
  • Space Complexity: O(K)

Phương pháp này dựa trên tính chất đơn điệu của hàm bình phương. Nếu ta lấy một số M, nếu M^2 < N thì M < sqrt(N), và ngược lại. Ta thực hiện tìm kiếm nhị phân trong khoảng từ 1 đến N để tìm M sao cho M^2 = N. Với mỗi bước, ta tính số giữa (mid), bình phương nó và so sánh với N. Do N rất lớn, phép cộng, nhân, chia và so sánh đều phải được cài đặt thủ công dưới dạng chuỗi.

Cách Phương pháp Newton-Raphson
#include <bits/stdc++.h>
using namespace std;

// Các hàm cộng, trừ, nhân, chia số lớn (được tổng hợp từ các giải pháp)
// Hàm so sánh cmp(a, b)
// Hàm nhân multiply(a, b)
// Hàm cộng add(a, b)
// Hàm trừ subtract(a, b) (với a >= b)
// Hàm chia cho 2 divTwo(s)

string sqrtNewton(string n) {
    string x = n;
    string prev = "";
    // Chọn giá trị khởi tạo tốt: x0 ~ n
    // Công thức Newton: x_{k+1} = (x_k + n / x_k) / 2
    // Lặp cho đến khi x không thay đổi (đủ chính xác)

    // Lưu ý: Kiểm tra điều kiện dừng
    while (cmp(x, prev) != 0) {
        prev = x;
        // x = (x + n / x) / 2
        // Cần hàm chia số lớn: n / x
        // Nếu dùng thư viện tự cài, code sẽ rất dài.
        // Trong code mẫu, ta cần cài đặt hàm divide(string a, string b)
        // nhưng do phức tạp, người ta thường dùng Newton cho số lớn với các thao tác cơ bản.
        // Code mẫu dưới đây minh họa ý tưởng:
        /*
        string term = divide(n, x); // n / x
        string sum = add(x, term);  // x + n/x
        x = divTwo(sum);            // (x + n/x) / 2
        */
        // Tuy nhiên, phép chia số lớn rất phức tạp để cài đặt thủ công.
    }
    return x;
}

int main() {
    string n;
    cin >> n;
    // Xử lý n = "0" hoặc "1"
    // Gọi hàm sqrtNewton
    return 0;
}
  • Time Complexity: O(K^2 * log K) (nhanh hơn Binary Search)
  • Space Complexity: O(K)

Phương pháp Newton-Raphson là một thuật toán tìm xấp xỉ nhanh chóng cho nghiệm của phương trình f(x) = 0. Để tìm căn bậc hai của N, ta giải phương trình x^2 - N = 0. Công thức lặp là x{k+1} = (xk + N / xk) / 2. Thuật toán này hội tụ rất nhanh (quadratic convergence), thường chỉ cần vài chục bước lặp là đủ để tìm chính xác. Tuy nhiên, việc cài đặt phép chia số lớn (N / xk) là khá phức tạp nếu không có thư viện hỗ trợ.

Cách Chuyển đổi sang số lớn BigInt và dùng hàm có sẵn
#include <bits/stdc++.h>
using namespace std;

// Sử dụng thư viện Boost hoặc tự định nghĩa class BigInt
// Code dưới đây là minh họa cho cách tiếp cận nếu ngôn ngữ hỗ trợ Big Integer nativly (như Python)
// Hoặc dùng thư viện Boost.Multiprecision trong C++

#include <boost/multiprecision/cpp_int.hpp>
using namespace boost::multiprecision;
using namespace std;

int main() {
    string s;
    cin >> s;
    cpp_int n(s);

    // Tính căn bậc hai bằng hàm sqrt của thư viện
    cpp_int root = sqrt(n);

    if (root * root == n) {
        cout << root << endl;
    } else {
        cout << "NO" << endl;
    }
    return 0;
}

// Nếu không dùng thư viện, phải tự cài đặt Class BigInt với phép nhân, chia, cộng, trừ.
  • Time Complexity: Phụ thuộc vào thư viện, thường hiệu quả
  • Space Complexity: O(K)

Đây là cách tiếp cận hiện đại và thực tế nhất trong lập trình thi đấu nếu ngôn ngữ cho phép (Python, Java với BigInteger) hoặc thư viện C++ hỗ trợ (Boost). Tuy nhiên, trong môi trường thi đấu online Judge thường hạn chế thư viện ngoài, hoặc yêu cầu tự cài đặt. Các giải pháp mẫu cung cấp các phép toán cơ bản (add, sub, mul) cho thấy việc tự cài đặt BigInt là hoàn toàn khả thi. Việc tự cài đặt full BigInt (kể cả phép chia) là bài toán lớn, nhưng với bài toán này chỉ cần nhân, cộng, trừ, so sánh là đủ cho tìm kiếm nhị phân.

Phân tích độ phức tạp

Cách tiếp cận Time Space Tên
1 O(K^2 * log N), với K là số digits của N O(K) Tìm kiếm nhị phân (Binary Search)
2 O(K^2 * log K) (nhanh hơn Binary Search) O(K) Phương pháp Newton-Raphson
3 Phụ thuộc vào thư viện, thường hiệu quả O(K) Chuyển đổi sang số lớn BigInt và dùng hàm có sẵn

Bài học kinh nghiệm

  • Do N rất lớn (lên đến 10^100), bắt buộc phải sử dụng String (hoặc Class BigInt) để lưu trữ và thao tác.
  • Bài toán có tính chất đơn điệu (monotonic), thích hợp cho các thuật toán tìm kiếm (Binary Search) hoặc các phương pháp giải phương trình số (Newton-Raphson).
  • Khi tự cài đặt các phép toán số lớn, cần lưu ý xử lý số nhớ (carry) và số mượn (borrow) cẩn thận, đặc biệt là khi nhân và chia.

Lỗi thường gặp

  • Lỗi tràn số khi sử dụng các kiểu dữ liệu integer cơ bản (int, long long) để lưu N.
  • Sai sót trong việc cài đặt phép nhân/chia số lớn, dẫn đến kết quả sai.
  • Điều kiện dừng sai trong thuật toán Newton-Raphson (không hội tụ hoặc lặp vô hạn).
  • Bỏ qua các trường hợp đặc biệt như N = 0, N = 1.

Bình luận

Please read the guidelines before commenting.


Không có bình luận tại thời điểm này.