Hậu quả tăng tuần tự K dài nhất


8

Tại sao tôi tạo một chủ đề trùng lặp

Tôi đã tạo chủ đề này sau khi đọc phần tiếp theo tăng dài nhất với K ngoại lệ được phép . Tôi nhận ra rằng người hỏi câu hỏi đã không thực sự hiểu vấn đề, bởi vì anh ta đang đề cập đến một liên kết giải quyết vấn đề "mảng con tăng dài nhất với một thay đổi được phép". Vì vậy, câu trả lời anh nhận được thực sự không liên quan đến vấn đề LIS.

Mô tả vấn đề

Giả sử rằng một mảng A được đưa ra với chiều dài N . Tìm chuỗi con tăng dài nhất với K ngoại lệ được phép.

Ví dụ
1) N = 9, K = 1

A = [3,9,4,5,8,6,1,3,7]

Trả lời: 7

Giải trình:

Chuỗi tăng dài nhất là: 3,4,5,8 (hoặc 6), 1 (ngoại lệ), 3,7 -> tổng = 7

2) N = 11, K = 2

A = [5,6,4,7,3,9,2,5,1,8,7]

trả lời: 8

Những gì tôi đã làm cho đến nay ...

Nếu K = 1 thì chỉ cho phép một ngoại lệ. Nếu thuật toán đã biết để tính toán Kết quả tăng dài nhất trong O (NlogN) được sử dụng ( bấm vào đây để xem thuật toán này ), thì chúng ta có thể tính LIS bắt đầu từ A [0] đến A [N-1] cho từng phần tử của mảng A. Chúng tôi lưu kết quả trong một mảng mới L với kích thước N . Nhìn vào ví dụ n.1, mảng L sẽ là: L = [1,2,2,3,4,4,4,4,5].

Sử dụng logic đảo ngược, chúng tôi tính toán mảng R , mỗi phần tử chứa Chuỗi giảm dần dài nhất hiện tại từ N-1 đến 0.

LIS với một ngoại lệ chỉ là sol = max (sol, L [i] + R [i + 1]), trong đó sol được khởi tạo là sol = L [N-1] . Vì vậy, chúng tôi tính LIS từ 0 cho đến khi chỉ số i (ngoại lệ), sau đó dừng và bắt đầu LIS mới cho đến N-1 .

A=[3,9,4,5,8,6,1,3,7]

L=[1,2,2,3,4,4,4,4,5]

R=[5,4,4,3,3,3,3,2,1]

Sol = 7

-> giải thích từng bước:

init: sol = L[N]= 5

i=0 : sol = max(sol,1+4) = 5 
i=1 : sol = max(sol,2+4) = 6
i=2 : sol = max(sol,2+3) = 6
i=3 : sol = max(sol,3+3) = 6
i=4 : sol = max(sol,4+3) = 7
i=4 : sol = max(sol,4+3) = 7
i=4 : sol = max(sol,4+2) = 7
i=5 : sol = max(sol,4+1) = 7

Độ phức tạp: O (NlogN + NlogN + N) = O (NlogN)

bởi vì mảng R, L cần thời gian NlogN để tính toán và chúng ta cũng cần (N) để tìm sol .

Mã cho k = 1 vấn đề

#include <stdio.h>
#include <vector>

std::vector<int> ends;

int index_search(int value, int asc) {
    int l = -1;
    int r = ends.size() - 1;
    while (r - l > 1) { 
        int m = (r + l) / 2; 
        if (asc && ends[m] >= value) 
            r = m; 
        else if (asc && ends[m] < value)
            l = m;
        else if (!asc && ends[m] <= value)
            r = m;
        else
            l = m;
    } 
    return r;
}

