Codeforces Round 906 (Div. 2)

A. Doremy's Paint 3

对于式子 \(b_1 + b_2 = b_2 + b_3 = \dots = b_{n-1}+b_n=k\),从中挑出 \(b_i + b_{i+1} = b_{i+1} + b_{i+2}\),得到 \(b_i = b_{i+2}\),这就意味着所有奇数位置上的数需要相等,所有偶数位置上的数也需要相等。

对于 \(n\) 个数而言,有 \(\lceil \frac{n}{2} \rceil\) 个奇数位置和 \(\lfloor \frac{n}{2} \rfloor\) 个偶数位置。因此,当且仅当可以找到 \(\lfloor \frac{n}{2} \rfloor\) 个相等的数并且余下部分内部也是相等的数时答案为 Yes。

情况可以分为以下三类:

  1. 所有的数都一样,比如 \([3,3,3,3,3,3]\),此时答案为 Yes
  2. 有两种不同的数,比如 \([1,2,1,2,1]\)。如果其中一种数出现了恰好 \(\lfloor \frac{n}{2} \rfloor\) 次则答案为 Yes。例如 \([1,2,1,2,1]\)\([2,3,2,3]\) 答案为 Yes 而 \([1,1,1,2]\)\([3,3,3,3,4,4]\) 答案为 No。

时间复杂度为 \(O(n)\)

参考代码
#include <cstdio>
const int N = 100005;
int a[N], cnt[N];
int main()
{
    int t; scanf("%d", &t);
    while (t--) {
        int n; scanf("%d", &n);
        int diff_num = 0;
        for (int i = 1; i <= n; i++) {
            scanf("%d", &a[i]); cnt[a[i]]++;
            if (cnt[a[i]] == 1) diff_num++;
        }  
        if (diff_num == 1) printf("Yes\n");
        else if (diff_num > 2) printf("No\n");
        else if (cnt[a[1]] == n / 2 || cnt[a[1]] == (n + 1) / 2) printf("Yes\n");
        else printf("No\n");
        for (int i = 1; i <= n; i++) cnt[a[i]]--;
    }
    return 0;
}

B. Qingshan Loves Strings

有三种情况答案为 Yes:

  1. s 本身是好的
  2. t 是好的,t 的头尾都是 '1',而 s 中没有 "11"
  3. t 是好的,t 的头尾都是 '0',而 s 中没有 "00"
参考代码
#include <cstdio>
const int N = 55;
char s1[N], s2[N]; 
bool good(char s[], int len) {
    for (int i = 1; i < len; i++)
        if (s[i] == s[i - 1]) return false;
    return true;
}
bool check(char s[], int len, char ch) {
    for (int i = 1; i < len; i++)
        if (s[i] == ch && s[i - 1] == ch) return false;
    return true;
}
int main()
{
    int t; scanf("%d", &t);
    while (t--) {
        int n, m; scanf("%d%d%s%s", &n, &m, s1, s2);
        bool res = good(s1, n);
        if (res) printf("Yes\n");
        else {
            res = good(s2, m);
            if (!res || s2[0] != s2[m - 1]) printf("No\n");
            else if (check(s1, n, s2[0])) printf("Yes\n");
            else printf("No\n");
        }
    }
    return 0;
}

C. Qingshan Loves Strings 2

首先,如果 \(0\) 的数量和 \(1\) 的数量不一致则必然无解。

否则,可以按如下方法构造:

如果 \(s_1 \ne s_n\),接下来不需要再考虑头尾字符,可以将剩余部分 \(s_{2...n-1}\) 看成新的 \(s\),如果消除完毕,则结束构造。

如果 \(s_1 = s_n\),假如两者都是 \(1\),可以在前面插入 \(01\),假如都是 \(0\),可以在后面插入 \(01\),例如:

  1. "110010"
  2. "1001"
  3. "011001"
  4. "1100"
  5. "10"
  6. ""

这样一来实际上将插入 \(01\) 这个操作转化成了把最后一位的 \(1\) 移到最前面或者是把第一位的 \(0\) 移到最后面。所以最坏情况下,每个字符都要移动,需要 \(n\) 次操作。

实际上最坏只需要 \(n/2\) 次操作,因为对于消除操作中的 \(0\)\(1\),最多只有其中一个需要移动。

时间复杂度为 \(O(n)\)

