CodeChef January Challenge 2021 题解

只有前面 9 题后面先咕

【DIVTHREE】 Chef and Division 3

输出 \(\min\left(d, \left\lfloor\frac{\sum_{i=1}^n a_i}{k}\right\rfloor\right)\) 即可。

【DECODEIT】Encoded String

对于输入串的每个四位进行解密。类似于二分:一开始答案区间为 \([\texttt a, \texttt p]\)。然后对于当前位,如果为 \(0\) 那么取左半部分,反之则为右半部分。

【FAIRELCT】Fair Elections

一个显然的贪心思路:每次选择最小的 \(A\) 与最大的 \(B\) 交换。那么维护两个 std::multiset 表示选票集合可以很方便完成这个操作。当出现最小的 \(A\) 大于最大的 \(B\) 是输出 -1

【BILLRD】Point Of Impact

考虑到桌面是个 \(N\times N\) 的正方形,于是这个球要么直接撞到桌角然后停止,要么永远撞不到桌角并有四个固定的撞击点。于是随便分讨就做完了,然而细节尤其要注意。

【WIPL】Watching CPL

题意:你有 \(N\) 个箱子,第 \(i\) 个高度 \(H_i(\le 10^5)\)。你需要用这些盒子搭出两座塔,高度均 \(\ge K\)。求最少需要几个盒子。\(N, K\le 4\times 10^3\)

首先毋庸置疑的是,选相同个数的盒子的情况下,必然去选高度大的。那么不妨将盒子按大小降序排序,然后二分答案前 \(x\) 个。

然后就是一个类似与背包的东西:求出用这些盒子可以到达的高度集合。对于每一个可到达的高度 \(i\),两座塔的高度就分别是 \(i\)\((\sum_{j=1}^x a_j)-i\)。显然只要判断高度集合中 \(K\) 的前驱后继即可。

最后就是如何求出高度集合 \(T\),方便操作先将 \(T\) 压成一个 std::bitset。设当前考虑到第 \(i\) 个物品,\(T\gets (T\ll H_i)\ |\ T\) 即可。复杂度 \(O\left(\frac{NH}{w}\log N \right)\)。代码出乎意料的好写:

const int N = 4e3 + 5;
const int H = 2e5 + 5;
int n, K, h[N];
std::bitset<H> f;

bool judge(int k) {
  f.reset(), f.set(0); long long s = 0;
  for (int i = 1; i <= k; i++)
    f |= (f << h[i]), f.set(h[i]), s += h[i];
  int L = f._Find_next(K - 1);
  if (L != H && s - L >= K) return 1;
  return 0;
}

void solve() {
  std::cin >> n >> K;
  for (int i = 1; i <= n; i++)
    std::cin >> h[i];
  std::sort(h + 1, h + 1 + n, std::greater<int>());
  int lb = 1, ub = n, ans = n + 1;
  while (lb <= ub) {
    int mid = (lb + ub) / 2;
    if (judge(mid)) ans = mid, ub = mid - 1;
    else lb = mid + 1;
  }
  if (ans == n + 1) std::cout << -1 << std::endl;
  else std::cout << ans << std::endl;
}

【ANTSCHEF】Chef and Ants

\(N(\le 2\times 10^5)\) 条不重合但交于一点 \(O\) 的直线,第 \(i\) 条直线上有 \(M_i(M_i\le 5\times 10^5, \sum_i M_i \le 10^6)\) 只蚂蚁,位置分别为 \(X_{i, 1}, X_{i, 2}, \cdots, X_{i, M_{i}}(|X_{i,j}\in [1, 10^9]|)\)。一开始所有蚂蚁面向 \(O\) 点同速度移动。当有两只蚂蚁发生碰撞时,所有碰撞的蚂蚁都会同时反向。求最终碰撞总次数(多只蚂蚁算一次)。

根据套路,蚂蚁相撞看做擦肩而过最好处理。对于多只蚂蚁在 \(O\) 处相撞的情况,不能简单地按原路继续走下去,和另外那些不会在 \(O\) 相撞的存在本质区别。那就先按是否会在 \(O\) 处发生相撞分为两类:不相撞的为孤立点,反之为非孤立点。

