Tính tổng độ điểm cho tất cả xâu con

1. Mô tả bài toán

Cho một xâu S, ta định nghĩa độ điểm f(S) là số ký tự xuất hiện đúng một lần trong xâu đó.

Ví dụ: f("aba") = 1, f("abc") = 3, f("aaa") = 0.

Cho xâu S độ dài n (chỉ gồm chữ cái thường), yêu cầu tính tổng của f(S[i..j]) cho tất cả các xâu con không rỗng của S.

Input: Một dòng duy nhất là xâu S (1 ≤ n ≤ 100000).

Output: Một số nguyên là tổng cần tìm.

Ví dụ: S = "ababc" → output = 21.

2. Phân tích

Phương pháp vét cạn O(n²) chỉ chạy được với n ≤ 1000. Để đạt O(n), ta cần xét đóng góp của từng ký tự riêng lẻ.

Xét ký tự c tại vị trí i. Nó chỉ đóng góp 1 điểm cho các xâu con mà trong xâu đó chỉ có duy nhất một ký tự c (tức là không có ký tự c nào khác).

Gọi vị trí xuất hiện trước của cùng ký tự đó là L, vị trí xuất hiện sau là R (nếu không có trước thì L=0, không có sau thì R=n+1). Khi đó, mọi xâu con bắt đầu từ vị trí trong (L, i] và kết thúc trong [i, R) sẽ chỉ chứa đúng một ký tự c.

Số lượng xâu con như vậy là (i - L) * (R - i).

Tổng đáp số = Σ cho tất cả vị trí i.

3. Cài đặt (C++)

3.1. Thuật toán vét cạn (chạy trong 50% test)

#include <iostream>
#include <string>
#include <vector>
using namespace std;
using ll = long long;

int main() {
    string s;
    cin >> s;
    int n = s.size();
    ll ans = 0;
    for (int i = 0; i < n; ++i) {
        vector<int> cnt(26, 0);
        int distinct = 0;          // số ký tự xuất hiện đúng 1 lần trong xâu con hiện tại
        for (int j = i; j < n; ++j) {
            int idx = s[j] - 'a';
            cnt[idx]++;
            if (cnt[idx] == 1) distinct++;
            else if (cnt[idx] == 2) distinct--;
            ans += distinct;
        }
    }
    cout << ans;
    return 0;
}

3.2. Thuật toán tối ưu O(n)

#include <iostream>
#include <string>
#include <vector>
#include <cstring>
using namespace std;
using ll = long long;

const int MAXN = 100005;

int main() {
    string s;
    cin >> s;
    int n = s.size();

    // left[i] = vị trí xuất hiện trước của ký tự s[i] (tính từ 1)
    // right[i] = vị trí xuất hiện sau
    vector<int> left(n+2, 0), right(n+2, n+1);
    int last[26];
    memset(last, 0, sizeof(last));

    for (int i = 1; i <= n; ++i) {
        int ch = s[i-1] - 'a';
        left[i] = last[ch];
        last[ch] = i;
    }

    for (int i = 0; i < 26; ++i) last[i] = n+1;
    for (int i = n; i >= 1; --i) {
        int ch = s[i-1] - 'a';
        right[i] = last[ch];
        last[ch] = i;
    }

    ll ans = 0;
    for (int i = 1; i <= n; ++i) {
        ans += (ll)(i - left[i]) * (right[i] - i);
    }

    cout << ans;
    return 0;
}

4. Giải thích code tối ưu

Mảng leftright lưu vị trí gần nhất bên trái và bên phải của cùng một ký tự. Với ký tự đầu tiên, left = 0; với ký tự cuối, right = n+1.

Công thức (i - left[i]) * (right[i] - i) cho số xâu con mà ký tự tại i là ký tự xuất hiện duy nhất.

Độ phức tạp: O(n) thời gian, O(n) bộ nhớ.

Thẻ: string substring contribution linear-time blue-bridge-cup

Đăng vào ngày 24 tháng 9 lúc 01:43