int main(void) {
    int n, *S, *A, *B, i, length, idx, max;

    scanf("%d",&n);
    S = new int[n];
    L = new int[n];
    R = new int[n];
    for (i=0; i<n; i++) {
        scanf("%d",&S[i]);
    }

    ends.push_back(S[0]);
    length = 1;
    L[0] = length;
    for (i=1; i<n; i++) {
        if (S[i] < ends[0]) {
            ends[0] = S[i];
        }
        else if (S[i] > ends[length-1]) {
            length++;
            ends.push_back(S[i]);
        }
        else {
            idx = index_search(S[i],1);
            ends[idx] = S[i];
        }
        L[i] = length;
    }

    ends.clear();
    ends.push_back(S[n-1]);
    length = 1;
    R[n-1] = length;
    for (i=n-2; i>=0; i--) {
        if (S[i] > ends[0]) {
            ends[0] = S[i];
        }
        else if (S[i] < ends[length-1]) {
            length++;
            ends.push_back(S[i]);
        }
        else {
            idx = index_search(S[i],0);
            ends[idx] = S[i];
        }
        R[i] = length;
    }

    max = A[n-1];
    for (i=0; i<n-1; i++) {
        max = std::max(max,(L[i]+R[i+1]));
    }

    printf("%d\n",max);
    return 0;
}

Khái quát hóa cho K ngoại lệ

Tôi đã cung cấp một thuật toán cho K = 1. Tôi không biết làm thế nào để thay đổi thuật toán trên để làm việc cho các ngoại lệ K. Tôi sẽ rất vui nếu ai đó có thể giúp tôi.

( PS. Nếu cần tôi có thể cung cấp mã cho thuật toán K = 1 trong C ++.)


Thủ thuật này có thể giúp giải quyết nó trong n log n . Cụ thể, bạn sử dụng thuật toán cây chỉ số nhị phân để tính toán số lượng LIS-x k 'trong đó * k' là các ngoại lệ bạn có và x là một số giá trị tùy ý; vì vậy bạn tìm kiếm nhị phân cho mỗi x và nhận được k ' cho đến khi bạn nhận được k' * = * k. Bạn sẽ cần sự chú ý đặc biệt trong một số trường hợp trong đó tìm kiếm nhị phân không bao giờ có được k bạn muốn mà nhảy từ nhỏ hơn sang lớn hơn.
ilias_pap

@ilias_pap vui lòng nêu chi tiết và xây dựng thuật toán đầy đủ. Hoàn toàn không rõ ràng từ nhận xét của bạn về cách mỗi lần lặp lại của tìm kiếm nhị phân mà bạn đề cập có thể được tiến hành trong O (n).
גלעד ברקן

Xem thêm cs.stackexchange.com/q/118858/755 cho một câu hỏi tương tự.
DW

Câu trả lời:


7

Câu trả lời này được sửa đổi từ câu trả lời của tôi cho một câu hỏi tương tự tại Computer Science Stackexchange.

Vấn đề LIS với hầu hết các ngoại lệ k thừa nhận thuật toán O (n log² n) sử dụng thư giãn Lagrangian. Khi k lớn hơn log n, điều này sẽ cải thiện tiệm cận trên O (nk log n) DP, chúng tôi cũng sẽ giải thích ngắn gọn.

Đặt DP [a] [b] biểu thị độ dài của chuỗi tăng dài nhất với hầu hết các ngoại lệ b (vị trí trong đó số nguyên trước lớn hơn số tiếp theo) kết thúc tại phần tử b a. DP này không liên quan đến thuật toán, nhưng việc xác định nó làm cho việc chứng minh thuật toán dễ dàng hơn.

Để thuận tiện, chúng tôi sẽ giả sử rằng tất cả các phần tử là khác biệt và phần tử cuối cùng trong mảng là tối đa của nó. Lưu ý rằng điều này không giới hạn chúng tôi, vì chúng tôi chỉ có thể thêm m / 2n vào giao diện thứ m của mỗi số và nối vô cực vào mảng và trừ một từ câu trả lời. Đặt V là hoán vị mà 1 <= V [i] <= n là giá trị của phần tử thứ i.