首先对于一个非孤立点,与之互相产生贡献的点有,这个点后面的点,以及在 \(O\) 处相撞的点。

然后考虑孤立点,与之产生贡献的除了同线上的其他孤立点,还有某些非孤立点。这些非孤立点会在到达 \(O\) 之前与这个孤立点产生贡献。

很显然这种题根本讲不清楚于是这里放一个我的阴间实现,复杂度一只 \(\log\)STL 党狂喜

const int N = 2e5 + 5;
const int M = 1e6 + 5;

int n, m[N];
std::vector<int> x[N], t[N], v[N];
std::map<int, std::set<std::pair<int, int> > > l;
long long ans = 0;

inline int countPre(std::vector<int>& a, int p) {
  return std::lower_bound(a.begin(), a.end(), p) - a.begin();
}
inline int countNxt(std::vector<int>& a, int p) {
  return a.end() - std::upper_bound(a.begin(), a.end(), p);
}
inline void clear() {
  for (int i = 1; i <= n; i++) x[i].clear();
  for (int i = 1; i <= n; i++) t[i].clear();
  for (int i = 1; i <= n; i++) v[i].clear();
  l.clear(), ans = 0;
}

void solve() {
  std::cin >> n;
  for (int i = 1; i <= n; i++) {
    std::cin >> m[i], x[i].resize(m[i]);
    for (auto& it : x[i]) std::cin >> it, l[abs(it)].emplace(i, it);
    std::sort(x[i].begin(), x[i].end());
  }

  for (auto& s : l) {
    if (s.second.size() == 1u) {
      t[s.second.begin()->first].push_back(s.second.begin()->second);
      continue;
    }
    for (auto i = s.second.begin(); i != s.second.end(); i++) {
      if (i->second < 0) ans += countPre(x[i->first], i->second);
      else ans += countNxt(x[i->first], i->second);
      v[i->first].push_back(i->second);
    }
    ++ans;
  }

  for (int i = 1; i <= n; i++) if (!t[i].empty()) {
    std::sort(t[i].begin(), t[i].end());
    std::sort(v[i].begin(), v[i].end());
    for (auto it = t[i].begin(); it != t[i].end(); it++) {
      if (*it < 0) ans += countNxt(t[i], 0), ans += countNxt(v[i], -*it);
      else ans += countPre(v[i], -*it);
    }
  }

  std::cout << ans << std::endl;
  clear();
}

【TILESQRS】Guess the Tiling

你在一个大小为 \(N\times N\) 的房间中,地上铺有 \(N\)\(N\) 列正方形地砖,地砖有两种花纹,为正反两种对角线。其中一组 \(2\times 2\) 的地砖在调整后可以构成一个 \(\sqrt 2 \times \sqrt 2\) 的正方形。其中“调整”指将某块地砖旋转 \(90^\circ\)

这是一道交互题。一开始你并不知道初始局面,只知道 \(N(\le 200)\)\(K\)(初始局面中构成的 \(\sqrt 2 \times \sqrt 2\) 的正方形(下称“次小正方形”)的总个数),以及你可以调整的最大次数 \(Q(\ge 4\cdot N^2)\)。接下来你可以进行若干次“调整”操作,选定一个地砖将其旋转,之后你又会马上得到新的 \(K\)

你需要在结束时输出结束时的局面。

首先一个 Naive 的想法是,通过 \(2^4=16\) 次调整(可以理解为格雷码)可以遍历一个 \(2\times 2\) 的正方形的所有状态,尽管并不知道具体是什么。然后对于 \(N = 2\) 的情况我们可以这样搞:直接遍历这些状态,什么时候得到的 \(K=1\) 说明现在是一个次小正方形,就可以直接输出了。

然后这个简单的方法比较难以拓展到更大的 \(N\),因为这四块地砖还有可能和周围的构成其他的次小正方形。但是我们将原思路稍加改变:对于一个 \(2\times 2\) 的区域,只遍历除第一个外的另外三个的 \(2^3\) 个状态,对于每个状态都试转一下第一块,根据前后的 \(K\) 的变化判断即可。

