题解 AtCoder Regular Contest 184 / ARC184A~E

A - Appraiser

如果可以询问 \(n-1\) 次就好了。那样的话我们只需要拿硬币 \(1\) 和其他所有硬币逐个询问一下,就能通过与 \(1\) 类型相同的硬币的数量判断哪些硬币是可疑的。也就是说,有 \(n\) 个硬币时,可以通过 \(n-1\) 次操作将这些硬币划分为两个集合,其中一个集合是可疑的,另外的集合都能排除嫌疑。

为此,我们将 \(n\) 枚硬币划分为 \(50\) 个集合,每个集合 \(20\) 枚,每个集合内部进行上述的算法,除非划分的两个集合大小都是 \(10\),否则数量少的集合一定是可疑的。而前者的情况只会出现至多一次,且出现时说明其他集合的硬币都不可疑,只需要拿集合中任意一枚硬币和已经排出嫌疑的硬币询问即可。一共 \(951\) 次询问。

为了卡这剩下的一次询问,我们拿最后一个大小为 \(20\) 的集合出来,单独讨论。如果在处理最后一个集合前,已经发现了 \(10\) 枚可疑的硬币,那么最后一个集合不用处理;否则如果发现了至少 \(1\) 枚,说明之前没有遇到过 \(10,10\) 的情况,最后一个集合运行上面的算法;否则可能出现 \(10,10\),为了避免这种情况,考虑拿出最后一个集合的两枚硬币,做如下分讨:

  • 如果其他 \(18\) 枚硬币划分的最大的集合超过 \(10\),可以发现这是不可能的,因为这时可疑的硬币数量不够。
  • 那么只有两种情况:划分了 \(9,9\)\(8,10\),剩余两次询问。
  • \(9, 9\):随意询问一枚硬币与不可疑的硬币,这样可以直接找到 \(9\) 枚可疑硬币,剩下两枚直接随意拿一枚和不可疑的硬币询问,找到最后一枚。
  • \(8,10\):随意询问 \(10\) 集合中的一枚硬币与不可疑的硬币,如果发现是可疑的,那么这 \(10\) 枚硬币都是可疑的硬币;否则另外 \(8\) 枚硬币和没询问的 \(2\) 枚硬币都是可疑的硬币。

询问次数刚好 \(950\),可以通过。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr, ##__VA_ARGS__)
#else
#define endl "\n"
#define debug(...) void(0)
#endif
using LL = long long;
int n, m, q;
vector<int> ans;
bool query(int x, int y) {
  cout << "? " << x << " " << y << endl;
  int ret;
  cin >> ret;
  return ret;
}
void work(int l, int r) {
  vector<int> st[2];
  st[0].push_back(l);
  for (int i = l + 1; i <= r; i++) st[query(l, i)].push_back(i);
  if (st[0].size() == st[1].size()) {
    int tmp = l == 1 ? r + 1 : 1;
    ans = st[!query(tmp, st[0].front())];
  } else {
    if (st[0].size() > st[1].size()) swap(st[0], st[1]);
    for (int x : st[0]) ans.push_back(x);
  }
}
int main() {
#ifndef LOCAL
#endif
  cin >> n >> m >> q;
  for (int i = 0; i < 50 - 1; i++) work(i * 20 + 1, (i + 1) * 20);
  if (!ans.empty() && ans.size() < 10) work(49 * 20 + 1, 50 * 20);
  else if (ans.empty()) {
    int l = 49 * 20 + 1, r = 50 * 20;
    vector<int> st[2];
    st[0].push_back(l);
    for (int i = l + 1; i <= r - 2; i++) st[query(l, i)].push_back(i);
    if (st[0].size() < st[1].size()) swap(st[0], st[1]);
    if (st[0].size() == 10) {
      if (query(st[0][0], 1)) ans = st[0];
      else ans = st[1], ans.push_back(r - 1), ans.push_back(r);
    } else {
      if (query(st[0][0], 1)) ans = st[0];
      else ans = st[1];
      if (query(r, 1)) ans.push_back(r);
      else ans.push_back(r - 1);
    }
  }
  cout << "! ";
  for (int x : ans) cout << x << " ";
  cout << endl;
  return 0;
}

B - 123 Set