参考代码
#include <cstdio>
#include <deque>
using namespace std;
const int N = 105;
char s[N];
int op[N];
int main()
{
    int t; scanf("%d", &t);
    while (t--) {
        int n; scanf("%d%s", &n, s);
        int cnt0 = 0, cnt1 = 0;
        for (int i = 0; i < n; i++)
            if (s[i] == '0') cnt0++;
            else cnt1++;
        if (cnt0 != cnt1) printf("-1\n");
        else {
            deque<char> dq;
            for (int i = 0; i < n; i++) dq.push_back(s[i]);
            int p = 0, del = 0;
            while (!dq.empty()) {
                if (dq.front() != dq.back()) {
                    dq.pop_front(); dq.pop_back(); del++;
                } else {
                    p++;
                    if (dq.front() == '0') {
                        op[p] = n - del; dq.push_back('0'); dq.push_back('1');
                    } else {
                        op[p] = del; dq.push_front('1'); dq.push_front('0');
                    }
                    n += 2;
                }
            }
            if (p == 0) printf("0\n\n");
            else {
                printf("%d\n", p);
                for (int i = 1; i <= p; i++) printf("%d%c", op[i], i == p ? '\n' : ' ');
            }
        }
    }
    return 0;
}

D. Doremy's Connecting Plan

