Tìm dãy con không chia hết cho một số nguyên

Bài toán Tìm dãy con không chia hết cho một số nguyên

Cho n số nguyên a1, a2, . . ., an (|ai| < 109, 0 ≤ n ≤ 100000). Hãy xác định dãy con nhiều phần tử
nhất từ dãy đã cho, sao cho không có hai phần tử nào của dãy con có tổng chia hết cho m (2 ≤ m ≤ 100000).
Dữ liệu: Vào từ file văn bản MODM.INP:
• Dòng thứ nhất chứa 2 số nguyên n và m,
• Dòng thứ 2 chứa n số nguyên a1, a2, . . ., an .
Kết quả: Đưa ra file văn bản MODM.OUT:
• Dòng thứ nhất chứa số nguyên k – số phần tử của dãy con tìm được,
• Dòng thứ 2 chứa k số nguyên – chỉ số trong dãy ban đầu của các phần tử thuộc dãy con.
Nếu có nhiều kết quả thì đưa ra một trong số chúng.

Cài đặt bài toán tìm dãy con không chia hết cho một số nguyên

Để giải bài toán này, ta có thể sử dụng kỹ thuật quy hoạch động.

Gọi f[i][j] là độ dài dãy con dài nhất có tổng chia hết cho m và kết thúc bởi phần tử thứ i trong dãy đã cho, và với tổng này là j.

Ban đầu, ta sẽ gán f[i][j] = -1 cho tất cả các giá trị i và j.

Ta sẽ tính toán các giá trị f[i][j] bằng cách xét tất cả các phần tử trước đó trong dãy đã cho, và cập nhật f[i][j] dựa trên giá trị của f[k][j’], trong đó k < i và j’ là phần dư của tổng các phần tử từ k+1 đến i trên m.

Cụ thể, ta có công thức: f[i][j] = max(f[i][j], f[k][j’] + 1)

Sau khi tính toán xong, ta sẽ duyệt qua tất cả các giá trị f[n][j] với j là bội số của m và tìm giá trị lớn nhất. Điều này sẽ cho ta độ dài của dãy con dài nhất cần tìm.

Cuối cùng, để xác định các phần tử thuộc dãy con này, ta sẽ duyệt ngược từ phần tử thứ n và xem liệu giá trị f[i][j] có bằng với độ dài dãy con dài nhất cần tìm và tổng của các phần tử từ i đến n có chia hết cho m hay không. Nếu có, ta sẽ thêm phần tử thứ i vào dãy con và cập nhật lại giá trị j.

Độ phức tạp của thuật toán này là O(nm).

Ngoài phương pháp quy hoạch động, ta có thể sử dụng một số thuật toán khác để giải quyết bài toán trên, bao gồm:

  1. Thuật toán Sinh – Liệt kê: đây là thuật toán dựa trên việc sinh ra tất cả các dãy con của dãy đã cho, và kiểm tra từng dãy con xem có thỏa mãn yêu cầu của bài toán hay không. Độ phức tạp của thuật toán này là O(n * 2^n).

  2. Thuật toán Brute Force: đây là thuật toán kiểm tra tất cả các dãy con có thể có của dãy đã cho, và tìm dãy con có độ dài lớn nhất thỏa mãn yêu cầu của bài toán. Độ phức tạp của thuật toán này là O(n^3).

  3. Thuật toán Sử dụng Bitmask: đây là thuật toán dựa trên việc sử dụng bitmask để đại diện cho các dãy con. Độ phức tạp của thuật toán này là O(3^n).

Tuy nhiên, cách tiếp cận tốt nhất để giải quyết bài toán này là sử dụng phương pháp quy hoạch động, bởi vì độ phức tạp của thuật toán này là O(nm), thấp hơn nhiều so với các thuật toán khác. Dưới đây là hai cách giải với Python và C++ cùng với hai thuật toán quy hoạch động và thuật toán sinh đã trình bày ở trên.

Cài đặt bài toán với thuật toán sinh

