Codeforces Round 949 (Div. 2)

写在前面

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

妈的昨晚硬撑打了场 edu 上午实验下午爬山考试困困困

妈的什么二进制场,C 吃了个爽呃呃写得什么史山

A

二进制。

显然最优的操作方案是每次除掉一个质因数,则应选择区间 \([l, r]\) 中质因数次数之和最大的数。

特别指出了限制 \(2l < r\),即区间 \([l, r]\) 中一定有至少一个 2 的幂,则显然最好的选择是 \(x=2^{\log_{2}^{r}}\),答案即为 \(\log_2 r\)

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
//=============================================================
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  int T; std::cin >> T;
  while (T --) {
    int l, r; std::cin >> l >> r;
    std::cout << (int) log2(r) << "\n";
  }
  return 0;
}

B

二进制。

上来就打表的话可能会误入歧途。

首先手玩下容易发现,询问 \((n, m)\) 的结果即为 \(\max(0, n - m) \sim n + m\) 的二进制或。

那么如何求得一段连续区间 \([l, r]\) 的二进制或呢?考虑将区间贡献,看做对一个从 \(l\) 开始不断加 1 直至 \(r\) 的变量不断取二进制或。首先取上两端点 \(l, r\) 的贡献,然后可以发现其他贡献均来自于进位。若出现累加后进位 \(2^{i}\) 说明末尾 \(i\) 位变为了连续的 1,此时末尾的 \(i+1\) 位均会产生贡献,值为 \(2^{i + 1} - 1\)

于是问题变为如何找到最高位的进位,显然有 \(l\) 中该位为 0,\(r\) 中该位为 1,于是仅需找到 \(l\oplus r\)\(\oplus\) 为按位异或运算)中最高位即可。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
const int kN = 1e5 + 10;
int a[kN], b[kN];
//=============================================================
LL get(int n_, int m_) {
  int l = std::max(0, n_ - m_), r = n_ + m_, ret = l | r;
  for (int i = 30; i >= 0; -- i) {
    int val = (1 << i);
    if (val & (l ^ r)) {
      ret |= (val | (val - 1));
      break;
    }
  }
  return ret;
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  
  int T; std::cin >> T;
  while (T --) {
    int n, m; std::cin >> n >> m;
    if (m == 0) std::cout << n << "\n";
    else std::cout << get(n, m) << "\n";
  }
  return 0;
}

C

二进制,构造。