Để giải quyết vấn đề trong O (nk log n), chúng tôi duy trì bất biến rằng DP [a] [b] đã được tính cho b <j. Vòng lặp j từ 0 đến k, tại lần lặp thứ j tính DP [a] [j] cho tất cả a. Để làm điều này, lặp i từ 1 đến n. Chúng tôi duy trì tối đa DP [x] [j-1] trên x <i và cấu trúc dữ liệu tối đa tiền tố mà tại chỉ mục i sẽ có DP [x] [j] tại vị trí V [x] cho x <i và 0 ở mọi vị trí khác.

Chúng tôi có DP [i] [j] = 1 + max (DP [i '] [j], DP [x] [j-1]) nơi chúng tôi đi qua i', x <i, V [i '] < V [i]. Tối đa tiền tố của DP [x] [j-1] cung cấp cho chúng tôi tối đa các thuật ngữ của loại thứ hai và truy vấn cấu trúc dữ liệu tối đa tiền tố cho tiền tố [0, V [i]] cung cấp cho chúng tôi tối đa các thuật ngữ đầu tiên kiểu. Sau đó cập nhật cấu trúc dữ liệu tối đa tiền tố và tối đa tiền tố.

Đây là một triển khai C ++ của thuật toán. Lưu ý rằng việc triển khai này không cho rằng phần tử cuối cùng của mảng là tối đa của nó hoặc mảng không chứa các bản sao.


#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// Fenwick tree for prefix maximum queries
class Fenwick {
    private:
        vector<int> val;
    public:
        Fenwick(int n) : val(n+1, 0) {}

        // Sets value at position i to maximum of its current value and 
        void inc(int i, int v) {
            for (++i; i < val.size(); i += i & -i) val[i] = max(val[i], v);
        }

        // Calculates prefix maximum up to index i
        int get(int i) {
            int res = 0;
            for (++i; i > 0; i -= i & -i) res = max(res, val[i]);
            return res;
        }
};

// Binary searches index of v from sorted vector
int bins(const vector<int>& vec, int v) {
    int low = 0;
    int high = (int)vec.size() - 1;
    while(low != high) {
        int mid = (low + high) / 2;
        if (vec[mid] < v) low = mid + 1;
        else high = mid;
    }
    return low;
}

// Compresses the range of values to [0, m), and returns m
int compress(vector<int>& vec) {
    vector<int> ord = vec;
    sort(ord.begin(), ord.end());
    ord.erase(unique(ord.begin(), ord.end()), ord.end());
    for (int& v : vec) v = bins(ord, v);
    return ord.size();
}

// Returns length of longest strictly increasing subsequence with at most k exceptions
int lisExc(int k, vector<int> vec) {
    int n = vec.size();
    int m = compress(vec);
    vector<int> dp(n, 0);
    for (int j = 0;; ++j) {
        Fenwick fenw(m+1); // longest subsequence with at most j exceptions ending at this value
        int max_exc = 0; // longest subsequence with at most j-1 exceptions ending before this
        for (int i = 0; i < n; ++i) {
            int off = 1 + max(max_exc, fenw.get(vec[i]));
            max_exc = max(max_exc, dp[i]);

            dp[i] = off;
            fenw.inc(vec[i]+1, off);
        }
        if (j == k) return fenw.get(m);
    }
}

int main() {
    int n, k;
    cin >> n >> k;

    vector<int> vec(n);
    for (int i = 0; i < n; ++i) cin >> vec[i];

    int res = lisExc(k, vec);
    cout << res << '\n';
}

Bây giờ chúng ta sẽ trở lại thuật toán O (n log² n). Chọn một số nguyên 0 <= r <= n. Xác định DP '[a] [r] = max (DP [a] [b] - rb), trong đó mức tối đa được lấy qua b, MAXB [a] [r] là b tối đa sao cho DP' [a] [ r] = DP [a] [b] - rb và MINB [a] [r] tương tự như mức tối thiểu như vậy b. Chúng tôi sẽ chỉ ra rằng DP [a] [k] = DP '[a] [r] + rk khi và chỉ khi MINB [a] [r] <= k <= MAXB [a] [r]. Hơn nữa, chúng tôi sẽ chỉ ra rằng với bất kỳ k tồn tại một r mà bất đẳng thức này giữ.

Lưu ý rằng MINB [a] [r]> = MINB [a] [r '] và MAXB [a] [r]> = MAXB [a] [r'] nếu r <r ', do đó nếu chúng tôi giả sử hai yêu cầu kết quả, chúng ta có thể thực hiện tìm kiếm nhị phân cho các giá trị r, thử O (log n). Do đó, chúng tôi đạt được độ phức tạp O (n log² n) nếu chúng tôi có thể tính toán DP ', MINB và MAXB trong thời gian O (n log n).

Để làm điều này, chúng ta sẽ cần một cây phân đoạn lưu trữ bộ dữ liệu P [i] = (v_i, low_i, high_i) và hỗ trợ các hoạt động sau:

  1. Cho một phạm vi [a, b], tìm giá trị tối đa trong phạm vi đó (tối đa v_i, a <= i <= b) và mức thấp tối thiểu và cao tối đa được ghép với giá trị đó trong phạm vi.

  2. Đặt giá trị của bộ dữ liệu P [i]

Điều này dễ thực hiện với thời gian O (log n) phức tạp trên mỗi thao tác giả sử có một số quen thuộc với cây phân đoạn. Bạn có thể tham khảo việc thực hiện thuật toán dưới đây để biết chi tiết.

Bây giờ chúng tôi sẽ trình bày cách tính DP ', MINB và MAXB trong O (n log n). Sửa r. Xây dựng cây phân đoạn ban đầu chứa n + 1 giá trị null (-INF, INF, -INF). Chúng tôi duy trì rằng P [V [j]] = (DP '[j], MINB [j], MAXB [j]) cho j ít hơn vị trí hiện tại i. Đặt DP '[0] = 0, MINB [0] = 0 và MAXB [0] thành 0 nếu r> 0, nếu không là INF và P [0] = (DP' [0], MINB [0], MAXB [ 0]).

Vòng lặp i từ 1 đến n. Có hai loại thứ tự kết thúc tại i: những loại có phần tử trước lớn hơn V [i] và loại có phần tử nhỏ hơn V [i]. Để tính đến loại thứ hai, truy vấn cây phân đoạn trong phạm vi [0, V [i]]. Đặt kết quả là (v_1, low_1, high_1). Đặt off1 = (v_1 + 1, low_1, high_1). Đối với loại đầu tiên, truy vấn cây phân đoạn trong phạm vi [V [i], n]. Đặt kết quả là (v_2, low_2, high_2). Đặt off2 = (v_2 + 1 - r, low_2 + 1, high_2 + 1), trong đó chúng tôi phải chịu hình phạt của r vì đã tạo ngoại lệ.

Sau đó, chúng tôi kết hợp off1 và off2 thành off. Nếu off1.v> off2.v đặt off = off1 và nếu off2.v> off1.v đặt off = off2. Nếu không, đặt off = (off1.v, min (off1.low, off2.low), max (off1.high, off2.high)). Sau đó đặt DP '[i] = off.v, MINB [i] = off.low, MAXB [i] = off.high và P [i] = off.

Vì chúng tôi thực hiện hai truy vấn cây phân đoạn ở mọi i, nên việc này mất tổng thời gian O (n log n). Thật dễ dàng để chứng minh bằng cảm ứng rằng chúng tôi tính toán các giá trị chính xác DP ', MINB và MAXB.

Vì vậy, trong ngắn hạn, thuật toán là:

  1. Tiền xử lý, sửa đổi các giá trị sao cho chúng tạo thành một hoán vị và giá trị cuối cùng là giá trị lớn nhất.

  2. Tìm kiếm nhị phân cho đúng r, với giới hạn ban đầu 0 <= r <= n

  3. Khởi tạo cây phân đoạn với các giá trị null, đặt DP '[0], MINB [0] và MAXB [0].

  4. Lặp lại từ i = 1 đến n, ở bước i

    • Phạm vi truy vấn [0, V [i]] và [V [i], n] của cây phân đoạn,
    • tính toán DP '[i], MINB [i] và MAXB [i] dựa trên các truy vấn đó và
    • đặt giá trị tại vị trí V [i] trong cây phân đoạn thành tuple (DP '[i], MINB [i], MAXB [i]).
  5. Nếu MINB [n] [r] <= k <= MAXB [n] [r], trả về DP '[n] [r] + kr - 1.

  6. Mặt khác, nếu MAXB [n] [r] <k, r đúng sẽ nhỏ hơn r hiện tại. Nếu MINB [n] [r]> k, r đúng sẽ lớn hơn r hiện tại. Cập nhật giới hạn trên r và quay lại bước 1.

Đây là một triển khai C ++ cho thuật toán này. Nó cũng tìm thấy sự kết hợp tối ưu.

    #include <iostream>
    #include <vector>
    #include <algorithm>
    using namespace std;
    using ll = long long;
    const int INF = 2 * (int)1e9;

    pair<ll, pair<int, int>> combine(pair<ll, pair<int, int>> le, pair<ll, pair<int, int>> ri) {
        if (le.first < ri.first) swap(le, ri);
        if (ri.first == le.first) {
            le.second.first = min(le.second.first, ri.second.first);
            le.second.second = max(le.second.second, ri.second.second);
        }
        return le;
    }

    // Specialised range maximum segment tree
    class SegTree {
        private:
            vector<pair<ll, pair<int, int>>> seg;
            int h = 1;

            pair<ll, pair<int, int>> recGet(int a, int b, int i, int le, int ri) const {
                if (ri <= a || b <= le) return {-INF, {INF, -INF}};
                else if (a <= le && ri <= b) return seg[i];
                else return combine(recGet(a, b, 2*i, le, (le+ri)/2), recGet(a, b, 2*i+1, (le+ri)/2, ri));
            }
        public:
            SegTree(int n) {
                while(h < n) h *= 2;
                seg.resize(2*h, {-INF, {INF, -INF}});
            }
            void set(int i, pair<ll, pair<int, int>> off) {
                seg[i+h] = combine(seg[i+h], off);
                for (i += h; i > 1; i /= 2) seg[i/2] = combine(seg[i], seg[i^1]);
            }
            pair<ll, pair<int, int>> get(int a, int b) const {
                return recGet(a, b+1, 1, 0, h);
            }
    };

    // Binary searches index of v from sorted vector
    int bins(const vector<int>& vec, int v) {
        int low = 0;
        int high = (int)vec.size() - 1;
        while(low != high) {
            int mid = (low + high) / 2;
            if (vec[mid] < v) low = mid + 1;
            else high = mid;
        }
        return low;
    }

    // Finds longest strictly increasing subsequence with at most k exceptions in O(n log^2 n)
    vector<int> lisExc(int k, vector<int> vec) {
        // Compress values
        vector<int> ord = vec;
        sort(ord.begin(), ord.end());
        ord.erase(unique(ord.begin(), ord.end()), ord.end());
        for (auto& v : vec) v = bins(ord, v) + 1;

        // Binary search lambda
        int n = vec.size();
        int m = ord.size() + 1;
        int lambda_0 = 0;
        int lambda_1 = n;
        while(true) {
            int lambda = (lambda_0 + lambda_1) / 2;
            SegTree seg(m);
            if (lambda > 0) seg.set(0, {0, {0, 0}});
            else seg.set(0, {0, {0, INF}});

            // Calculate DP
            vector<pair<ll, pair<int, int>>> dp(n);
            for (int i = 0; i < n; ++i) {
                auto off0 = seg.get(0, vec[i]-1); // previous < this
                off0.first += 1;

                auto off1 = seg.get(vec[i], m-1); // previous >= this
                off1.first += 1 - lambda;
                off1.second.first += 1;
                off1.second.second += 1;

                dp[i] = combine(off0, off1);
                seg.set(vec[i], dp[i]);
            }

            // Is min_b <= k <= max_b?
            auto off = seg.get(0, m-1);
            if (off.second.second < k) {
                lambda_1 = lambda - 1;
            } else if (off.second.first > k) {
                lambda_0 = lambda + 1;
            } else {
                // Construct solution
                ll r = off.first + 1;
                int v = m;
                int b = k;
                vector<int> res;
                for (int i = n-1; i >= 0; --i) {
                    if (vec[i] < v) {
                        if (r == dp[i].first + 1 && dp[i].second.first <= b && b <= dp[i].second.second) {
                            res.push_back(i);
                            r -= 1;
                            v = vec[i];
                        }
                    } else {
                        if (r == dp[i].first + 1 - lambda && dp[i].second.first <= b-1 && b-1 <= dp[i].second.second) {
                            res.push_back(i);
                            r -= 1 - lambda;
                            v = vec[i];
                            --b;
                        }
                    }
                }
                reverse(res.begin(), res.end());
                return res;
            }
        }
    }

    int main() {
        int n, k;
        cin >> n >> k;

        vector<int> vec(n);
        for (int i = 0; i < n; ++i) cin >> vec[i];

        vector<int> ans = lisExc(k, vec);
        for (auto i : ans) cout << i+1 << ' ';
        cout << '\n';
    }

Bây giờ chúng tôi sẽ chứng minh hai tuyên bố. Chúng tôi muốn chứng minh rằng

  1. DP '[a] [r] = DP [a] [b] - rb khi và chỉ khi MINB [a] [r] <= b <= MAXB [a] [r]

  2. Với tất cả a, k tồn tại một số nguyên r, 0 <= r <= n, sao cho MINB [a] [r] <= k <= MAXB [a] [r]

Cả hai đều xuất phát từ sự đồng tình của vấn đề. Độ đồng nhất có nghĩa là DP [a] [k + 2] - DP [a] [k + 1] <= DP [a] [k + 1] - DP [a] [k] với mọi a, k. Điều này là trực quan: chúng ta càng được phép ngoại lệ, càng ít cho phép một người nữa giúp chúng ta nhiều hơn.

Sửa a và r. Đặt f (b) = DP [a] [b] - rb và d (b) = f (b + 1) - f (b). Chúng ta có d (k + 1) <= d (k) từ tính đồng nhất của vấn đề. Giả sử x <y và f (x) = f (y)> = f (i) cho tất cả i. Do đó d (x) <= 0, do đó d (i) <= 0 cho i trong [x, y). Nhưng f (y) = f (x) + d (x) + d (x + 1) + ... + d (y - 1), do đó d (i) = 0 cho i trong [x, y). Do đó f (y) = f (x) = f (i) cho i trong [x, y]. Điều này chứng minh cho tuyên bố đầu tiên.

Để chứng minh lần thứ hai, đặt r = DP [a] [k + 1] - DP [a] [k] và xác định f, d như trước đây. Khi đó d (k) = 0, do đó d (i)> = 0 đối với i <k và d (i) <= 0 đối với i> k, do đó f (k) là tối đa như mong muốn.

Chứng minh sự đồng tình là khó khăn hơn. Để biết bằng chứng, hãy xem câu trả lời của tôi tại cs.stackexchange.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.