Codeforces Round 919 (Div. 2)

写在前面

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

考试周突然发癫想打 cf 但是觉得肯定打不完所以拿小号打的。

手速慢了没上大分呃呃

A

求出上下界来,再求被限制的数有多少个在区间内即可。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kInf = 1e9 + 2077;
//=============================================================
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); 
  return f * w;
}
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  int T = read();
  while (T --) {
    int minn = -kInf, maxx = kInf;
    std::map <int, bool> vis;
    int n = read();
    while (n --) {
      int opt = read();
      if (opt == 1) {
        minn = std::max(minn, read());
      } else if (opt == 2) {
        maxx = std::min(maxx, read());
      } else {
        vis[read()] = 1;
      }
    }

    int num = maxx - minn + 1;
    for (auto x: vis) {
      num -= (minn <= x.first && x.first <= maxx);
    }
    printf("%d\n", minn > maxx ? 0 : num);
  }
  return 0;
}

B

所有数均为正数,则当 Alice 确定了保留哪些数之后 Bob 的行动就确定了,一定会把其中最大的 \(k\) 个变成负数。

另外发现 Alice 保留的数一定是一段连续的前缀。考虑反证,若 Alice 保留的数按升序排序后是 \(a_1\sim a_{i-1}, a_{i + 1}\sim a_{m}\),若 \(k\le m - i\),则 Alice 少删一个 \(a_i\) 可以获得更大的价值;若 \(k\ge m-i\),则 Alice 把删 \(a_{i}\) 换成删 \(a_{m}\) 可以损失得更少。

则考虑枚举 Alice 保留的前缀长度,再减去 Bob 删的数的贡献,所有情况取最大值即可。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
const LL kInf = 1e15 + 2077;
//=============================================================
int n, k, x, a[kN];
LL sum[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); 
  return f * w;
}
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  int T = read();
  while (T --) {
    n = read(), k = read(), x = read();
    for (int i = 1; i <= n; ++ i) a[i] = read();
    std::sort(a + 1, a + n + 1);
    for (int i = 1; i <= n; ++ i) sum[i] = sum[i - 1] + a[i];
    // for (int i = n; i >= 1; -- i) sum[i] = sum[i + 1] + 1ll * a[i];

    LL ans = -kInf, s;
    for (int i = n, j = 0; i >= 0 && j <= k; -- i, ++ j) {
      s = sum[i];
      s -= 2ll * sum[i] - 2ll * sum[std::max(0, i - x)];
      ans = std::max(ans, s);
    }
    printf("%lld\n", ans);
  }
  return 0;
}

C

\(O(10^5)\)\(n\) 的因子个数最多只有 128 个(https://oeis.org/A066150),于是考虑枚举每段的长度 \(k\),先考虑如何每段的同一个位置的数 \(\{a_i, a_{k + i}, a_{2\times k + i}\cdots\}\) 变为相同的数。对某个数 \(m\) 取模之后 \(\{a_i, a_{k + i}, a_{2\times k + i}\cdots\}\) 变为相同的数,即这些数模 \(m\) 均相等,说明这些数之间的差均是 \(m\) 的倍数,则考虑将这些数升序排序并求相邻数之差,将差取 \(\gcd\) 即为 \(m\) 可能的最大值,且这个数的所有因子均可作为 \(m\)

于是得到了使每段中同一位置变为相同的最大值 \(m_1, m_2, \cdots, m_k\),由上述分析,再将这些数取 \(\gcd\) 即为满足题意的最大的 \(m\),若 \(\ge 2\) 则当前枚举的 \(k\) 对答案贡献为 1,否则为 0。

实际实现上并不需要显式地把每段同一位置的数分离出来排序后作差再求 gcd,仅需枚举数列中所有距离为 \(k\) 的数对作差并对差取 gcd 即可。

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

下面是赛时的麻烦写法、、、

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int n, ans, a[kN], b[kN], c[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); 
  return f * w;
}
int gcd(int x_, int y_) {
  return y_ ? gcd(y_, x_ % y_) : x_;
}
void Solve(int k_) {
  if (k_ == n) {
    ++ ans;
    return ;
  }
  int g = 0;
  for (int i = 1; i <= k_; ++ i) {
    int p = 0;
    for (int j = 0; j * k_ + i <= n; ++ j) {
      b[++ p] = a[j * k_ + i];
    }
    std::sort(b + 1, b + p + 1);
    for (int j = p; j > 1; -- j) b[j] -= b[j - 1];
    for (int j = 2; j <= p; ++ j) {
      if (b[j] == 0) continue;
      g = gcd(g, b[j]);
    }
  }
  ans += (g != 1);
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  int T = read();
  while (T --) {
    ans = 0;

    n = read();
    for (int i = 1; i <= n; ++ i) a[i] = read();
    for (int i = 1, lim = sqrt(n); i <= lim; ++ i) {
      if (n % i) continue;
      Solve(i);
      if (i * i != n) Solve(n / i);
    }
    printf("%d\n", ans);
  }
  return 0;
}

D

挺一眼的递归分治题呃呃

在读入操作过程中计算此时数列的长度,若长度大于 \(10^{18}\) 则记为极大值。