更具体的,对于 \(N\times N\) 的区域,四个一组处理。如果这个组在上或左边缘上,可以单纯根据 \(K\) 是否变化确定;而对内部的组而言,原先四周复杂的情况变得只需讨论左上角,又因为我们是顺序处理下来的,于是一旦凑成次小正方形,前后 \(K\) 的变化量一定为 \(2\)(左上已经凑成了)。

不过如果 \(N\) 为奇数是会出现剩下最后一行 & 列。这个不难搞,只要先降倒数第二行 & 列都转一遍,然后最后一行两块两块枚举状态即可。注意最后一块也需要特殊处理。

然后就做完了,不难发现每四个最多花费 \(15\) 次确定,不会超过交互次数。接下来放个代码:

const int N = 205;
const int order_x[7] = {1, 0, 1, 1, 1, 0, 1};
const int order_y[7] = {0, 1, 0, 1, 0, 1, 0};
int n, K, Q;
bool ans[N][N];

inline int test(int i, int j) {
  std::cout << "1 " << i << ' ' << j << std::endl;
  int ret; std::cin >> ret;
  if (ret == -1) std::cout << -1 << std::endl, exit(0);
  return ret;
}

#define abs(x) ((x) > 0 ? (x) : -(x))
inline bool invaild(int i, int j) {
  int cK = test(i, j);
  bool isIn = (i > 1) && (j > 1);
  if ((cK == K && !isIn) || (abs(cK - K) <= 1 && isIn))
    return K = cK, 1;
  ans[i + 1][j] = ans[j][i + 1] = 1;
  ans[i][j] = bool(cK < K), ans[i + 1][j + 1] = 0;
  return K = cK, 0;
}
#undef abs

void solve() {
  std::cin >> n >> Q >> K;
  for (int i = 1; i <= n; i++)
    for (int j = 1; j <= n; j++)
      ans[i][j] = 0;
  
  for (int i = 1; i < n; i += 2)
    for (int j = 1; j < n; j += 2)
      if (invaild(i, j)) for (int d = 0; d < 7; d++) {
        K = test(i + order_x[d], j + order_y[d]);
        if (!invaild(i, j)) break;
      }

  if (n & 1) {
    for (int i = 1; i < n; i++)
      K = test(i, n - 1), ans[i][n - 1] ^= 1;
    for (int j = 1; j < n - 1; j++)
      K = test(n - 1, j), ans[n - 1][j] ^= 1;

    for (int j = 1, cK; j < n; j += 2) {
      // (n, j), (n, j + 1)
      K = test(n, j + 1), cK = test(n, j);
      if (cK != K) {
        ans[n][j] = (cK > K), ans[n][j + 1] = 0, K = cK;
      } else {
        K = test(n, j + 1), cK = test(n, j);
        if (cK != K) ans[n][j] = (cK > K), ans[n][j + 1] = 0, K = cK;
      }
    }
    
    for (int i = 1, cK; i < n; i += 2) {
      // (i, n), (i + 1, n)
      K = test(i + 1, n), cK = test(i, n);
      if (cK != K) {
        ans[i][n] = (cK > K), ans[i + 1][n] = 0, K = cK;
      } else {
        K = test(i + 1, n), cK = test(i, n);
        if (cK != K) ans[i][n] = (cK > K), ans[i + 1][n] = 0, K = cK;
      }
    }

    K = test(n - 1, n - 1);
    K = test(n, n - 1), K = test(n - 1, n);
    ans[n - 1][n - 1] = 0;
    ans[n][n - 1] = ans[n - 1][n] = 1;
    ans[n][n] = bool(K > test(n, n));
  }

  std::cout << 2 << std::endl;
  for (int i = 1; i <= n; i++)
    for (int j = 1; j <= n; j++) {
      std::cout << ans[i][j];
      if (j == n) std::cout << std::endl;
      else std::cout << ' ';
    }
  
  int result; std::cin >> result;
  if (result == -1) exit(0);
}

