1. 程式人生 > 實用技巧 >Monotonicity 2(資料加強版)

Monotonicity 2(資料加強版)

C. Monotonicity 2(資料加強版)

題目描述

  • 對於一個整數序列\(a_1,a_2,...,a_n\),我們定義其“單調序列"為一個由 <,> 和 = 組成的符號序列 \(s_1,s_1,...s_{n-1}\),其中符號 \(s_i\) 表示 \(a_i\)\(a_{i+1}\) 之間的關係。例如,數列 2,4,3,3,5,3 的單調序列為 <,>,=,<,> 。
  • 對於整數序列 \(b_1,b_2,...,b_{n+1}\) 以及其單調序列 \(s_1,s_2,..s_n\),如果符號序列 \(s'_1,s'_2,...s'_k\)
    滿足對所有 \(1\le i\le n\)\(s_i=s'_{((i-1)\mod k)+1}\),我們就說序列 \(s_1,s_2,...,s_n\)「實現」了序列 \(s'_1,s'_2,...,s'_k\)。也就是說,序列 \(s_1,s_1,...s_n\) 可以通過重複多次 \(s'_1,s'_2,...,s'_k\) 序列並刪除一個字尾得到。例如,整數數列 2,4,3,3,5,3 至少實現了以下符號序列:
    • <,>,=
    • <,>,=,<,>
    • <,>,=,<,>,<,<,=
    • <,>,=,<,>,=,>,>
  • 給定一個整數序列 \(a_1,a_2,...,a_n\) 以及一個單調序列 \(s_1,s_2,..s_k\),求出原整數序列最長的子序列\(a_{i_1},a_{i_2},...,a_{i_m}(1\le i_1<i_2<...<i_m\le n)\) 使得前者的單調序列實現後者的符號序列。

輸入格式

  • 第一行包含用空格分隔的兩個整數 n,k,分別表示整數序列 \(a_i\) 的長度和單調序列 \(s_j\) 的長度。
  • 第二行包含用空格分隔的 n 個整數,表示序列 \(a_i\).
  • 第三行包含用空格分隔的 k 個符號,表示符號序列 \(s_j\).

輸出格式

  • 第一行輸出一個整數 m (保證答案不小於 2),表示序列 \(a_1,a_2,...,a_n\)
    的最長的「實現」了單調序列 \(s_1,s_2,..s_n\) 的子序列。
  • 第二行輸出任意一個這樣的子序列\(a_{i_1},a_{i_2},...,a_{i_m}\),元素之間用空格分隔。

樣例輸入

7 3
2 4 3 1 3 5 3
< > =

樣例輸出

6
2 4 3 3 5 3

資料範圍與提示

  • 對於 100% 的資料\(1\le 5\times 10^5,1\le k\le 100,1\le a_i \le 10^6,s_j\in \{<,>,=\}\)

Solve

  • 可以用樹狀陣列完成的題,為什麼要用線段樹呢?

  • 先簡化一下題意:

    • 就是將 s 序列複製幾次展開,讓 a 的子序列的符號是 s 序列的字首。
    • 就像 <,>,= 可以寫成 <,>,=,<,>,=,<,>,=,... a 序列的一個子序列 2,4,3,3,5,3 的符號序列 <,>,=,<,> 就是上面展開的那個序列的字首,所以合法。
  • 根據題目可以想到\(O(n^2)\)的寫法:定義\(f_i\)為以\(a_i\)為結尾的最長合法序列。(有些類似最長上升子序列)

  • 第一維列舉狀態 i,第二維選取決策 j,就是在 1 到 i-1 中選取一個 j 使得\(a_i\)可以接在 \(a_j\) 後面且 \(f_j\) 最大。(在類比一下最長上升子序列)

  • 這裡考慮\(a_i\)可以接在 \(a_j\) 後面條件,因為\(f_j\)是最長長度,這樣它後面的符號其實就確定了,就是\(s[f_i]\)(預處理是先把 s 序列按我描述的題意展開),是如果要接到\(a_j\)的後面,必須要滿足\(a_j s[f_i]a_i,s[f_i]\in \{<,>,=\}\)

  • \(n^2\)的解法是跑不了n的範圍是\(5\times 10^5\)的資料的,考慮優化。

  • 關於DP的優化,什麼單調佇列優化,斜率優化其實都是在選決策 j 的時侯進行優化,這裡的決策也可以進行優化。

  • 每次都只有三種情況,而且是選取的最大值,其實決策 j 的取值也有三種:

    1. \(a_j<a_i\)中 f 值最大的 j
    2. \(a_j>a_i\)中 f 值最大的 j
    3. \(a_j=a_i\)中 f 值最大的 j
  • 都是取最大值而且小於號情況是字首最大值,大於號情況是字尾最大值,這兩個用開在 0-1e6 樹狀陣列維護,等於號的其實開個陣列就夠了,因為它相當與單點操作,不需要樹狀陣列或線段樹維護

  • 關於樹狀陣列如何維護字尾最大值,我們想,維護字首最大值的時候是向後更新,向前查詢,那維護字尾最大值就可以向前更新,向後查詢,其實是一個道理的。

  • 我的程式碼也不是很長,還有不理解的地方可以看一看程式碼

Code

#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 5e5 + 5, M = 1e6 + 5;
char c[N];
int n, k, a[N], f[N], t1[M], t2[M], t[M], p[N], ans, last;
int low(int x) {//lowbit函式
    return x & -x;
}
void Change1(int x) {字首最大值向後更新
    for (int i = a[x]; i <= 1e6; i += low(i))
        if (f[x] > f[t1[i]]) t1[i] = x;
}
int Ask1(int x) {字首最大值向前查詢
    int b = 0;
    for (int i = a[x] - 1; i; i -= low(i))
        if (f[b] < f[t1[i]]) b = t1[i];
    return b;
}
void Change2(int x) {字尾最大值向前更新
    for (int i = a[x]; i; i -= low(i))
        if (f[x] > f[t2[i]]) t2[i] = x;
}
int Ask2(int x) {字尾最大值向後查詢
    int b = 0;
    for (int i = a[x] + 1; i <= 1e6; i += low(i))
        if (f[b] < f[t2[i]]) b = t2[i];
    return b;
}
void Print(int x) {//遞迴輸出方案
    if (!x) return;
    Print(p[x]);
    printf("%d ", a[x]);
}
int main() {
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; ++i)
         scanf("%d", &a[i]), f[i] = 1;//f初始化為1
    for (int i = 1; i <= k; ++i)
        scanf(" %c", &c[i]);
    for (int i = k + 1; i < n; ++i)
        c[i] = c[(i-1)%k+1];//展開
    for (int i = 1, j; i <= n; ++i) {
        if (f[i] < f[j=Ask1(i)] + 1)//查詢小於號
            f[i] = f[j] + 1, p[i] = j;
        if (f[i] < f[j=Ask2(i)] + 1)//查詢大於號
            f[i] = f[j] + 1, p[i] = j;
        if (f[i] < f[j=t[a[i]]] + 1)//查詢等於號
            f[i] = f[j] + 1, p[i] = j;
        if (ans < f[i]) ans = f[i], last = i;//更新答案
        if (c[f[i]] == '<') Change1(i);//更新小於號
        if (c[f[i]] == '>') Change2(i);//更新大於號
        if (c[f[i]] == '=' && f[i] > f[t[a[i]]]) //更新等於號
            t[a[i]] = i;
    }
    printf("%d\n", ans);
    Print(last);//輸出方案
    return 0;
}