操作 2 并不会生成新种类的权值,则所有询问实质上都可以对应到某次操作 1 上。于是考虑反向地撤销操作并减少数列的长度,设撤销后数列长度为 \(l\),枚举所有大于 \(l\) 的询问,若这次操作为 1 则该询问的值即为此次操作 1 增加的值;若这次操作为 2 则计算该询问在撤销后的数列对应的位置,留到之后的撤销中解决即可。

在撤销过程中,对询问维护大根堆即可实现。

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

注意维护数列长度时可能爆 LL,开 __int128

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define pr std::pair
#define mp std::make_pair
#define LL __int128
// #define LL long long
const int kN = 1e5 + 10;
const LL kInf = 1e18 + 2077;
//=============================================================
int n, q;
struct Data {
  int opt, x;
  LL nowl;
} a[kN];
int ans[kN];
//=============================================================
inline LL read() {
  LL f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3ll) + (w << 1ll) + (ch ^ '0'); 
  return f * w;
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  int T = read();
  while (T --) {
    n = read(), q = read();
    for (int i = 1; i <= n; ++ i) {
      a[i].opt = read(), a[i].x = read();
      if (a[i].opt == 2) {
        a[i].nowl = a[i - 1].nowl + (LL) a[i].x * a[i - 1].nowl;
      } else {
        a[i].nowl = a[i - 1].nowl + 1;
      } 
      if (a[i].nowl > kInf) a[i].nowl = kInf;
    }

    std::priority_queue <pr <LL, int> > query;
    for (int i = 1; i <= q; ++ i) query.push(mp(read(), i));
    for (int i = n; i; -- i) {
      while (!query.empty() && (query.top().first) > a[i - 1].nowl) {
        pr <LL, int> now = query.top(); query.pop();
        if (a[i].opt == 1) {
          ans[now.second] = a[i].x;
        } else {
          now.first = now.first % a[i - 1].nowl;
          if (now.first == 0) now.first = a[i - 1].nowl;
          query.push(now);
        }
      }
    }
    for (int i = 1; i <= q; ++ i) printf("%d ", ans[i]);
    printf("\n");
  }
  return 0;
}

E

挺一眼的 DP,手速慢了没开到亏死,还是菜。

发现任意 01 串都可以通过不断地向字符串尾部追加一段 1000....(0 个数任意),最后再删去第一个 1 来得到。设上述过程中一共追加了 \(m\) 段且第 \(i\) 段长度为 \(a_i\),则其中仅包含一个 1 的子串个数为:

\[\sum_{i = 1}^{m - 1} a_i\times a_{i + 1} \]

另外为了满足仅包含一个 1 的子串长不大于 \(k\),需要钦定 \(a_i + a_{i + 1} - 1\le k\)

于是考虑在按照上述规则构造的同时 DP,设 \(f_{i, j}\) 表示含有 \(i\) 个合法子串,且最后一段追加的 1000... 长度为 \(j\) 的方案数。初始化考虑第一段的长度,有:\(\forall 1\le i\le k, f_{0, i} = 1\),转移时枚举上一段的长度 \(l\),则有:

\[f_{i, j} = \sum_{l=1}^{\min\left\{ \left\lfloor\frac{i}{j}\right\rfloor, k - j + 1\right\}} f_{i - j\times l, l} \]

注意这里的枚举上界在考虑了合法子串长度不大于 \(k\) 的同时,为了保证 \(i-j\times l\ge 0\),还需要对 \(\left\lfloor\frac{i}{j}\right\rfloor\)\(\min\)。则在固定了 \(i\) 情况下,\(l\) 的枚举次数之和为调和级数,即有:\(\left\lfloor\frac{i}{1}\right\rfloor + \left\lfloor\frac{i}{2}\right\rfloor + \cdots + \left\lfloor\frac{i}{i}\right\rfloor\approx i\ln i\)。则虽然有三重循环,DP 的时间复杂度只有 \(O(nk\ln n)\) 级别。

答案即为:

\[\sum_{i = 1}^{k} f_{n, i} \]

总空间复杂度 \(O(nk)\) 级别,时间复杂度 \(O(nk\ln n)\) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2510;
const LL p = 998244353;
//=============================================================
int n, k;
LL ans, f[kN][kN];
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); 
  return f * w;
}
void Init() {
  ans = 0;
  for (int i = 0; i <= n; ++ i) {
    for (int j = 1; j <= k; ++ j) {
      f[i][j] = 0;
    }
  }
}
void DP() {
  for (int i = 1; i <= k; ++ i) f[0][i] = 1;
  for (int i = 1; i <= n; ++ i) {
    for (int j = 1; j <= k; ++ j) {
      for (int l = 1, lim = std::min(k - j + 1, i / j); l <= lim; ++ l) {
        f[i][j] = (f[i][j] + f[i - j * l][l]) % p;
      }
    }
  }
  for (int i = 1; i <= k; ++ i) ans = (ans + f[n][i]) % p;
}
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  int T = read();
  while (T --) {
    n = read(), k = read();
    Init();
    DP();
    printf("%lld\n", ans);
  }
  return 0;
}

写在最后

学到了什么:

  • C:余数相同的数作差后肯定是模数的倍数。一张非常经典的表格。
  • E:枚举上限是 \(\frac{n}{i}\) 的形式,复杂度可能是调和级数。
posted @ 2024-01-19 19:09  Luckyblock  阅读(11)  评论(0编辑  收藏  举报