Codeforces Round 906 (Div. 2)

A. Doremy's Paint 3

对于式子 b1+b2=b2+b3==bn1+bn=k,从中挑出 bi+bi+1=bi+1+bi+2,得到 bi=bi+2,这就意味着所有奇数位置上的数需要相等,所有偶数位置上的数也需要相等。

对于 n 个数而言,有 n2 个奇数位置和 n2 个偶数位置。因此,当且仅当可以找到 n2 个相等的数并且余下部分内部也是相等的数时答案为 Yes。

情况可以分为以下三类:

  1. 所有的数都一样,比如 [3,3,3,3,3,3],此时答案为 Yes
  2. 有两种不同的数,比如 [1,2,1,2,1]。如果其中一种数出现了恰好 n2 次则答案为 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 的数量不一致则必然无解。

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

如果 s1sn,接下来不需要再考虑头尾字符,可以将剩余部分 s2...n1 看成新的 s,如果消除完毕,则结束构造。

如果 s1=sn,假如两者都是 1,可以在前面插入 01,假如都是 0,可以在后面插入 01,例如:

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

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

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

时间复杂度为 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,实际上当 c1 时,只需要令 ai=aic 即和 c=1 的情况等价。

si=aj,其中 ji 当前所连接的结点。

如果 ij 之间可以连边(i1,j1),相当于要求 si+sjiji+j

而这实际上可以推出 siisjj 这两个条件中至少有一个成立(否则 si+sj<i+j),不妨设 sii。由此推出 si+s11i,也就是说可以在 i1 之间连一条边。

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

对于式子 si+s11i,可以发现 sii 越大,结点 i 就越有可能和 1 相连,所以我们可以按 aii 降序排序,按照这个顺序依次处理每个结点是否可以和 1 相连。

时间复杂度为 O(nlogn)

参考代码
#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+maxB

要计算 B,考虑每一对线段 I1,I2

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

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

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

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

参考代码
#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

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

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

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

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

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

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

最终时间复杂度为 O(nk2+nklogn+mlogm)

参考代码
#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 @   RonChen  阅读(50)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示