Codeforces Round 847

写在前面

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

据说,狐狸、黄鼠狼等动物在修炼到一定时间之后,会陷入瓶颈,这时候就需要借助人的一口气,让自己的修为突破瓶颈,甚至可以直接成仙、化作人形。如果在山里遇到,你通常会看到一个形似侏儒的“怪人”,它头戴旧社会的狗皮帽子,上身穿着破旧的皮袄,下身穿着臃肿的棉裤。
为了不吓到人,它们通常会把尾巴藏在身后,用古怪的、细声的人声问你:“你看我像个什么?”
这时你可要当心了。
如果你回答:“我看你像个人",那么它就可以化作人型,修为更进了一层;
如果你说:“我看你像个仙人啊”,它就可以直接成仙,这是它最为期待的答案。
这两种情况都属于人有恩于它,得道的狐狸会给予恩人丰厚的回报,保佑其家人一生平安、富贵。
当然,也有一些愣头,会暴躁地冲它喊:“我看你像个P"等不大好听的话,那么狐狸等动物就是讨封失败,羞愧得飞快逃走,地上只留下它身上的衣物,不得不重新修炼。

我刚高考完的那一个深秋,去到老家附近的荒山上。
老家的长辈曾经三番五次讲过山里有鬼怪,但是作为一个坚定的无神论者,我去山上目的就是找点磷火带回去给他们当乐子看看。但是背后的声音,让我的价值观差点跟我的魂一起飞走。
“您看我像人吗?”头上盖着叶子的狐狸向我问道。
.…..花了点时间,稳住我抖的比我的手速都快的腿和心态。
我看你像148公分的金毛贫乳傲娇兽耳九尾狐萝莉。
狐狸:?

和吊群友瞎扯的时候回想起的老梗,想起当年在小机房一个人挨冻看 A 岛的时候了,可惜现在我也不在小机房,A 岛也没了,属实令人感慨。

A

//By:Luckyblock
/*
*/
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
//=============================================================
char s[100], t[100] = "0314159265358979323846264338327";
//=============================================================
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 --) {
    scanf("%s", s + 1);
    int lth = strlen(s + 1), ans = 0;
    for (int i = 1; i <= lth; ++ i) {
      if (s[i] != t[i]) break;
      ans = i;
    }
    printf("%d\n", ans);
  }
	return 0;
}

B

\(r\) 点平均分配到剩下 \(n-1\) 个骰子上。

//By:Luckyblock
/*
*/
#include <cstdio>
#include <cctype>
#include <algorithm>
const int kN = 51;
//=============================================================
int a[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 --) {
    int n = read(), s = read(), r = read();
    for (int i = 1; i <= n; ++ i) a[i] = 0;
    a[n] = s - r;
    while (r) {
      for (int i = n - 1; i; -- i) {
        a[i] ++;
        r --;
        if (!r) break;
      }
    }
    for (int i = 1; i <= n; ++ i) printf("%d ", a[i]);
    printf("\n");
  }
	return 0;
}

C

\(i\) 个位置上出现 \(n-1\) 次的就是原排列这个位置上的数。

//By:Luckyblock
/*
*/
#include <cstdio>
#include <cctype>
#include <algorithm>
//=============================================================
int a[110][110], ans[110], cnt[110];
//=============================================================
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 n = read();
    for (int i = 1; i <= n; ++ i) {
      for (int j = 1; j < n; ++ j) {
        a[i][j] = read();
      }
      a[i][n] = 0;
    }

    for (int i = 1; i <= n; ++ i) {
      for (int j = 1; j <= n; ++ j) cnt[j] = 0;
      for (int j = 1; j <= n; ++ j) ++ cnt[a[j][i]];
      for (int j = 1; j <= n; ++ j) {
        if (cnt[j] == n - 1) ans[i] = j;
      }
      for (int j = 1; j <= n; ++ j) {
        if (a[j][i] != ans[i]) {
          for (int k = n; k; -- k) a[j][k] = a[j][k - 1];
          break;
        }
      }
    }
    for (int i = 1; i <= n; ++ i) printf("%d ", ans[i]);
    printf("\n");
  }
  return 0;
}