【ORAND】And-Or Game

给定两个集合 \(A,B\)(值域 & 大小为 \(2^{20}\))。一开始你有一个数 \(V\),初值为 \(0\)。每次操作你可以:

  • 选出一个 \(X\in A\),执行 \(V \gets V \lor X\)(按位或);
  • 选出一个 \(X\in B\),执行 \(V \gets V \land X\)(按位与);

你可以进行任意轮(可以为 \(0\))操作,求 \(V\) 最终值可能有几个。

首先,一个数可以作为答案,必然可以表示为 \(\widehat a_1 \land \widehat b_1 \lor \widehat a_2 \land \widehat b_2 \lor \widehat a_3 \land \widehat b_3\lor \cdots\) 的形式,其中 \(\widehat a\) 表示 \(A\) 中选出若干个元素进行 \(\lor\) 运算的结果,\(\widehat b\) 同理。

考虑处理出 \(\widehat a\)\(\widehat b\)。不妨先试着能不能搞出选出两个进行运算的结果的集合,然后这个事情做 \(|A|\) 次就可以得到 \(\widehat a\)。不难想到位运算卷积:\(a^\prime_i=\sum_{j\lor k=i} a_j\cdot a_k\)。注意这里 \(a_i = [i\in A]\)。使用 FMT 可以简单解决。拓展到 \(\widehat a\),只要对 FMT 之后的结果跑一轮快速幂即可。\(\widehat b\) 同理。这部分复杂度 \(O(N\log N)\)

然后就是将这些 \(\widehat a\)\(\widehat b\) 两者交错拼接。考虑一个性质:一个可能的最终结果至少可以由 \(10\)\(\widehat a\)\(\widehat b\) 构成。一次 \(\land\)\(\lor\) 操作可以使部分 \(0/1\) 位置反,如果这个操作没有效果显然可有可无,于是去除无效操作必然至少搞掉一个位,然后就卷 位数/2 次即可。

于是只要再做十次位运算卷积即可。总复杂度为小常数(不然怎么两 \(\log\) 过百万)的 \(O(N\log^2 N)\)

inline int get() {
  return rand() % 100 + 1;
}
void solve() {
  read(n), read(m), t = U = 0;
  for (int i = 0, a; i < n; i++) A[read(a)] = get(), U = std::max(U, a);
  for (int i = 0, b; i < m; i++) B[read(b)] = get(), U = std::max(U, b);
  t = (int)log2(U) + 1, U = 1 << t;

  A[0] = get(), B[U - 1] = get();
  or_fmt(A, t, 0), and_fmt(B, t, 0);
  for (int i = 0; i < U; i++) A[i] = fastpow(A[i], n);
  for (int i = 0; i < U; i++) B[i] = fastpow(B[i], m);

  memcpy(C, A, U << 2), or_fmt(C, t, 1);
  for (int test = 1; test <= 10; test++) {
    and_fmt(C, t, 0);
    for (int i = 0; i < U; i++) C[i] = (LL)C[i] * B[i] % mod;
    and_fmt(C, t, 1), or_fmt(C, t, 0);
    for (int i = 0; i < U; i++) C[i] = (LL)C[i] * A[i] % mod;
    or_fmt(C, t, 1);
  }

  int ans = 0;
  for (int i = 0; i < U; i++) if (C[i]) ++ans;
  printf("%d\n", ans);
  memset(A, 0, U << 2), memset(B, 0, U << 2), memset(C, 0, U << 2);
}

【BLKJK】Blackjack

你手头有 \(N(\le 10^3)\) 张卡牌,第 \(i\) 张卡牌的权值为 \(A_i(\le 10^3)\)。你现在开始从第一张开始将卡牌发出。当某一时刻发出卡牌的权值和正好处于一个给定区间 \([X(\le 10^3), Y(\le 10^3)]\) 内,那么你就赢了。如果这种时刻始终没有出现那么你就输了。现在有一个外挂,可以调换两张卡牌的顺序。你需要求出最少的调换次数是这副卡牌可以赢。

