1. 程式人生 > >JXOI 2017 簡要題解

JXOI 2017 簡要題解

題意

九條可憐手上有一個長度為 \(n\) 的整數數列 \(r_i\) ,她現在想要構造一個長度為 \(n\) 的,滿足如下條件的整數數列 \(A\) :

  • \(1\leq A_i \leq r_i\)
  • 對於任意 \(3 \leq i \leq n\) ,令 \(R\)\(A_1\)\(A_{i-2}\) 中大於等於 \(A_{i-1}\) 的最小值, \(L\)\(A_1\)\(A_{i-2}\) 中小於等於 \(A_{i-1}\) 的最大值。\(A_i\) 必須滿足 \(L \leq A_i \leq R\)。如果不存在大於等於 \(A_{i-1}\) 的,那 麼 \(R = +\infty\)
    ;如果不存在小於等於 \(A_{i-1}\) 的,那麼 \(L = −\infty\)

現在可憐想要知道共有多少不同的數列滿足這個條件。兩個數列 \(A\)\(B\) 是不同的當且僅當至少存在一個位置 \(i\) 滿足 \(A_i \neq B_i\)

\(n \le 50, A_i \le 150\)

題解

首先不難發現 \(L_i\) 是單調不降,\(R_i\) 是單調不升的,也就是說 \([L_i, R_i]\) 是不斷收縮的。

然後發現 \(A_i\) 會在一定會充當 \(L_{i + 1}\)\(R_{i + 1}\) ,注意 \(A_i \in \{L_i, R_i\}\)

的時候,會同時充當 \(L_{i + 1}, R_{i + 1}\)

不難得出一個 \(dp\)\(dp[i][l][r]\) 為到第 \(i\) 個點,下界為 \(l\) 上界為 \(r\) 的方案數。

至於邊界,一開始暴力列舉前面兩個數取值就行了,討論三種情況就不贅述了。

然後轉移的話我們對於 \([l, r]\) 這段區間只需要列舉 \([l, r]\) 之中的數來轉移即可。

具體來說我們假設把 \([l, r]\) 這段區間分成 \([l, p]\)\([p, r]\) 這兩種不同的區間,直接更新 \(dp[i + 1][l][p]\)\(dp[i + 1][p][r]\)

就行了,但是會發現 \(p\) 會被更新兩次,那麼在 \(dp[i + 1][p][p]\) 減去即可。

然後注意當 \(p\) 取到 \(l~or~r\) 的時候,轉移的就是 \(dp[i + 1][p][p]\) 了。

最後複雜度就是 \(O(n \max^3(A_i))\) ,常數很小可以跑過。

好像看到了孔爺是 \(O(n \max^2(A_i))\)做法 ,恐怖如斯。。。

其實似乎是把一系列相同轉移的轉移到一起,用填表法轉移就行了。

程式碼

懶得特判 \(n=1,2\) 的情況了,反正資料中沒有。。

#include <bits/stdc++.h>

#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)

using namespace std;

template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; }

inline int read() {
    int x(0), sgn(1); char ch(getchar());
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
    for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
    return x * sgn;
}

void File() {
#ifdef zjp_shadow
    freopen ("2273.in", "r", stdin);
    freopen ("2273.out", "w", stdout);
#endif
}

const int N = 55, M = 155, Mod = 998244353;

int n, R[N], dp[N][M][M], maxr;

inline void Update(int pos, int l, int r, int mid, int val) {
    (dp[pos][l][mid] += val) %= Mod;
    (dp[pos][mid][r] += val) %= Mod;
    (dp[pos][mid][mid] += Mod - val) %= Mod;
}

int main () {

    File();

    n = read(); For (i, 1, n) R[i] = read();

    maxr = *max_element(R + 1, R + n + 1) + 1;

    For (i, 1, R[1]) For (j, 1, R[2]) {
        if (i == j) Update(3, i, j, i, 1);
        if (i > j) Update(3, 0, i, j, 1);
        if (i < j) Update(3, i, maxr, j, 1);
    }

    For (i, 3, n - 1) For (l, 0, maxr) For (r, l, maxr) if (dp[i][l][r]) 
        For (cur, l, min(r, R[i]))
            if (cur == l || cur == r) Update(i + 1, cur, cur, cur, dp[i][l][r]);
            else Update(i + 1, l, r, cur, dp[i][l][r]);

    int ans = 0;
    For (l, 0, maxr) For (r, 0, maxr)
        ans = (ans + 1ll * dp[n][l][r] * max(0, (min(r, R[n]) - max(l, 1) + 1))) % Mod;
    printf ("%d\n", ans);

    return 0;

}

