「号爸十一集训 Day 10.1」 CF 六题题解合集

Codeforces 1272F Two Bracket Sequences

解题报告

一个听他们说好像很“套路”的做法。可惜我不会。

f[i][j][k] = {i', j', k'} 表示匹配了 S 的前 i 位,T 的前 j 位,有 k 个多余的左括号没有匹配,需要的括号最少是多少,这个状态下次要往哪里转移(因为要输出方案)

于是跑个 BFS 求出 f[n][m][0] 然后倒推回去就行了。这个过程中每一步都是最优解,因此实际上并不需要记录状态的值。

代码实现

还是要多练习输出方案的题目。

const int MAXN = 200 + 10;

int n, m;
char s[MAXN], t[MAXN]; std::string ans;

struct Node { int i, j, k; };

Node dp[MAXN][MAXN][MAXN * 2]; 
// 匹配了 s 的前 i 位,t 的前 j 位,多出 k 个左括号
// 上一个状态是从哪转移的

void bfs() {
    std::queue<Node> q;
    memset(dp, -1, sizeof dp); dp[0][0][0] = {0, 0, 0};
    q.push({0, 0, 0});
    while (!q.empty()) {
        Node now = q.front(); q.pop();

        int nxi = now.i + (s[now.i + 1] == '(');
        int nxj = now.j + (t[now.j + 1] == '(');
        int nxk = now.k + 1;
        if (nxk <= n + m && dp[nxi][nxj][nxk].i == -1) {
            dp[nxi][nxj][nxk] = now;
            q.push({nxi, nxj, nxk});
        }

        nxi = now.i + (s[now.i + 1] == ')');
        nxj = now.j + (t[now.j + 1] == ')');
        nxk = now.k - 1;
        if (nxk >= 0 && dp[nxi][nxj][nxk].i == -1) {
            dp[nxi][nxj][nxk] = now;
            q.push({nxi, nxj, nxk});
        }
    }
}

int main() {
    std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    cin >> (s + 1) >> (t + 1); n = (int) strlen(s + 1); m = (int) strlen(t + 1);
    bfs();
    int si = n, sj = m, sk = 0;
    while (si || sj || sk) {
        Node last = dp[si][sj][sk];
        if (last.k < sk) ans = '(' + ans;
        else ans = ')' + ans;
        si = last.i, sj = last.j, sk = last.k;
    } cout << ans << endl;
    return 0;
}

Codeforces 1579F Array Stabilization (AND version)

解题报告

观察到对于每一个 \(a_i\),它将变成 \(a_i \text{ and } a_{i - d} \text{ and } a_{i - 2d} \text{ and } a_{i - 3d} \dots\)

所以这个过程是成了一个环的,按数组下标 \(\bmod m\) 每个剩余系里的下标是一个环。

一个数要几次才能变成 \(0\),就是它在环里走几步才会遇到 \(0\)。如果遇不到 \(0\) 就说明一定会剩下一个 \(1\)

代码实现

代码是反着写的,也就是先找 \(0\) 然后用 \(0\) 去更新其他的数。

const int MAXN = 1e6 + 10;

int n, d;
int aa[MAXN];

int SolveCircle(int p) {
    int ans = 0; 
    while (true) {
        p = (p + d) % n;
        if (aa[p] == 0) break;
        aa[p] = 0; ++ans;
    } return ans;
}

int main() {
    std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    int T; cin >> T;
    while (T --> 0) {
        cin >> n >> d;
        rep (i, 0, n - 1) cin >> aa[i];
        int ans = 0;
        rep (i, 0, n - 1) {
            if (aa[i] == 0) ans = std::max(ans, SolveCircle(i)); // 循环模拟
        }
        bool flg = false; rep (i, 0, n - 1) {
            if (aa[i]) flg = true; // 1 环
        }
        if (flg) cout << "-1" << endl;
        else cout << ans << endl;
    }
    return 0;
}

Codeforces 1579G Minimal Coverage

解题报告

分享一个非常智慧的做法,从 tutorial 评论区看到的

发现答案长度越大,留给这堆线段施展的空间就越大,也就是答案更容易合法。于是考虑二分答案 \(t\)

如何 check

当时想了半天如何贪心,总是想不出正确的策略,遂放弃。

但是贪心 check 的本质还是逐条插入线段,验证每条线段的结尾端点能否落到这个长度为 \(t\) 的区间里。

于是我们考虑直接维护这个东西!!

\(01\) 数组 \(b_{j, i}\) 表示考虑前 \(j\) 条线段,第 \(i\) 个位置能(\(1\))否(\(0\))成为第 \(j\) 条线段结尾位置;换句话说,第 \(i\) 个位置能否成为第 \(j + 1\) 条线段开始位置。

考虑新加入一条线段长度为 \(d\),对所有(答案不会越界的)位置 \(i\),它能成为结尾当且仅当位置 \(i \pm d\) 至少有一个能成为开头,也就是:

\[b_{j, i} = b_{j - 1, i - d} \text{ or } b_{j - 1, i + d} \]

当前的答案满足条件意味着 \(b_{n}\) 至少有一个位置为 \(1\)

快速维护这个 \(01\) 数组可以通过 std::bitset 实现。可以直接滚动数组把第一维优化掉。

代码实现

注释写的英文是因为我同时在用 Dev-C++ 和 VSCode,为了防止编码混乱才写成英文的。

关于那个神奇的式子可以参考下图(线是 1,实心块是 0):

47LOb9.png

const int MAXN = 10000 + 10;

int n, aa[MAXN];

std::bitset<MAXN> s, t; 
// (after j loops) s[i] means (after considering the first j segs) 
// whether the jth seg's end can be placed on pos i
// in other words, (before the (j + 1)th loop) s[i] means (after considering the first j segs)
// whether it's possible to place the (j + 1)th seg's start on pos i

// jth seg's end can be placed on pos i if and only if its start can be placed on pos (i - len) or (i + len)
// that is, forall i, new_s[i] = s[i - len] | s[i + len]


bool check(int mid) {
    s = 0; t = 0;
    for (int i = 0; i <= std::min(mid, MAXN - 1); ++i) t.set(i);
    s = t;
    for (int i = 1; i <= n; ++i) {
        s = ((s >> aa[i]) | (s << aa[i])) & t;
        // this magic thing equals to:
        // for (int j = 1; j <= n; ++j) {
        //     new_s[j] = s[j - aa[i]] | s[j + aa[i]];
        // }
    } return s.count();
}

int main() {
    std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    int T; cin >> T;
    while (T --> 0) {
        cin >> n;
        for (int i = 1; i <= n; ++i) cin >> aa[i];
        int l = 1, r = 2e7, ans = 0;
        while (l <= r) {
            int mid = (l + r) >> 1;
            if (check(mid)) r = mid - 1, ans = mid;
            else l = mid + 1;
        } cout << ans << endl;
    }
    return 0;
}

Codeforces 1256F Equalizing Two Strings

解题报告

首先如果字母构成不一样一定 -NO.

发现一个性质:如果一个字符串里有连续相同的字母,那么通过不断交换两个相邻的相同字母,我们可以实现对另一个字符串的任意修改。

接下来考虑字母没有重复的情况。仍然是考虑交换两个相邻字母,此时问题类似于冒泡排序(怎么哪都有你)。

这两个字符串能变换成相同字符串的充要条件是:逆序对奇偶性相同。

考虑都把它们排成递增,需要的次数就是逆序对数。如果一个多一个少,那么少的那个可以通过不断地交换最后两个字母来平衡一下。

根据鸽巢原理字符串长度一定不超过 \(26\)。直接暴力求逆序对即可。

代码实现

const int MAXN = 2e5 + 10;

std::string ss, tt;

int main() {
    std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    int T; cin >> T;
    while (T --> 0) {
        int n; cin >> n;
        cin >> ss >> tt;

        int cnt[26], cnt2[26]; 
        memset(cnt, 0, sizeof cnt);
        memset(cnt2, 0, sizeof cnt2);
        int flg = 0;
        for (int i = 0; i < n; ++i) {
            ++cnt[ss[i] - 'a']; ++cnt2[tt[i] - 'a'];
        } 
        for (int i = 0; i < 26; ++i) {
            if (cnt[i] != cnt2[i]) { flg = 1; break; }
            if (cnt[i] >= 2 || cnt2[i] >= 2) { flg = 2; }
        }
        if (flg == 1) { cout << "NO" << endl; continue; }
        else if (flg == 2) { cout << "YES" << endl; continue; }

        int invp[2] = {0, 0};
        for (int i = 0; i < n; ++i) {
            for (int j = i + 1; j < n; ++j) {
                invp[0] += (ss[i] > ss[j]);
                invp[1] += (tt[i] > tt[j]);
            }
        }
        if ((invp[0] & 1) != (invp[1] & 1)) cout << "NO" << endl;
        else cout << "YES" << endl;
    }
    return 0;
}

Codeforces 1077F2 Pictures with Kittens (hard version)

解题报告

设状态 f[i][j] 表示前 \(i\) 张图选了 \(j\) 张的最大美观度。

转移是三维的,需要枚举上一次选的哪张图片,取最大的美观度,还要保证两张图距离不超过 \(k\)

那这实际上就是一个滑动窗口求最大值,搞个单调队列优化一下就可以了。

代码实现

各种 DP 优化都要熟练掌握。

const int MAXN = 5000 + 10;

#define int lli

int n, k, x;
int aa[MAXN];
int dp[MAXN][MAXN];

std::deque<int> mq[MAXN];

signed main() {
    std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    cin >> n >> k >> x;
    rep (i, 1, n) cin >> aa[i];
    memset(dp, -0x3f, sizeof dp);
    dp[0][0] = 0; mq[0].push_back(0);
    for (int i = 1; i <= n; ++i) {
        for (int j = x; j >= 1; --j) {
            while (!mq[j - 1].empty() && mq[j - 1].front() < i - k) mq[j - 1].pop_front();
            if (!mq[j - 1].empty()) {
                dp[i][j] = std::max(dp[i][j], dp[mq[j - 1].front()][j - 1] + aa[i]);
                while (!mq[j].empty() && dp[mq[j].back()][j] <= dp[i][j]) mq[j].pop_back();
                mq[j].push_back(i);
            }
        }
    } int ans = -1;
    for (int i = n - k + 1; i <= n; ++i) ans = std::max(ans, dp[i][x]);
    cout << ans << endl;
    return 0;
}

Codeforces 1256E Yet Another Division Into Teams

解题报告

首先显然是按从小到大顺序选最优。于是先排个序。

f[i] 表示前 \(i\) 个学生组队完的最小极差和。

转移需要从 \(1\) 开始枚举上一组是在哪截止的,取最小的那一个记录方案。

于是直接套一个前缀最小值优化。

代码实现

写的有亿点点长。主要还是输出方案不熟练。

const int MAXN = 2e5 + 10;

struct T { lli val, id; } ts[MAXN];

bool cmp1(T x, T y) { return x.val < y.val; }
bool cmp2(T x, T y) { return x.id < y.id; }

int n; lli aa[MAXN];
lli dp[MAXN], prefdp[MAXN];
int last[MAXN], last_pref[MAXN];
// last[i] 记录的是 f[i] 从哪转移过来
// last_pref[i] 记录的是 prefdp[i] 在 dp 数组中的下标

void getDetail() {
    /* DEBUG */ 

    int r = n;
    int now = n;
    std::vector<std::pair<int, int> > ans;
    while (now) {
        now = last[now];
        ans.push_back({now + 1, r});
        r = now;
    }
    std::reverse(ALL(ans)); int teams = 0;
    for (auto v : ans) {
        ++teams;
        for (int i = v.first; i <= v.second; ++i) ts[i].val = teams;
    } cout << ' ' << teams << endl;
    std::sort(ts + 1, ts + 1 + n, cmp2);
    for (int i = 1; i <= n; ++i) cout << ts[i].val << ' ';
    cout << endl;
}

int main() {
    std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    cin >> n;
    
    for (int i = 1; i <= n; ++i) { cin >> ts[i].val; ts[i].id = i; }
    std::sort(ts + 1, ts + 1 + n, cmp1);
    for (int i = 1; i <= n; ++i) {
        aa[i] = ts[i].val;
    }

    memset(dp, 0x3f, sizeof dp); memset(prefdp, 0x3f, sizeof prefdp);
    dp[3] = aa[3] - aa[1]; prefdp[3] = dp[3] - aa[4]; 
    last[3] = 0; last_pref[3] = 3;
    for (int i = 4; i <= n; ++i) {
        // dp[i] = std::min(dp[i - 1] - aa[i - 1], prefdp[i - 3]) + aa[i];
        // prefdp[i] = std::min(prefdp[i - 1], dp[i] - aa[i + 1]);
        
        if (dp[i - 1] - aa[i - 1] < prefdp[i - 3]) {
            dp[i] = dp[i - 1] - aa[i - 1];
            last[i] = last[i - 1];
        } else {
            dp[i] = prefdp[i - 3];
            last[i] = last_pref[i - 3];
        } dp[i] += aa[i];
        if (prefdp[i - 1] < dp[i] - aa[i + 1]) {
            prefdp[i] = prefdp[i - 1];
            last_pref[i] = last_pref[i - 1];
        } else {
            prefdp[i] = dp[i] - aa[i + 1];
            last_pref[i] = i;
        }
    } cout << dp[n];
    getDetail();
    return 0;
}
posted @ 2021-10-01 23:47  Handwer  阅读(166)  评论(0编辑  收藏  举报