Educational Codeforces Round 176 (Rated for Div. 2)

写在前面

比赛地址:https://codeforces.com/contest/2075

哈哈打成这个史样子还能上分啊。

A

签到。

奇数减奇数变偶数,偶数减偶数变偶数。则可以得到显然的操作次数上界,且可以证明一定能取到该上界:

  • \(n\) 为奇数:\(1 + \left\lceil \frac{n - k}{k - 1}\right\rceil\)
  • \(n\) 为偶数:\(\left\lceil \frac{n}{k - 1}\right\rceil\)
#include <bits/stdc++.h>
#define LL long long

int main() {
  std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
  int T; std::cin >> T;
  while (T --) {
    int n, k, ans; std::cin >> n >> k;
    if (n % 2) {
      ans = 1 + ceil(1.0 * (n - k) / (k - 1));
    } else {
      ans = ceil(1.0 * n / (k - 1));
    }
    std::cout << ans << "\n";
  }
  return 0;
}

B

讨论。

题面写的好怪呃呃两个 Announcement 发完才会做。

手玩容易发现,当 \(k\ge 2\) 时,最后一定可以取到数列中前 \(k+1\) 大的贡献。仅需初始时保证选择前 \(k+1\) 大中最靠左和最靠右的数即可;当 \(k=1\) 时,最后获得贡献的数一定是 \(a_1\)\(a_n\),则需要讨论最大值是否是 \(a_1\)\(a_n\)

  • \(\max a \in \{a_1, a_n\}\),则初始时选择次大值一定最好,答案为最大值与次大值之和;
  • 否则,初始时选择最大值一定最好,答案为最大值加上 \(\max(a_1, a_n)\)
#include <bits/stdc++.h>
#define LL long long

const int kN = 5010;

int n, k;

struct Data {
  int x, p;
} a[kN];

bool cmp(Data fir_, Data sec_) {
  return fir_.x > sec_.x;
}

int main() {
  std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
  int T; std::cin >> T;
  while (T --) {
    std::cin >> n >> k;
    for (int i = 1; i <= n; ++ i) {
      std::cin >> a[i].x;
      a[i].p = i;
    }
    std::sort(a + 1, a + n + 1, cmp);
    
    LL ans = 0;
    if (k == 1) {
      if (a[1].p == 1 || a[1].p == n) {
        ans = a[1].x + a[2].x;
      } else {
        for (int i = 2; i <= n; ++ i) if (a[i].p == 1 || a[i].p == n) ans = std::max(ans, 1ll * a[1].x + a[i].x);
      }
    } else {

      for (int i = 1; i <= k + 1; ++ i) ans += a[i].x;
    }

    std::cout << ans << "\n";
  }
  return 0;
}
/*
1
5 2
1 2 3 4 1

1
5 1
1 1 5 1 2

1
5 1
5 1 1 1 1
*/

C

双指针,线段树。

我的做法比较大力。

先对 \(a\) 排序,然后正序枚举涂色时涂在前一半的颜色 \(i\),则能与颜色 \(i\) 组合成合法方案的颜色,在 \(a\) 中一定对应一段后缀 \([j, n]\)(可以为空),且当 \(i\) 递增时 \(j\) 一定递减。

于是考虑双指针维护上述的 \(i, j\),容易发现当前枚举到的前一半的颜色 \(i\),涂色数量分别为 \(x\in [1, a_i]\) 时,对应的合法方案数即为:

\[\sum_{j\le y\le n, y\not= i} \sum_{x\in[1, a_i]} [a_y\ge n - x] \]

发现上式即为若干个后缀 \([n - a_y, n - 1]\) 与前缀 \([1, x]\) 的交集长度之和,很容易通过权值线段树维护。考虑每次 \(j\) 左移时令区间 \([n - a_j, n - 1]\) 加一,上式的答案即为减去 \(i\) 的贡献后,区间 \([1, a_i]\) 的和。

总时间复杂度 \(O(n\log n)\) 级别。

#include <bits/stdc++.h>
#define LL long long

const int kN = 2e5 + 10;

int n, m, a[kN];

namespace Seg {
  #define ls (now_<<1)
  #define rs (now_<<1|1)
  #define mid ((L_+R_)>>1)
  LL sum[kN << 2], tag[kN << 2];
  void Pushup(int now_) {
    sum[now_] = sum[ls] + sum[rs];
  }
  void Pushdown(int now_, int L_, int R_) {
    sum[ls] += 1ll * tag[now_] * (mid - L_ + 1);
    sum[rs] += 1ll * tag[now_] * (R_ - mid);
    tag[ls] += tag[now_];
    tag[rs] += tag[now_];
    tag[now_] = 0ll;
  }
  void Build(int now_, int L_, int R_) {
    sum[now_] = 0, tag[now_] = 0ll;
    if (L_ == R_) return ;
    Build(ls, L_, mid);
    Build(rs, mid + 1, R_);
    Pushup(now_); 
  }
  void Modify(int now_, int L_, int R_, int l_, int r_, LL val_) {
    if (l_ <= L_ and R_ <= r_) {
      sum[now_] += 1ll * (R_ - L_ + 1) * val_;
      tag[now_] += val_;
      return ;
    }
    Pushdown(now_, L_, R_);
    if (l_ <= mid) Modify(ls, L_, mid, l_, r_, val_);
    if (r_ > mid) Modify(rs, mid + 1, R_, l_, r_, val_);
    Pushup(now_);
  }
  LL Query(int now_, int L_, int R_, int l_, int r_) {
    if (l_ <= L_ and R_ <= r_) return sum[now_];
    Pushdown(now_, L_, R_);
    LL ret = 0;
    if (l_ <= mid) ret += Query(ls, L_, mid, l_, r_);
    if (r_ > mid) ret += Query(rs, mid + 1, R_, l_, r_);
    return ret;
  }
  #undef ls
  #undef rs
  #undef mid
}