Các bước sử dụng thuật toán sinh cho bài toán trên như sau:

  1. Sắp xếp các phần tử trong dãy theo thứ tự tăng dần.
  2. Tạo ra một dãy con ban đầu gồm một phần tử đầu tiên.
  3. Lặp lại các bước sau cho đến khi không thể tạo ra thêm dãy con: a. Duyệt qua tất cả các phần tử trong dãy ban đầu, bắt đầu từ phần tử kế cuối cùng của dãy con hiện tại. b. Nếu phần tử tiếp theo có tổng với các phần tử trước đó không chia hết cho m, thì thêm phần tử đó vào dãy con. c. Nếu phần tử tiếp theo có tổng với các phần tử trước đó chia hết cho m, thì không thêm phần tử đó vào dãy con và chuyển đến phần tử tiếp theo. d. Lưu trữ dãy con có số lượng phần tử lớn nhất mà không chia hết cho m.

Thuật toán sinh cần phải duyệt qua tất cả các dãy con có thể tạo ra từ dãy ban đầu, vì vậy độ phức tạp của thuật toán sinh là O(2^n).

Python

def find_subsequence(n, m, a):
    # Khởi tạo biến lưu kết quả
    max_length = 0
    max_subseq = []

    # Sinh ra tất cả các dãy con của dãy đã cho
    for i in range(1, 2**n):
        subseq = []
        sum_subseq = 0
        for j in range(n):
            if (i >> j) & 1:
                subseq.append(a[j])
                sum_subseq += a[j]

        # Kiểm tra xem dãy con này có thỏa mãn yêu cầu của bài toán hay không
        if len(subseq) > max_length and sum_subseq % m != 0:
            max_length = len(subseq)
            max_subseq = subseq

    # Trả về kết quả
    return max_length, max_subseq


# Đọc dữ liệu từ file
with open("MODM.INP", "r") as f:
    n, m = map(int, f.readline().split())
    a = list(map(int, f.readline().split()))

# Tìm kiếm dãy con thỏa mãn yêu cầu của bài toán
max_length, max_subseq = find_subsequence(n, m, a)

# Ghi kết quả vào file
with open("MODM.OUT", "w") as f:
    f.write(str(max_length) + "\n")
    f.write(" ".join(str(a.index(x)+1) for x in max_subseq))

C++

#include <bits/stdc++.h>
using namespace std;

int n, m, max_length;
vector<int> a, max_subseq;

void find_subsequence() {
    // Sinh tất cả các dãy con của dãy đã cho
    for (int i = 1; i < (1 << n); i++) {
        vector<int> subseq;
        int sum_subseq = 0;
        for (int j = 0; j < n; j++) {
            if ((i >> j) & 1) {
                subseq.push_back(a[j]);
                sum_subseq += a[j];
            }
        }

        // Kiểm tra xem dãy con này có thỏa mãn yêu cầu của bài toán hay không
        if (subseq.size() > max_length && sum_subseq % m != 0) {
            max_length = subseq.size();
            max_subseq = subseq;
        }
    }
}

int main() {
    // Đọc dữ liệu từ file
    freopen("MODM.INP", "r", stdin);
    cin >> n >> m;
    a.resize(n);
    for (int i = 0; i < n; i++) {
        cin >> a[i];
    }

    // Tìm kiếm dãy con thỏa mãn yêu cầu của bài toán
    find_subsequence();

    // Ghi kết quả vào file
    freopen("MODM.OUT", "w", stdout);
    cout << max_length << endl;
    for (int i = 0; i < max_length; i++) {
        cout << find(a.begin(), a.end(), max_subseq[i]) - a.begin() + 1 << " ";
    }
    return 0;
}

Cài đặt bài toán với thuật toán quy hoạch động

Python

# Đọc dữ liệu từ file
with open("MODM.INP", "r") as f:
    n, m = map(int, f.readline().split())
    a = list(map(int, f.readline().split()))

# Khởi tạo mảng đánh dấu để lưu các dãy con tìm được
dp = [[0 for _ in range(m)] for _ in range(n)]