D

数据结构写傻了,上来就最大子段和笑死我了。

考虑记录每个数的出现次数,每次把没有被使用的最小的数拿出来作为等差数列的第一个元素并贪心地构造最长的等差数列。优先队列记录值和出现次数实现即可。

//By:Luckyblock
/*
*/
#include <map>
#include <queue>
#include <cstdio>
#include <cctype>
#include <algorithm>
#define pr std::pair
#define mp std::make_pair
const int kN = 2e5 + 10;
//=============================================================
int n, a[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 --) {
    std::priority_queue <pr <int, int> > q;
    std::map <int, int> cnt;
    n = read();
    for (int i = 1; i <= n; ++ i) {
      a[i] = read();
      if (!cnt.count(a[i])) cnt[a[i]] = 0;
      cnt[a[i]] ++;
    }
    for (int i = 1; i <= n; ++ i) {
      if (!cnt[a[i]]) continue;
      q.push(mp(-a[i], cnt[a[i]]));
      cnt[a[i]] = 0;
    }
    int ans = 0;
    while (true) {
      std::queue <pr <int, int> > tmp;
      pr <int, int> last = q.top(); 
      tmp.push(last); q.pop();
      while (!q.empty() && last.first - q.top().first == 1) {
      	last = q.top(); q.pop();
        tmp.push(last); 
      }
      ++ ans;
      while (!tmp.empty()) {
        pr <int, int> p = tmp.front(); tmp.pop();
        if (p.second == 1) continue;
        q.push(mp(p.first, p.second - 1));
      }
      if (q.empty()) break;
    }
    printf("%d\n", ans);
  }
  return 0;
}

E

\(t\) 组数据,每组数据给定整数 \(x\),要求构造一对 \((a, b)\),满足:\(x = a \oplus b = \left\lfloor\frac{a + b}{2}\right\rfloor\)\(\oplus\) 代表按位异或操作。
\(1\le t\le 10^4\)\(1\le x\le 2^{29}\)
1S,256MB。

开始要点脑子的题。

按位考虑 \(x\):若 \(x\)\(i(0\le i\le 29)\) 位为 1,则 \(a, b\) 中有且仅有一个数这一位为 1,两种情况对 \(a+b\) 的贡献均为 \(2^i\),任意令一个数这一位为 1 即可。若 \(x\) 某一位为 0,则 \(a, b\) 中这一位均为 0 或均为 1。均为 1 时对 \(a+b\) 贡献为 \(2\times 2^i\),否则为 \(0\)。需要讨论这一位的取值情况。

经过上述过程后我们可以确定 \(a,b\) 中某些位的取值情况,记这些位对 \(a+b\) 的贡献为 \(s_1\)\(s_1\) 为上述二进制位对应的十进制数的和),问题变为能否通过确定需要讨论的二进制位对 \(a+b\) 的贡献 \(s_2\),使得 \(\left\lfloor\frac{s_2}{2}\right\rfloor = x - \left\lfloor\frac{s_1}{2}\right\rfloor\)。 又 \(x - \left\lfloor\frac{s_1}{2}\right\rfloor\) 的二进制分解是唯一的,仅需判断它的二进制分解中的每一位是否均包含在待确定的二进制位中即可,实现时用待确定的二进制位对应的十进制数试减 \(x-\left\lfloor\frac{s_1}{2}\right\rfloor\) 即可。

总复杂度 \(O(t\log x)\) 级别。

//By:Luckyblock
/*
*/
#include <cstdio>
#include <cctype>
#include <vector>
#include <algorithm>
//=============================================================
//=============================================================
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 --) {
    std::vector <int> p;
    int x = read(), temp = x, a = 0, b = 0, sum = 0;
    for (int y = 1; temp; temp >>= 1, y <<= 1) {
      if (temp & 1) {
        if (!b) b |= y;
        else a |= y;
        sum += y;
      } else {
        p.push_back(y);
      }
    }
    x -= sum / 2;
    if (x) {
      for (int i = 0, sz = p.size(); i < sz; ++ i) {
        if (x & p[i]) {
          a |= p[i], b |= p[i];
          x -= p[i];
        }
      }
    }
    if (x) printf("-1\n");
    else printf("%d %d\n", a, b);
  }
	return 0;
}

F

\(t\) 组数据,每组数据给定一棵 \(n\) 个节点的无根树,两个节点 \(u,v\) 之间的距离定义为两点间简单路径的边的数量。
初始时仅有 \(c_0\) 节点为黑色,其余节点均为白色。之后要进行 \(n-1\) 次操作,每次操作将会选择一个白色的节点将其变为黑色。现给定每次操作节点的编号,求每次操作之后,树上所有黑点两两之间距离的最小值。
\(1\le t\le 10^4\)\(2\le n\le 2\times 10^5\)\(1\le c_0\le n\)\(\sum n\le 2\times 10^5\)
4S,256MB。

这数据范围怎么写啊,我不会写 \(O(n\log n)\)。什么?这不是 \(O(n\log n)\),这是压缩暴力。

\(c_0\) 为根,转化为有根树上的问题。操作过程中答案单调不增,我们每次仅需考虑以当前操作的节点 \(u\) 为端点之一的路径 \((u,v)\) 对答案的影响即可。套路的,这样的路径有三类:

  1. \(v\)\(u\) 的子树中,则最有可能有贡献的 \(v\) 一定是距离 \(u\) 最近的 \(v\)
  2. \(v\)\(u\) 的祖先,则贡献为深度之差。
  3. \(v\) 不在 \(u\) 的子树中且 \(v\) 不是 \(u\) 的祖先,考虑枚举 \(\operatorname{lca}(u, v)\),最有可能有贡献的 \(v\) 是距离 \(\operatorname{lca}(u,v)\) 最近的 \(v\)

综上,操作过程中对于每个节点 \(u\) 维护 \(f_u\) 表示 \(u\) 的子树中距离 \(u\) 最近的黑点的距离(包括 \(u\) 本身)。每进行一次操作,先求得操作对象 \(u\) 子树中 1 类的 \(v\) 的贡献,再枚举 \(u\) 的祖先,计算上述 2、3 类的 \(v\) 的贡献,统计贡献后再更新祖先的 \(f\)。特别地,枚举祖先时祖先与 \(u\) 的距离是单调增的,如果该距离大于当前答案,则停止枚举祖先,也停止更新 \(f\)

没想出来怎么快速维护这个过程,于是先暴力写了一发……然后过了而且跑挺快。

关于复杂度的分析……官方题解没出来先感性理解一下:

复杂度瓶颈出在暴力跳祖先上。由于先统计了 1 类的 \(v\) 的贡献,暴力跳祖先的次数不大于子树中最近的黑点与 \(u\) 的距离、祖先中最近的黑点与 \(u\) 的距离、之前的答案这三者的最小值。可以估计出极限情况会出现在一条链上,\(c_0 = 1\),并且操作的顺序为:

\[n\rightarrow \frac{1}{2} n\rightarrow \frac{1}{4}n\rightarrow \frac{3}{4}n\rightarrow \frac{1}{8}n\rightarrow \frac{3}{8}n\rightarrow \dots \]

答案的变化顺序为:

\[n\rightarrow \frac{n}{2}\rightarrow\frac{n}{4} \rightarrow \frac{n}{4}\rightarrow \frac{n}{8}\rightarrow\dots \]

如果不按照这个顺序进行操作,那么上述三者中的最小值衰减地一定会更快,则在相同操作次数下答案会更小,使得暴力跳祖先的次数变少。

在这种操作顺序下,暴力跳祖先的次数为:

\[n + \frac{n}{2} + 2\times \frac{n}{4} + 4\times \frac{n}{8} + \cdots + \frac{n}{2}\times 1 = \frac{n}{2}\log\left(\frac{n}{2}\right) \]

我去,居然真是 \(O(n\log n)\) 级别,什么?这不是压缩暴力,这是牛逼 \(O(n \log n)\) 正解。

upd:这题好像有一万种写法,官方给了一种 \(O(n\sqrt n)\) 的写法,赛时写了超级暴力没写这个亏死了乐。

//By:Luckyblock
/*
*/
#include <queue>
#include <cstdio>
#include <cctype>
#include <algorithm>
const int kN = 2e5 + 10;
const int kM = kN << 1;
//=============================================================
int n, c0, ans, opt[kN];
int f[kN], fa[kN];
int edgenum, head[kN], v[kM], ne[kM];
//=============================================================
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 Add(int u_, int v_) {
  v[++ edgenum] = v_;
  ne[edgenum] = head[u_];
  head[u_] = edgenum;
}
void Dfs1(int u_, int fa_) {
  fa[u_] = fa_;
  f[u_] = n;
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (v_ == fa_) continue;
    Dfs1(v_, u_);
  }
}
void Init() {
  n = read(), c0 = read();
  edgenum = 0;
  ans = n;
  for (int i = 1; i <= n; ++ i) head[i] = 0;
  for (int i = 1; i < n; ++ i) opt[i] = read();
  for (int i = 1; i < n; ++ i) {
    int u_ = read(), v_ = read();
    Add(u_, v_), Add(v_, u_);
  }
  Dfs1(c0, 0);
  f[c0] = 0;
}
void Dfs2(int u_, int dis_) {
  ans = std::min(ans, dis_ + f[u_]);
  if (dis_ >= ans) return ;
  f[u_] = std::min(f[u_], dis_);
  Dfs2(fa[u_], dis_ + 1); 
}
void Check(int now_) {
  ans = std::min(ans, f[now_]);
  Dfs2(fa[now_], 1);
}
//=============================================================
int main() {
//  freopen("1.txt", "r", stdin);
  int T = read();
  while (T --) {
    Init();
    for (int i = 1; i < n; ++ i) {
      Check(opt[i]);
      f[opt[i]] = 0;
      printf("%d ", ans);
    }
    printf("\n");
  }
	return 0;
}

G

\(t\) 组数据,每组数据给定一张 \(n\) 个节点 \(m\) 条边的无向图。图中有 \(p\) 个 token 类点,每个点上都有一匹黄金船,其他点上没有黄金船。同时有 \(b\) 个 bonus 类点,bonus 类点上也可能有黄金船。
现在作为拖累纳的你和黄金船正在这张图上进行一种游戏。游戏开始时,你可以选择任意一匹黄金船,将这匹黄金船移动到任意相邻的节点上。如果上一次操作将黄金船移动到了 bonus 类点上,那么你可以选择任意一匹与上一次不同的黄金船,并重复上述操作,否则停止操作。
判断是否存在一种操作方案,使得某匹黄金船能够到达 1 号节点。
\(1\le t\le 10^4\)\(1\le n\le 2\times 10^5\)\(0\le m\le 2\times 10^5\)\(1\le p\le n\)\(0\le b\le n\)\(\sum n\le 2\times 10^5,\sum m\le 2\times 10^5\)
2S,256MB。

实在是没想到怎么简化好了……谢谢你,啊船——

一些显然的结论:

  1. 有一匹啊船初始位置在节点 1 或与节点 1 相邻时一定可行。

  2. 若不存在结论 1 中的啊船,如果一匹啊船能到达节点 1,那么一定存在一条从 1 到这匹啊船初始所在节点 \(u\) 的路径,使得这条路径上除了 1 和 \(u\) 之外都是 bonus 类的。但满足该条件并不意味着某匹啊船一定能到达节点 1,我们还需要保证其他的啊船也可以提供一些移动,来满足两次不能移动同一匹啊船的限制。

  3. 考虑处理出每匹黄金船通过结论 2 中所述路径到达 1 号节点的最短距离 \(d\)。显然令距离最近的啊船首先到达节点 1 一定是较优的,因为交替操作这匹啊船和另一匹距离较远的啊船一定是一种合法的方案。

  4. 在确定了首先到达节点 1 的啊船后,显然其他啊船需要提供 \(d-1\) 次移动。其他啊船可以分为三种:

    • 不与 bonus 节点相连。这样的啊船没有贡献。
    • 与 bonus 节点 \(v\) 相连,但 \(v\) 无法到达其他 bonus 节点。这样的啊船可以提供 1 次移动。
    • 与 bonus 节点 \(v\) 相连且 \(v\) 可以到达其他 bonus 节点。这样的啊船可以提供无数次移动。

    判断 3 类啊船是否存在,如果存在则有解。否则计算 2 类啊船的数量并与 \(d-1\) 比较即可。

结论 3 中的过程可以通过 bfs 实现,结论 4 的过程可以通过枚举啊船的初始位置并大力搜索实现。总复杂度 \(O(t(n+m))\) 级别。

//By:Luckyblock
/*
*/
#include <queue>
#include <cstdio>
#include <cctype>
#include <algorithm>
const int kN = 2e5 + 10;
const int kM = kN << 2;
const int kInf = 1e9;
//=============================================================
int n, m, p, b, id, mindis, dis[kN];
bool flag, token[kN], bonus[kN], temp1[kN], temp2[kN];
int edgenum, head[kN], v[kM], ne[kM];
//=============================================================
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 Add(int u_, int v_) {
  v[++ edgenum] = v_;
  ne[edgenum] = head[u_];
  head[u_] = edgenum;
}
void Init() {
  n = read(), m = read();
  edgenum = flag = id = mindis = 0;

  for (int i = 1; i <= n; ++ i) token[i] = bonus[i] = head[i] = 0;
  
  p = read(), b = read();
  for (int i = 1, x; i <= p; ++ i) x = read(), token[x] = 1;
  for (int i = 1, x; i <= b; ++ i) x = read(), bonus[x] = 1;
  while (m --) {
    int u_ = read(), v_ = read();
    Add(u_, v_), Add(v_, u_);
  }
}
void Bfs() {
  if (token[1]) flag = 1;
  for (int i = head[1]; i; i = ne[i]) {
    if (token[v[i]]) flag = 1;
  }
  if (flag) return ;

  for (int i = 0; i <= n; ++ i) {
    dis[i] = kInf;
    temp1[i] = token[i];
    temp2[i] = bonus[i];
  }
  std::queue <int> q, d;
  q.push(1), d.push(0);
  dis[1] = 0;
  while (!q.empty()) {
    int u_ = q.front(), d_ = d.front(); q.pop(), d.pop();
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      dis[v_] = std::min(dis[v_], d_ + 1);
      if (temp1[v_]) {
        if (dis[v_] < dis[id]) id = v_;
      }
      if (temp2[v_]) {
        q.push(v_);
        d.push(dis[v_]);
        temp2[v_] = 0;
      }
    }
  }
  if (!id) return ;
	 
  token[id] = 0, mindis = dis[id] - 1;
  for (int i = 1; i <= n; ++ i) {
    if (!token[i]) continue;
    int delta = 0;
    std::queue <int> p, dep; p.push(i), dep.push(0);
    while (!p.empty()) {
      int u_ = p.front(), dep_ = dep.front(); p.pop(), dep.pop();
      delta = std::max(delta, dep_);
      if (delta >= mindis) break;
      for (int j = head[u_]; j; j = ne[j]) {
        if (bonus[v[j]]) p.push(v[j]), dep.push(dep_ + 1);
      }
    }
    mindis -= delta;
    if (mindis <= 0) break;
  }
  if (mindis <= 0) flag = 1;
}
//=============================================================
int main() {
//  freopen("1.txt", "r", stdin);
  int T = read();
  while (T --) {
    Init();
    Bfs();
    printf("%s\n", flag ? "YES" : "NO");
  }
	return 0;
}

写在最后

  • 写数据结构很好,但是只想到写数据结构很坏。
  • 对于一个暴力算法,如果某一步暴力会影响到之后暴力的复杂度,那它有可能是均摊复杂度的正解。
  • 复杂度分析时可以优先考虑极限情况。
  • 想不出来就再看看题,确保没看错题再说。

华哥这是在什么几把厕纸番里配了个啥 [流汗黄豆],受不了了,好想听新海兄妹说相声,但是华哥已经上岸了呜呜呜 [大哭黄豆]

邻家天使动画画了个什么玩意呕

posted @ 2023-01-28 19:19  Luckyblock  阅读(59)  评论(1编辑  收藏  举报