int main() {
  std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
  int T; std::cin >> T;
  while (T --) {
    std::cin >> n >> m;
    for (int i = 1; i <= m; ++ i) std::cin >> a[i];
    std::sort(a + 1, a + m + 1);

    Seg::Build(1, 1, n);
    LL ans = 0;
    for (int i = 1, j = m + 1; i <= m; ++ i) {
      while (1 <= j - 1 && a[i] + a[j - 1] >= n) {
        -- j;
        Seg::Modify(1, 1, n, n - a[j], n - 1, 1);
      }
      if (i >= j) Seg::Modify(1, 1, n, n - a[i], n - 1, -1);
      ans += Seg::Query(1, 1, n, 1, a[i]);
      if (i >= j) Seg::Modify(1, 1, n, n - a[i], n - 1, 1);
    }
    std::cout << ans << "\n";
  }
  return 0;
}
/*
1
12 3
5 9 8
*/

D

预处理,DP,枚举。

唉唉写到这题的时候才来手感太坏了。

发现一次操作相当于以 \(2^k\) 的代价,选择一个数右移 \(k\) 位。最终两个数相等,等价于两个数通过若干次右移之后变成了某两个相等的前缀。发现 \(x, y\le 10^{17}\),则右移总次数并不多,于是考虑大力枚举两个数分别右移的次数 \(j, k\),并检查对应的前缀是否相等,并求得所需代价即可。

考虑预处理 \(f_{i, j, k}\) 表示使用操作 \(k\in[1, i]\),使得 \(x\) 右移 \(j\) 位,\(y\) 右移 \(k\) 位所需的最小代价。初始化 \(f_{0, 0, 0}=0\),则很容易得到一个类似 01 背包的转移:

\[f_{i, j, k} \leftarrow \begin{cases} f_{i - 1, j, k}\\ f_{i - 1, j - i, k} + 2^i\\ f_{i - 1, j, k - i} + 2^i \end{cases}\]

上式显然可以滚动数组优化成 \(f_{j, k}\),则可以在 \(O(\log^2 v)\) 的空间复杂度、\(O(\log^3 v)\) 的时间复杂度内预处理;则对于每次询问 \(x, y\),仅需大力枚举分别右移的次数 \(j, k\) 并检查对应的前缀是否相等,取合法的 \(f_{j, k}\) 的最小值即可。

单组询问时间复杂度为 \(O(\log x \log y)\) 级别,则总时间复杂度 \(O\left(\log^3 v + T\log^2 v\right)\) 级别。

#include <bits/stdc++.h>
#define LL long long

const int kN = 70 + 10;
const LL kInf = 1e18;
const int lim = 60;

LL x, y;
LL f[2][kN][kN];

int main() {
  std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
  int T; std::cin >> T;

  for (int j = 0; j <= lim; ++ j) {
    for (int k = 0; k <= lim; ++ k) {
      f[0][j][k] = f[1][j][k] = kInf;
    }
  }
  int p = 1;
  f[0][0][0] = 0;    
  for (int i = 1; i <= std::max(lim, lim); ++ i, p ^= 1) {
    for (int j = lim; ~j; -- j) {
      for (int k = lim; ~k; -- k) {
        f[p][j][k] = f[p ^ 1][j][k];
      }
    }

    for (int j = lim; ~j; -- j) {
      for (int k = lim; k >= i; -- k) {
        f[p][j][k] = std::min(f[p][j][k], f[p ^ 1][j][k - i] + (1ll << i));
      }
    }
    for (int k = lim; ~k; -- k) {
      for (int j = lim; j >= i; -- j) {
        f[p][j][k] = std::min(f[p][j][k], f[p ^ 1][j - i][k] + (1ll << i));
      }
    }
  }

  while (T --) {
    std::cin >> x >> y;
    int limx = ceil(log2(1.0 * x + 1)) + 1, limy = ceil(log2(1.0 * y + 1)) + 1;
    LL ans = kInf;

    for (int j = 0; j <= limx; ++ j) {
      for (int k = 0; k <= limy; ++ k) {
        if ((x >> j) == (y >> k)) ans = std::min(ans, f[p ^ 1][j][k]);
      }
    }
    std::cout << ans << "\n";

  }
  return 0;
}

E

牛逼提。

显然当 \(a, b\) 两个数组中,某一方权值种类数为 1 时一定合法,这部分答案很容易球的;某一方权值种类数大于 2 时一定不合法;则重点是考虑两方权值种类数均为 2 时的合法性。

记此时两个数组的权值分别为:\(a_1, a_2, b_1, b_2(a_1\not= a_2, b_1, \not= b_2)\),此时矩阵中一共有四种权值:\(a_1\oplus b_1, a_1\oplus b_2, a_2\oplus b_1, a_2\oplus b_2\),且根据异或的性质,容易推出合法的充要条件为:

\[a_1 \oplus a_2 = b_1 \oplus b_2 \]

然后不会了,看别人代码发现可以直接拆位求出来分别在两个数组中选出符合条件的权值的种类数,起床再补。

写在最后

学到了什么:

  • D:枚举;

然后是好久没干的夹带私货,这次放个经典老番:

posted @ 2025-03-18 01:49  Luckyblock  阅读(185)  评论(0)    收藏  举报