The 2024 Hunan Multi-School Programming Training Contest, Round 3 部分题解

写在前面

部分简略题解,以下按个人难度向排序。

L

纯签到。

//
/*
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 n; std::cin >> n;
  
  int flag = 1;
  for (int i = 1; i <= 3; ++ i) {
    int flag7 = 0;
    for (int j = 1; j <= n; ++ j) {
      int x; std::cin >> x;
      if (x == 7) flag7 = 1;
    }
    flag &= flag7;
  }
  std::cout << (flag ? "777\n" : "0\n");
  return 0;
}

K

贪心。

顺序枚举题目的过程中能做就做。

因为每道题的贡献均为 1 则贪心策略不会更劣。

//
/*
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 n, s; std::cin >> n >> s;
  for (int i = 1; i <= n; ++ i) {
    int l, r; std::cin >> l >> r;
    if (l <= s && s <= r) ++ s;
  }
  std::cout << s << "\n";
  return 0;
}

F

模拟。

每页独立,于是讨论每页哪种策略更优:

  • 全删除,再单选。
  • 全选,再单个删除。
  • 单个删除,单个选择。

然后根据需要修改的页码范围和当前页码讨论如何换页即可。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e3 + 10;
const int kInf = 1e9 + 2077;
//=============================================================
int n, m, s, p, q;
int pagenum, ans;
int left = kInf, right = -kInf;
bool yes1[kN], yes2[kN];
int cnt1[kN], cnt2[kN];
//=============================================================
int calc(int page_) {
  int ret = 0, sz = ((page_ == pagenum && (n % m)) ? n % m : m);
  int l = (page_ - 1) * m + 1, r = l + sz - 1;
  for (int i = l; i <= r; ++ i) {
    ret += (yes1[i] ^ yes2[i]);
  }
  ret = std::min(ret, 1 + cnt2[page_]);
  ret = std::min(ret, 1 + sz - cnt2[page_]);
  return ret;
}
int calc2() {
  if (left == kInf && right == -kInf) return 0;

  if (s <= left) ans += right - s;
  else if (s >= right) ans += s - left;
  else ans += std::min(s - left, right - s) + right - left;
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  std::cin >> n >> m >> s >> p >> q;
  pagenum = ceil(1.0 * n / m);
  for (int i = 1; i <= p; ++ i) {
    int x; std::cin >> x;
    yes1[x] = true;
    cnt1[(x - 1) / m + 1] ++;
  }
  for (int i = 1; i <= q; ++ i) {
    int x; std::cin >> x;
    yes2[x] = true;
    cnt2[(x - 1) / m + 1] ++;
  }
  for (int i = 1; i <= pagenum; ++ i) {
    int ret = calc(i);
    if (ret) left = std::min(left, i), right = std::max(right, i);
    ans += ret;
  }
  std::cout << calc2() << "\n";
  return 0;
}

I

大力区间 DP,以下为写得很简洁的 std 做法。

考虑到初始全为 0 的影响,钦定 \(a_0 = a_{n + 1} = 0\)

先考虑对每个位置均进行一次操作,在此基础上检查能否合并某些操作。一种显然的贪心策略是对于一段连续的需要修改的区间,操作顺序应当从外向内,操作范围应当从大到小,尽可能在一次操作中修改到区间两端目标相同的多个位置,从而节省操作次数。

则记 \(f_{l, r}(0\le l\le r\le n + 1)\) 表示将区间 \([l, r]\) 修改为目标最多能节省的操作次数,初始化 \(f_{i, i}(0\le i\le n+1) = 0\),转移时枚举区间分界并考虑两端的位置能否通过一次操作达到目标,则有转移:

\[\forall l\le m<r, f_{l, m} + f_{m + 1, r} + [a_l = a_r] \rightarrow f_{l, r} \]

共有 \(n + 2\) 个位置,考虑到钦定了 \(a_0 = a_{n + 1} = 0\) 的影响,则答案为:

\[(n + 2 - 1) - f_{0, n + 1} \]

时间复杂度 \(O(n^3)\) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 810;
//=============================================================
int n, a[kN], f[kN][kN];
//=============================================================
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  std::cin >> n;
  for (int i = 1; i <= n; ++ i) std::cin >> a[i];

  for (int len = 2; len <= n + 2; ++ len) {
    for (int l = 0, r = len - 1 ; r <= n + 1; ++ l, ++ r) {
      for (int m = l; m < r; ++ m) {
        f[l][r] = std::max(f[l][r], f[l][m] + f[m + 1][r] + (a[l] == a[r]));
      }
    }
  }
  std::cout << (n + 2) - f[0][n + 1] - 1;
  return 0;
}

H

图论转化。

\(n\) 个工程师和 \(m\) 个问题的对应关系抽象成一张二分图 \(G = <V_1, V_2, E>\),若工程师 \(i(i\in V_1)\) 可解决问题 \(j(j\in V_2)\),在二分图中连边 \(i\rightarrow j\)。则问题实质为求得阈值 \(k\),使得对于所有大小不超过 \(k\)\(V_2\) 的子集 \(S\subset V_2\),均存在从 \(V_1\)\(S\) 的一个完美匹配。

考虑霍尔定理:对于二分图 \(G = <V_1, V_2, E>\)\(S\subset V_2, |S| \le |V_1|\),则 \(G\) 中存在 \(V_1\)\(S\) 的完美匹配当且仅当对于任意 \(T \subset S\),均有 \(|T|\le |N(T)|\),其中 \(N(T)\)\(T\) 的邻域,代表所有与 \(T\) 中结点相邻的点集,即有 \(N(T) = \{u | u\in V_1, v\in T, (u, v)\in E\}\)

于是考虑枚举所有 \(V_2\) 的子集,枚举它们的邻域并检查是否符合上式,若不符合则标记该子集的大小。根据霍尔定理答案即为阈值 \(k\) 使得点集大小 \(i\le k\) 全部都没有被标记。

注意实现,时间复杂度为 \(O(m\times 2^m)\) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 3e4 + 10;
const int kM = 20 + 10;
const int kS = 2e6 + 10;
//=============================================================
int n, m, all, ans;
std::string t;
bool no[kM];
std::vector <int> v[kM];
int cnt, now_time, tim[kN], vis[kN];
//=============================================================
void add(int x_) {
  if (tim[x_] != now_time) vis[x_] = 0, tim[x_] = now_time;
  if (vis[x_]) return ;
  vis[x_] = true;
  ++ cnt;
}
void clear() {
  cnt = 0;
  ++ now_time;
}
bool check_graph(int s_, int sz_) {
  clear();
  for (int i = 0; i < m; ++ i) {
    if (cnt >= sz_) break;
    if (s_ >> i & 1) {
      for (auto x: v[i]) {
        add(x);
        if (cnt >= sz_) break;
      }
    }
  }
  return cnt >= sz_;
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  std::cin >> n >> m;
  all = (1 << m) - 1;
  for (int i = 1; i <= n; ++ i) {
    std::cin >> t;
    for (int j = 0; j < m; ++ j) {
      if (t[j] == '1') v[j].push_back(i);
    }
  }

  ans = m;
  for (int s = 1; s <= all; ++ s) {
    int sz = __builtin_popcount(s);
    if (!check_graph(s, sz)) {
      ans = std::min(ans, sz - 1);
    }
  }
  std::cout << ans << "\n";
  return 0;
}
/*
4 6
001101
111001
001110
100100
*/

