2024“钉耙编程”中国大学生算法设计超级联赛(8)

写在前面

补提地址:https://acm.hdu.edu.cn/listproblem.php?vol=66,题号 7517~7528。

以下按个人向难度排序。

dztlb 大神回去和npy约会了,于是悲惨单刷。

最后 6 题好歹签到是都签完了不算太烂,唉一个人单刷前期看到大家飞速过完好多题自己签到都签不上实在红温!

1004

签到。

发现操作后一定会移动到最外一圈的格子上,且之后只能在最外圈的格子上移动,则这些格子的贡献一定都能取到。

然后考虑能否获得其他格子的贡献,发现可以通过在开始时进行反复横跳,从而多获得某一行/某一列的贡献。

特判下即可。

//
/*
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 --) {
    LL n, m, a, b; std::cin >> n >> m >> a >> b;
    LL ans = 0;
    if (n == 1 || m == 1) ans = n * m;
    else ans = 2 * n + 2 * m - 4;
    
    if ((a == 1 && b == 1) || (a == 1 && b == 1) || (a == n && b == 1) || (a == n && b == m)) {
      //hina daisuki!!!
    } else if (a == 1 || a == n) {
      ans += (n - 2);
    } else if (b == 1 || b == m) {
      ans += (m - 2);
    } else {
      ans += std::max(n - 2, m - 2);
    }
    std::cout << ans << "\n";
  }
  return 0;
}

1007

进制,数论。

先考虑何时会出现答案无限的情况,考虑 \(k\) 极大的情况可知,这是因为在任意进制下 \(a+b\) 均不发生进位导致的。此时一定有 \(a+b = c\),特判下即可。

则某个进制 \(k\) 对答案有贡献当且仅当此时出现了进位,且减去进位 \(d\) 后有 \(a+b - d = c\)。众所周知,异或作为二进制不进位加法是有个性质对应的:

\[a \oplus b = (a \operatorname{or} b) - (a \operatorname{and} b) \]

考虑对于此题扩展一下该性质。发现若 \(k\) 进制下某一位上出现了 \(a_i+b_i\) 的进位,则一定有:

\[c_i = a_i + b_i - k \]

否则有:

\[c_i = a_i + b_i \]

则对于某一位上进位的情况,移项可知 \(k = a_i + b_i - c_i\),否则有 \(a_i + b_i - c_i = 0\)。则可知在 \(k\) 进制下,\(a+b-c\) 一定可以表示成若干 \(k\) 的幂的和(即发生的进位之和),即有:

\[\begin{aligned} a + b - c &= k^{c_1} + k^{c_2} + \cdots + k^{c_m} &(1\le c_1<c_2 <\cdots)\\ &=k(k^{c_1 - 1} + k^{c_2 - 1} + \cdots + k^{c_m - 1}) \end{aligned}\]

则可知若某个进制 \(k\) 是对答案有贡献的,则 \(k\) 一定是 \(a+b-c\) 的因数。有 \(a,b,c\le 10^9\),则可以直接大力枚举 \(a+b-c\) 的所有因数,并大力枚举各位检查。当 \(a+b-c = 0\) 也即不发生进位时无解。

总时间复杂度 \(O(T\sqrt{v}\log v)\) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
LL a, b, c, d;
//=============================================================
bool check(LL k_) {
  LL x = a, y = b, z = c;
  while (x || y || z) {
    LL t1 = x % k_, t2 = y % k_, t3 = z % k_;
    if ((t1 + t2) % k_ != t3) return false;
    x /= k_, y /= k_, z /= k_;
  }
  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 >> a >> b >> c;
    d = a + b - c;
    if (d == 0) {
      std::cout << -1 << "\n";
      continue;
    }
    LL ans = 0;
    if (check(d)) ++ ans;
    for (LL i = 2; i * i <= d; ++ i) {
      if (d % i != 0) continue;
      if (check(i)) ++ ans;
      if (i * i != d && check(d / i)) ++ ans;
    }
    std::cout << ans << "\n";
  }
  return 0;
}

1012

思维。

这题是我倒数第二个过的题哈哈单刷的唐氏签到失败是这样的

这题最后的公式是跟着感觉走半懂不懂地吃了两发试出来的、、、我现在还不太懂不太好解释,请参考官方写得很详细的题解。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
int n, dis[5];
std::string s[4];
//=============================================================
//=============================================================
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 <= 3; ++ i) std::cin >> s[i];
    
    for (int i = 1; i <= 4; ++ i) dis[i] = 0;
    for (int i = 0; i < n; ++ i) {
      if (s[1][i] == s[2][i] && 
          s[1][i] == s[3][i] && 
          s[2][i] == s[3][i]) ++ dis[4];
      else if (s[1][i] == s[2][i]) ++ dis[1];
      else if (s[1][i] == s[3][i]) ++ dis[2];
      else if (s[2][i] == s[3][i]) ++ dis[3];
    }
    std::sort(dis + 1, dis + 3 + 1);
    // for (int i = 1; i <= 4; ++ i) std::cout << dis[i] << "--";
    int ans = dis[4] + dis[1] + dis[2] + (dis[3] - dis[2]) / 2;
    std::cout << ans << "\n";
  }
  return 0;
}

1006

最小生成树,枚举

实际大力题呃呃,赛时被骗了最后半小时才恍然大悟实在唐得一批

考虑能否在直接枚举所有边做 Kruscal 时,一次性地把所有轮的最小生成树全做出来。即考虑对于每次枚举到一条边 \((u, v)\) 时,找到这条边应当加到哪一轮做的最小生成树上,以求得这条边对应答案。

发现若某条边 \((u, v)\) 应当被加到第 \(i\) 轮上,说明在第 \(1\sim i - 1\) 轮中 \(u, v\) 两节点均已经联通,且在 \(i+1\sim \cdots\) 轮中两节点均不联通,发现存在单调关系,于是考虑维护每轮中各节点的连通性,并二分检查各轮中两节点的连通性即可求得其被加入的轮数。

又发现由于最多只会做 \(\frac{m}{n-1}\) 轮,且仅有 \(n\) 个节点,完全可以直接开 \(\frac{m}{n-1}\) 个大小为 \(n\) 的并查集来维护连通性,时空复杂度均摊均仅有 \(O(m)\) 级别。

在做最小生成树的过程中再顺便维护下对于每一轮已经加了几条边,最后再枚举每条边,若这条边对应的轮数上有 \(n-1\) 条边说明这一轮被做完了即可直接输出,否则输出 -1

精细实现并查集总时间复杂度能做到纯 \(O(m)\) 级别,懒狗可以像我一样直接大力上 map,总时间复杂度 \(O(m\log m)\) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kN = 1e5 + 10;
const int kM = 3e5 + 10;
//=============================================================
int n, m, u[kM], v[kM], ans[kM];
int cnt[kM];
std::map<int, int> fa[kM];
//=============================================================
int find(int id_, int x_) {
  if (!fa[id_].count(x_)) fa[id_][x_] = x_;
  return (x_ == fa[id_][x_]) ? x_ : (fa[id_][x_] = find(id_, fa[id_][x_]));
}
void merge(int id_, int x_, int y_) {
  int fx = find(id_, x_), fy = find(id_, y_);
  fa[id_][fx] = fy;
}
//=============================================================
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 >> m;
    for (int i = 1; i <= m; ++ i) ans[i] = -1, cnt[i] = 0, fa[i].clear();
    for (int i = 1; i <= m; ++ i) {
      int u_, v_; std::cin >> u_ >> v_;
      u[i] = u_, v[i] = v_;
      ans[i] = -1;
    }

    int maxround = 0;
    for (int i = 1; i <= m; ++ i) {
      int u_ = u[i], v_ = v[i], nowround = maxround + 1;
      for (int l = 1, r = maxround; l <= r; ) {
        int mid = (l + r) >> 1;
        if (cnt[mid] >= n - 1 || find(mid, u_) == find(mid, v_)) {
          l = mid + 1;
        } else {
          nowround = mid;
          r = mid - 1;
        }
      }
      maxround = std::max(maxround, nowround);
      ans[i] = nowround;
      ++ cnt[nowround];
      merge(nowround, u_, v_);
    }
    
    for (int i = 1; i <= m; ++ i) {
      if (cnt[ans[i]] < n - 1) ans[i] = -1;
      std::cout << ans[i] << " ";
    }
    std::cout << "\n";
  }
  return 0;
}

1005

DP,记忆化搜索

大概是记忆化搜索题?

先特判下 \(k\ge 60\)\(n\) 可在区间内任意取,答案即 \(r-l+1\)

然后一个很显然的想法是直接递归求解。记 \(f(l, r, k)\) 表示对区间 \([l, r]\) 进行二分且允许出现 \(k\) 次越界情况下,区间内 \(n\) 的合法取值个数。考虑在这一轮上检查 \(\operatorname{mid}\) 的结果,则有:

\[f(l, r, k) = 1 + f(l, \operatorname{mid} - 1, k - 1) + f(\operatorname{mid} + 1, r, k) \]

然后这个时候大家写完过了样例看着挺优美的就会想直接冲一发交上发现 TLE 了哈哈,然后才发现这东西在 \(k=59\) 时实际上会把整个 \([l,r]\) 全部遍历一遍,实际是非常丑的东西。

然后又发现实际上 \(f(l, r, k)\) 的答案与区间的具体位置是无关的,实际仅与区间长度 \(\operatorname{len}=r-l+1\) 有关。于是考虑在上述递归过程中记忆化 \(g(\operatorname{len},k)\) 表示长度为 \(\operatorname{len}\) 的区间允许 \(k\) 次越界的答案即可。

可以证明在 \(k\) 次二分后二分的区间长度至多仅有 2 种,精细实现时间复杂度能做到 \(O(T\log^2 (r - l + 1))\) 级别,懒狗还可以像我一样直接大力上 map,总时间复杂度 \(O(T\log^3 (r - l + 1))\) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pr std::pair
#define mp std::make_pair
//=============================================================
LL l, r, k, ans;
std::map<pr<LL, int>, LL> cnt;
//=============================================================
LL check(LL l_, LL r_, LL c_) {
  if (l_ > r_) return 0;
  if (c_ < 0) return 0;
  if (cnt.count(mp(r_ - l_ + 1, c_))) return cnt[mp(r_ - l_ + 1, c_)];
  
  LL mid = (l_ + r_) / 2ll, delta = 1;
  delta += check(l_, mid - 1, c_ - 1);
  delta += check(mid + 1, r_, c_);
  cnt[mp(r_ - l_ + 1, c_)] = delta;
  return delta;
}
//=============================================================
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 >> l >> r >> k;
    if (k >= 60) {
      std::cout << (r - l + 1) << "\n";
      continue;
    }
    cnt.clear();
    ans = check(l, r, k);
    std::cout << ans << "\n";
  }
  return 0;
}
/*
1
1 100 0
*/