考虑将数字写为 \(2^{p_2}3^{p_3}C\) 的形式,其中 \(\gcd(C, 6)=1\)。这样以后,将 \(C\) 相同的数字放在一起,可以发现将它们按照 \((p_2, p_3)\) 的坐标写在纸上,将组成一个阶梯形网格图,操作就是将 \((p_2, p_3), (p_2+1, p_3), (p_2, p_3+1)\) 加进去。由于较短的一边的长度为 \(\log_310^9+1<20\),可以对这条边做状压 dp。有一个枚举集合有交并的部分,我们使用 FWT 将所有位置同步打一份到它的子集上,将有交并改成无交并。\(C=1\) 时计算量约为 \(20\times 2^{20}\times \log_210^9\)。大概是可以接受的。

但是 \(C\) 会有 \(O(n)\) 个,即使你发现 \(C=6k+1\)\(6k+5\) 将常数除以 \(3\) 也难以通过。我们发现因为我们的限制是 \(2^{p_2}3^{p_3}C\leq n\) 也就是 \(2^{p_2}3^{p_3}\leq n/C\),可以直接对 \(n/C\) 整除分块,将 \(\left\lfloor n/C\right\rfloor\) 相同的 \(C\) 直接一起计算,可以通过本题。复杂度不知道。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr, ##__VA_ARGS__)
#else
#define endl "\n"
#define debug(...) void(0)
#endif
#define popcount __builtin_popcount
using LL = long long;
void work(vector<int>& a) {
  for (int j = 0; 1 << j < a.size(); j++) {
    for (size_t i = a.size(); i--; ) {
      if (i >> j & 1) a[i ^ (1 << j)] = min(a[i ^ (1 << j)], a[i]);
    }
  }
}
int solve(int n) {
  vector<int> a;
  for (LL now = n; now; now /= 3) a.push_back(__lg(now));
  vector<int> b(a[0] + 1);
  for (size_t i = 0; i < a.size(); i++) b[a[i]] = i + 1;
  for (int i = a[0]; i >= 1; i--) b[i - 1] = max(b[i - 1], b[i]);
  vector<int> f(1 << b[0], 1e9);
  f[0] = 0;
  for (int i = 0; i <= a[0]; i++) {
    debug("b[%d] = %d\n", i, b[i]);
    int U = (1 << b[i]) - 1;
    for (size_t i = 0; i < f.size(); i++) f[i & U] = min(f[i & U], f[i]);
    f.resize(1 << b[i]);
    vector<int> nxt(1 << b[i], 1e9);
    work(f);
    for (int S = 0; S < 1 << b[i]; S++) {
      int T = (S | S << 1) & U;
      nxt[S] = min(nxt[S], popcount(S) + f[U ^ T]);
    }
    f = move(nxt);
  }
  debug("solve(%d) = %d\n", n, *min_element(f.begin(), f.end()));
  return *min_element(f.begin(), f.end());
}
int n, ans;
int calc(int r) {
  return (r >= 1 ? (r - 1) / 6 + 1 : 0) + (r >= 5 ? (r - 5) / 6 + 1 : 0);
}
int main() {
#ifndef LOCAL
  cin.tie(nullptr)->sync_with_stdio(false);  
#endif
  cin >> n;
//ans += (n - 1) / 6 + 1;
//if (n >= 5) ans += (n - 5) / 6 + 1;
//for (int x = 0; x <= n / 3 - 1; x += 6) ans += solve(n / (x + 1)) - 1;
//for (int x = 0; x <= n / 3 - 5; x += 6) ans += solve(n / (x + 5)) - 1;
  for (int l = 1, r; l <= n; l = r + 1) {
    r = n / (n / l);
    if (calc(l - 1) != calc(r)) ans += solve(n / l) * (calc(r) - calc(l - 1));
  }
  cout << ans << endl;
  return 0;
}

C - Mountain and Valley Folds

你去找一张纸折一下,折 \(k\) 次,然后将折痕剪掉,发现 \(2^k\) 张小纸是反面朝上-正面朝上-……-反面朝上-正面朝上的顺序堆叠的。再把折痕搞回来,对 \(2^{k}-1\) 条折痕,从 \(1\) 开始编号,那么所有奇数项是 \(010101\cdots01\),去掉所有奇数项后又有这个规律,直到删空。归纳一下,\(x\) 是 Mountain Fold 当且仅当 \(\left\lfloor \frac{x/\text{lowbit}(x)}{2}\right\rfloor\) 是奇数。

