CEOI2016 部分题解

今天好摸啊

CEOI2016 match

我们称一个字符串合法当且仅当其能够对应一个合法的括号序,称一个区间 \([l,r]\) 合法当且仅当 \(s_{[l,r]}\) 合法。

显然 \([1,n]\) 必须合法,否则一定无解。

考虑一个简单的事实,对于一个字符串 \(s=s_1s_2\cdots s_n\) 和它的前缀 \(s_1s_2\cdots s_k\),如果他们都合法,那么 \(s_{k+1}s_{k+2}\cdots s_n\) 也一定合法。对于原题,我们有一个简单的想法:从左往右依次考虑每个左括号,将其对应的右括号尽可能的往右移,这样中间空出来的部分能够多放左括号。

事实上这个贪心的策略是正确的。假设当前字符串 \(s=s_1s_2\cdots s_n\) 合法,当前 \(s_1\) 匹配了 \(s_i\) 且可以匹配 \(s_j(1 < i < j)\),当前括号序列为 \((t_2t_3 \cdots t_{i-1})t_{i+1} \cdots t_j \cdots t_n\)。那么容易知道 \([1,i]\)\([1,j]\)\((i,n)\) 均合法,因此 \((i,j]\)\((1,i)\)\((1,j)\) 也均合法,可以进一步得到 \((j+1,n]\)\([i,j)\) 也均合法。假设这两段分别对应 \(t_1'\)\(t_2'\),那么将 \(s_1\) 匹配 \(s_j\) 得到 \((t_2t_3 \cdots t_{i-1}t_2')t_1'\),其中第 \(i\) 个字符为 \((\),更优。由此可知贪心策略正确。

此时我们就得到了一个 \(O(n^2)\) 暴力:找到 \(1\) 最大的合法匹配位置 \(p\),然后递归到 \((1,p)\)\((p,n)\) 两个子问题。

考虑如何优化这个暴力,我们希望对于某个合法区间 \([l,r]\) 快速找到 \(p\) 的位置。由于 \((l,p)\) 合法且 \(a_l = a_p\),那么 \([l,p]\) 合法,由此可知 \((p,r]\) 也合法。显然这也是充分的,也就是说,如果 \([l,r]\) 合法,那么对于固定的 \(r\)\(p\) 的位置只与 \(a_l\) 的值有关。

此时状态数只有 \(O(n \sum)\) 个了,考虑 DP,设 \(f_{r,i}\) 表示 \(a_l = i\) 时的答案,容易得到转移:\(f_{r,i} \gets f_{f_{r-1,a_r} - 1,i}\),边界为 \(f_{r,a_r} = r\)。预处理 \(f\) 数组之后我们就可以用个栈 \(O(n)\) 求出答案了,总时间复杂度为 \(O(n \sum)\)

Code
/*
最黯淡的一个 梦最为炽热
万千孤单焰火 让这虚构灵魂鲜活
至少在这一刻 热爱不问为何
存在为将心声响彻
*/
#include <bits/stdc++.h>
#define pii pair<int, int>
#define mp(x, y) make_pair(x, y)
#define pb push_back
#define eb emplace_back
#define fi first
#define se second
#define int long long
#define mem(x, v) memset(x, v, sizeof(x))
#define mcpy(x, y, n) memcpy(x, y, sizeof(int) * (n))
#define lob lower_bound
#define upb upper_bound
using namespace std;

inline int read() {
	int x = 0, w = 1;char ch = getchar();
	while (ch > '9' || ch < '0') { if (ch == '-')w = -1;ch = getchar(); }
	while (ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar();
	return x * w;
}

const int MN = 2e5 + 5;
const int MM = 26;
const int Mod = 1e9 + 7;
const int inf = 1e9;

inline int min(int x, int y) { return x < y ? x : y; }
inline int max(int x, int y) { return x > y ? x : y; }

inline int qPow(int a, int b = Mod - 2, int ret = 1) {
    while (b) {
        if (b & 1) ret = ret * a % Mod;
        a = a * a % Mod, b >>= 1;
    }
    return ret;
}

#define dbg

int N, stk[MN], tp, f[MN][26], a[MN]; char s[MN];

signed main(void) {
    scanf("%s", s + 1);
    N = strlen(s + 1);
    for (int i = 1; i <= N; i++) a[i] = s[i] - 'a';
    for (int i = 1; i <= N; i++) {
        for (int j = 0; j < 26; j++) 
            if (f[i - 1][a[i]]) f[i][j] = f[f[i - 1][a[i]] - 1][j];
        if (tp && stk[tp] == a[i]) tp--;
        else stk[++tp] = a[i];
        f[i][a[i]] = i;
    }
    if (tp) return puts("-1"), 0;
    stk[++tp] = N + 1;
    for (int i = 1; i <= N; i++) {
        if (i == stk[tp]) putchar(')'), tp--;
        else putchar('('), stk[tp + 1] = f[stk[tp] - 1][a[i]], tp++;
    }
    return 0;
}

CEOI2016 kangaroo

连续段 DP。设 \(f_{i,j}\) 表示已经插入了 \(i\) 个数,形成了 \(j\) 个连续段,并且每一段都符合条件的方案数。转移:

  • \(i \neq s,t\) 时:\(f_{i,j} \gets (j - [j > s] - [j > t]) \times f_{i-1,j-1} + j \times f_{i-1,j+1}\)

  • 否则:\(f_{i,j} \gets f_{i-1,j-1} + f_{i-1,j}\)

对以上两种转移的解释:当插入的数不是开头或结尾时,插入的数可以插入原来两段之间把两段连起来,也可以单独成段。当插入的数比开头大的时候不能插入到最左边(因为此时开头一定已经插入过了),结尾同理。当插入的数是开头或结尾时,插入的数只能放在两边,可以单独成段,也可以与已有的段合并。

容易证明所有合法序列都能够通过上述操作得到,而上述 DP 的合法性显然。时间复杂度 \(O(n^2)\)

Code
/*
最黯淡的一个 梦最为炽热
万千孤单焰火 让这虚构灵魂鲜活
至少在这一刻 热爱不问为何
存在为将心声响彻
*/
#include <bits/stdc++.h>
#define pii pair<int, int>
#define mp(x, y) make_pair(x, y)
#define pb push_back
#define eb emplace_back
#define fi first
#define se second
#define int long long
#define mem(x, v) memset(x, v, sizeof(x))
#define mcpy(x, y, n) memcpy(x, y, sizeof(int) * (n))
#define lob lower_bound
#define upb upper_bound
using namespace std;

inline int read() {
	int x = 0, w = 1;char ch = getchar();
	while (ch > '9' || ch < '0') { if (ch == '-')w = -1;ch = getchar(); }
	while (ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar();
	return x * w;
}

const int MN = 2e3 + 5;
const int Mod = 1e9 + 7;
const int inf = 1e9;

inline int min(int x, int y) { return x < y ? x : y; }
inline int max(int x, int y) { return x > y ? x : y; }

inline int qPow(int a, int b = Mod - 2, int ret = 1) {
    while (b) {
        if (b & 1) ret = ret * a % Mod;
        a = a * a % Mod, b >>= 1;
    }
    return ret;
}

#define dbg

int N, S, T, f[MN][MN];

signed main(void) {
    N = read(), S = read(), T = read();
    f[1][1] = 1;
    for (int i = 2; i <= N; i++) {
        for (int j = 1; j <= i; j++) {
            if (i != S && i != T) f[i][j] = ((j - (i > S) - (i > T)) * f[i - 1][j - 1] + j * f[i - 1][j + 1]) % Mod;
            else f[i][j] = (f[i - 1][j] + f[i - 1][j - 1]) % Mod;
        }
    }
    printf("%lld\n", f[N][1]);
    return 0;
}

CEOI2016 popeala

暴力 DP 谁都会,这里不讲了。

\(s\)\(a\) 的前缀和,\(c(l,r)\) 表示能通过 \([l,r]\) 测试点的人数,设 \(f_{i,j}\) 为前 \(i\) 个测试点分成 \(j\) 个区间的最小得分总和,转移可以写成:

\[f_{i,j} = \min_{0 \leq k < i} \{ f_{k,j-1} + c(k+1,i) \times (s_i - s_k)\} \]

注意到 \(n \leq 50\),所以 \(c\) 的范围其实是很小的。我们将转移按照 \(c\) 的值分类,此时在每一类中 \(c\) 都是一个常数。稍作变形可以得到:

\[f_{i,j} = c \times a_i + \min_{0 \leq k < i} \{ f_{k,j-1} - c \times a_k \} \]

重要的观察是,当常数 \(c\) 固定时,满足 \(c(k+1,i) = c\)\(k\) 一定是一个区间,并且随着 \(i\) 增大,这些区间的左右端点都单调不降。于是我们现在要做的事情是对一些左右端点单调不降的区间取区间最小值,可以用单调队列维护。

具体来说可以对每个 \(i\) 和每个 \(c\) 处理出对应的 \(k\) 的区间,按 \(j\) 从小到大一层层转移(\(j\) 这维可以滚动数组)。在每一层中按 \(c\) 分类,每一类用单调队列可以做到 \(O(m)\) 转移,总时间复杂度为 \(O(nmS)\)。细节详见代码。

Code
/*
最黯淡的一个 梦最为炽热
万千孤单焰火 让这虚构灵魂鲜活
至少在这一刻 热爱不问为何
存在为将心声响彻
*/
#include <bits/stdc++.h>
#define pii pair<int, int>
#define mp(x, y) make_pair(x, y)
#define pb push_back
#define eb emplace_back
#define fi first
#define se second
// #define int long long
#define mem(x, v) memset(x, v, sizeof(x))
#define mcpy(x, y) memcpy(x, y, sizeof(y))
#define lob lower_bound
#define upb upper_bound
using namespace std;

inline int read() {
	int x = 0, w = 1;char ch = getchar();
	while (ch > '9' || ch < '0') { if (ch == '-')w = -1;ch = getchar(); }
	while (ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar();
	return x * w;
}

const int MN = 55;
const int MM = 2e5 + 5;
const int Mod = 1e9 + 7;
const int inf = 1e9;

inline int min(int x, int y) { return x < y ? x : y; }
inline int max(int x, int y) { return x > y ? x : y; }

inline int qPow(int a, int b = Mod - 2, int ret = 1) {
    while (b) {
        if (b & 1) ret = ret * a % Mod;
        a = a * a % Mod, b >>= 1;
    }
    return ret;
}

#define dbg

int N, M, K, a[MM], f[MM], g[MM], L[MN][MM], R[MN][MM], q[MM], w[MM], pre[MN];
char s[MN][MM];

inline void upd(int x, int y) {
    for (int i = 1; i <= N; i++) if (q[i] == x) {
        for (int j = i; j < N; j++) q[j] = q[j + 1];
        break;
    }
    q[N] = y;
}

signed main(void) {
    N = read(), M = read(), K = read();
    for (int i = 1; i <= M; i++) a[i] = a[i - 1] + read();
    for (int i = 1; i <= N; i++) scanf("%s", s[i] + 1);
    for (int j = 1; j <= M; j++) {
        for (int i = 1; i <= N; i++) 
            if (s[i][j] == '0') upd(pre[i], j), pre[i] = j;
        L[N][j] = q[N] + 1, R[N][j] = j;
        for (int i = 1; i < N; i++) L[i][j] = q[i] + 1, R[i][j] = q[i + 1];
        L[0][j] = 1, R[0][j] = q[1];
    }
    for (int i = 1; i <= M; i++) f[i] = inf;
    for (int k = 1; k <= K; k++) {
        for (int i = 0; i <= M; i++) g[i] = inf;
        for (int i = 0; i <= N; i++) {
            int hd = 1, tl = 0, p = -1;
            for (int j = 1; j <= M; j++) {
                while (p < R[i][j] - 1) {
                    p++;
                    int cur = f[p] - i * a[p];
                    while (hd <= tl && cur <= w[tl]) tl--;
                    q[++tl] = p, w[tl] = cur;
                }
                while (hd <= tl && q[hd] < L[i][j] - 1) hd++;
                if (hd <= tl) g[j] = min(g[j], w[hd] + i * a[j]);
            }
        }
        mcpy(f, g);
        printf("%lld\n", f[M]);
    }
    return 0;
}
posted @ 2022-08-21 23:51  came11ia  阅读(47)  评论(0编辑  收藏  举报