不妨假设 \(c=1\),实际上当 \(c \ne 1\) 时,只需要令 \(a_i'= \frac{a_i}{c}\) 即和 \(c=1\) 的情况等价。

\(s_i = \sum a_j\),其中 \(j\)\(i\) 当前所连接的结点。

如果 \(i\)\(j\) 之间可以连边(\(i \ne 1, j \ne 1\)),相当于要求 \(s_i + s_j \ge i \cdot j \ge i + j\)

而这实际上可以推出 \(s_i \ge i\)\(s_j \ge j\) 这两个条件中至少有一个成立(否则 \(s_i + s_j < i + j\)),不妨设 \(s_i \ge i\)。由此推出 \(s_i + s_1 \ge 1 \cdot i\),也就是说可以在 \(i\)\(1\) 之间连一条边。

而且,添加一条新的边不会导致原来能加的边变得不能加,所以我们可以永远考虑 \(1\)\(i\) 之间能不能连边,这样一来只需要考虑顺序即可。

对于式子 \(s_i + s_1 \ge 1 \cdot i\),可以发现 \(s_i - i\) 越大,结点 \(i\) 就越有可能和 \(1\) 相连,所以我们可以按 \(a_i - i\) 降序排序,按照这个顺序依次处理每个结点是否可以和 \(1\) 相连。

时间复杂度为 \(O(n \log n)\)

参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 200005;
int order[N], c;
LL a[N];
bool cmp(int i, int j) {
    return a[i] - a[j] > 1ll * c * (i - j);
}
int main()
{
    int t; scanf("%d", &t);
    while (t--) {
        int n; scanf("%d%d", &n, &c);
        for (int i = 1; i <= n; i++) {
            scanf("%lld", &a[i]); order[i] = i;
        }
        sort(order + 2, order + n + 1, cmp);
        LL sum = a[1];
        bool ok = true;
        for (int i = 2; i <= n; i++) {
            int idx = order[i];
            if (sum + a[idx] >= 1ll * idx * c) {
                sum += a[idx];
            } else {
                ok = false; break;
            }
        }
        printf("%s\n", ok ? "Yes" : "No");
    }
    return 0;
}

E1. Doremy's Drying Plan (Easy Version)

首先考虑暴力做法。

对于每一个位置 \(i=1,2,3,...,n\) 我们可以通过差分与前缀和预处理每个位置被线段覆盖的次数。首先,对于每个覆盖次数为 \(0\) 的位置,可以放到最后再考虑,因为这部分点的数量与移除的两条线段无关,设这部分点的数量为 \(A\),再设移除两条线段后产生的新的不被覆盖的点的数量是 \(B\),则本题的答案为 \(A + \max B\)

要计算 \(B\),考虑每一对线段 \(I_1, I_2\)

  • 如果两者不交叉,\(B\) 等于 \(I_1\)\(I_2\) 中刚好覆盖一次的点的数量之和;
  • 如果两者交叉,假设两者交叉的线段为 \(J\),则 \(B\) 等于\(I_1\)\(I_2\) 中刚好覆盖一次的点的数量之和再加上 \(J\) 中恰好覆盖两次的点的数量。

如果直接枚举每一对线段,则时间复杂度为 \(O(n + m^2)\),考虑优化这个算法:

  • 对于两线段不交叉的情况,实际上直接从所有线段中挑出覆盖一次的点的数量最多的两条即可;
  • 对于两线段交叉的情况,实际上真正有用的最多只有 \(n\) 对线段:对于每一个位置 \(i\),如果它恰好被覆盖两次,此时就考虑覆盖这个点的两条线段即可。

对于某一个点,如何维护覆盖它的线段?这里我们可以把每个线段 \([l,r]\) 看成是在 \(l\) 位置开始计算时引入,在 \(r\) 位置完成计算后移除,可以利用一个 multiset 来维护当前涉及到的线段,这样一来时间复杂度优化到 \(O(n+m \log m)\)

参考代码
#include <cstdio>
#include <vector>
#include <set>
#include <algorithm>
using namespace std;
const int N = 200005;
vector<int> left[N], right[N];
int diff[N], cnt[N], sum1[N], sum2[N];
multiset<pair<int, int>> s;
int main()
{
    int t; scanf("%d", &t);
    while (t--) {
        int n, m, k; scanf("%d%d%d", &n, &m, &k);
        for (int i = 1; i <= n; i++) {
            left[i].clear(); right[i].clear();
            diff[i] = 0;
        }
        for (int i = 1; i <= m; i++) {
            int l, r; scanf("%d%d", &l, &r);
            left[l].push_back(r); right[r].push_back(l);
            diff[l]++; diff[r + 1]--;
        }
        for (int i = 1; i <= n; i++) cnt[i] = cnt[i - 1] + diff[i];
        for (int i = 1; i <= n; i++) {
            sum1[i] = sum1[i - 1]; sum2[i] = sum2[i - 1];
            if (cnt[i] == 1) sum1[i]++;
            else if (cnt[i] == 2) sum2[i]++;
        }
        // only 1
        int max1 = 0, max2 = 0;
        for (int i = 1; i <= n; i++) 
            for (int x : left[i]) {
                // [i, x]
                int cur = sum1[x] - sum1[i - 1];
                if (cur > max1) {
                    max2 = max1; max1 = cur;
                } else if (cur > max2) max2 = cur;
            }
        int ans = max1 + max2;
        // 1+2
        s.clear();
        for (int i = 1; i <= n; i++) {
            for (int x : left[i]) s.insert({i, x});
            if (cnt[i] == 2) {
                auto it = s.begin();
                int l1 = it->first, r1 = it->second;
                it++;
                int l2 = it->first, r2 = it->second;
                int a = l1, b = r1, c = l2, d = r2;
                // sort
                if (a > b) swap(a, b); 
                if (a > c) swap(a, c);
                if (a > d) swap(a, d);
                if (b > c) swap(b, c);
                if (b > d) swap(b, d);
                if (c > d) swap(c, d); 
                ans = max(ans, sum1[d] - sum1[a - 1] + sum2[c] - sum2[b - 1]);
            }
            for (int x : right[i]) s.erase(s.find({x, i}));
        }
        for (int i = 1; i <= n; i++) if (cnt[i] == 0) ans++;
        printf("%d\n", ans);
    }
    return 0;
}

E2. Doremy's Drying Plan (Hard Version)

考虑每个点 \(i\),如果我们希望点 \(i\) 不被覆盖,那么需要清空覆盖点 \(i\) 的所有线段。

从左向右依次考虑每个点是否可以不被覆盖,那么在考虑 \(i\) 点之前已经删除了一些线段,这时实际上有一部分覆盖在 \(i\) 上的线段可能会在之前的阶段被考虑删除过,所以决定因素在于上一个不被覆盖的点的位置。

image

这里得到了一个子问题结构,考虑动态规划。设 \(dp_{i,j}\) 表示考虑前 \(i\) 个点且最后一个不被覆盖的点为 \(i\),当前总共删去 \(j\) 条线段,最多能有多少个点不被覆盖,有转移方程:\(dp_{i,j} = \max \limits_{t=0}^{i-1} \{ dp_{t,j-cost(t,i)} \} + 1\),其中 \(cost(t,i)\) 表示的是满足 \(t < l \le i \le r\) 的线段数量,也就是绿色类型的线段的数量。基于这个状态转移方程,可以得到一个转移时间复杂度为 \(O(n^2 k)\) 的算法,所以需要进一步优化。

如果覆盖在 \(i\) 上面的线段超过了 \(k\) 条,那么 \(i\) 上的覆盖一定无法消除,只考虑 \(i\) 上的覆盖线段数小于等于 \(k\) 的情况,此时 \(cost(t,i) \le k\)

容易发现当 \(t\) 减小时,\(cost(t,i)\) 会增大,且不会超过 \(k\)

假设有 \(c\) 条线段覆盖在 \(i\) 上面,将覆盖在 \(i\) 上的线段按左端点从小到大排序,得到左端点序列 \([l_1, l_2, \dots, l_c]\)。这个序列将区间 \([0, i-1]\) 划分成了很多个区间 \([0, l_1-1], [l_1, l_2-1], \dots, [l_c, i-1]\),对于每一段内部而言,它们的 \(cost\) 是相等的。因此这里的每一小段区间内部的 \(dp\) 最大值可以用数据结构优化维护。

将 Sparse Table 倒过来使用,用 \(st_{i,j,len}\) 表示 \(dp\) 状态中第二维取 \(j\) 时第一维以 \(i\) 结尾的 \(2^{len}\) 个数的最大值,就能够动态地在序列末尾以 \(O(\log n)\) 的时间复杂度实现插入操作,以 \(O(1)\) 的时间复杂度完成某个区间的查询。

对于每个端点 \(i\) 维护覆盖它的线段参考 Easy Version 的题解。

最终时间复杂度为 \(O(nk^2+nk\log n + m \log m)\)

参考代码
#include <cstdio>
#include <vector>
#include <set>
#include <algorithm>
using namespace std;
const int N = 200005;
const int K = 15;
const int LOG = 18;
vector<int> left[N], right[N];
int diff[N], cnt[N], st[N][K][LOG], Log2[N];
// st[i][j][len] -> max(dp[i-(1<<len)+1],...,dp[i][j])
multiset<pair<int, int>> s;
int query(int l, int r, int used) {
    int len = Log2[r - l + 1];
    return max(st[r][used][len], st[l + (1 << len) - 1][used][len]);
}
int main()
{
    Log2[1] = 0;
    for (int i = 2; i < N; i++) Log2[i] = Log2[i / 2] + 1;
    int t; scanf("%d", &t);
    while (t--) {
        int n, m, k; scanf("%d%d%d", &n, &m, &k);
        for (int i = 1; i <= n; i++) {
            left[i].clear(); right[i].clear();
            diff[i] = 0;
        }
        for (int i = 1; i <= m; i++) {
            int l, r; scanf("%d%d", &l, &r);
            left[l].push_back(r); right[r].push_back(l);
            diff[l]++; diff[r + 1]--;
        }
        for (int i = 1; i <= n; i++) cnt[i] = cnt[i - 1] + diff[i];
        for (int i = 1; i <= n; i++)
            for (int j = 0; j <= k; j++)
                for (int len = 0; len < LOG; len++)
                    st[i][j][len] = 0;
        for (int i = 1; i <= n; i++) {
            for (int x : left[i]) s.insert({i, x});
            for (int j = 0; j <= k; j++) {
                if (cnt[i] <= k) {
                    int prej = j - cnt[i], pre = 0;
                    for (auto p : s) {
                        int l = p.first;
                        // dp[i][j] = max(dp[...][j-cost]) + 1
                        if (l != pre && j >= cnt[i]) 
                            st[i][j][0] = max(st[i][j][0], query(pre, l - 1, prej) + 1);
                        prej++; pre = l;
                    }
                    if (pre != i && j >= cnt[i]) 
                        st[i][j][0] = max(st[i][j][0], query(pre, i - 1, prej) + 1);
                }  
                for (int len = 1; (1 << len) <= i + 1; len++) {
                    int mid = i - (1 << (len - 1));
                    st[i][j][len] = max(st[i][j][len - 1], st[mid][j][len - 1]);
                }
            }                    
            for (int x : right[i]) s.erase(s.find({x, i}));
        }
        int ans = 0;
        for (int i = 0; i <= k; i++) ans = max(ans, query(0, n, i));
        printf("%d\n", ans);
    }
    return 0;
}
posted @ 2024-02-16 08:37  RonChen  阅读(75)  评论(0)    收藏  举报