那么做法就很显然了,搞个类似匹配的东西,对于 \(i+a_k\),枚举 \(\text{lowbit}=2^j\),当 \(i+a_k\) 的长度为 \(j\) 的前缀全零时,将 \(i\) 的这段前缀插入 Trie 树,然后枚举 \(\text{lowbit}\) 上一位取什么值,找这个儿子打个标记。答案就是 Trie 上标记最多的一条链。\(O(n\log a_i)\)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr, ##__VA_ARGS__)
#else
#define endl "\n"
#define debug(...) void(0)
#endif
using LL = long long;
constexpr int N = 3e5;
int n, ch[N][2], tot, cnt[N], ans;
int conbit(LL x, int b) { return b <= 60 ? x >> b & 1 : 0; }
void dfs(int u, int now) {
  ans = max(ans, now += cnt[u]);
  for (int r : {0, 1}) if (ch[u][r]) dfs(ch[u][r], now);
}
int main() {
#ifndef LOCAL
  cin.tie(nullptr)->sync_with_stdio(false);  
#endif
  cin >> n;
  for (int i = 1; i <= n; i++) {
    LL x;
    cin >> x;
    int p = 0, u = 0;
    for (int j = 0; j < 100; j++) {
      for (int r : {0, 1}) if (!ch[u][r]) ch[u][r] = ++tot;
      int b = conbit(x, j), r = p + b == 1;
      for (int rp : {0, 1}) {
        int v = ch[u][!r];
        if ((conbit(x, j + 1) + (!r + b + p) / 2 + rp) & 1) {
          if (!ch[v][rp]) ch[v][rp] = ++tot;
          cnt[ch[v][rp]] += 1;
        }
      }
      p = (r + b + p) >> 1;
      u = ch[u][r];
    }
  }
  dfs(0, 0);
  cout << ans << endl;
  return 0;
}

D - Erase Balls 2D

定义 \(a[i]\) 为横坐标是 \(i\) 的小球的纵坐标。发现选择的任意两个 \(k\) 必然不能满足那个偏序关系(\(k_1<k_2, a[k_1]<a[k_2]\)),也就是说只能 \(a[k_1]>a[k_2]\)。不妨直接枚举选择的 \(k\) 的序列,\(dp_i\) 表示当前最后一个 \(k\)\(i\),然后可以转移。但是可能会算重,我们只需要保证 \(dp_i\to dp_j\) 的转移中间不存在 \(k\in (i, j)\) 使得 \(dp_i\to dp_k\to dp_j\)\(dp_i\to dp_j\) 两种转移能保留的球一样就能转移过去。记一个 \(cnt_{i, j}\) 表示 \(\sum_{k=i}^j[a_i>a_k>a_j]\),直接加一下判断有没有多的点。\(O(n^3)\)

点击查看代码
#include "atcoder/modint"
#include <bits/stdc++.h>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr, ##__VA_ARGS__)
#else
#define endl "\n"
#define debug(...) void(0)
#endif
using LL = long long;
using mint = atcoder::modint998244353;
int n, a[310];
mint f[310], c[310][310];
int main() {
#ifndef LOCAL
  cin.tie(nullptr)->sync_with_stdio(false);  
#endif
  cin >> n;
  for (int i = 1, x, y; i <= n; i++) cin >> x >> y, a[x] = n - y + 1;
  f[0] = 1;
  a[n + 1] = n + 1;
  for (int i = 1; i <= n; i++) {
    for (int l = 0; l < i; l++) if (a[l] < a[i]) {
      for (int r = i + 1; r <= n + 1; r++) if (a[i] < a[r]) c[l][r] += 1;
    }
  }
  for (int i = 0; i <= n; i++) {
    for (int j = i + 1; j <= n + 1; j++) if (a[i] < a[j]) {
      bool flag = false;
      for (int k = i + 1; k < j && !flag; k++) if (a[i] < a[k] && a[k] < a[j] && c[i][k] + c[k][j] + 1 == c[i][j]) flag = true;
      if (!flag) f[j] += f[i];
    }
  }
  cout << f[n + 1].val() << endl;
  return 0;
}

E - Accumulating Many Times

首先不要读错题目了。然后我们稍微想一下怎么求单个 \(f\)。为了求这个,我们至少需要会快速做 \(x\) 次操作。我们有一个经典的东西,就是考虑 \(b\) 为一个长为 \(m\) 的 0/1 数组,然后 \(x\) 次操作后,从 \(b_i\) 走到 \(b_j\)\(\binom {j - i+x - 1}{x - 1}\) 种走法(每次可以前进任意非负整数步,插板法),所以 \(b_i\)\(b_j\) 的贡献是 \(b_i\binom {j-i+x-1}{x-1}\bmod 2\)。可以写一个 \(O(m^2)\) 的东西,如果你观察到那坨东西 \(=[z^{j-i}](1-z)^x\) 然后也发现不了什么 0/1 序列卷积有什么性质,你就不得不写一个卷积,将 \(\sum_{i=0}^{m-1}b_iz^i\)\((1-z)^x\) 做卷积,\(O(m\log m)\) 的时间就可以快速对 \(b\)\(x\) 次操作。