神必的 Bitset 标解:https://discuss.codechef.com/t/blkjk-editorial/83432

先 sto SegmentTree

本质上是要求调整后使存在一个前缀和落在区间 \([X,Y]\) 内。那么我们枚举这个前缀和是由几个元素构成的,即枚举一个断点 \(i\),使前 \(i\) 个与后 \(N-i\) 个进行若干交换后可以进入 \([X,Y]\)

注意到一个事情,结果上面的转化后,已经限定了左右之间交换,因为内部换来换去没有意义。另外,也不应该有一个元素换过去又换回来。

然后就是换几个的问题。考虑一个类似背包的 dp:\(f_{i,j}\) 表示前 \(i\) 个中,选出总和为 \(j\),最多可以选几个;\(g_{i,j}\) 表示后 \(i\) 个中选出总和 \(j\),至少选几个。可以把 \(f\) 理解为保留前面的,把 \(g\) 理解为换来后面的。那么预处理 \(f,g\) 之后,枚举断点 \(i\),再枚举前 \(i\) 个保留的权值和 \(j\),把剩下的扔到后面去,这样离目标就相差 \([X-j,Y-j]\) 了,这些就把另一半的给换过来。这里对答案产生 \(\max\left(i-f_{i,j},\min_{k = X-j}^{Y-j} g_{i+1,k}\right)\) 的贡献。

但是不知道各位是否发现一个问题:如果两边需要移走的个数不一样怎么办?注意题目只允许交换而不能插入。但是实际上,左边多的可以直接将 \(i\) 换进来,这个就直接被排出统计范围(实际只需要前 \(i-1\) 个的和);右边多的话也可以如法炮制将 \(i+1\) 换出去然后扩大统计范围。

不过这个算法是 \(O(N^3)\) 的,我们发现贡献的式子是个滑动窗口,上一个单调队列就能 \(O(N^2)\) 了。一个细节就是,虽然 \(A_i\le 10^3\),但 \(\sum_{i}A_i\) 可能在 \(10^6\) 级别。事实上一个离 \([X,Y]\) 很远的断点并不会对答案有什么贡献。

下面放我的代码,\(O(N^2\log N)\)(偷懒用了个 std::multiset):

const int N = 1.5e3 + 5;
const int inf = 0x3f3f3f3f;
int n, x, y, a[N];
int f[N][N], g[N][N];

int solve() {
  std::cin >> n >> x >> y;
  for (int i = 1; i <= n; i++) std::cin >> a[i];
  for (int i = 0; i <= n + 1; i++)
    memset(f[i], -inf, N << 2), memset(g[i], inf, N << 2);

  f[0][0] = 0;
  for (int i = 1; i <= n; i++)
    for (int j = 0; j < N; j++) {
      f[i][j] = f[i - 1][j];
      if (j - a[i] >= 0) f[i][j] = std::max(f[i][j], f[i - 1][j - a[i]] + 1);
    }
  g[n + 1][0] = 0;
  for (int i = n; i >= 1; i--) 
    for (int j = 0; j < N; j++) {
      g[i][j] = g[i + 1][j];
      if (j - a[i] >= 0) g[i][j] = std::min(g[i][j], g[i + 1][j - a[i]] + 1);
    }
  
  int ans = inf;
  for (int i = 1; i <= n; i++) {
    std::multiset<int> frog(g[i + 1] + x, g[i + 1] + y + 1);
    for (int j = 0; j < N; j++) {
      if (!frog.empty()) ans = std::min(ans, std::max(i - f[i][j], *frog.begin()));
      if (x - j - 1 >= 0 && x - j - 1 < N) frog.insert(g[i + 1][x - j - 1]);
      if (y - j >= 0 && y - j < N) frog.erase(frog.find(g[i + 1][y - j]));
    }
  }
  return ans >= inf / 2 ? -1 : ans;
}
posted @ 2021-01-07 17:01  -Wallace-  阅读(407)  评论(0编辑  收藏  举报