1008

构造。

好玩构造,最人类智慧的一集。

显然数列 \(a, b\) 均可以构造成 \(1\sim n\) 的排列的形式,若不是排列仅需离散化,即可使其保持性质不变的同时变为排列。

我的做法是考虑把每个节点的 \((a, b)\) 当做坐标,以 \(a\) 为横坐标 \(b\) 为纵坐标扔到二维坐标系上考虑。那么对于一个大小为 7 的完全二叉树的样例可以画成这个形式:

输入:

1
7
1 1 2 2 3 3

输出:

7 7 3 6 6 3 1 5 2 4 4 2 5 1

可以发现有如下性质:

  1. 根节点坐标一定为 \((n, n)\)
  2. 节点 \(u\) 的子树中所有节点,一定全部位于该节点坐标 \((a_u, b_u)\) 的左下角。
  3. 对于节点 \(u\) 的不同的子节点 \(v', v''\),若有 \(a_{v'} < a_{v''}\),则为了维持性质一定有 \(b_{v'} > b_{v''}\)

有矩阵的包含关系,于是考虑从根开始递归构造:

  1. 由于最终答案里 \(a\) 在前面,则应当首先令编号较小的节点的 \(a\) 尽可能小,于是考虑优先向子树内编号最小的节点更小的子节点递归构造,并尝试最小化子树的所有点 \(a\) 的值。
  2. 最优情况下应当构造成节点 \(u\) 满足:\(a_u\) 为其子节点 \(a\) 最大值+1, \(b_u\)为其子节点 \(b\) 最大值+1。
  3. 由上述性质 3,在最小化子树内编号最小的节点更小的子节点的 \(a\) 的同时,其子树内的 \(b\) 应当是更大的,且由构造 2 可知子树内编号最小的节点最小的子节点 \(v\)\(b_v=b_{u} - 1\)
  4. 可知递归构造时,对于某个子树内节点可以很容易维护其 \(a\)下界 \(A\)\(b\)上界 \(B\)
  5. 对于两个遍历顺序相邻的子节点 \(v', v''\),有下界 \(A_{v''} = a_{v'} + 1\),上界 \(B_{v'} = b_{v''} - \operatorname{size}_{v'}\)

由上述性质,发现仅需预处理一下子树大小 \(\operatorname{size}\) 和子树内最小编号节点的编号 \(\operatorname{id}\),对每个节点子节点按照 \(\operatorname{id}\) 排个序后递归构造。递归时下传每个节点内子树的 \(a\) 的下界和 \(b\) 的上界,递归时直接计算 \(b\),回溯时求得每个节点 \(a\) 的值即可。总时间复杂度 \(O(n)\) 级别。

发现钦定了递归构造的顺序即 dfs 序。虽然上述构造方法有点麻烦但实际上述构造方法和题解本质是完全一样的,属实是殊途同归了!

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int n, a[kN], b[kN], sz[kN], id[kN];
std::vector<int> son[kN];
//=============================================================
void dfs1(int u_) {
  sz[u_] = 1;
  id[u_] = u_;
  for (auto v_: son[u_]) {
    dfs1(v_);
    sz[u_] += sz[v_];
    id[u_] = std::min(id[u_], id[v_]);
  }
}
void dfs2(int u_, int a_, int b_) {
  a[u_] = a_, b[u_] = b_;
  -- b_;
  
  for (auto v_: son[u_]) {
    dfs2(v_, a_, b_);
    b_ = b[v_] - sz[v_];
    a_ = a[v_] + 1;
    a[u_] = std::max(a[u_], a[v_] + 1);
  }
}
bool cmp(int fir_, int sec_) {
  return id[fir_] < id[sec_];
}
//=============================================================
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) son[i].clear();
    for (int i = 2; i <= n; ++ i) {
      int fa; std::cin >> fa;
      son[fa].push_back(i);
    }
    dfs1(1);
    for (int i = 1; i <= n; ++ i) std::sort(son[i].begin(), son[i].end(), cmp);
    dfs2(1, 1, sz[1]);
    for (int i = 1; i <= n; ++ i) std::cout << a[i] << " " << b[i] << " ";
    std::cout << "\n";
  }
  return 0;
}
/*
1
7 
1 1 2 2 3 3
*/

1003

DP,期望。

1010

01 Trie,数据结构。

我测好牛逼的标记合并!

写在最后

学到了什么:

  • 1008:二维偏序关系扔到坐标系上考虑。
  • 1010:一种对于位运算的懒标记合并。

昨天是花冈柚子的生日,把这条信息转发至三个群电脑就会自动下载柚子社全家桶。我试过了是假的,而且我的电脑自动下载了原神,但昨天真的是花冈柚子的生日。

posted @ 2024-08-13 01:03  Luckyblock  阅读(256)  评论(0编辑  收藏  举报