J

对于一点线对 \((x, y, l, r)\),一个显然的想法是若 \(l\le x\le r\),则尝试作垂线段,否则最优的选择是连向 \((l, 0)\)\((r, 0)\)

考虑处理出在线段两两不相交限制下,点 \((x_i, y_i)\) 连向对应的线段 \((l_i, r_i)\) 的合法范围 \((l'_i, r'_i)\)。所有区间 \((l_i, r_i)\) 互不相交,手玩下发现这使得所有合法范围 \((l'_i, r'_i)\) 必然是一段连续的区间,或者为空集。

那么如何处理 \((l'_i, r'_i)\)?同样是根据 \((l_i, r_i)\) 互不相交,发现仅需考虑 \((x_i, y_i)\) 两侧满足 \(y_j\le y_i\) 的点 \(j\) 的影响,需要满足 \((x_i, y_i)\) 连向 \((l'_i, r'_i)\) 的线段斜率小于 \((x_i, y_i)\) 连向 \((x_j, y_j)\) 的斜率,发现寻找有影响的点的过程可以用单调栈维护。考虑正向反向枚举点线对,通过维护 \(y\) 坐标递减的单调栈即可求得所有点线对的合法区间。检查其中是否有空集,若无空集则讨论连向什么位置最优即可。

实现细节详见代码,总时间复杂度 \(O(n)\) 级别。

开 8s 大概是因为 std 常数挺大的,在 cf 神机上都要跑 2s。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
struct PtLin {
  double x, y, l, r;
} ptl[kN];
int n;
//=============================================================
double dis(double x1_, double y1_, double x2_, double y2_) {
  return sqrt((x1_ - x2_) * (x1_ - x2_) + (y1_ - y2_) * (y1_ - y2_));
}
void Init() {
  std::stack <PtLin> st; 
  for (int i = 1; i <= n; ++ i) {
    while (!st.empty() && st.top().y <= ptl[i].y) {
      PtLin t = st.top(); st.pop();
      if (t.y < ptl[i].y) {
        ptl[i].l = std::max(ptl[i].l, ptl[i].x - ptl[i].y * (ptl[i].x - t.x) / (ptl[i].y - t.y));
      } else if (ptl[i].x < t.x) {
        std::cout << -1 << "\n";
        exit(0);
      }
    }
    st.push(ptl[i]);
  }
  while (!st.empty()) st.pop();
  for (int i = n; i; -- i) {
    while (!st.empty() && st.top().y <= ptl[i].y) {
      PtLin t = st.top(); st.pop();
      if (t.y < ptl[i].y) {
        ptl[i].r = std::min(ptl[i].r, ptl[i].x - ptl[i].y * (ptl[i].x - t.x) / (ptl[i].y - t.y));
      } else if (t.x < ptl[i].x) {
        std::cout << -1 << "\n";
        exit(0);
      }
    }
    st.push(ptl[i]);
  }
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  std::cin >> n;
  for (int i = 1; i <= n; ++ i) {
    double x, y, l, r; std::cin >> x >> y >> l >> r;
    ptl[i] = (PtLin) {x, y, l, r};
  }
  Init();

  double ans = 0;
  for (int i = 1; i <= n; ++ i) {
    if (ptl[i].l > ptl[i].r) {
      std::cout << -1 << "\n";
      exit(0);
    }
    if (ptl[i].l <= ptl[i].x && ptl[i].x <= ptl[i].r) {
      ans += ptl[i].y;
    } else {
      ans += std::min(dis(ptl[i].x, ptl[i].y, ptl[i].l, 0), dis(ptl[i].x, ptl[i].y, ptl[i].r, 0));
    }
  }
  std::cout << std::fixed << std::setprecision(15) << ans << "\n";
  return 0;
}

写在最后

数据范围和时限都有点抽象呃呃

posted @ 2024-03-21 12:18  Luckyblock  阅读(240)  评论(0)    收藏  举报