題意

有一個長度為 \(n\) 的正整數序列 \(A\)

一共有 \(m\) 個區間 \([l_i, r_i]\) 和兩個正整數 \(a, k\) 。從這 \(m\) 個區間裡選出恰好 \(k\) 個區間,並對每個區間執行一次區間加 \(a\) 的操作。(每個區間最多隻能選擇一次。)

最後最大化 \(\min A_i\)

\(n, m \le 2 \times 10^5\)

題解

最大化最小值,基本二分答案跑不掉了。

二分 \(\min A_i\) ,也就是使得 \(\forall A_i \ge mid\)

然後考慮如何 \(check\) 。可以貪心,考慮把 \(m\) 個區間放到對應的左端點處。

從左往右依次考慮每個點,貪心選擇左端點在之前,右端點儘量遠的區間,這樣肯定最優。

然後不斷加 \(a\) 使得當且 \(A_i \ge mid\) 即可,如果區間不夠那麼就不可行。

至於具體實現,用堆儲存右端點最靠右的點,區間加可以直接差分掉就行了。

複雜度是 \(O((n + m) \log m \log A_i)\) 的,常數不大,跑得很快。

程式碼

#include <bits/stdc++.h>
 
#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)
 
using namespace std;
 
template<typename T> inline bool chkmin(T &a, T b) {return b < a ? a = b, 1 : 0;}
template<typename T> inline bool chkmax(T &a, T b) {return b > a ? a = b, 1 : 0;}
 
inline int read() {
    int x(0), sgn(1); char ch(getchar());
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
    for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
    return x * sgn;
}
 
inline void File() {
#ifdef zjp_shadow
    freopen ("2274.in", "r", stdin);
    freopen ("2274.out", "w", stdout);
#endif
}
 
const int N = 2e5 + 1e3, inf = 0x7f7f7f7f;
 
int n, m, k, a, val[N], tag[N];
 
vector<int> seg[N];
 
priority_queue<int> P;
 
inline bool check(int limit) {
    int cnt = 0, add = 0, now, here;
    while (!P.empty()) P.pop();
    For (i, 1, n) tag[i] = 0;
    For (i, 1, n) {
        add += tag[i];
        here = val[i] + add;
        for(int j : seg[i]) P.push(j);
             
        while (here < limit) {
            if (++ cnt > k || !(bool)P.size()) return false;
            now = P.top(); P.pop();
            if (now < i) return false;
            tag[now + 1] -= a; here += a; add += a;
        }
    }
    return true;
}
 
int main() {
 
    File();
 
    int cases = read();
    while (cases--) {
        int minv = inf;
        n = read(); m = read(); k = read(); a = read();
        For (i, 1, n) 
            chkmin (minv, val[i] = read());         
         
        For (i, 1, m) {
            int pos = read();
            seg[pos].push_back(read());
        }
         
        int l = 1, r = minv + a * k, ans = 0;
        while (l <= r) {
            int mid = (l + r) >> 1;
            if (check(mid)) l = mid + 1, ans = mid;
            else r = mid - 1;
        }
        printf ("%d\n", ans);
        For (i, 1, n) seg[i].clear();
    }
 
    return 0;
 
}

題意

有一個長度為 \(n\) 的顏色序列 \(A_i\) ,選擇一些顏色把這些顏色的所有位置都刪去。

刪除顏色 \(i\) 可以定義為把所有滿足 \(A_j = i\) 的位置 \(j\) 都從序列中刪去。

想要知道有多少種刪去顏色的方案使得最後剩下來的序列非空且連續。

\(n \le 3 \times 10^5\)

題解

認真讀題,注意是 一起 刪除。

考慮刪除方案顯然不太好算的,可以考慮最後剩下的序列。

我們記 \(\min_i\)\(i\) 這種顏色最早出現的位置,\(\max_i\)\(i\) 這種顏色最晚出現的位置。

列舉最後剩下序列的右端點 \(r\) ,我們只需要查詢左端點 \(l\) 在哪裡合法。

  1. 對於一種顏色 \(k\) ,如果存在 \(\max_k > r\) ,那麼這種顏色 \(k\) 不能存在於 \([l, r]\) 之中。我們只需要找出 \(\max_{j < r} j\) 滿足 \(\max_{A_j} > r\) ,那麼 \(l \in (j, r]\)
  2. 對於一種顏色 \(k\) ,如果存在 \(\max_k \le r\) ,那麼 \(l \notin (\min_k,\max_k]\)