先特判下整个数列均为 -1 的情况,直接构造 1 2 1 2 ... 即可。然后发现 \(a'_i \not= i\) 的位置实际上将原数列分为了若干互不影响的段,仅需考虑如何构造每段 \([l, r]\)。保证了 \(a_i\le 10^8 < \frac{10^9}{2}\),于是第一段和最后一段分别构造为 \(\cdots a_{l}, 2\times a_{l}, a_{l}\)\(a_{r}, 2\times a_r, a_r \cdots\) 即可。考虑其他的有两个端点 \(a_l, a_r\) 限制的段如何构造。

先直接特判掉长度为 2 的段是否合法。

发现一种很重要的特殊情况是 \(a_{l} = a_{r}\),此时仅需检查区间长度的奇偶性即可判断是否有解,若有解仅需构造 \(a_l, 2\times a_l, a_l, \cdots, a_r, 2\times a_r, a_r\) 即可。

然后考虑一般情况。一个显然的想法是分别从两端 \(l, r\) 开始向内构造,找到某个位置 \(p\) 使得从两端开始构造在此处的元素相等。发现对于已知的 \(x\),若元素 \(y\) 与它可以相邻,则一定是下列三种情况之一:

  • \(y = \left\lfloor\frac{x}{2}\right\rfloor\),由 \(x\) 删去二进制最后一位得到。
  • \(y = 2\times x\),由 \(x\) 在二进制最后添加 0 得到。
  • \(y = 2\times x + 1\),由 \(x\) 在二进制最后添加 1 得到。

显然从两端开始构造时,比较好的方案是首先不断进行第一种构造,删去 \(a_l, a_r\) 的末位直至两者出现公共前缀后,才可以使用另外两种构造。此时即可套用 \(a_l = a_r\) 的做法了。

实现时可以先对 \(a_l, a_r\) 进行二进制分解,对二进制串求最长公共前缀即可。如果在此过程中出现该段不够长的情况,或套用 \(a_l = a_r\) 做法时出现奇偶性不合法的则无解。

上述做法需要对 \(O(n)\) 数量级的元素进行二进制分解,总时间复杂度上界 \(O(n\log v)\) 级别。


实际上述做法还可以简化,并不需要限定删去 \(a_l, a_r\) 的末位直至两者出现的公共前缀一定是最长公共前缀。可以分别从两端开始一直删直到 1,并考虑此时构造了多少位出来。若构造的位数大于区间总长仅需把多余的忽略,若不足区间总长再套用 \(a_l = a_r\) 做法。

特别的,因为没有最小化删去数量并判断奇偶性,需要最后再枚举构造出的数列检查合法性。

实现起来相当好写,而且复杂度降为线性。可以见这发提交:https://codeforces.com/contest/1981/submission/263497967

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 5e5 + 10;
const int kLim = 1e9;
//=============================================================
int n, a[kN], ans[kN];
//=============================================================
std::string get(int val_) {
  // if (val_ >= kLim || val_ <= 0) while(1);
  std::string s;
  while (val_) {
    s.push_back(val_ % 2 + '0');
    val_ /= 2;
  }
  std::reverse(s.begin(), s.end());
  return s;
}
bool solve(int l_, int r_) {
  ans[l_] = a[l_], ans[r_] = a[r_];
  if (l_ == 0 && r_ == n + 1) {
    for (int i = 1; i <= n; ++ i) {
      if (i % 2 == 1) ans[i] = 1;
      else ans[i] = 2;
    }
    return true;
  } else if (l_ == 0) {
    for (int i = r_ - 1; i; -- i) {
      if ((r_ - i) % 2 == 1) ans[i] = 2 * ans[i + 1];
      else ans[i] = ans[i + 1] / 2;
    }
    return true;
  } else if (r_ == n + 1) {
    for (int i = l_ + 1; i < r_; ++ i) {
      if ((i - l_) % 2 == 1) ans[i] = 2 * ans[i - 1];
      else ans[i] = ans[i - 1] / 2; 
    }
    return true;
  }
  if (r_ == l_ + 1) return (a[l_] / 2 == a[r_]) || (a[r_] / 2 == a[l_]);

  int logl = (int) log2(a[l_]), logr = (int) log2(a[r_]), d = abs(logr - logl);
  if (d > r_ - l_) return false;
  if ((r_ - l_) % 2 != d % 2) return false;

  if (ans[l_] != ans[r_]) {
    int maxlen = 0;
    std::string sl = get(a[l_]), sr = get(a[r_]);
    for (int i = 0; i < std::min(logl, logr) + 1; ++ i) {
      if (sl[i] != sr[i]) break;
      ++ maxlen;
    }

    for (int i = 1; i <= logl - maxlen + 1 && l_ <= n; ++ i) {
      ++ l_, ans[l_] = ans[l_ - 1] / 2;
    }
    for (int i = 1; i <= logr - maxlen + 1 && r_; ++ i) {
      -- r_, ans[r_] = ans[r_ + 1] / 2;
    }
    if (l_ > r_) return false;
  }
  
  for (int i = l_ + 1; i < r_; ++ i) {
    if ((i - l_) % 2 == 1) ans[i] = 2 * ans[i - 1];
    else ans[i] = ans[i - 1] / 2; 
  }
  
  return true;
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  int T; std::cin >> T;
  while (T --) {
    std::cin >> n;
    for (int i = 1; i <= n; ++ i) std::cin >> a[i];
    
    int lst = 0, flag = 1;
    for (int i = 1; i <= n; ++ i) {
      if (a[i] == -1) continue;
      flag &= solve(lst, i);
      lst = i;
    }
    flag &= solve(lst, n + 1);

    if (!flag) std::cout << -1 << "\n";
    else {
      for (int i = 1; i <= n; ++ i) std::cout << ans[i] << " ";
      std::cout << "\n";
    }
  }
  return 0;
}

D

构造,数论。

妈的好诡异的题拿到这题我都不知道我要干啥呃呃

考虑到每个合数均为若干质数的乘积,则若构造方案中有合数,可以将合数替换为质数从而减少使用的权值的种类数,于是仅需考虑使用质数构造,则此时并不需要考虑具体使用了哪些质数,且此时 \(a_{i}\times a_{i + 1} = a_{j}\times a_{j + 1}\) 的充要条件即为无序对 \((a_{i}, a_{i + 1}) = (a_{j}, a_{j + 1})\)。该限制和无重边很像。则若已知需要 \(m\) 种质数进行构造,问题等价于在一张 \(m\) 个点的带自环的完全图中,找到一条边数为 \(n - 1\) 的欧拉路径。使用 Hierholzer 算法即可 \(O(m^2)\) 地解决。

于是考虑至少需要多少种质数才可构造出 \(n-1\) 条边的欧拉路径。由于自环的贡献显然可以全部得到,于是仅需考虑非自环边的影响。若 \(m\) 为奇数则所有点度数均为偶数,显然最长欧拉路径可以包括所有非自环边,数量为 \(\frac{m(m - 1)}{2}\);若 \(m\) 为偶数则所有点度数均为奇数,则需要屏蔽一些边使得该图存在欧拉路径,使非零度点是连通的,且有不多于 2 个奇度点。发现删去一条边至多使奇度数点减 2,则至少需要删除 \(\frac{m}{2} - 1\) 条边,且使这些边无交点。则钦定删除 \((2, 3), (4, 5), \cdots, (m - 2), (m - 1)\) 即可,则最长欧拉路径包括的非自环边数为 \(\frac{m(m - 1)}{2} - \frac{m}{2} + 1\)

发现上面的式子关于 \(m\) 是单调的,则可以二分求得至少需要多少种质数。发现 \(n=10^6\)\(m\approx 1100\),显然不会超过上界 \(3\times 10^5\)

另外先尝试了不显式地建图然而比用 vector 显式建图怎么慢十倍啊我草

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pii std::pair<int,int>
#define mp std::make_pair
const int kLim = 1e6;
const int kM = 1e4 + 10;
const int kN = 4e6 + 10;
//=============================================================
int n, m, ans[kN];
int pnum, prime[kM + 10], vis[kN];
int edgenum, top, st[kN];
std::vector<pii> g[kM];
//=============================================================
void getPrime() {
  for (int i = 2; i <= kLim; ++ i) {
    if (!vis[i]) prime[++ pnum] = i;
    for (int j = 1; j <= pnum && i * prime[j] <= kLim; ++ j) {
      vis[i * prime[j]] = 1;
      if (i % prime[j] == 0) break;
    }
  }
}
bool check(int m_) {
  if (m_ % 2 == 1) {
    return 1ll * m_ * (m_ - 1) / 2ll + m_ >= n - 1;
  } else {
    return 1ll * m_ * (m_ - 1) / 2ll - m_ / 2 + 1 + m_ >= n - 1;
  }
}
void getM() {
  int l = 1, r = kLim, ret = kLim;
  while (l <= r) {
    int mid = (l + r) >> 1;
    if (check(mid)) {
      ret = mid;
      r = mid - 1;
    } else {
      l = mid + 1;
    }
  }
  m = ret;
}
bool haveEdge(int u_, int v_) {
  if (u_ > v_) std::swap(u_, v_);
  return !(m % 2 == 0 && u_ % 2 == 0 && v_ == u_ + 1);
}
// int edge(int u_, int v_) {
//   if (u_ > v_) std::swap(u_, v_);
//   return (u_ - 1) * m + v_;
// }
void dfs(int u_) {
  // for (int v_ = 1; v_ <= m; ++ v_) {
  //   int e = edge(u_, v_);
  //   if (!haveEdge(u_, v_) || vis[e]) continue;
  //   vis[e] = 1;
  //   dfs(v_);
  // }
  while (!g[u_].empty()) {
    pii e = g[u_].back(); g[u_].pop_back();
    if (vis[e.second]) continue;
    vis[e.second] = 1;
    dfs(e.first);
  }
  st[++ top] = prime[u_];
}
void getAns() {
  edgenum = top = 0;
  for (int i = 1; i <= m * m; ++ i) vis[i] = 0;
  for (int i = 1; i <= m; ++ i) g[i].clear();
  
  for (int i = 1; i <= m; ++ i) {
    for (int j = i; j <= m; ++ j) {
      if (!haveEdge(i, j)) continue;
      g[i].push_back(mp(j, ++ edgenum));
      g[j].push_back(mp(i, edgenum));
    }
  }

  dfs(1);
  std::reverse(st + 1, st + n + 1);
  for (int i = 1; i <= n; ++ i) std::cout << st[i] << " ";
  std::cout << "\n";
}
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  getPrime();

  int T; std::cin >> T;
  while (T --) {
    std::cin >> n;
    getM();
    // std::cout << m << "--\n";
    getAns();
  }
  return 0;
}
/*
1
787
*/

写在最后

参考:

学到了什么:

  • A:特别指出的反常限制————有诈!
  • B:考虑贡献来自于什么过程。
  • C:从二进制角度考虑。
  • D:根据相邻关系构造数列转成构造路径。
posted @ 2024-05-31 21:50  Luckyblock  阅读(494)  评论(0编辑  收藏  举报