但是单个 \(f\) 没有二分性,还是比较自闭,但是我们看这个 \(\binom {j-i+x-1}{x-1}\bmod 2\) 因为有个 Lucas 定理可以写成 \([x-1\subseteq j-i+x-1]\),先小小换个元 \(stp=x-1\) 那就是 \([stp\subseteq j-i+stp]\),观察到如果 \(stp\)\(j-i\) 还大,就是 \(stp\) 的二进制上有一些位比 \(j-i\) 的最高位还高,去掉它必然不影响结果,也就是说 \(stp\) 会有个关于 \(j-i\) 的周期。于是就有一个方法:先确定到达前 \(2^0\) 个正确需要多少步,然后到达前 \(2^1\) 正确需要多少步(这里只有两种决策 \(+0\)\(+2^0\)),然后到达前 \(2^2\) 正确需要多少步(这里只有两种决策 \(+0\)\(+2^1\))……以此类推,就可以求出单个 \(f\)(当然有很大可能无法相等,直接退出)复杂度还是 \(O(m\log m)\)

接下来的一步是尝试将可以互相转化的 \(b\) 缩成等价类,我们先去掉前导零,然后将 \(b\) 变化为这样的形式:\(b_0, b_1, b_2, b_4, b_8, \cdots\) 都为 \(1\),显然如果可以互相转化则都可以转化到这种状态,而且和刚才的方法差不多。缩完等价类后,就知道了到达这个代表元的步数,自然互相转化的步数就是到达代表元的步数的差,当然要和 \(2\) 的某个次幂取模,用树状数组维护一下这个取模要加多少个 \(2^k\)\(O(nm\log m)\)

点击查看代码

atcoder:: 含量达到了惊人的 \(4\)

#include "atcoder/all"
#include <bits/stdc++.h>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr, ##__VA_ARGS__)
#else
#define endl "\n"
#define debug(...) void(0)
#endif
using LL = long long;
using mint = atcoder::modint998244353;
int n, m;
vector<int> perf(const vector<int>& vec, int stp) {
  int n = vec.size();
  vector<int> tmp(n);
  for (int j = 0; j < n; j++) tmp[j] = (stp & (j + stp)) == stp;
  auto&& ret = atcoder::convolution<998244353>(vec, tmp);
  for (int j = 0; j < n; j++) tmp[j] = ret[j] & 1;
  return tmp;
}
auto work() {
  vector<int> vec(m);
  for (int i = 0; i < m; i++) cin >> vec[i];
  auto bg = vec.begin(), ed = vec.end();
  while (bg != vec.end() && *bg == 0) ++bg;
  int stp = 0, sz = ed - bg;
  for (int j = 1; j < sz; j <<= 1) {
    vector<int> slic(bg, bg + min(j << 1, sz));
    if (!perf(slic, stp)[j]) stp += j, assert(perf(slic, stp)[j]);
  }
  return make_pair(perf(vec, stp), stp);
}
map<vector<int>, vector<int>> mp;
int main() {
#ifndef LOCAL
  cin.tie(nullptr)->sync_with_stdio(false);  
#endif
  cin >> n >> m;
  for (int i = 1; i <= n; i++) {
    auto&& e = work();
    mp[e.first].push_back(e.second);
  }
  mint ans = 0;
  for (auto&& e : mp) {
    int pos0 = 0;
    while (pos0 < (int)e.first.size() && e.first[pos0] == 0) ++pos0;
    if (pos0 == (int)e.first.size()) continue;
    int p = atcoder::internal::bit_ceil(e.first.size() - pos0);
    atcoder::fenwick_tree<mint> fw0(p);
    mint prev = 0, pre0 = 0;
    for (auto&& x : e.second) {
      ans += prev - pre0 * x + fw0.sum(0, x) * p;
      prev += x, pre0 += 1, fw0.add(x, 1);
    }
  }
  cout << ans.val() << endl;
  return 0;
}

update:避免使用 NTT 的方法,参见 https://www.luogu.com/article/1z7le77j

posted @ 2024-09-23 17:38  caijianhong  阅读(149)  评论(0编辑  收藏  举报