不難發現只要滿足這兩個限制,外面的顏色可以一起刪完且不會影響到中間這段區間的點。

第一個保證右端點向右合法,第二個保證左端點向左合法。

如何維護呢?

  1. 對於第一個求 \(j\) 直接維護一個 \(i\) 遞增 \(\max_{A_i}\) 遞減的單調棧就行了。
  2. 第二個直接到 \(max_{A_i}\) 的時候,線上段樹上打一個 $ (\min_k,\max_k]$ 區間不可行的標記就行。

然後每次就直接線段樹上查詢可行節點個數就能輕鬆做完了。

程式碼

#include <bits/stdc++.h>
#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
using namespace std;

inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}

inline int read() {
    int x = 0, fh = 1; char ch = getchar();
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1;
    for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
    return x * fh;
}

void File() {
#ifdef zjp_shadow
    freopen ("2275.in", "r", stdin);
    freopen ("2275.out", "w", stdout);
#endif
}

const int N = 3e5 + 1e3;
struct Stack {
    int pos[N], val[N], top;

    void Clear() { top = 0; }

    inline void Push(int p, int v) { pos[++ top] = p; val[top] = v; }

    inline int Max_Pos() { return pos[top]; } 

    inline void Pop(int p) { while (top && val[top] <= p) -- top; }
} S;

#define lson o << 1, l, mid
#define rson o << 1 | 1, mid + 1, r
struct Segment_Tree {
    int sumv[N << 2]; bitset<(N << 2)> Tag;

    Segment_Tree () { Tag.reset(); }

    inline void push_down(int o) {
        if (!Tag[o]) return ; sumv[o << 1] = sumv[o << 1 | 1] = 0; Tag[o << 1] = Tag[o << 1 | 1] = true; Tag[o] = false;
    }

    inline void push_up(int o) { sumv[o] = sumv[o << 1] + sumv[o << 1 | 1]; }

    void Build(int o, int l, int r) {
        Tag[o] = false; sumv[o] = r - l + 1; if (l == r) return ;
        int mid = (l + r) >> 1; Build(lson); Build(rson);
    }

    void Update(int o, int l, int r, int ul, int ur) {
        if (!sumv[o] || ul > ur) return ; 
        if (ul <= l && r <= ur) { sumv[o] = 0; Tag[o] = true; return ; }
        push_down(o); int mid = (l + r) >> 1; 
        if (ul <= mid) Update(lson, ul, ur); if (ur > mid) Update(rson, ul, ur); push_up(o);
    }

    int Query(int o, int l, int r, int ql, int qr) {
        if (!sumv[o] || ql > qr) return 0; 
        if (ql <= l && r <= qr) return sumv[o];
        int res = 0, mid = (l + r) >> 1; push_down(o);
        if (ql <= mid) res += Query(lson, ql, qr); if (qr > mid) res += Query(rson, ql, qr); return res; push_up(o);
    }
} T;

const int inf = 0x7f7f7f7f;
int n, Col[N], Min[N], Max[N];

int main () {
    File();
    int cases = read();
    while (cases --) {
        n = read(); 
        For (i, 1, n) Col[i] = read(), Min[Col[i]] = inf, Max[Col[i]] = -inf;
        For (i, 1, n) chkmax(Max[Col[i]], i), chkmin(Min[Col[i]], i);

        T.Build(1, 1, n); S.Clear();
        long long ans = 0;
        For (i, 1, n) {
            S.Push(i, Max[Col[i]]); S.Pop(i); int Pos = S.Max_Pos();
            if (i == Max[Col[i]]) T.Update(1, 1, n, Min[Col[i]] + 1, Max[Col[i]]);
            ans += T.Query(1, 1, n, Pos + 1, i);
        }
        printf ("%lld\n", ans);
    }
    return 0;
}

總結

總的來說,這套吉老師出的題水平很高,做起來十分的舒服。

據說現場有大樣例,但是沒人看到。。。

如果給 \(5h\) 就很舒服,因為三題都需要對拍比較好。。。但是據說現場只有 \(3.5h\) 喵喵喵?

第一題考察了比較基礎的計數 \(dp\) 和對性質的觀察。

第二題考察了二分答案的基本應用然後轉化為貪心選擇區間覆蓋的經典問題,用堆維護即可。

第三題依舊考察了對於性質的觀察以及用資料結構維護可行節點的經典操作。

整體來說,是套好題。