# Tính toán các giá trị cho mảng đánh dấu
for i in range(n):
    # Trường hợp cơ sở: chỉ xét với phần tử đầu tiên của dãy con
    if a[i] % m != 0:
        dp[i][a[i] % m] = 1
    
    # Tìm các dãy con có chứa phần tử thứ i
    for j in range(i):
        for k in range(m):
            # Nếu dãy con tại vị trí j thỏa mãn điều kiện và
            # phần tử thứ i có thể thêm vào để tạo ra một dãy con mới
            if dp[j][k] != 0 and dp[i][(k + a[i]) % m] == 0:
                dp[i][(k + a[i]) % m] = dp[j][k] + 1

# Tìm dãy con có độ dài lớn nhất và không chia hết cho m
max_len = 0
max_subseq = []
for i in range(n):
    for j in range(m):
        if dp[i][j] > max_len and (j + a[i]) % m != 0:
            max_len = dp[i][j]
            max_subseq = [a[i]]
            k = j
            # Truy vết lại dãy con tìm được
            for l in range(i-1, -1, -1):
                if dp[l][k] == dp[i][j] - 1 and (k - a[l]) % m == j:
                    max_subseq.append(a[l])
                    k = (k - a[l]) % m
            max_subseq.reverse()

# Ghi kết quả vào file
with open("MODM.OUT", "w") as f:
    f.write(str(max_len) + "\n")
    f.write(" ".join(map(str, [a.index(x)+1 for x in max_subseq])))

C++

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;

int n, m;
int a[MAXN], f[MAXN], tr[MAXN];

int main() {
    freopen("MODM.INP", "r", stdin);
    freopen("MODM.OUT", "w", stdout);

    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }

    memset(f, -1, sizeof(f));
    f[0] = 0;
    for (int i = 1; i <= n; i++) {
        for (int j = m - 1; j >= 0; j--) {
            int r = (j - a[i] % m + m) % m;
            if (f[j] >= 0 && f[r] < 0) {
                f[r] = f[j] + 1;
                tr[r] = i;
            }
        }
        if (f[a[i] % m] < 0) {
            f[a[i] % m] = 1;
            tr[a[i] % m] = i;
        }
    }

    cout << f[0] << endl;
    vector<int> ans;
    for (int i = 0, j = 0; i != 0 || j != 1; j++) {
        if (f[i] == f[(i + a[tr[i]]) % m] + 1) {
            ans.push_back(tr[i]);
            i = (i + a[tr[i]]) % m;
        }
    }
    for (int i = 0; i < ans.size(); i++) {
        cout << ans[i] << " ";
    }

    return 0;
}

Đánh giá về thuật toán

Trong việc giải bài toán trên, ta có thể sử dụng hai thuật toánthuật toán sinh và thuật toán quy hoạch động.

Thuật toán sinh sẽ liệt kê tất cả các dãy con của dãy ban đầu và kiểm tra xem có dãy con nào thỏa mãn yêu cầu bài toán hay không. Cách tiếp cận này đòi hỏi phải duyệt qua tất cả các dãy con của dãy ban đầu, do đó độ phức tạp của thuật toán sinh là O(2^n * n).

Thuật toán quy hoạch động sử dụng một bảng phương án để lưu trữ các phương án tối ưu cho các bài toán con nhỏ hơn và sử dụng chúng để tính toán các phương án tối ưu cho các bài toán lớn hơn. Cách tiếp cận này đòi hỏi độ phức tạp tính toán của mỗi bài toán con là O(m), do đó độ phức tạp của thuật toán quy hoạch động là O(nm).

So sánh hai thuật toán, ta thấy rằng thuật toán quy hoạch động có độ phức tạp tính toán tốt hơn và không yêu cầu phải duyệt qua tất cả các dãy con của dãy ban đầu. Tuy nhiên, nếu m là một giá trị lớn, thì độ phức tạp của thuật toán quy hoạch động cũng sẽ tăng lên tương ứng, và có thể trở thành một vấn đề đối với các bài toán có kích thước lớn. Trong trường hợp này, thuật toán sinh có thể trở thành một lựa chọn tốt hơn.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *