China Collegiate Programming Contest (CCPC) Jinan Site (The 3rd Universal Cup. Stage 17: Jinan) 题解

我的评价是济南题就是恶心

整体上就是一种感觉:怎么感觉这个做法很暴力啊……唉复杂度怎么是对的……唉怎么就是这么做?比较无语(

虽然最后一题是个有趣的思维题,但是角度有点过于刁钻不是很好

优点是题面很用心,题解写的也很好。虽然题目都有点刁钻,但是看得出来出题人对这套题非常上心

Problem A. The Fool

写不出来退役吧。真的需要写题解吗?

#include <iostream>
#include <map>
#include <random>

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 1e6 + 100;

int rand(int l, int r) {
  static std::random_device seed;
  static std::mt19937_64 e(seed());
  std::uniform_int_distribution<int> rd(l, r);
  return rd(e);
}

template <class type>
void upmin(type& a, const type& b) {
  a = std::min(a, b);
}

template <class type>
void upmax(type& a, const type& b) {
  a = std::max(a, b);
}

std::map<std::string, int> cnt;
std::map<std::string, std::pair<int, int>> pos;

int main() {
  std::ios::sync_with_stdio(false);
  cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);
  int n, m, k;
  cin >> n >> m >> k;
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
      char ch;
      std::string str;
      for (int t = 1; t <= k; t++) {
        cin >> ch;
        str += ch;
      }
      cnt[str]++;
      pos[str] = {i, j};
    }
  }
  for (auto [str, c] : cnt) {
    if (c == 1) {
      cout << pos[str].first << ' ' << pos[str].second << endl;
    }
  }
  return 0;
}

Problem B. The Magician

我一开始看错题了。以为每张牌都有一个权值(捂脸)

恋人和死神显然是等价的。贪心用掉牌后一定只会剩下四组牌,且每组牌一定不多于四张。而且功能牌不管怎么操作,都最多最多为某个花色再凑成一次五张。

直接进行一个全排列枚举用来贪心表示优先满足哪些花色,这个贪心策略一定是正确的(你都枚举了所有情况了还有啥正确不正确的)

复杂度其实是常数。

#include <algorithm>
#include <iostream>
#include <random>

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 1e6 + 100;

int rand(int l, int r) {
  static std::random_device seed;
  static std::mt19937_64 e(seed());
  std::uniform_int_distribution<int> rd(l, r);
  return rd(e);
}

template <class type>
void upmin(type& a, const type& b) {
  a = std::min(a, b);
}

template <class type>
void upmax(type& a, const type& b) {
  a = std::max(a, b);
}

int t, ans, max = 0;
int n;

void solve(std::vector<int>& p, std::vector<int> now, std::vector<int> spe) {
  int tmp = 0;
  for (int i = 0; i < p.size(); i++) {
    int need = 5 - now[p[i]];
    // 如果无法满足,无论如何无法满足
    int sum = 0;
    if (need > spe[p[i]] * 3 + spe[0]) continue;
    for (int j = p.size() - 1; j > i; j--) sum += now[p[j]];
    if (sum < need) continue;
    if (need > spe[p[i]] * 3) {
      spe[0] -= need - spe[p[i]] * 3;
      spe[p[i]] = 0;
    }
    now[p[i]] += need;
    tmp++;
    for (int j = p.size() - 1; j > i; j--) {
      if (now[p[j]] >= need) {
        now[p[j]] -= need;
        need = 0;
      } else {
        need -= now[p[j]];
        now[p[j]] = 0;
      }
    }
  }
  upmax(max, tmp);
}

void solve() {
  std::vector<int> spe(5, 0);
  std::vector<int> cnt(5, 0);
  cin >> n;
  for (int i = 1; i <= n; i++) {
    std::string str;
    cin >> str;
    char ch = str[1];
    int type = 0;
    if (ch == 'D') type = 1;
    if (ch == 'C') type = 2;
    if (ch == 'H') type = 3;
    if (ch == 'S') type = 4;
    cnt[type]++;
  }
  for (int i = 1; i <= 4; i++) {
    cin >> spe[i];
  }
  int x;
  cin >> spe[0] >> x;
  spe[0] += x;
  std::vector<int> p(4);
  for (int i = 0; i < p.size(); i++) p[i] = i + 1;
  int ans = 0;
  for (int i = 1; i <= 4; i++) ans += cnt[i] / 5, cnt[i] %= 5;
  max = 0;
  do {
    solve(p, cnt, spe);
  } while (std::next_permutation(p.begin(), p.end()));
  ans += max;
  cout << ans << endl;
}

int main() {
  std::ios::sync_with_stdio(false);
  cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> t;
  while (t--) solve();
  return 0;
}

Problem C. The Empresso

好玩的构造题,个人认为其实比 \(D\) 题简单,但是貌似通过的人其实比 \(D\) 题少。

假设前 \(t\) 行执行完后可以执行 \(f(t)\) 次并且栈刚好为空,那么在 \(t+1\) 行我们可以构造出来:\(f(t+1) = f(t)\times 2 + 2\) 或者 \(f(t+1) = f(t)+ 2\)。也就是加上类似:

POP a GOTO a + 1; PUSH a GOTO 1
POP a GOTO a + 1; PUSH a GOTO a

的命令即可实现这两种效果,方便讨论称作操作一和操作二。

也就是我们最后需要某个 \(t\) 使得 \(f(t) = k - 1\)。因为退出命令本身就算作一次,所以不妨直接将 \(k\)\(1\) 去讨论。

从二进制位的角度去观察 \(f(t)\) 的变化:

如果我们只有操作一,那么最后的数字一定形如:

11111111111110

这个时候来一次操作二就可以将其变成某个 \(2^x\)

我们可以将 \(k\) 进行二进制分解后,钦定最高位就是通过这种操作进行构造出来的,其他的位就是在某一位被操作二进来后不断乘 \(2\) 出来的。也就是类似:

k = 10100
  = 10000 +
    00100
  = 01110 +
    00100 +
    00010

这种分解方式是唯一的,\(f(t)\) 进行转移的时候,操作一相当于为所有数字末尾塞 \(0\) 并且为钦定位塞 \(1\),操作二则是添加一个新的 10 进去。

总结一下就是钦定一个位去消化掉 \(f(t) \times 2 + 2\) 转移中那个讨厌的 \(+2\)

太麻烦了有没有更简单的方法……好像没有(挠头

官方题解中貌似也没有完全消除掉操作一里那个讨厌的东西。

最劣的情况就是 \(2^{31} - 1\),上述构造只会出现 \(60\) 行命令。非常的充裕。

#include <iostream>
#include <random>

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 1e6 + 100;

int rand(int l, int r) {
  static std::random_device seed;
  static std::mt19937_64 e(seed());
  std::uniform_int_distribution<int> rd(l, r);
  return rd(e);
}

template <class type>
void upmin(type& a, const type& b) {
  a = std::min(a, b);
}

template <class type>
void upmax(type& a, const type& b) {
  a = std::max(a, b);
}

// 记 f(x) 为子函数操作次数
// 则加一行以后,可以将操作次数变成 f(x) * 2 + 2 或者 f(x) + 2

int k, tmp = 0;

// 若去掉最后一位,那么每次相当于 t + 1 或者 t * 2 + 1
// 我们不放钦定某一位来享受所有的 1

std::vector<std::pair<int, int>> ans;

void add(int tot, int type) {
  ans.push_back({tot, type});
}

void end(int tot) {
  ans.push_back({tot, -1});
}

void print(int tot, int type) {
  if (type == -1) {
    cout << "HALT; PUSH 99 GOTO 1" << endl;
    return;
  }
  cout << "POP " << tot << " GOTO " << tot + 1 << "; PUSH " << tot << " GOTO " << (type ? 1 : tot) << endl;
  if (type == 1) {
    tmp = tmp * 2 + 2;
  } else {
    tmp = tmp + 2;
  }
}

int main() {
  std::ios::sync_with_stdio(false);
  cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);
  int tot = 1, first = -1;
  cin >> k;
  if (k == 1) {
    end(1);
    goto ans;
  }
  for (int i = 31; i >= 0; i--) {
    if ((k >> i) & 1) {
      int j;
      for (j = i; j >= 0; j--) {
        if (!((k >> j) & 1)) break;
      }
      i = j + 1;
    }
  }
  for (int i = 31; i >= 1; i--) {
    if (first != -1) {
      add(tot, 1), tot++;
      if ((k >> i) & 1) add(tot, 0), tot++;
    } else if ((k >> i) & 1) {
      first = i;
    }
  }
  // cerr << "NOW OUT" << endl;
  add(tot, 0), tot++;
  end(tot);
ans:
  cout << ans.size() << endl;
  for (auto [tot, type] : ans) {
    print(tot, type);
  }
  return 0;
}

Problem D. The Emperor

有一点点的,额,暴力?(事实上这套题这种感觉的题很多

我们直接进行一个记忆化的搜 \(f(u, a) = (v, cnt)\) 表示栈顶为 \(a\) 的时候,进入第 \(u\) 条命令,在刚好弹出这个栈顶的时候会到达哪里并且执行了多少次。

……

然后限制一下每个状态在递归栈里面的数量,限制一下递归深度或者限制一下某个循环次数……然后就可以判定是否死循环了……额……

然后就结束了……

比较无语的一个题……

不过这毕竟是一个典型的停机问题,所以事实上简单一点、数据松一点很正常吧。

#include <cassert>
#include <iostream>
#include <random>
#include <utility>
#include <vector>

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 1e6 + 100;
const int MOD = 998244353;

int rand(int l, int r) {
  static std::random_device seed;
  static std::mt19937_64 e(seed());
  std::uniform_int_distribution<int> rd(l, r);
  return rd(e);
}

template <class type>
void upmin(type& a, const type& b) {
  a = std::min(a, b);
}

template <class type>
void upmax(type& a, const type& b) {
  a = std::max(a, b);
}

int n;
int type[kMaxN];
int opt[4][kMaxN];
std::vector<int> stack;

int inc(int x, int y) {
  return (x + y >= MOD ? x + y - MOD : x + y);
}

int dec(int x, int y) {
  return (x - y < 0 ? x - y + MOD : x - y);
}

int bf() {
  int ans = 0;
  for (int i = 1;;) {
    ans++;
    if (type[i] == 1) {
      if (stack.empty() || stack.back() != opt[0][i]) {
        stack.push_back(opt[2][i]);
        i = opt[3][i];
      } else {
        assert(stack.back() == opt[0][i]);
        stack.pop_back();
        i = opt[1][i];
      }
    } else {
      if (stack.empty()) break;
      stack.push_back(opt[0][i]);
      i = opt[1][i];
    }
  }
  return ans;
}

struct node {
  int v, cnt;
} dp[1055][1055];

int dep = 0;
bool vis[1055][1055];
int in[1055][1055];

// 进入 u 语句,此时栈顶是 a
// 结束后进入 v 语句,且进入前会弹出 a
node dfs(int u, int a) {
  // 如果递归深度过高,或者进入这个 dfs 次数有点多
  if (vis[u][a]) return dp[u][a];
  if (in[u][a] > 8) {
    cout << -1 << endl;
    exit(0);
  }
  in[u][a]++;
  int now = u, sum = 0;
  // cerr << "DFS IN " << u << ' ' << a << endl;
  int cnt = 0;
  while (1) {
    cnt++;
    // if (cnt >= n * 4) {
    //   cerr << "BAD!" << endl;
    // }
    sum = inc(1, sum);
    if (type[now] == 2) {
      if (a == 0) {
        cout << sum << endl;
        exit(0);
        return {0, 0};
      } else {
        auto [v, cnt] = dfs(opt[1][now], opt[0][now]);
        now = v, sum = inc(sum, cnt);
      }
    } else {
      if (opt[0][now] == a) {
        now = opt[1][now];
        break;
      } else {
        auto [v, cnt] = dfs(opt[3][now], opt[2][now]);
        now = v, sum = inc(sum, cnt);
      }
    }
    if (cnt > n * 4) {
      cout << -1 << endl;
      exit(0);
    }
  }
  // cerr << "OUT WITH " << u << ' ' << a << endl;
  in[u][a]--;
  vis[u][a] = true;
  return dp[u][a] = {now, sum};
}

int main() {
  std::ios::sync_with_stdio(false);
  cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> n;
  for (int i = 1; i <= n; i++) {
    std::string opt;
    cin >> opt;
    if (opt == "HALT;") {
      type[i] = 2;
      for (int j = 0; j < 2; j++) {
        cin >> opt;
        cin >> ::opt[j][i];
      }
    } else {
      type[i] = 1;
      cin >> ::opt[0][i] >> opt >> ::opt[1][i] >> opt >> opt;
      cin >> ::opt[2][i] >> opt >> ::opt[3][i];
    }
  }
  // cout << bf() << endl;
  dfs(1, 0);
  return 0;
}

Problem E. The Chariot

我当时还琢磨为什么这套题为什么有人写 python,直到我看到了这个题……

我甚至还写了半个小时的高精度板子才发现这套题可以……交 pythonjava……

基于一个贪心思路:究竟怎么走单价会比较优秀。

  • 如果 \(A\) 的单价足够优秀,那么一定是走一万个 \(A\),最后留下一部分走 \(A\),或者交给 \(B\)\(C\) 消耗。

  • 如果 \(B\) 的单价足够优秀,那么最后坐车的状态一定类似:\(ABABABABABAB...\)

    • 最后会留下来一部分,这一部分如果选择坐 \(A\),那么有可能会浪费掉一部分行程。这个时候从前面的 \(B\) 中去掉一部分路程可以最小化浪费。
    • 或者最后这部分用 \(C\) 来收尾。
  • 如果 \(C\) 的单价足够优秀,那么最后的坐车状态就可能是:\(ABCCCCCCCCCCCCCC...\)

容易发现决策数量非常少,直接全部跑一次然后取 \(\min\) 即可。

我讨厌 python 这诡异的语法

t = int(input())

def solve():
    a, b, c, x, y, d = map(int, input().split())
    # 尽量用 a,最后接一个 b 或者 c
    tmp1 = d // x
    d2 = d - tmp1 * x
    sum = tmp1 * a
    ans = 0
    if d2 != 0:
        ans = sum + a
        if tmp1 != 0:
            # 还有一些没有分配出去
            # print(tmp1 * y, ans)
            # print(sum)
            if d2 <= tmp1 * y:
                # 全部考虑给 b
                ans = min({ans, sum + d2 * b})
            if d2 >= y:
                ans = min({ans, sum + y * b + (d2 - y) * c})

    else:
        ans = sum

    # print(ans)

    # 一辆车
    tmp2 = 0
    if d <= x:
        tmp2 = a
    elif d <= x + y:
        tmp2 = a + (d - x) * b
    else:
        tmp2 = a + y * b + (d - x - y) * c
    ans = min({ans, tmp2})

    # print("NOW END TO A PLUS B", ans)

    # 尽量用 a + b
    round = d // (x + y)
    least = d - round * (x + y)
    cost = round * (a + y * b)
    if round != 0:
        ableToReduce = round * y
        ans = min({ans, cost + least * c})
        if least <= x:
            weast = x - least
            reduce = 0
            if (weast <= ableToReduce):
                reduce = weast
            else:
                reduce = ableToReduce
            ans = min({ans, cost + a - reduce * b})
        else:
            ans = min({ans, cost + a + (least - x) * b})

    print(ans)

while t:
    t -= 1
    solve()

python 的官方标准缩进是四格,不好改还是懒得动了。

Problem F. The Hermit

居然还有黑神话悟空

如果一个集合需要被删除数字,那么必须删掉最小值,否则不可能合法。

基于这个思路出发,我们发现判定一组合法数到底是哪些集合的子集是比较麻烦的,不管是组合数还是冗斥都很诡异。

直接考虑一个数会被删除多少次。

容易发现一个数 \(x\) 要是一定会被删除,那么在集合中,小于它的数字构成一个以 \(x\) 结尾的倍数链,大于 \(x\) 的数字一定是 \(x\) 的倍数。

这样我们可以枚举 \(x\) 然后统计它会属于多少一定会被删除的集合内。倍数链 \(dp\) 处理,\(x\) 的倍数组合数可以算出来。

然后做完了。

#include <iostream>
#include <random>

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 1e5 + 100;
const int MOD = 998244353;

int rand(int l, int r) {
  static std::random_device seed;
  static std::mt19937_64 e(seed());
  std::uniform_int_distribution<int> rd(l, r);
  return rd(e);
}

template <class type>
void upmin(type& a, const type& b) {
  a = std::min(a, b);
}

template <class type>
void upmax(type& a, const type& b) {
  a = std::max(a, b);
}

int inc(int x, int y) {
  return (x + y >= MOD ? x + y - MOD : x + y);
}

int dec(int x, int y) {
  return (x - y < 0 ? x - y + MOD : x - y);
}

int mul(int x, int y) {
  return 1ll * x * y % MOD;
}

int pow(int x, int p) {
  int ans = 1;
  while (p) {
    if (p & 1) ans = mul(ans, x);
    x = mul(x, x), p >>= 1;
  }
  return ans;
}

int m, n;
int fact[kMaxN], invf[kMaxN];
// 倍数链长度为c,并且以i结尾的方案数量
int f[20][kMaxN];

int C(int n, int r) {
  return mul(fact[n], mul(invf[r], invf[n - r]));
}

int solve(int x) {
  int ans = 0;
  // 表示能够挑出 count 个倍数
  int count = m / x - 1;
  for (int c = 1; c < 20; c++) {
    if (f[c][x] == 0) continue;
    if (c + count < n) continue;
    ans = inc(ans, mul(f[c][x], C(count, n - c)));
  }
  return ans;
}

int main() {
  std::ios::sync_with_stdio(false);
  cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> m >> n;
  fact[0] = 1;
  for (int i = 1; i < kMaxN; i++) fact[i] = mul(fact[i - 1], i);
  invf[kMaxN - 1] = pow(fact[kMaxN - 1], MOD - 2);
  for (int i = kMaxN - 2; i >= 0; i--) invf[i] = mul(invf[i + 1], i + 1);

  for (int i = 1; i <= m; i++) {
    f[1][i] = 1;
  }

  for (int i = 1; i < 20; i++) {
    for (int j = 1; j <= m; j++) {
      if (f[i][j] == 0) continue;
      for (int k = 2; k * j <= m; k++) {
        f[i + 1][k * j] = inc(f[i + 1][k * j], f[i][j]);
      }
    }
  }
  // 考虑 i 会被删除多少次
  int sum = 0;
  for (int i = 1; i <= m; i++) {
    sum = inc(sum, solve(i));
    // cerr << i << ' ' << solve(i) << endl;
  }
  int ans = dec(mul(n, C(m, n)), sum);
  cout << ans << endl;
  return 0;
}

Problem G. The Wheel of Fortune

噩梦的开始啊。

总结题解感觉好像挺短的,但是本人确实不太会计算几何所以这道题写了很久(而且写了很长。

这个题的题解写的其实不怎么好。

首先算出来原多边形的质心,然后可以算出来加上小磁铁后新质心可能在的位置。新质心一定在原质心的一个相似多边形内部。然后我们会发现,某个区域获奖概率一定与这个区域与相似多边形的交的有关。

将旋转中心视作原点平移一下。

某个区域和相似多边形的交一定只由一个凸包决定,这个凸包由两部分构成:

  • 夹在射线中间的顶点
  • 某个边和射线的交点
  • 如果原点在相似多边形里面,那么这个凸包应当还有原点

将相似多边形的顶点和射线一起进行极角排序,就可以获得夹在两条射线中间的点集。每个点一定对应两条边,而这两条边一定只会和相邻不超过三四条的射线有交,所以直接枚举可能相交的射线然后添加交点到对应的凸包点集中就可以了。

获得凸包以后可以用 \(O(n)\) 的算法计算凸包面积,当然如果比较懒狗也可以直接获取顶点平均值后极角排序再算凸包面积(逃。

#include <iostream>
#include <cassert>
#include <algorithm>
#include <cmath>
#include <vector>

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 1e5 + 100;
const double pi = std::acos(-1);
const double eps = 1e-7;

struct node {
  double x, y;
  bool operator==(const node& p) const { return x == p.x && y == p.y; }
  bool operator<(const node& p) const {
    if (x == p.x) return y < p.y;
    return x < p.x;
  }
};
node operator+(const node& a, const node& b) {
  return {a.x + b.x, a.y + b.y};
}
node operator-(const node& a, const node& b) {
  return {a.x - b.x, a.y - b.y};
}
node operator*(const node& a, double p) {
  return {a.x * p, a.y * p};
}
node operator*(double p, const node& a) {
  return a * p;
}
double operator*(node a, node b) {
  return a.x * b.y - a.y * b.x;
}
bool leq(double x, double y) {
  if (std::abs(x - y) <= eps) return true;
  return x < y;
}
bool le(double x, double y) {
  if (std::abs(x - y) <= eps) return false;
  return x < y;
}
double angle(double x, double y) {
  auto a = atan2(y, x);
  if (le(a, 0)) a += 2 * pi;
  return a;
}
struct line {
  node a, b;
  line() {}
  line(const node& p1, const node& p2) { a = p1, b = p2 - p1; }
};
node cross(const line& a, const line& b) {
  if (a.b * b.b == 0) return {-1, -1};
  // p1 + A * ((P3 - P1) * B) / (AB)
  return a.a + a.b * (((b.a - a.a) * b.b) / (a.b * b.b));
}

double crossT(const line& a, const line& b) {
  if (a.b * b.b == 0) return -1;
  return (((b.a - a.a) * b.b) / (a.b * b.b));
}

int n;
double w;
std::vector<node> v, img;
std::vector<int> bel[kMaxN];

std::vector<node> set[kMaxN];

// 确认每个扇形的顶点集合
// std::vector<int> point[(int)(1e5 + 10)];

double calc(std::vector<node>& p) {
  std::sort(p.begin(), p.end());
  p.erase(std::unique(p.begin(), p.end()), p.end());
  node mid = {0, 0};
  for (auto& i : p) {
    mid = mid + i;
  }
  // cerr << "NOW I WANNA CALC " << endl;
  // cerr << p.size() << endl;
  if (p.size() == 0) return 0;
  // for (auto& i : p) {
  //   cerr << i.x << ' ' << i.y << endl;
  // }
  mid = mid * (1.0 / p.size());
  std::sort(p.begin(), p.end(), [&](const node& a, const node& b) {
    return angle(a.x - mid.x, a.y - mid.y) < angle(b.x - mid.x, b.y - mid.y);
  });
  double ans = 0, tmp;
  for (auto lst = p.back(); auto now : p) {
    ans += (tmp = (lst - mid) * (now - mid));
    // cerr << tmp << endl;
    // assert(leq(0, tmp));
    lst = now;
  }
  ans /= 2;
  return ans;
}

int main() {
  std::ios::sync_with_stdio(false);
  cin.tie(nullptr), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> n >> w;
  v.resize(n), img.resize(n);
  for (auto& [x, y] : v) {
    cin >> x >> y;
  }
  int ox, oy;
  cin >> ox >> oy;
  for (auto& [x, y] : v) {
    x -= ox, y -= oy;
    // cerr << x << ' ' << y << endl;
  }
  double S = 0;
  node m = {0, 0};
  for (auto lst = v.back(); auto now : v) {
    S += lst * now;
    m.x += (lst.x + now.x) * (lst * now);
    m.y += (lst.y + now.y) * (lst * now);
    lst = now;
  }
  S /= 2;
  m = m * (1.0 / (6 * S));
  // cerr << "GET THE POINT " << m.x << ' ' << m.y << ' ' << S << endl;
  for (int i = 0; auto u : v) {
    img[i] = (m * S + u * w) * (1.0 / (S + w));
    // cerr << img[i].x << ' ' << img[i].y << endl;
    i++;
  }
  int cnt = 0;
  for (auto lst = img.back(); auto now : img) {
    auto t = cross(line(lst, now), line({0, 0}, {0, -1}));
    auto [x1, y1] = lst;
    auto [x2, y2] = now;
    if (!leq(x1, x2)) std::swap(x1, x2);
    if (leq(x1, 0) && le(0, x2) && leq(t.y, 0)) {
      cnt ^= 1;
    }
    lst = now;
  }
  // cerr << cnt << endl;
  // 进行一遍极角排序,把射线也丢进去就可以直到两条射线中间夹的顶点
  std::vector<std::pair<node, int>> v;
  v.reserve(2 * n);
  for (int i = 0; i < n; i++) {
    v.push_back({img[i], i});
  }
  // 将射线也放进去
  for (int i = 0; i < n; i++) {
    v.push_back({::v[i], -i - 1});
  }
  std::sort(v.begin(), v.end(),
            [](const auto& a, const auto& b) { return angle(a.first.x, a.first.y) < angle(b.first.x, b.first.y); });
  auto nxt = [](int x) { return x + 1 >= 2 * n ? 0 : x + 1; };
  for (int i = 0, j, cnt = 0; i < 2 * n;) {
    if (v[i].second >= 0) {
      i = nxt(i);
      continue;
    }
    cnt++;
    for (j = nxt(i); v[j].second >= 0; j = nxt(j)) {
      bel[-v[i].second - 1].push_back(v[j].second);
      // cerr << "YOU ADD A POINT " << v[j].second << " TO " << -v[i].second - 1 << endl;
    }
    i = j;
    if (cnt == n) break;
  }
  // cerr << "NOW END" << endl;
  // 考虑交点
  for (int i = 0; i < n; i++) {
    // 考虑每个扇形内的点,每个点会对应两条线,然后这两条线一定只会和相邻有限的扇形有交点
    if (cnt) {
      set[i].push_back({0, 0});
    }
    // cerr << "NOW INIT LINE " << i << endl;
    for (auto id : bel[i]) {
      int pre = id - 1, nxt = id + 1;
      if (pre < 0) pre = n - 1;
      if (nxt == n) nxt = 0;
      set[i].push_back(img[id]);
      for (line l : {line(::img[nxt], ::img[id]), line(::img[pre], ::img[id])}) {
        auto x1 = l.a.x, x2 = l.a.x + l.b.x;
        auto y1 = l.a.y, y2 = l.a.y + l.b.y;
        auto begin = l.a, end = l.a + l.b;
        if (!leq(x1, x2)) std::swap(x1, x2);
        if (!leq(y1, y2)) std::swap(y1, y2);
        for (int j = i + 1, lst = i, cnt = 1; cnt <= n; j++, cnt++) {
          if (j == n) j = 0;
          auto t = cross(line({0, 0}, ::v[j]), l);
          auto v = crossT(line({0, 0}, ::v[j]), l);
          if (leq(x1, t.x) && leq(t.x, x2) && leq(y1, t.y) && leq(t.y, y2) && leq(0, v)) {
            set[lst].push_back(t);
            set[j].push_back(t);
            // cerr << "CROSS " << ::v[j].x << ' ' << ::v[j].y << endl;
            // cerr << t.x << ' ' << t.y << endl;
            // cerr << v << endl;
          } else {
            break;
          }
          lst = j;
        }
        for (int j = i, nxt = i - 1, cnt = 1; cnt <= n; nxt--, cnt++) {
          if (nxt == -1) nxt = n - 1;
          auto t = cross(line({0, 0}, ::v[j]), l);
          auto v = crossT(line({0, 0}, ::v[j]), l);
          if (leq(x1, t.x) && leq(t.x, x2) && leq(y1, t.y) && leq(t.y, y2) && leq(0, v)) {
            set[nxt].push_back(t);
            set[j].push_back(t);
          } else {
            break;
          }
          j = nxt;
        }
      }
    }
  }
  double sum = 0;
  std::vector<double> ans(n);
  for (int i = 0; i < n; i++) {
    ans[i] = calc(set[i]);
    sum += ans[i];
  }
  for (int i = 0; i < n; i++) {
    printf("%.9lf\n", ans[i] / sum);
    // cout << (ans[i] / sum) << endl;
  }
  return 0;
}

Problem H. Strength

挺简单的一个 \(dp\) 题目。但是状态设计要比较小心。

由于进位只会影响下一位,所以阶段划分是比较明显的。状态设计:\(dp_{i, j, k}\) 为考虑完末尾 \(i\) 位,同时数字是否小于等于 \(z\),第三维比较复杂:

  • \(k=0\),不会进位
  • \(k=1\),可以通过操作是否会对下一位进位
  • \(k=2\),一定会对下一位进位

考虑一下 \(4\) 这个比较抽象的角色,你会发现上一位是否进位、或者这一位先舍掉了,都会导致有可能进位也有可能不进位。所以要但开一个维度来应付这个抽象的东西(

状态转移的时候,记 \(x\) 当前位为 \(t\),只有 \(t=0\) 或者 \(t=1\) 的时候转移会相对诡异一点,只有这两种情况需要考虑是否会进位。其余情况比较平凡。枚举一下当前位的数然后考虑转移就可以了。

多转移几位然后答案就是 \(dp_{19, 1, 0}\) 了(。

#include <cassert>
#include <iostream>
#include <cstring>
#include <random>
#define int long long

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 1e6 + 100;

int rand(int l, int r) {
  static std::random_device seed;
  static std::mt19937_64 e(seed());
  std::uniform_int_distribution<int> rd(l, r);
  return rd(e);
}

template <class type>
void upmin(type& a, const type& b) {
  a = std::min(a, b);
}

template <class type>
void upmax(type& a, const type& b) {
  a = std::max(a, b);
}

long long dp[20][2][3];
long long p[20];

int t;
long long x, z;

void solve() {
  cin >> x >> z;
  cerr << x << ' ' << z << endl;
  dp[0][1][0] = 1;
  for (int i = 1; i <= 19; i++) {
    memset(dp[i], 0, sizeof(dp[i]));
    // 提取z的最后一位
    int zlst = z / p[i - 1], xlst = x / p[i - 1];
    zlst %= 10, xlst %= 10;
    // 枚举当前位置的原始数字
    for (int j = 0; j <= 1; j++) {
      for (int k = 0; k <= 2; k++) {
        for (int s = 0; s <= 9; s++) {
          bool leq = (s < zlst) | ((s == zlst) & j);
          if (xlst == 0) {
            if (k == 0) {
              dp[i][leq][s >= 5 ? 2 : 0] += dp[i - 1][j][k];
            } else if (k == 1 || k == 2) {
              if (s <= 3) dp[i][leq][0] += dp[i - 1][j][k];
              if (s == 4) dp[i][leq][1] += dp[i - 1][j][k];
              if (5 <= s) dp[i][leq][2] += dp[i - 1][j][k];
            }
          } else if (xlst == 1) {
            if (k == 0) {
              if (s != 1) continue;
              dp[i][leq][0] += dp[i - 1][j][k];
            } else if (k == 1 || k == 2) {
              if (s <= 4) dp[i][leq][0] += dp[i - 1][j][k];
              if (5 <= s) dp[i][leq][2] += dp[i - 1][j][k];
            }
          } else {
            if (k == 0 && s != xlst) continue;
            if (k == 1 && (s != xlst && s + 1 != xlst)) continue;
            if (k == 2 && s + 1 != xlst) continue;
            dp[i][leq][0] += dp[i - 1][j][k];
          }
        }
      }
    }
  }
  cout << dp[19][1][0] << endl;
}

signed main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  p[0] = 1;
  for (int i = 1; i <= 18; i++) p[i] = p[i - 1] * 10;
  cin >> t;
  while (t--) solve();
  return 0;
}

Problem I. The Hanged Man

才发现我这个题是稀里糊涂做对的……

写题解的时候才发现思路有点问题。

容易发现无刺图的一个必要条件是存在欧拉回路,也就是所有节点度数为偶数。所以可以先用这个性质判掉一些不合法情况。注意不是充要条件,一条边只能在一个简单环而且无重边自环。

当存在奇数个奇度点的时候,一定无法重新连边凑出欧拉回路,判掉。

题解给了一个动态规划的思路:

现在一定只剩下偶数个奇度点了。题目就变成了:是否可以在树上选取若干不重合链,且链的两段必须是奇度点。

\(dp\) 状态也很好设计,直接设计 \(dp_{u, 0/1}\) 表示 \(u\) 的子树中,是否以 \(u\) 为链的一段是否可行。若 \(dp_{root, 0}\)\(true\) 说明存在解。

但是这么 \(dp\) 要记录转移,导致逆推答案的时候会很复杂。

考虑构造。无论你如何操作,非根子树一定会留下来一个奇度点。

  • 考虑某个只有叶子节点的子树 \(u\),那么无论如何操作都一定剩下一个奇度点。对于 \(u\) 的父亲来说 \(u\) 和一个叶子没啥区别了。
  • 然后将这个步骤不断向上传递,就可以得到所有非根子树构造后一定留下来一个奇度点。

然后你将这个构造向上传递会发现,只有存在某个点度数为偶数的时候才有可能有解。找到那个节点然后当根,遍历一遍随便连连,这题就做完了。

#include <cassert>
#include <iostream>
#include <random>
#include <vector>

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 1e6 + 100;

int rand(int l, int r) {
  static std::random_device seed;
  static std::mt19937_64 e(seed());
  std::uniform_int_distribution<int> rd(l, r);
  return rd(e);
}

template <class type>
void upmin(type& a, const type& b) {
  a = std::min(a, b);
}

template <class type>
void upmax(type& a, const type& b) {
  a = std::max(a, b);
}

// 合法的必要条件是所有点度数都是偶数
// 也就是是否存在欧拉回路

std::vector<int> go[kMaxN];
std::vector<std::pair<int, int>> ans;
int cnt = 0;

int dfs(int u, int fa) {
  int lst = 0;
  for (auto v : go[u]) {
    if (v == fa) continue;
    auto now = dfs(v, u);
    assert(now);
    if (lst == 0) {
      lst = now;
    } else {
      ans.push_back({lst, now});
      lst = 0;
    }
  }
  if (lst == 0) {
    assert(fa == 0 || (go[u].size() & 1));
    lst = u;
  }
  return lst;
}

void solve() {
  int n;
  cin >> n;
  for (int i = 1; i <= n; i++) {
    go[i].clear();
  }
  for (int i = 1; i <= n - 1; i++) {
    int u, v;
    cin >> u >> v;
    go[u].push_back(v), go[v].push_back(u);
  }
  cnt = 0;
  ans.clear();
  for (int i = 1, flag = false; i <= n; i++) {
    if (go[i].size() & 1) {
      cnt++;
    } else if (!flag) {
      dfs(i, 0);
      flag = true;
    }
  }
  if (cnt & 1) return cout << -1 << endl, void();
  if (cnt == n) return cout << -1 << endl, void();
  cout << ans.size() << endl;
  for (auto [u, v] : ans) {
    cout << u << ' ' << v << endl;
  }
  // 任何节点不能和fa连边
}

int main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  int t;
  cin >> t;
  while (t--) solve();
  return 0;
}

Problem J. Temperance

反套路的简单题。我居然一开始没想到怎么做。

假设拔掉所有密度小于 \(d\) 的植物,会发现密度大于等于 \(d\) 的植物一定不会被因为这些被拔掉的植物影响。

想到这里就做出来了。

#include <iostream>
#include <algorithm>
#include <map>
#include <random>

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 1e6 + 100;

int rand(int l, int r) {
  static std::random_device seed;
  static std::mt19937_64 e(seed());
  std::uniform_int_distribution<int> rd(l, r);
  return rd(e);
}

template <class type>
void upmin(type& a, const type& b) {
  a = std::min(a, b);
}

template <class type>
void upmax(type& a, const type& b) {
  a = std::max(a, b);
}

int t, n;
int a[kMaxN][3];

void solve() {
  std::map<int, int> cnt[3];
  cin >> n;
  std::vector<int> d(n + 1);
  for (int i = 1; i <= n; i++) {
    for (int j = 0; j < 3; j++) cin >> a[i][j], cnt[j][a[i][j]]++;
  }
  for (int i = 1; i <= n; i++) {
    for (int j = 0; j < 3; j++) upmax(d[i], cnt[j][a[i][j]] - 1);
    // cerr << d[i] << endl;
  }
  std::sort(d.begin() + 1, d.end());
  int j = 1;
  for (int k = 0; k < n; k++) {
    while (j <= n && d[j] < k) j++;
    cout << j - 1 << ' ';
  }
  cout << endl;
}

int main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> t;
  while (t--) solve();
  return 0;
}

Problem K. The Devil

比较难绷的一个题。

首先要想到二分图匹配:每个缩写对应若干缩写,但是这些缩写可能有重合的。这个行为相当于一个二分图最小权匹配。你写匈牙利写费用流都是可以的。(但是我只会写费用流)

对于每个名字我们只需要保留长度最小的 \(n\) 个缩写。缩写和其他字符串最多重合 \(n-1\) 个,所以保留前 \(n\) 短的就可以。

接下来考虑如何获取每个名字的前 \(n\) 短的字符串。这个过程要求做到 \(O(n^3)\),但是正确的 \(trie\) 树实现可以做到 \(O(n^2)\)

  • 一个名字可以被分割成若干字符串 \(s_1, s_2...\),以此遍历每个字符串。同时遍历过程中仅保留最短的 \(n\) 个缩写,然后将 \(s_i\) 添加到缩写考虑范围内。

  • 在扩展新缩写过程中,一个新缩写只有两种可能出现:由上轮的缩写添加 \(s_{i, 0}\) 获得,或者已经拥有的缩写中向后扩展一位。这个过程可以同步维护一个队列和一个指针来进行较优秀的构造,能够做到 \(O(n)\)。如果比较麻烦也可以用一个优先队列来维护,能够做到 \(O(n \log n)\)

也就是说正确的缩写获取其实可以做到 \(O(n^3)\),而且空间复杂度大概也是这个数量级。

在这个二分图中,左边的点至多 \(O(n)\),右边的点至多 \(O(n^2)\),边数至多 \(O(n^2)\)。写一个匈牙利大概复杂度是 \(O(n^4)\)。由于二分图的性质比较好,正确的 \(dinic\) 费用流应该可以做到近似 \(O(n^3)\)也就是说这个题还能把数据往上面提

等什么时候有时间把 \(n\) 开到\(500\)去恶心别人

#include <cassert>
#include <iostream>
#include <algorithm>
#include <queue>
#include <map>
#include <random>
#include <vector>

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 3e6 + 100;
const int N = 128;

int rand(int l, int r) {
  static std::random_device seed;
  static std::mt19937_64 e(seed());
  std::uniform_int_distribution<int> rd(l, r);
  return rd(e);
}

template <class type>
void upmin(type& a, const type& b) {
  a = std::min(a, b);
}

template <class type>
void upmax(type& a, const type& b) {
  a = std::max(a, b);
}

int n;

int tot = 0;
std::map<char, int> trie[kMaxN];
int len[kMaxN], lst[kMaxN];
char ch[kMaxN];
bool count[kMaxN];

int nxt(int p, char ch) {
  if (trie[p].count(ch)) return trie[p][ch];
  assert(tot < kMaxN);
  len[++tot] = len[p] + 1;
  lst[tot] = p;
  ::ch[tot] = ch;
  return trie[p][ch] = tot;
}

std::vector<int> insert(const std::string& str) {
  std::vector<std::string> list(1, "");
  for (auto& ch : str) {
    if (ch == ' ')
      list.push_back("");
    else
      list.back().push_back(ch);
  }
  while (list.back().empty()) list.pop_back();
  // trie节点
  std::vector<int> node;
  node.push_back(0);
  for (auto& str : list) {
    // cerr << "YES BEGIN " << str.size() << endl;
    // 已经插入了str的前i个字符后的节点位置
    std::queue<std::pair<int, int>> q;
    std::vector<int> tmp;

    // cerr << "BEGIN WITH " << str.size() << endl;
    // cerr << node.size() << endl;
    auto record = [&](int p, int j) {
      p = nxt(p, str[j]);
      if (!count[p]) {
        count[p] = true, tmp.push_back(p);
        if (j + 1 == str.size()) return;
        q.push({p, j + 1});
      }
    };

    for (int j = 0; tmp.size() <= n && (j < node.size() || !q.empty());) {
      if (j == node.size()) {
        record(q.front().first, q.front().second), q.pop();
      } else if (q.empty()) {
        record(node[j], 0), j++;
      } else {
        if (len[node[j]] < len[q.front().first]) {
          record(node[j], 0), j++;
        } else {
          record(q.front().first, q.front().second), q.pop();
        }
      }
    }
    node.swap(tmp);
    for (int i = 0; i < node.size() - 1; i++) {
      assert(len[node[i]] <= len[node[i + 1]]);
    }
    std::sort(node.begin(), node.end(), [](int i, int j) { return len[i] < len[j]; });
    for (auto& i : node) count[i] = false;
  }
  return node;
}

void print(int p) {
  if (p == 0) return;
  print(lst[p]), cout << ch[p];
}

// MCMF 板子,由于要做二分图匹配,这个时候用费用流
int tot2;
int tonode[kMaxN];
int remap[kMaxN];
std::vector<int> node[kMaxN];
int s, t;

struct edge {
  int u, v, cap, cst, nxt;
} e[kMaxN];

int etot = 1;
int dis[kMaxN];
int head[kMaxN], cur[kMaxN];
bool vis[kMaxN], kill[kMaxN];

void add(int u, int v, int cap, int cst) {
  auto _ = [](int u, int v, int cap, int cst) {
    e[++etot] = {u, v, cap, cst, head[u]};
    head[u] = etot;
  };
  _(u, v, cap, cst), _(v, u, 0, -cst);
}

bool SPFA() {
  for (int i = 1; i <= t; i++) {
    cur[i] = head[i], vis[i] = false, kill[i] = false, dis[i] = 1e9;
  }
  std::queue<int> q;
  auto record = [&](int v, int d) {
    if (d >= dis[v]) return;
    dis[v] = d;
    if (!vis[v]) vis[v] = true, q.push(v);
  };
  record(s, 0);
  while (!q.empty()) {
    int f = q.front();
    q.pop(), vis[f] = false;
    for (int i = head[f]; i; i = e[i].nxt) {
      if (e[i].cap) record(e[i].v, e[i].cst + dis[f]);
    }
  }
  return dis[t] < 1e9;
}

int all = 0;
int dinic(int u, int in) {
  if (u == t) return in;
  vis[u] = true;
  int out = 0;
  for (int& i = cur[u]; i; i = e[i].nxt) {
    if (int v = e[i].v; !vis[v] && !kill[v] && e[i].cap && e[i].cst + dis[u] == dis[v]) {
      int res = dinic(v, std::min(in, e[i].cap));
      in -= res, out += res;
      e[i].cap -= res, e[i ^ 1].cap += res;
      all += res * e[i].cst;
      if (in == 0) break;
    }
  }
  vis[u] = false;
  if (out == 0) kill[u] = true;
  return out;
}

int dinic() {
  int out = 0;
  while (SPFA()) {
    while (int t = dinic(s, 1e9)) out += t;
  }
  return out;
}

int main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> n, cin.get();
  tot2 = n;
  for (int i = 1; i <= n; i++) {
    std::string str;
    std::getline(cin, str);
    // cerr << "YOU GET " << str << endl;
    node[i] = insert(str);
    for (auto& v : node[i]) {
      if (tonode[v] == 0) tonode[v] = ++tot2, remap[tot2] = v;
    }
  }
  int p = 0;
  for (int i = 1; i <= 128; i++) p = nxt(p, 'z');
  s = ++tot2, t = ++tot2;
  for (int i = 1; i <= n; i++) {
    add(s, i, 1, 0);
    for (auto v : node[i]) {
      add(i, tonode[v], 1, 0);
      // print(v);
      // nerr << v << ' ' << tonode[v] << endl;
    }
  }
  for (int i = n + 1; i < s; i++) add(i, t, 1, len[remap[i]]);
  // cerr << "HAS " << s - n - 1 << endl;
  int t = dinic();
  if (t != n) {
    return cout << "no solution" << endl, 0;
  }
  for (int i = 1; i <= n; i++) {
    for (int j = head[i]; j; j = e[j].nxt) {
      if (int v = e[j].v; n < v && v < s && e[j].cap == 0) {
        // cerr << "YES " << i << " LINK TO " << remap[v] << endl;
        print(remap[v]), cout << endl;
      }
    }
  }
  return 0;
}

Problem L. The Towerr

我讨厌这个题

很无聊的一个,线段树分裂合并板子。

而我更是弱智,直接对着一个范围 \(10^9\) 的线段树进行一个动态开点,然后再往这个动态开点的线段树上面怼线段树分裂……后果就是似的不知道是什么样子了。

我们只关心被询问的那些节点的情况,超过最后一个被询问节点的所有数值其实都不再关心了。然后这个题又允许我们做离线处理,所以直接将被查询节点进行离散化处理,然后对着这坨离散化后的数组建树,这样就避免了又是动态开点又是线段树分裂的悲惨状况(。

每一个操作一就相当于把线段树的一个前缀撕了下来开一个新的线段树,操作二就相当于把这个撕下来的线段树粘回去了。

同时我们保存一个指针(其实保存数组下标也彳亍)来记录被查询点最后落到的叶子节点,同时记录一下每个线段树的根对应哪个用户。这样从叶子节点不断上跳到根就可以得知这个叶子在哪个线段树了和哪个用户了。

……嗯做完了。就很暴力。因为操作二最多撕下来 \(\log^2 n\) (还是 \(\log n\))这个级别的节点数量,合并的复杂度也差不多是这样,所以合并和分裂复杂度是正确的。

唯一的问题就是空间问题,为了防止炸空间不能够往节点里面存左右界了,而且也不能用指针来表示儿子。回收节点是无法优化空间复杂度的。

#include <algorithm>
#include <iostream>
#include <random>
#include <vector>

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 1e6 + 100;

int rand(int l, int r) {
  static std::random_device seed;
  static std::mt19937_64 e(seed());
  std::uniform_int_distribution<int> rd(l, r);
  return rd(e);
}

template <class type>
void upmin(type& a, const type& b) {
  a = std::min(a, b);
}

template <class type>
void upmax(type& a, const type& b) {
  a = std::max(a, b);
}

struct node {
  int size, fa;
  int son[2];
  node() { size = fa = son[0] = son[1] = 0; }
} nd[(int)(2e7)];

int tot;
int head[kMaxN];
int a[kMaxN];
std::vector<int> v;
int map[kMaxN];
int n, m;
int s1[kMaxN], s2[kMaxN];

void pushup(int p) {
  nd[p].size = nd[nd[p].son[0]].size + nd[nd[p].son[1]].size;
  nd[nd[p].son[0]].fa = nd[nd[p].son[1]].fa = p;
}

void build(int& p, int l, int r) {
  p = ++tot;
  if (l == r) {
    nd[p].size = a[l] - a[l - 1], map[l] = p;
    return;
  }
  int mid = (l + r) >> 1;
  build(nd[p].son[0], l, mid), build(nd[p].son[1], mid + 1, r);
  pushup(p);
}

void split(int& p, int q, int l, int r, int k) {
  if (k == 0) return;
  p = ++tot;
  if (nd[q].size <= k) {
    nd[p] = nd[q], nd[q] = node();
    nd[nd[p].son[0]].fa = nd[nd[p].son[1]].fa = p;
    return;
  }
  nd[p].size += k, nd[q].size -= k;
  if (l == r) return;
  int mid = (l + r) >> 1;
  if (int s = nd[nd[q].son[0]].size; s <= k) {
    nd[p].son[0] = nd[q].son[0], nd[q].son[0] = 0;
    split(nd[p].son[1], nd[q].son[1], mid + 1, r, k - s);
  } else {
    split(nd[p].son[0], nd[q].son[0], l, mid, k);
  }
  pushup(p);
}

void merge(int& p, int q, int l, int r) {
  if (p == 0 || q == 0) {
    p += q;
    return;
  }
  if (l == r) {
    nd[p].size += nd[q].size;
    return;
  }
  int mid = (l + r) >> 1;
  merge(nd[p].son[0], nd[q].son[0], l, mid);
  merge(nd[p].son[1], nd[q].son[1], mid + 1, r);
  pushup(p), nd[q] = node();
}

int usr[(int)(2e7)];

int main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> n;
  for (int i = 1; i <= n; i++) {
    cin >> s1[i] >> s2[i];
    if (s1[i] == 3) {
      v.push_back(s2[i]);
    }
  }
  if (v.empty()) return 0;
  std::sort(v.begin(), v.end());
  v.erase(std::unique(v.begin(), v.end()), v.end());
  for (int i = 0; i < v.size(); i++) {
    if ((i == 0 ? 1 : v[i - 1] + 1) < v[i]) a[++m] = v[i] - 1;
    a[++m] = v[i];
  }
  build(head[0], 1, m);
  int cnt = 0;
  for (int i = 1; i <= n; i++) {
    if (s1[i] == 1) {
      cnt++;
      split(head[cnt], head[0], 1, m, s2[i]);
      usr[head[cnt]] = cnt;
    } else if (s1[i] == 2) {
      merge(head[0], head[s2[i]], 1, m);
    } else {
      int x = std::lower_bound(a + 1, a + 1 + m, s2[i]) - a;
      x = map[x];
      while (nd[x].fa) x = nd[x].fa;
      cout << usr[x] << endl;
    }
  }
  return 0;
}

主要问题还是不会线段树分裂和线段树合并(

Problem M. Judgement

有点小逆天的一个思维题。代码不难实现。

这个题比较建议看官方题解,写到要好很多。

大本营不应当视作和旁边的联通块相连,因为任何一个颜色都无法覆盖大本营地。

非法只有四种情况:

  • 不联通
  • 某个颜色无法通过联通块到达自己的大本营
  • 当所有颜色刚好构成一个链的时候,颜色变化过多。也就是类似:
    RSRBTB 合法,但是 RSRRBRBBT 不合法。
  • 对于某一个独立的联通块,它刚好构成红蓝相间的情况。

非常诡异,非常非常非常诡异。如果考场上看到这个题我是写不出的。

至少我现在还不会证明这些性质

#include <iostream>
#include <queue>
#include <random>

using std::cerr;
using std::cin;
using std::cout;

const char endl = '\n';
const int kMaxN = 505;

int rand(int l, int r) {
  static std::random_device seed;
  static std::mt19937_64 e(seed());
  std::uniform_int_distribution<int> rd(l, r);
  return rd(e);
}

template <class type>
void upmin(type& a, const type& b) {
  a = std::min(a, b);
}

template <class type>
void upmax(type& a, const type& b) {
  a = std::max(a, b);
}

int t;
int a[kMaxN][kMaxN];
int vis[kMaxN][kMaxN];
int n, m;
bool used[kMaxN][kMaxN];
bool spe[kMaxN][kMaxN];

int rx, ry, bx, by;
const int dx[4] = {0, 1, 0, -1};
const int dy[4] = {1, 0, -1, 0};

void col(int x, int y, int k) {
  spe[x][y] = true;
  std::queue<std::pair<int, int>> q;

  int cnt = 0;
  auto record = [&](int x, int y) {
    if (x < 1 || x > n || y < 1 || y > m || vis[x][y] & k || a[x][y] == 0 || spe[x][y]) return;
    q.push({x, y});
    vis[x][y] |= k;
  };
  q.push({x, y});
  vis[x][y] |= k;
  while (!q.empty()) {
    auto [x, y] = q.front();
    q.pop();
    for (int i : {-1, 1}) {
      record(x + i, y), record(x, y + i);
    }
  }
}

bool isList = true;
int change = 0;
bool dfs(int x, int y) {
  bool flag = false;
  used[x][y] = true;
  int deg = 0;
  for (int i = 0; i < 4; i++) {
    int tx = dx[i] + x, ty = dy[i] + y;
    if (tx < 1 || tx > n || ty < 1 || ty > m) continue;
    if (a[tx][ty]) deg++;

    flag |= a[tx][ty] == a[x][y];
    if (used[tx][ty] || a[tx][ty] == 0) continue;
    change += a[tx][ty] != a[x][y];
    if (spe[tx][ty]) {
      continue;
    }
    flag |= dfs(tx, ty);
  }
  isList &= deg <= 2;
  return flag;
}

bool solve() {
  static int cas = 0;
  cin >> n >> m;
  cin >> rx >> ry >> bx >> by;
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
      char ch;
      cin >> ch;
      a[i][j] = ch == 'R' ? 1 : 2;
      if (ch == '.') a[i][j] = 0;
      spe[i][j] = used[i][j] = vis[i][j] = 0;
    }
  }
  if (a[rx][ry] != 1 || a[bx][by] != 2) return false;
  spe[rx][ry] = spe[bx][by] = true;
  col(rx, ry, 1), col(bx, by, 2);
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
      if (a[i][j] != 0 && (vis[i][j] & a[i][j]) == 0) return false;
    }
  }
  bool ans = true;
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
      if (a[i][j] == 0 || used[i][j] || spe[i][j]) continue;
      isList = true;
      change = 0;
      auto flag = dfs(i, j);
      cerr << "CHECK IN " << i << ' ' << j << endl;
      cerr << isList << endl;
      cerr << flag << endl;
      if (isList) {
        ans &= change <= 1;
      } else {
        ans &= flag;
      }
    }
  }
  return ans;
}

int main() {
  cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
  cin >> t;
  while (t--) {
    cout << (solve() ? "YES" : "NO") << endl;
  }
  return 0;
}
posted @ 2025-11-08 20:48  sudoyc  阅读(99)  评论(0)    收藏  举报